feat: Improved UX for model information, modality interactions etc
This commit is contained in:
parent
c35dee3bd7
commit
4bf82a10f1
|
|
@ -253,9 +253,11 @@
|
||||||
|
|
||||||
<ChatFormActions
|
<ChatFormActions
|
||||||
canSend={message.trim().length > 0 || uploadedFiles.length > 0}
|
canSend={message.trim().length > 0 || uploadedFiles.length > 0}
|
||||||
|
hasText={message.trim().length > 0}
|
||||||
{disabled}
|
{disabled}
|
||||||
{isLoading}
|
{isLoading}
|
||||||
{isRecording}
|
{isRecording}
|
||||||
|
{uploadedFiles}
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
onMicClick={handleMicClick}
|
onMicClick={handleMicClick}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Mic } from '@lucide/svelte';
|
import { Mic, Square } from '@lucide/svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { supportsAudio } from '$lib/stores/server.svelte';
|
import { supportsAudio } from '$lib/stores/server.svelte';
|
||||||
|
|
@ -27,8 +27,6 @@
|
||||||
<Button
|
<Button
|
||||||
class="h-8 w-8 rounded-full p-0 {isRecording
|
class="h-8 w-8 rounded-full p-0 {isRecording
|
||||||
? 'animate-pulse bg-red-500 text-white hover:bg-red-600'
|
? 'animate-pulse bg-red-500 text-white hover:bg-red-600'
|
||||||
: 'bg-transparent text-muted-foreground hover:bg-foreground/10 hover:text-foreground'} {!supportsAudio()
|
|
||||||
? 'cursor-not-allowed opacity-50'
|
|
||||||
: ''}"
|
: ''}"
|
||||||
disabled={disabled || isLoading || !supportsAudio()}
|
disabled={disabled || isLoading || !supportsAudio()}
|
||||||
onclick={onMicClick}
|
onclick={onMicClick}
|
||||||
|
|
@ -36,7 +34,11 @@
|
||||||
>
|
>
|
||||||
<span class="sr-only">{isRecording ? 'Stop recording' : 'Start recording'}</span>
|
<span class="sr-only">{isRecording ? 'Stop recording' : 'Start recording'}</span>
|
||||||
|
|
||||||
<Mic class="h-4 w-4" />
|
{#if isRecording}
|
||||||
|
<Square class="h-4 w-4 animate-pulse fill-white" />
|
||||||
|
{:else}
|
||||||
|
<Mic class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,25 @@
|
||||||
import { Square, ArrowUp } from '@lucide/svelte';
|
import { Square, ArrowUp } from '@lucide/svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import {
|
import {
|
||||||
|
BadgeModelName,
|
||||||
ChatFormActionFileAttachments,
|
ChatFormActionFileAttachments,
|
||||||
|
ChatFormModelSelector,
|
||||||
ChatFormActionRecord,
|
ChatFormActionRecord,
|
||||||
ChatFormModelSelector
|
DialogModelInformation
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { FileTypeCategory } from '$lib/enums/files';
|
||||||
import type { FileTypeCategory } from '$lib/enums/files';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
|
import { isRouterMode, supportsAudio } from '$lib/stores/server.svelte';
|
||||||
|
import type { ChatUploadedFile } from '$lib/types/chat';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canSend?: boolean;
|
canSend?: boolean;
|
||||||
class?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isRecording?: boolean;
|
isRecording?: boolean;
|
||||||
|
hasText?: boolean;
|
||||||
|
uploadedFiles?: ChatUploadedFile[];
|
||||||
onFileUpload?: (fileType?: FileTypeCategory) => void;
|
onFileUpload?: (fileType?: FileTypeCategory) => void;
|
||||||
onMicClick?: () => void;
|
onMicClick?: () => void;
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
|
|
@ -22,22 +28,38 @@
|
||||||
|
|
||||||
let {
|
let {
|
||||||
canSend = false,
|
canSend = false,
|
||||||
class: className = '',
|
className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isRecording = false,
|
isRecording = false,
|
||||||
|
hasText = false,
|
||||||
|
uploadedFiles = [],
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onMicClick,
|
onMicClick,
|
||||||
onStop
|
onStop
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let currentConfig = $derived(config());
|
let inRouterMode = $derived(isRouterMode());
|
||||||
|
let hasAudioModality = $derived(supportsAudio());
|
||||||
|
let hasAudioAttachments = $derived(
|
||||||
|
uploadedFiles.some((file) => getFileTypeCategory(file.type) === FileTypeCategory.AUDIO)
|
||||||
|
);
|
||||||
|
let shouldShowRecordButton = $derived(hasAudioModality && !hasText && !hasAudioAttachments);
|
||||||
|
let shouldShowSubmitButton = $derived(!shouldShowRecordButton || hasAudioAttachments);
|
||||||
|
|
||||||
|
let showModelInfoDialog = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full items-center gap-2 {className}">
|
<div class="flex w-full items-center gap-3 {className}">
|
||||||
<ChatFormActionFileAttachments class="mr-auto" {disabled} {onFileUpload} />
|
<ChatFormActionFileAttachments class="mr-auto" {disabled} {onFileUpload} />
|
||||||
|
|
||||||
{#if currentConfig.modelSelectorEnabled}
|
{#if !inRouterMode}
|
||||||
|
<BadgeModelName
|
||||||
|
class="clickable max-w-80"
|
||||||
|
onclick={() => (showModelInfoDialog = true)}
|
||||||
|
showTooltip
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<ChatFormModelSelector class="shrink-0" />
|
<ChatFormModelSelector class="shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -51,15 +73,24 @@
|
||||||
<Square class="h-8 w-8 fill-destructive stroke-destructive" />
|
<Square class="h-8 w-8 fill-destructive stroke-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<ChatFormActionRecord {disabled} {isLoading} {isRecording} {onMicClick} />
|
{#if shouldShowRecordButton}
|
||||||
|
<ChatFormActionRecord {disabled} {isLoading} {isRecording} {onMicClick} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Button
|
{#if shouldShowSubmitButton}
|
||||||
type="submit"
|
<Button
|
||||||
disabled={!canSend || disabled || isLoading}
|
type="submit"
|
||||||
class="h-8 w-8 rounded-full p-0"
|
disabled={!canSend || disabled || isLoading}
|
||||||
>
|
class="h-8 w-8 rounded-full p-0"
|
||||||
<span class="sr-only">Send</span>
|
>
|
||||||
<ArrowUp class="h-12 w-12" />
|
<span class="sr-only">Send</span>
|
||||||
</Button>
|
<ArrowUp class="h-12 w-12" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DialogModelInformation
|
||||||
|
bind:open={showModelInfoDialog}
|
||||||
|
onOpenChange={(open) => (showModelInfoDialog = open)}
|
||||||
|
/>
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,10 @@
|
||||||
return message.model;
|
return message.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!serverModel || serverModel === 'none' || serverModel === 'llama-server') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return serverModel;
|
return serverModel;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
DialogEmptyFileAlert,
|
DialogEmptyFileAlert,
|
||||||
DialogChatError,
|
DialogChatError,
|
||||||
ServerErrorSplash,
|
ServerErrorSplash,
|
||||||
ServerInfo,
|
|
||||||
ServerLoadingSplash,
|
ServerLoadingSplash,
|
||||||
DialogConfirmation
|
DialogConfirmation
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
|
|
@ -44,6 +43,7 @@
|
||||||
import { fade, fly, slide } from 'svelte/transition';
|
import { fade, fly, slide } from 'svelte/transition';
|
||||||
import { Trash2 } from '@lucide/svelte';
|
import { Trash2 } from '@lucide/svelte';
|
||||||
import ChatScreenDragOverlay from './ChatScreenDragOverlay.svelte';
|
import ChatScreenDragOverlay from './ChatScreenDragOverlay.svelte';
|
||||||
|
import { ModelModality } from '$lib/enums/model';
|
||||||
|
|
||||||
let { showCenteredEmpty = false } = $props();
|
let { showCenteredEmpty = false } = $props();
|
||||||
|
|
||||||
|
|
@ -334,14 +334,14 @@
|
||||||
role="main"
|
role="main"
|
||||||
>
|
>
|
||||||
<div class="w-full max-w-[48rem] px-4">
|
<div class="w-full max-w-[48rem] px-4">
|
||||||
<div class="mb-8 text-center" in:fade={{ duration: 300 }}>
|
<div class="mb-10 text-center" in:fade={{ duration: 300 }}>
|
||||||
<h1 class="mb-2 text-3xl font-semibold tracking-tight">llama.cpp</h1>
|
<h1 class="mb-4 text-3xl font-semibold tracking-tight">llama.cpp</h1>
|
||||||
|
|
||||||
<p class="text-lg text-muted-foreground">How can I help you today?</p>
|
<p class="text-lg text-muted-foreground">
|
||||||
</div>
|
{serverStore.supportedModalities.includes(ModelModality.AUDIO)
|
||||||
|
? 'Record audio, type a message '
|
||||||
<div class="mb-6 flex justify-center" in:fly={{ y: 10, duration: 300, delay: 200 }}>
|
: 'Type a message'} or upload files to get started
|
||||||
<ServerInfo />
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if serverWarning()}
|
{#if serverWarning()}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea class="h-[100vh]">
|
<ScrollArea class="h-[100vh]">
|
||||||
<Sidebar.Header class=" top-0 z-10 gap-6 bg-sidebar/50 px-4 pt-4 pb-2 backdrop-blur-lg md:sticky">
|
<Sidebar.Header class=" top-0 z-10 gap-6 bg-sidebar/50 px-4 py-4 pb-2 backdrop-blur-lg md:sticky">
|
||||||
<a href="#/" onclick={handleMobileSidebarItemClick}>
|
<a href="#/" onclick={handleMobileSidebarItemClick}>
|
||||||
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
|
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import * as Table from '$lib/components/ui/table';
|
||||||
|
import { BadgeModality } from '$lib/components/app';
|
||||||
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
|
import { ChatService } from '$lib/services/chat';
|
||||||
|
import type { ApiModelListResponse } from '$lib/types/api';
|
||||||
|
import { Copy } from '@lucide/svelte';
|
||||||
|
import { copyToClipboard } from '$lib/utils/copy';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(), onOpenChange }: Props = $props();
|
||||||
|
|
||||||
|
let serverProps = $derived(serverStore.serverProps);
|
||||||
|
let modalities = $derived(serverStore.supportedModalities);
|
||||||
|
|
||||||
|
let modelsData = $state<ApiModelListResponse | null>(null);
|
||||||
|
let isLoadingModels = $state(false);
|
||||||
|
|
||||||
|
// Fetch models data when dialog opens
|
||||||
|
$effect(() => {
|
||||||
|
if (open && !modelsData) {
|
||||||
|
loadModelsData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadModelsData() {
|
||||||
|
isLoadingModels = true;
|
||||||
|
try {
|
||||||
|
modelsData = await ChatService.getModels();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load models data:', error);
|
||||||
|
// Set empty data to prevent infinite loading
|
||||||
|
modelsData = { object: 'list', data: [] };
|
||||||
|
} finally {
|
||||||
|
isLoadingModels = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format helpers
|
||||||
|
function formatSize(sizeBytes: number | unknown): string {
|
||||||
|
if (typeof sizeBytes !== 'number') return 'Unknown';
|
||||||
|
|
||||||
|
// Convert to GB for better readability
|
||||||
|
const sizeGB = sizeBytes / (1024 * 1024 * 1024);
|
||||||
|
if (sizeGB >= 1) {
|
||||||
|
return `${sizeGB.toFixed(2)} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to MB for smaller models
|
||||||
|
const sizeMB = sizeBytes / (1024 * 1024);
|
||||||
|
return `${sizeMB.toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatParameters(params: number | unknown): string {
|
||||||
|
if (typeof params !== 'number') return 'Unknown';
|
||||||
|
if (params >= 1e9) {
|
||||||
|
return `${(params / 1e9).toFixed(2)}B`;
|
||||||
|
}
|
||||||
|
if (params >= 1e6) {
|
||||||
|
return `${(params / 1e6).toFixed(2)}M`;
|
||||||
|
}
|
||||||
|
if (params >= 1e3) {
|
||||||
|
return `${(params / 1e3).toFixed(2)}K`;
|
||||||
|
}
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num: number | unknown): string {
|
||||||
|
if (typeof num !== 'number') return 'Unknown';
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open {onOpenChange}>
|
||||||
|
<Dialog.Content class="@container z-9999 !max-w-[60rem] max-w-full">
|
||||||
|
<style>
|
||||||
|
@container (max-width: 56rem) {
|
||||||
|
.resizable-text-container {
|
||||||
|
max-width: calc(100vw - var(--threshold));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Model Information</Dialog.Title>
|
||||||
|
<Dialog.Description>Current model details and capabilities</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<div class="space-y-6 py-4">
|
||||||
|
{#if isLoadingModels}
|
||||||
|
<div class="flex items-center justify-center py-8">
|
||||||
|
<div class="text-sm text-muted-foreground">Loading model information...</div>
|
||||||
|
</div>
|
||||||
|
{:else if modelsData && modelsData.data.length > 0}
|
||||||
|
{@const modelMeta = modelsData.data[0].meta}
|
||||||
|
|
||||||
|
{#if serverProps}
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Head class="w-[10rem]">Model</Table.Head>
|
||||||
|
|
||||||
|
<Table.Head>
|
||||||
|
<span
|
||||||
|
class="resizable-text-container block min-w-0 flex-1 truncate"
|
||||||
|
style:--threshold="12rem"
|
||||||
|
>
|
||||||
|
{serverStore.modelName}
|
||||||
|
</span>
|
||||||
|
</Table.Head>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
<!-- Model Path -->
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="h-10 align-middle font-medium">File Path</Table.Cell>
|
||||||
|
|
||||||
|
<Table.Cell
|
||||||
|
class="inline-flex h-10 items-center gap-2 align-middle font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="resizable-text-container min-w-0 flex-1 truncate"
|
||||||
|
style:--threshold="14rem"
|
||||||
|
>
|
||||||
|
{serverProps.model_path}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Copy
|
||||||
|
class="h-3 w-3 flex-shrink-0"
|
||||||
|
aria-label="Copy model path to clipboard"
|
||||||
|
onclick={() => copyToClipboard(serverProps.model_path)}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
|
||||||
|
<!-- Context Size -->
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="h-10 align-middle font-medium">Context Size</Table.Cell>
|
||||||
|
<Table.Cell
|
||||||
|
>{formatNumber(serverProps.default_generation_settings.n_ctx)} tokens</Table.Cell
|
||||||
|
>
|
||||||
|
</Table.Row>
|
||||||
|
|
||||||
|
<!-- Training Context -->
|
||||||
|
{#if modelMeta?.n_ctx_train}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="h-10 align-middle font-medium">Training Context</Table.Cell>
|
||||||
|
<Table.Cell>{formatNumber(modelMeta.n_ctx_train)} tokens</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Model Size -->
|
||||||
|
{#if modelMeta?.size}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="h-10 align-middle font-medium">Model Size</Table.Cell>
|
||||||
|
<Table.Cell>{formatSize(modelMeta.size)}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Parameters -->
|
||||||
|
{#if modelMeta?.n_params}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="h-10 align-middle font-medium">Parameters</Table.Cell>
|
||||||
|
<Table.Cell>{formatParameters(modelMeta.n_params)}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Embedding Size -->
|
||||||
|
{#if modelMeta?.n_embd}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Embedding Size</Table.Cell>
|
||||||
|
<Table.Cell>{formatNumber(modelMeta.n_embd)}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Vocabulary Size -->
|
||||||
|
{#if modelMeta?.n_vocab}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Vocabulary Size</Table.Cell>
|
||||||
|
<Table.Cell>{formatNumber(modelMeta.n_vocab)} tokens</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Vocabulary Type -->
|
||||||
|
{#if modelMeta?.vocab_type}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Vocabulary Type</Table.Cell>
|
||||||
|
<Table.Cell class="align-middle capitalize">{modelMeta.vocab_type}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Total Slots -->
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Parallel Slots</Table.Cell>
|
||||||
|
<Table.Cell>{serverProps.total_slots}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
|
||||||
|
<!-- Modalities -->
|
||||||
|
{#if modalities.length > 0}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Modalities</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<BadgeModality {modalities} />
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Build Info -->
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Build Info</Table.Cell>
|
||||||
|
<Table.Cell class="align-middle font-mono text-xs"
|
||||||
|
>{serverProps.build_info}</Table.Cell
|
||||||
|
>
|
||||||
|
</Table.Row>
|
||||||
|
|
||||||
|
<!-- Chat Template -->
|
||||||
|
{#if serverProps.chat_template}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="align-middle font-medium">Chat Template</Table.Cell>
|
||||||
|
<Table.Cell class="py-10">
|
||||||
|
<div class="max-h-120 overflow-y-auto rounded-md bg-muted p-4">
|
||||||
|
<pre
|
||||||
|
class="font-mono text-xs whitespace-pre-wrap">{serverProps.chat_template}</pre>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
{/if}
|
||||||
|
{:else if !isLoadingModels}
|
||||||
|
<div class="flex items-center justify-center py-8">
|
||||||
|
<div class="text-sm text-muted-foreground">No model information available</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
@ -45,11 +45,14 @@ export { default as DialogConfirmation } from './dialogs/DialogConfirmation.svel
|
||||||
export { default as DialogConversationSelection } from './dialogs/DialogConversationSelection.svelte';
|
export { default as DialogConversationSelection } from './dialogs/DialogConversationSelection.svelte';
|
||||||
export { default as DialogConversationTitleUpdate } from './dialogs/DialogConversationTitleUpdate.svelte';
|
export { default as DialogConversationTitleUpdate } from './dialogs/DialogConversationTitleUpdate.svelte';
|
||||||
export { default as DialogEmptyFileAlert } from './dialogs/DialogEmptyFileAlert.svelte';
|
export { default as DialogEmptyFileAlert } from './dialogs/DialogEmptyFileAlert.svelte';
|
||||||
|
export { default as DialogModelInformation } from './dialogs/DialogModelInformation.svelte';
|
||||||
|
|
||||||
// Miscellanous
|
// Miscellanous
|
||||||
|
|
||||||
export { default as ActionButton } from './misc/ActionButton.svelte';
|
export { default as ActionButton } from './misc/ActionButton.svelte';
|
||||||
export { default as ActionDropdown } from './misc/ActionDropdown.svelte';
|
export { default as ActionDropdown } from './misc/ActionDropdown.svelte';
|
||||||
|
export { default as BadgeModelName } from './misc/BadgeModelName.svelte';
|
||||||
|
export { default as BadgeModality } from './misc/BadgeModality.svelte';
|
||||||
export { default as ConversationSelection } from './misc/ConversationSelection.svelte';
|
export { default as ConversationSelection } from './misc/ConversationSelection.svelte';
|
||||||
export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte';
|
export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte';
|
||||||
export { default as MarkdownContent } from './misc/MarkdownContent.svelte';
|
export { default as MarkdownContent } from './misc/MarkdownContent.svelte';
|
||||||
|
|
@ -60,4 +63,3 @@ export { default as RemoveButton } from './misc/RemoveButton.svelte';
|
||||||
export { default as ServerStatus } from './server/ServerStatus.svelte';
|
export { default as ServerStatus } from './server/ServerStatus.svelte';
|
||||||
export { default as ServerErrorSplash } from './server/ServerErrorSplash.svelte';
|
export { default as ServerErrorSplash } from './server/ServerErrorSplash.svelte';
|
||||||
export { default as ServerLoadingSplash } from './server/ServerLoadingSplash.svelte';
|
export { default as ServerLoadingSplash } from './server/ServerLoadingSplash.svelte';
|
||||||
export { default as ServerInfo } from './server/ServerInfo.svelte';
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Eye, Mic } from '@lucide/svelte';
|
||||||
|
import { ModelModality } from '$lib/enums/model';
|
||||||
|
import { cn } from '$lib/components/ui/utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modalities: ModelModality[];
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { modalities, class: className = '' }: Props = $props();
|
||||||
|
|
||||||
|
function getModalityIcon(modality: ModelModality) {
|
||||||
|
switch (modality) {
|
||||||
|
case ModelModality.VISION:
|
||||||
|
return Eye;
|
||||||
|
case ModelModality.AUDIO:
|
||||||
|
return Mic;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModalityLabel(modality: ModelModality): string {
|
||||||
|
switch (modality) {
|
||||||
|
case ModelModality.VISION:
|
||||||
|
return 'Vision';
|
||||||
|
case ModelModality.AUDIO:
|
||||||
|
return 'Audio';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each modalities as modality, index (index)}
|
||||||
|
{@const IconComponent = getModalityIcon(modality)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs font-medium',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{#if IconComponent}
|
||||||
|
<IconComponent class="h-3 w-3" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{getModalityLabel(modality)}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Package } from '@lucide/svelte';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
|
import { cn } from '$lib/components/ui/utils';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
onclick?: () => void;
|
||||||
|
showTooltip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className = '', onclick, showTooltip = false }: Props = $props();
|
||||||
|
|
||||||
|
let model = $derived(serverStore.modelName);
|
||||||
|
let isModelMode = $derived(serverStore.isModelMode);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet badge()}
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class={cn(
|
||||||
|
'text-xs',
|
||||||
|
onclick ? 'cursor-pointer transition-colors hover:bg-foreground/20' : '',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{onclick}
|
||||||
|
>
|
||||||
|
<div class="icons mr-0.5 flex items-center gap-1.5">
|
||||||
|
<Package class="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="block truncate">{model}</span>
|
||||||
|
</Badge>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if model && isModelMode}
|
||||||
|
{#if showTooltip}
|
||||||
|
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
{@render badge()}
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
|
<Tooltip.Content>
|
||||||
|
{onclick ? 'Click for model details' : 'Model name'}
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{:else}
|
||||||
|
{@render badge()}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Server, Eye, Mic } from '@lucide/svelte';
|
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
|
||||||
|
|
||||||
let modalities = $derived(serverStore.supportedModalities);
|
|
||||||
let model = $derived(serverStore.modelName);
|
|
||||||
let props = $derived(serverStore.serverProps);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if props}
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
|
|
||||||
{#if model}
|
|
||||||
<Badge variant="outline" class="text-xs">
|
|
||||||
<Server class="mr-1 h-3 w-3" />
|
|
||||||
|
|
||||||
<span class="block max-w-[50vw] truncate">{model}</span>
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex gap-4">
|
|
||||||
{#if props.default_generation_settings.n_ctx}
|
|
||||||
<Badge variant="secondary" class="text-xs">
|
|
||||||
ctx: {props.default_generation_settings.n_ctx.toLocaleString()}
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modalities.length > 0}
|
|
||||||
{#each modalities as modality (modality)}
|
|
||||||
<Badge variant="secondary" class="text-xs">
|
|
||||||
{#if modality === 'vision'}
|
|
||||||
<Eye class="mr-1 h-3 w-3" />
|
|
||||||
{:else if modality === 'audio'}
|
|
||||||
<Mic class="mr-1 h-3 w-3" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{modality}
|
|
||||||
</Badge>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import Root from './table.svelte';
|
||||||
|
import Body from './table-body.svelte';
|
||||||
|
import Caption from './table-caption.svelte';
|
||||||
|
import Cell from './table-cell.svelte';
|
||||||
|
import Footer from './table-footer.svelte';
|
||||||
|
import Head from './table-head.svelte';
|
||||||
|
import Header from './table-header.svelte';
|
||||||
|
import Row from './table-row.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Body,
|
||||||
|
Caption,
|
||||||
|
Cell,
|
||||||
|
Footer,
|
||||||
|
Head,
|
||||||
|
Header,
|
||||||
|
Row,
|
||||||
|
//
|
||||||
|
Root as Table,
|
||||||
|
Body as TableBody,
|
||||||
|
Caption as TableCaption,
|
||||||
|
Cell as TableCell,
|
||||||
|
Footer as TableFooter,
|
||||||
|
Head as TableHead,
|
||||||
|
Header as TableHeader,
|
||||||
|
Row as TableRow
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tbody
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-body"
|
||||||
|
class={cn('[&_tr:last-child]:border-0', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</tbody>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<caption
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-caption"
|
||||||
|
class={cn('mt-4 text-sm text-muted-foreground', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</caption>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLTdAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLTdAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<td
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-cell"
|
||||||
|
class={cn(
|
||||||
|
'bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</td>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tfoot
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-footer"
|
||||||
|
class={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</tfoot>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLThAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLThAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<th
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-head"
|
||||||
|
class={cn(
|
||||||
|
'h-10 bg-clip-padding px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pe-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</th>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<thead
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-header"
|
||||||
|
class={cn('[&_tr]:border-b', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</thead>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-row"
|
||||||
|
class={cn(
|
||||||
|
'border-b transition-colors data-[state=selected]:bg-muted hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</tr>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLTableAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLTableAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div data-slot="table-container" class="relative w-full overflow-x-auto">
|
||||||
|
<table
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table"
|
||||||
|
class={cn('w-full caption-bottom text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
@ -76,7 +76,6 @@ export class ChatService {
|
||||||
onReasoningChunk,
|
onReasoningChunk,
|
||||||
onToolCallChunk,
|
onToolCallChunk,
|
||||||
onModel,
|
onModel,
|
||||||
onFirstValidChunk,
|
|
||||||
// Generation parameters
|
// Generation parameters
|
||||||
temperature,
|
temperature,
|
||||||
max_tokens,
|
max_tokens,
|
||||||
|
|
@ -225,7 +224,6 @@ export class ChatService {
|
||||||
onReasoningChunk,
|
onReasoningChunk,
|
||||||
onToolCallChunk,
|
onToolCallChunk,
|
||||||
onModel,
|
onModel,
|
||||||
onFirstValidChunk,
|
|
||||||
conversationId,
|
conversationId,
|
||||||
abortController.signal
|
abortController.signal
|
||||||
);
|
);
|
||||||
|
|
@ -300,7 +298,6 @@ export class ChatService {
|
||||||
onReasoningChunk?: (chunk: string) => void,
|
onReasoningChunk?: (chunk: string) => void,
|
||||||
onToolCallChunk?: (chunk: string) => void,
|
onToolCallChunk?: (chunk: string) => void,
|
||||||
onModel?: (model: string) => void,
|
onModel?: (model: string) => void,
|
||||||
onFirstValidChunk?: () => void,
|
|
||||||
conversationId?: string,
|
conversationId?: string,
|
||||||
abortSignal?: AbortSignal
|
abortSignal?: AbortSignal
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
@ -317,7 +314,6 @@ export class ChatService {
|
||||||
let lastTimings: ChatMessageTimings | undefined;
|
let lastTimings: ChatMessageTimings | undefined;
|
||||||
let streamFinished = false;
|
let streamFinished = false;
|
||||||
let modelEmitted = false;
|
let modelEmitted = false;
|
||||||
let firstValidChunkEmitted = false;
|
|
||||||
let toolCallIndexOffset = 0;
|
let toolCallIndexOffset = 0;
|
||||||
let hasOpenToolCallBatch = false;
|
let hasOpenToolCallBatch = false;
|
||||||
|
|
||||||
|
|
@ -384,15 +380,6 @@ export class ChatService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed: ApiChatCompletionStreamChunk = JSON.parse(data);
|
const parsed: ApiChatCompletionStreamChunk = JSON.parse(data);
|
||||||
|
|
||||||
if (!firstValidChunkEmitted && parsed.object === 'chat.completion.chunk') {
|
|
||||||
firstValidChunkEmitted = true;
|
|
||||||
|
|
||||||
if (!abortSignal?.aborted) {
|
|
||||||
onFirstValidChunk?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = parsed.choices[0]?.delta?.content;
|
const content = parsed.choices[0]?.delta?.content;
|
||||||
const reasoningContent = parsed.choices[0]?.delta?.reasoning_content;
|
const reasoningContent = parsed.choices[0]?.delta?.reasoning_content;
|
||||||
const toolCalls = parsed.choices[0]?.delta?.tool_calls;
|
const toolCalls = parsed.choices[0]?.delta?.tool_calls;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { DatabaseStore } from '$lib/stores/database';
|
import { DatabaseStore } from '$lib/stores/database';
|
||||||
import { chatService, slotsService } from '$lib/services';
|
import { chatService, slotsService } from '$lib/services';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
|
||||||
import { normalizeModelName } from '$lib/utils/model-names';
|
import { normalizeModelName } from '$lib/utils/model-names';
|
||||||
import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/utils/branching';
|
import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/utils/branching';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
@ -365,41 +364,15 @@ class ChatStore {
|
||||||
|
|
||||||
let resolvedModel: string | null = null;
|
let resolvedModel: string | null = null;
|
||||||
let modelPersisted = false;
|
let modelPersisted = false;
|
||||||
const currentConfig = config();
|
|
||||||
const preferServerPropsModel = !currentConfig.modelSelectorEnabled;
|
|
||||||
let serverPropsRefreshed = false;
|
|
||||||
let updateModelFromServerProps: ((persistImmediately?: boolean) => void) | null = null;
|
|
||||||
|
|
||||||
const refreshServerPropsOnce = () => {
|
|
||||||
if (serverPropsRefreshed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
serverPropsRefreshed = true;
|
|
||||||
|
|
||||||
const hasExistingProps = serverStore.serverProps !== null;
|
|
||||||
|
|
||||||
serverStore
|
|
||||||
.fetchServerProps({ silent: hasExistingProps })
|
|
||||||
.then(() => {
|
|
||||||
updateModelFromServerProps?.(true);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn('Failed to refresh server props after streaming started:', error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
|
const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
|
||||||
const serverModelName = serverStore.modelName;
|
if (!modelName) {
|
||||||
const preferredModelSource = preferServerPropsModel
|
|
||||||
? (serverModelName ?? modelName ?? null)
|
|
||||||
: (modelName ?? serverModelName ?? null);
|
|
||||||
|
|
||||||
if (!preferredModelSource) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedModel = normalizeModelName(preferredModelSource);
|
const normalizedModel = normalizeModelName(modelName);
|
||||||
|
|
||||||
|
console.log('Resolved model:', normalizedModel);
|
||||||
|
|
||||||
if (!normalizedModel || normalizedModel === resolvedModel) {
|
if (!normalizedModel || normalizedModel === resolvedModel) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -423,20 +396,6 @@ class ChatStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (preferServerPropsModel) {
|
|
||||||
updateModelFromServerProps = (persistImmediately = true) => {
|
|
||||||
const currentServerModel = serverStore.modelName;
|
|
||||||
|
|
||||||
if (!currentServerModel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordModel(currentServerModel, persistImmediately);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateModelFromServerProps(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
slotsService.startStreaming();
|
slotsService.startStreaming();
|
||||||
slotsService.setActiveConversation(assistantMessage.convId);
|
slotsService.setActiveConversation(assistantMessage.convId);
|
||||||
|
|
||||||
|
|
@ -445,9 +404,6 @@ class ChatStore {
|
||||||
{
|
{
|
||||||
...this.getApiOptions(),
|
...this.getApiOptions(),
|
||||||
|
|
||||||
onFirstValidChunk: () => {
|
|
||||||
refreshServerPropsOnce();
|
|
||||||
},
|
|
||||||
onChunk: (chunk: string) => {
|
onChunk: (chunk: string) => {
|
||||||
streamedContent += chunk;
|
streamedContent += chunk;
|
||||||
this.setConversationStreaming(
|
this.setConversationStreaming(
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ class ServerStore {
|
||||||
return this._serverProps.model_path.split(/(\\|\/)/).pop() || null;
|
return this._serverProps.model_path.split(/(\\|\/)/).pop() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get supportedModalities(): string[] {
|
get supportedModalities(): ModelModality[] {
|
||||||
const modalities: string[] = [];
|
const modalities: ModelModality[] = [];
|
||||||
if (this._serverProps?.modalities?.audio) {
|
if (this._serverProps?.modalities?.audio) {
|
||||||
modalities.push(ModelModality.AUDIO);
|
modalities.push(ModelModality.AUDIO);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
/**
|
/**
|
||||||
* Normalizes a model name by extracting the filename from a path.
|
* Normalizes a model name by extracting the filename from a path, but preserves Hugging Face repository format.
|
||||||
*
|
*
|
||||||
* Handles both forward slashes (/) and backslashes (\) as path separators.
|
* Handles both forward slashes (/) and backslashes (\) as path separators.
|
||||||
* If the model name is just a filename (no path), returns it as-is.
|
* - If the model name has exactly one slash (org/model format), preserves the full "org/model" name
|
||||||
|
* - If the model name has no slash or multiple slashes, extracts just the filename
|
||||||
|
* - If the model name is just a filename (no path), returns it as-is.
|
||||||
*
|
*
|
||||||
* @param modelName - The model name or path to normalize
|
* @param modelName - The model name or path to normalize
|
||||||
* @returns The normalized model name (filename only)
|
* @returns The normalized model name
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* normalizeModelName('models/llama-3.1-8b') // Returns: 'llama-3.1-8b'
|
* normalizeModelName('models/llama-3.1-8b') // Returns: 'llama-3.1-8b' (multiple slashes -> filename)
|
||||||
* normalizeModelName('C:\\Models\\gpt-4') // Returns: 'gpt-4'
|
* normalizeModelName('C:\\Models\\gpt-4') // Returns: 'gpt-4' (multiple slashes -> filename)
|
||||||
* normalizeModelName('simple-model') // Returns: 'simple-model'
|
* normalizeModelName('meta-llama/Llama-3.1-8B') // Returns: 'meta-llama/Llama-3.1-8B' (Hugging Face format)
|
||||||
|
* normalizeModelName('simple-model') // Returns: 'simple-model' (no slash)
|
||||||
* normalizeModelName(' spaced ') // Returns: 'spaced'
|
* normalizeModelName(' spaced ') // Returns: 'spaced'
|
||||||
* normalizeModelName('') // Returns: ''
|
* normalizeModelName('') // Returns: ''
|
||||||
*/
|
*/
|
||||||
|
|
@ -22,6 +25,20 @@ export function normalizeModelName(modelName: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = trimmed.split(/[\\/]/);
|
const segments = trimmed.split(/[\\/]/);
|
||||||
|
|
||||||
|
// If we have exactly 2 segments (one slash), treat it as Hugging Face repo format
|
||||||
|
// and preserve the full "org/model" format
|
||||||
|
if (segments.length === 2) {
|
||||||
|
const [org, model] = segments;
|
||||||
|
const trimmedOrg = org?.trim();
|
||||||
|
const trimmedModel = model?.trim();
|
||||||
|
|
||||||
|
if (trimmedOrg && trimmedModel) {
|
||||||
|
return `${trimmedOrg}/${trimmedModel}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other cases (no slash, or multiple slashes), extract just the filename
|
||||||
const candidate = segments.pop();
|
const candidate = segments.pop();
|
||||||
const normalized = candidate?.trim();
|
const normalized = candidate?.trim();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue