refactor: Enhance model info and attachment handling
This commit is contained in:
parent
491fe2d3f7
commit
eed1bd9b97
|
|
@ -3,7 +3,7 @@
|
||||||
import { getFileTypeLabel, getPreviewText } from '$lib/utils/file-preview';
|
import { getFileTypeLabel, getPreviewText } from '$lib/utils/file-preview';
|
||||||
import { formatFileSize } from '$lib/utils/formatters';
|
import { formatFileSize } from '$lib/utils/formatters';
|
||||||
import { isTextFile } from '$lib/utils/attachment-type';
|
import { isTextFile } from '$lib/utils/attachment-type';
|
||||||
import type { DatabaseMessageExtra } from '$lib/types/database';
|
import type { DatabaseMessageExtra, DatabaseMessageExtraPdfFile } from '$lib/types/database';
|
||||||
import { AttachmentType } from '$lib/enums';
|
import { AttachmentType } from '$lib/enums';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -35,14 +35,31 @@
|
||||||
|
|
||||||
let isText = $derived(isTextFile(attachment, uploadedFile));
|
let isText = $derived(isTextFile(attachment, uploadedFile));
|
||||||
|
|
||||||
// Get file type for display - check uploadedFile first, then attachment mimeType
|
let fileTypeLabel = $derived.by(() => {
|
||||||
let fileType = $derived.by(() => {
|
if (uploadedFile?.type) {
|
||||||
if (uploadedFile?.type) return uploadedFile.type;
|
return getFileTypeLabel(uploadedFile.type);
|
||||||
// For audio attachments stored in DB, get mimeType from the attachment
|
|
||||||
if (attachment?.type === AttachmentType.AUDIO && 'mimeType' in attachment) {
|
|
||||||
return attachment.mimeType;
|
|
||||||
}
|
}
|
||||||
return 'unknown';
|
|
||||||
|
if (attachment) {
|
||||||
|
if ('mimeType' in attachment && attachment.mimeType) {
|
||||||
|
return getFileTypeLabel(attachment.mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.type) {
|
||||||
|
return getFileTypeLabel(attachment.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFileTypeLabel(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
let pdfProcessingMode = $derived.by(() => {
|
||||||
|
if (attachment?.type === AttachmentType.PDF) {
|
||||||
|
const pdfAttachment = attachment as DatabaseMessageExtraPdfFile;
|
||||||
|
|
||||||
|
return pdfAttachment.processedAsImages ? 'Sent as Image' : 'Sent as Text';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -123,17 +140,19 @@
|
||||||
<div
|
<div
|
||||||
class="flex h-8 w-8 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary"
|
class="flex h-8 w-8 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary"
|
||||||
>
|
>
|
||||||
{getFileTypeLabel(fileType)}
|
{fileTypeLabel}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span
|
<span
|
||||||
class="max-w-24 truncate text-sm font-medium text-foreground group-hover:pr-6 md:max-w-32"
|
class="max-w-24 truncate text-sm font-medium text-foreground group-hover:pr-6 md:max-w-32"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#if size}
|
{#if pdfProcessingMode}
|
||||||
|
<span class="text-left text-xs text-muted-foreground">{pdfProcessingMode}</span>
|
||||||
|
{:else if size}
|
||||||
<span class="text-left text-xs text-muted-foreground">{formatFileSize(size)}</span>
|
<span class="text-left text-xs text-muted-foreground">{formatFileSize(size)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSendMessage(message: string, files?: ChatUploadedFile[]): Promise<boolean> {
|
async function handleSendMessage(message: string, files?: ChatUploadedFile[]): Promise<boolean> {
|
||||||
const result = files ? await parseFilesToMessageExtras(files) : undefined;
|
const result = files
|
||||||
|
? await parseFilesToMessageExtras(files, activeModelId ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (result?.emptyFiles && result.emptyFiles.length > 0) {
|
if (result?.emptyFiles && result.emptyFiles.length > 0) {
|
||||||
emptyFileNames = result.emptyFiles;
|
emptyFileNames = result.emptyFiles;
|
||||||
|
|
@ -292,7 +294,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportedFiles.length > 0) {
|
if (supportedFiles.length > 0) {
|
||||||
const processed = await processFilesToChatUploaded(supportedFiles);
|
const processed = await processFilesToChatUploaded(
|
||||||
|
supportedFiles,
|
||||||
|
activeModelId ?? undefined
|
||||||
|
);
|
||||||
uploadedFiles = [...uploadedFiles, ...processed];
|
uploadedFiles = [...uploadedFiles, ...processed];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
|
import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { ChatService } from '$lib/services/chat';
|
import { ChatService } from '$lib/services/chat';
|
||||||
import type { ApiModelListResponse } from '$lib/types/api';
|
import type { ApiModelListResponse } from '$lib/types/api';
|
||||||
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils/formatters';
|
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils/formatters';
|
||||||
|
|
@ -15,7 +16,14 @@
|
||||||
let { open = $bindable(), onOpenChange }: Props = $props();
|
let { open = $bindable(), onOpenChange }: Props = $props();
|
||||||
|
|
||||||
let serverProps = $derived(serverStore.props);
|
let serverProps = $derived(serverStore.props);
|
||||||
let modalities = $derived(serverStore.supportedModalities);
|
let modelName = $derived(modelsStore.singleModelName);
|
||||||
|
|
||||||
|
// Get modalities from modelStore using the model ID from the first model
|
||||||
|
let modalities = $derived.by(() => {
|
||||||
|
if (!modelsData?.data?.[0]?.id) return [];
|
||||||
|
|
||||||
|
return modelsStore.getModelModalitiesArray(modelsData.data[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
let modelsData = $state<ApiModelListResponse | null>(null);
|
let modelsData = $state<ApiModelListResponse | null>(null);
|
||||||
let isLoadingModels = $state(false);
|
let isLoadingModels = $state(false);
|
||||||
|
|
@ -77,12 +85,12 @@
|
||||||
class="resizable-text-container min-w-0 flex-1 truncate"
|
class="resizable-text-container min-w-0 flex-1 truncate"
|
||||||
style:--threshold="12rem"
|
style:--threshold="12rem"
|
||||||
>
|
>
|
||||||
{serverStore.modelName}
|
{modelName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<CopyToClipboardIcon
|
<CopyToClipboardIcon
|
||||||
text={serverStore.modelName || ''}
|
text={modelName || ''}
|
||||||
canCopy={!!serverStore.modelName}
|
canCopy={!!modelName}
|
||||||
ariaLabel="Copy model name to clipboard"
|
ariaLabel="Copy model name to clipboard"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Package } from '@lucide/svelte';
|
import { Package } from '@lucide/svelte';
|
||||||
import { BadgeInfo, CopyToClipboardIcon } from '$lib/components/app';
|
import { BadgeInfo, CopyToClipboardIcon } from '$lib/components/app';
|
||||||
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||||
|
|
@ -21,7 +22,7 @@
|
||||||
showTooltip = false
|
showTooltip = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let model = $derived(modelProp || serverStore.modelName);
|
let model = $derived(modelProp || modelsStore.singleModelName);
|
||||||
let isModelMode = $derived(serverStore.isModelMode);
|
let isModelMode = $derived(serverStore.isModelMode);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@
|
||||||
modelsLoading,
|
modelsLoading,
|
||||||
modelsUpdating,
|
modelsUpdating,
|
||||||
selectedModelId,
|
selectedModelId,
|
||||||
routerModels
|
routerModels,
|
||||||
|
propsCacheVersion,
|
||||||
|
singleModelName
|
||||||
} from '$lib/stores/models.svelte';
|
} from '$lib/stores/models.svelte';
|
||||||
import { usedModalities, conversationsStore } from '$lib/stores/conversations.svelte';
|
import { usedModalities, conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { ServerModelStatus } from '$lib/enums';
|
import { ServerModelStatus } from '$lib/enums';
|
||||||
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||||
import { DialogModelInformation } from '$lib/components/app';
|
import { DialogModelInformation } from '$lib/components/app';
|
||||||
import type { ModelOption } from '$lib/types/models';
|
import type { ModelOption } from '$lib/types/models';
|
||||||
|
|
||||||
|
|
@ -50,7 +52,7 @@
|
||||||
let updating = $derived(modelsUpdating());
|
let updating = $derived(modelsUpdating());
|
||||||
let activeId = $derived(selectedModelId());
|
let activeId = $derived(selectedModelId());
|
||||||
let isRouter = $derived(isRouterMode());
|
let isRouter = $derived(isRouterMode());
|
||||||
let serverModel = $derived(serverStore.modelName);
|
let serverModel = $derived(singleModelName());
|
||||||
|
|
||||||
// Reactive router models state - needed for proper reactivity of status checks
|
// Reactive router models state - needed for proper reactivity of status checks
|
||||||
let currentRouterModels = $derived(routerModels());
|
let currentRouterModels = $derived(routerModels());
|
||||||
|
|
@ -69,9 +71,19 @@
|
||||||
* Returns true if the model can be selected, false if it should be disabled.
|
* Returns true if the model can be selected, false if it should be disabled.
|
||||||
*/
|
*/
|
||||||
function isModelCompatible(option: ModelOption): boolean {
|
function isModelCompatible(option: ModelOption): boolean {
|
||||||
const modelModalities = option.modalities;
|
void propsCacheVersion();
|
||||||
|
|
||||||
if (!modelModalities) return true;
|
const modelModalities = modelsStore.getModelModalities(option.model);
|
||||||
|
|
||||||
|
if (!modelModalities) {
|
||||||
|
const status = getModelStatus(option.model);
|
||||||
|
|
||||||
|
if (status === ServerModelStatus.LOADED) {
|
||||||
|
if (requiredModalities.vision || requiredModalities.audio) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (requiredModalities.vision && !modelModalities.vision) return false;
|
if (requiredModalities.vision && !modelModalities.vision) return false;
|
||||||
if (requiredModalities.audio && !modelModalities.audio) return false;
|
if (requiredModalities.audio && !modelModalities.audio) return false;
|
||||||
|
|
@ -84,8 +96,24 @@
|
||||||
* Returns object with vision/audio booleans indicating what's missing.
|
* Returns object with vision/audio booleans indicating what's missing.
|
||||||
*/
|
*/
|
||||||
function getMissingModalities(option: ModelOption): { vision: boolean; audio: boolean } | null {
|
function getMissingModalities(option: ModelOption): { vision: boolean; audio: boolean } | null {
|
||||||
const modelModalities = option.modalities;
|
void propsCacheVersion();
|
||||||
if (!modelModalities) return null;
|
|
||||||
|
const modelModalities = modelsStore.getModelModalities(option.model);
|
||||||
|
|
||||||
|
if (!modelModalities) {
|
||||||
|
const status = getModelStatus(option.model);
|
||||||
|
|
||||||
|
if (status === ServerModelStatus.LOADED) {
|
||||||
|
const missing = {
|
||||||
|
vision: requiredModalities.vision,
|
||||||
|
audio: requiredModalities.audio
|
||||||
|
};
|
||||||
|
|
||||||
|
if (missing.vision || missing.audio) return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const missing = {
|
const missing = {
|
||||||
vision: requiredModalities.vision && !modelModalities.vision,
|
vision: requiredModalities.vision && !modelModalities.vision,
|
||||||
|
|
@ -93,6 +121,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!missing.vision && !missing.audio) return null;
|
if (!missing.vision && !missing.audio) return null;
|
||||||
|
|
||||||
return missing;
|
return missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,9 +189,10 @@
|
||||||
await tick();
|
await tick();
|
||||||
updateMenuPosition();
|
updateMenuPosition();
|
||||||
requestAnimationFrame(() => updateMenuPosition());
|
requestAnimationFrame(() => updateMenuPosition());
|
||||||
|
|
||||||
|
modelsStore.fetchModalitiesForLoadedModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export open function for programmatic access
|
|
||||||
export function open() {
|
export function open() {
|
||||||
if (isRouter) {
|
if (isRouter) {
|
||||||
openMenu();
|
openMenu();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
import { AlertTriangle, Server } from '@lucide/svelte';
|
import { AlertTriangle, Server } from '@lucide/svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { serverProps, serverLoading, serverError, modelName } from '$lib/stores/server.svelte';
|
import { serverProps, serverLoading, serverError } from '$lib/stores/server.svelte';
|
||||||
|
import { singleModelName } from '$lib/stores/models.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
@ -13,7 +14,7 @@
|
||||||
|
|
||||||
let error = $derived(serverError());
|
let error = $derived(serverError());
|
||||||
let loading = $derived(serverLoading());
|
let loading = $derived(serverLoading());
|
||||||
let model = $derived(modelName());
|
let model = $derived(singleModelName());
|
||||||
let serverData = $derived(serverProps());
|
let serverData = $derived(serverProps());
|
||||||
|
|
||||||
function getStatusColor() {
|
function getStatusColor() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { ModelsService } from '$lib/services/models';
|
import { ModelsService } from '$lib/services/models';
|
||||||
import { PropsService } from '$lib/services/props';
|
import { PropsService } from '$lib/services/props';
|
||||||
import { ServerModelStatus } from '$lib/enums';
|
import { ServerModelStatus, ModelModality } from '$lib/enums';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import type { ModelOption, ModelModalities } from '$lib/types/models';
|
import type { ModelOption, ModelModalities } from '$lib/types/models';
|
||||||
import type { ApiModelDataEntry } from '$lib/types/api';
|
import type { ApiModelDataEntry } from '$lib/types/api';
|
||||||
|
|
@ -56,6 +56,11 @@ class ModelsStore {
|
||||||
private modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
private modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
||||||
private modelPropsFetching = $state<Set<string>>(new Set());
|
private modelPropsFetching = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version counter for props cache - used to trigger reactivity when props are updated
|
||||||
|
*/
|
||||||
|
propsCacheVersion = $state(0);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Computed Getters
|
// Computed Getters
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -77,6 +82,21 @@ class ModelsStore {
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model name in MODEL mode (single model).
|
||||||
|
* Extracts from model_path or model_alias from server props.
|
||||||
|
* In ROUTER mode, returns null (model is per-conversation).
|
||||||
|
*/
|
||||||
|
get singleModelName(): string | null {
|
||||||
|
if (serverStore.isRouterMode) return null;
|
||||||
|
|
||||||
|
const props = serverStore.props;
|
||||||
|
if (props?.model_alias) return props.model_alias;
|
||||||
|
if (!props?.model_path) return null;
|
||||||
|
|
||||||
|
return props.model_path.split(/(\\|\/)/).pop() || null;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Modalities
|
// Modalities
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -118,6 +138,21 @@ class ModelsStore {
|
||||||
return this.getModelModalities(modelId)?.audio ?? false;
|
return this.getModelModalities(modelId)?.audio ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model modalities as an array of ModelModality enum values
|
||||||
|
*/
|
||||||
|
getModelModalitiesArray(modelId: string): ModelModality[] {
|
||||||
|
const modalities = this.getModelModalities(modelId);
|
||||||
|
if (!modalities) return [];
|
||||||
|
|
||||||
|
const result: ModelModality[] = [];
|
||||||
|
|
||||||
|
if (modalities.vision) result.push(ModelModality.VISION);
|
||||||
|
if (modalities.audio) result.push(ModelModality.AUDIO);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get props for a specific model (from cache)
|
* Get props for a specific model (from cache)
|
||||||
*/
|
*/
|
||||||
|
|
@ -300,6 +335,9 @@ class ModelsStore {
|
||||||
|
|
||||||
return { ...model, modalities };
|
return { ...model, modalities };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Increment version to trigger reactivity
|
||||||
|
this.propsCacheVersion++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch modalities for loaded models:', error);
|
console.warn('Failed to fetch modalities for loaded models:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -322,6 +360,9 @@ class ModelsStore {
|
||||||
this.models = this.models.map((model) =>
|
this.models = this.models.map((model) =>
|
||||||
model.model === modelId ? { ...model, modalities } : model
|
model.model === modelId ? { ...model, modalities } : model
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Increment version to trigger reactivity
|
||||||
|
this.propsCacheVersion++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to update modalities for model ${modelId}:`, error);
|
console.warn(`Failed to update modalities for model ${modelId}:`, error);
|
||||||
}
|
}
|
||||||
|
|
@ -583,3 +624,5 @@ export const selectedModelName = () => modelsStore.selectedModelName;
|
||||||
export const selectedModelOption = () => modelsStore.selectedModel;
|
export const selectedModelOption = () => modelsStore.selectedModel;
|
||||||
export const loadedModelIds = () => modelsStore.loadedModelIds;
|
export const loadedModelIds = () => modelsStore.loadedModelIds;
|
||||||
export const loadingModelIds = () => modelsStore.loadingModelIds;
|
export const loadingModelIds = () => modelsStore.loadingModelIds;
|
||||||
|
export const propsCacheVersion = () => modelsStore.propsCacheVersion;
|
||||||
|
export const singleModelName = () => modelsStore.singleModelName;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { PropsService } from '$lib/services/props';
|
import { PropsService } from '$lib/services/props';
|
||||||
import { ServerRole, ModelModality } from '$lib/enums';
|
import { ServerRole } from '$lib/enums';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* serverStore - Server connection state, configuration, and role detection
|
* serverStore - Server connection state, configuration, and role detection
|
||||||
|
|
@ -16,12 +16,6 @@ import { ServerRole, ModelModality } from '$lib/enums';
|
||||||
* - **Server State**: Connection status, loading, error handling
|
* - **Server State**: Connection status, loading, error handling
|
||||||
* - **Role Detection**: MODEL (single model) vs ROUTER (multi-model)
|
* - **Role Detection**: MODEL (single model) vs ROUTER (multi-model)
|
||||||
* - **Default Params**: Server-wide generation defaults
|
* - **Default Params**: Server-wide generation defaults
|
||||||
*
|
|
||||||
* **Note on Modalities:**
|
|
||||||
* Model-specific modalities (vision, audio) are now managed by modelsStore.
|
|
||||||
* Use `modelsStore.getModelModalities(modelId)` for per-model modality info.
|
|
||||||
* The `supportsVision`/`supportsAudio` getters here are deprecated and only
|
|
||||||
* apply to MODEL mode (single model).
|
|
||||||
*/
|
*/
|
||||||
class ServerStore {
|
class ServerStore {
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -38,45 +32,6 @@ class ServerStore {
|
||||||
// Getters
|
// Getters
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Get model name from server props.
|
|
||||||
* In MODEL mode: extracts from model_path or model_alias
|
|
||||||
* In ROUTER mode: returns null (model is per-conversation)
|
|
||||||
*/
|
|
||||||
get modelName(): string | null {
|
|
||||||
if (this.role === ServerRole.ROUTER) return null;
|
|
||||||
if (this.props?.model_alias) return this.props.model_alias;
|
|
||||||
if (!this.props?.model_path) return null;
|
|
||||||
return this.props.model_path.split(/(\\|\/)/).pop() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use modelsStore.getModelModalities(modelId) for per-model modalities.
|
|
||||||
* This only works in MODEL mode (single model).
|
|
||||||
*/
|
|
||||||
get supportedModalities(): ModelModality[] {
|
|
||||||
const modalities: ModelModality[] = [];
|
|
||||||
if (this.props?.modalities?.audio) modalities.push(ModelModality.AUDIO);
|
|
||||||
if (this.props?.modalities?.vision) modalities.push(ModelModality.VISION);
|
|
||||||
return modalities;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use modelsStore.modelSupportsVision(modelId) for per-model check.
|
|
||||||
* This only works in MODEL mode (single model).
|
|
||||||
*/
|
|
||||||
get supportsVision(): boolean {
|
|
||||||
return this.props?.modalities?.vision ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use modelsStore.modelSupportsAudio(modelId) for per-model check.
|
|
||||||
* This only works in MODEL mode (single model).
|
|
||||||
*/
|
|
||||||
get supportsAudio(): boolean {
|
|
||||||
return this.props?.modalities?.audio ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null {
|
get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null {
|
||||||
return this.props?.default_generation_settings?.params || null;
|
return this.props?.default_generation_settings?.params || null;
|
||||||
}
|
}
|
||||||
|
|
@ -179,10 +134,6 @@ export const serverProps = () => serverStore.props;
|
||||||
export const serverLoading = () => serverStore.loading;
|
export const serverLoading = () => serverStore.loading;
|
||||||
export const serverError = () => serverStore.error;
|
export const serverError = () => serverStore.error;
|
||||||
export const serverRole = () => serverStore.role;
|
export const serverRole = () => serverStore.role;
|
||||||
export const modelName = () => serverStore.modelName;
|
|
||||||
export const supportedModalities = () => serverStore.supportedModalities;
|
|
||||||
export const supportsVision = () => serverStore.supportsVision;
|
|
||||||
export const supportsAudio = () => serverStore.supportsAudio;
|
|
||||||
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
|
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
|
||||||
export const defaultParams = () => serverStore.defaultParams;
|
export const defaultParams = () => serverStore.defaultParams;
|
||||||
export const contextSize = () => serverStore.contextSize;
|
export const contextSize = () => serverStore.contextSize;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
||||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||||
import { FileTypeCategory, AttachmentType } from '$lib/enums';
|
import { FileTypeCategory, AttachmentType } from '$lib/enums';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { supportsVision } from '$lib/stores/server.svelte';
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
import { readFileAsText, isLikelyTextFile } from './text-files';
|
import { readFileAsText, isLikelyTextFile } from './text-files';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
@ -31,7 +31,8 @@ export interface FileProcessingResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseFilesToMessageExtras(
|
export async function parseFilesToMessageExtras(
|
||||||
files: ChatUploadedFile[]
|
files: ChatUploadedFile[],
|
||||||
|
activeModelId?: string
|
||||||
): Promise<FileProcessingResult> {
|
): Promise<FileProcessingResult> {
|
||||||
const extras: DatabaseMessageExtra[] = [];
|
const extras: DatabaseMessageExtra[] = [];
|
||||||
const emptyFiles: string[] = [];
|
const emptyFiles: string[] = [];
|
||||||
|
|
@ -80,7 +81,10 @@ export async function parseFilesToMessageExtras(
|
||||||
// Always get base64 data for preview functionality
|
// Always get base64 data for preview functionality
|
||||||
const base64Data = await readFileAsBase64(file.file);
|
const base64Data = await readFileAsBase64(file.file);
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
const hasVisionSupport = supportsVision();
|
// Use per-model vision check for router mode
|
||||||
|
const hasVisionSupport = activeModelId
|
||||||
|
? modelsStore.modelSupportsVision(activeModelId)
|
||||||
|
: false;
|
||||||
|
|
||||||
// Force PDF-to-text for non-vision models
|
// Force PDF-to-text for non-vision models
|
||||||
let shouldProcessAsImages = Boolean(currentConfig.pdfAsImage) && hasVisionSupport;
|
let shouldProcessAsImages = Boolean(currentConfig.pdfAsImage) && hasVisionSupport;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,38 @@
|
||||||
/**
|
/**
|
||||||
* Gets a display label for a file type
|
* Gets a display label for a file type from various input formats
|
||||||
* @param fileType - The file type/mime type
|
*
|
||||||
* @returns Formatted file type label
|
* Handles:
|
||||||
|
* - MIME types: 'application/pdf' → 'PDF'
|
||||||
|
* - AttachmentType values: 'PDF', 'AUDIO' → 'PDF', 'AUDIO'
|
||||||
|
* - File names: 'document.pdf' → 'PDF'
|
||||||
|
* - Unknown: returns 'FILE'
|
||||||
|
*
|
||||||
|
* @param input - MIME type, AttachmentType value, or file name
|
||||||
|
* @returns Formatted file type label (uppercase)
|
||||||
*/
|
*/
|
||||||
export function getFileTypeLabel(fileType: string): string {
|
export function getFileTypeLabel(input: string | undefined): string {
|
||||||
return fileType.split('/').pop()?.toUpperCase() || 'FILE';
|
if (!input) return 'FILE';
|
||||||
|
|
||||||
|
// Handle MIME types (contains '/')
|
||||||
|
if (input.includes('/')) {
|
||||||
|
const subtype = input.split('/').pop();
|
||||||
|
if (subtype) {
|
||||||
|
// Handle special cases like 'vnd.ms-excel' → 'EXCEL'
|
||||||
|
if (subtype.includes('.')) {
|
||||||
|
return subtype.split('.').pop()?.toUpperCase() || 'FILE';
|
||||||
|
}
|
||||||
|
return subtype.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file names (contains '.')
|
||||||
|
if (input.includes('.')) {
|
||||||
|
const ext = input.split('.').pop();
|
||||||
|
if (ext) return ext.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle AttachmentType or other plain strings
|
||||||
|
return input.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { isTextFileByName } from './text-files';
|
||||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory } from '$lib/enums';
|
||||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
import { supportsVision } from '$lib/stores/server.svelte';
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
|
@ -47,7 +47,10 @@ function readFileAsUTF8(file: File): Promise<string> {
|
||||||
* @param files - Array of File objects to process
|
* @param files - Array of File objects to process
|
||||||
* @returns Promise resolving to array of ChatUploadedFile objects
|
* @returns Promise resolving to array of ChatUploadedFile objects
|
||||||
*/
|
*/
|
||||||
export async function processFilesToChatUploaded(files: File[]): Promise<ChatUploadedFile[]> {
|
export async function processFilesToChatUploaded(
|
||||||
|
files: File[],
|
||||||
|
activeModelId?: string
|
||||||
|
): Promise<ChatUploadedFile[]> {
|
||||||
const results: ChatUploadedFile[] = [];
|
const results: ChatUploadedFile[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
|
@ -96,7 +99,9 @@ export async function processFilesToChatUploaded(files: File[]): Promise<ChatUpl
|
||||||
results.push(base);
|
results.push(base);
|
||||||
|
|
||||||
// Show suggestion toast if vision model is available but PDF as image is disabled
|
// Show suggestion toast if vision model is available but PDF as image is disabled
|
||||||
const hasVisionSupport = supportsVision();
|
const hasVisionSupport = activeModelId
|
||||||
|
? modelsStore.modelSupportsVision(activeModelId)
|
||||||
|
: false;
|
||||||
const currentConfig = settingsStore.config;
|
const currentConfig = settingsStore.config;
|
||||||
if (hasVisionSupport && !currentConfig.pdfAsImage) {
|
if (hasVisionSupport && !currentConfig.pdfAsImage) {
|
||||||
toast.info(`You can enable parsing PDF as images with vision models.`, {
|
toast.info(`You can enable parsing PDF as images with vision models.`, {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue