DatabaseMessage,
DatabaseMessageExtra,
DatabaseMessageExtraAudioFile,
+ DatabaseMessageExtraVideoFile,
DatabaseMessageExtraImageFile,
DatabaseMessageExtraTextFile,
DatabaseMessageExtraPdfFile,
DatabaseMessage,
DatabaseMessageExtra,
DatabaseMessageExtraAudioFile,
+ DatabaseMessageExtraVideoFile,
DatabaseMessageExtraImageFile,
DatabaseMessageExtraTextFile,
DatabaseMessageExtraPdfFile,
<script lang="ts">
- import { Eye, Mic } from '@lucide/svelte';
+ import { Eye, Mic, Video } from '@lucide/svelte';
import { ModelModality } from '$lib/enums';
interface Props {
</script>
{#each modalities as modality (modality)}
- {#if modality === ModelModality.VISION || modality === ModelModality.AUDIO}
+ {#if modality === ModelModality.VISION || modality === ModelModality.AUDIO || modality === ModelModality.VIDEO}
<span
class={[
'inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs font-medium',
{#if modality === ModelModality.VISION}
<Eye class="h-3 w-3" />
- Vision
+ Vision (Image)
+ {:else if modality === ModelModality.VIDEO}
+ <Video class="h-3 w-3" />
+
+ Vision (Video)
{:else}
<Mic class="h-3 w-3" />
<script lang="ts">
- import { X } from '@lucide/svelte';
+ import { X, Music, Video } from '@lucide/svelte';
import {
formatFileSize,
getFileTypeLabel,
getPreviewText,
isPdfFile,
+ isAudioFile,
+ isVideoFile,
isTextFile
} from '$lib/utils';
import { ActionIcon } from '$lib/components/app';
}: Props = $props();
let isPdf = $derived(isPdfFile(attachment, uploadedFile));
+ let isAudio = $derived(isAudioFile(attachment, uploadedFile));
+ let isVideo = $derived(isVideoFile(attachment, uploadedFile));
let isPdfWithContent = $derived(isPdf && !!textContent);
let isText = $derived(isTextFile(attachment, uploadedFile));
<div
class="flex h-8 w-8 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary"
>
- {fileTypeLabel}
+ {#if isAudio}
+ <Music class="h-4 w-4 text-white/70" />
+ {:else if isVideo}
+ <Video class="h-4 w-4 text-white/70" />
+ {:else}
+ {fileTypeLabel}
+ {/if}
</div>
{/snippet}
getAttachmentDisplayItems,
getLanguageFromFilename,
isAudioFile,
+ isVideoFile,
isImageFile,
isMcpPrompt,
isMcpResource,
textContent?: string;
isImage: boolean;
isAudio: boolean;
+ isVideo: boolean;
}
interface Props {
(item): PreviewItem => ({
...item,
isImage: isImageFile(item.attachment, item.uploadedFile),
- isAudio: isAudioFile(item.attachment, item.uploadedFile)
+ isAudio: isAudioFile(item.attachment, item.uploadedFile),
+ isVideo: isVideoFile(item.attachment, item.uploadedFile)
})
)
);
let isAudio = $derived(
currentItem ? isAudioFile(currentItem.attachment, currentItem.uploadedFile) : false
);
+ let isVideo = $derived(
+ currentItem ? isVideoFile(currentItem.attachment, currentItem.uploadedFile) : false
+ );
let isImage = $derived(
currentItem ? isImageFile(currentItem.attachment, currentItem.uploadedFile) : false
);
: null
);
+ let videoSrc = $derived(
+ isVideo && currentItem
+ ? (currentItem.uploadedFile?.preview ??
+ (currentItem.attachment &&
+ 'mimeType' in currentItem.attachment &&
+ 'base64Data' in currentItem.attachment
+ ? createBase64DataUrl(
+ currentItem.attachment.mimeType,
+ currentItem.attachment.base64Data
+ )
+ : null))
+ : null
+ );
+
export function prev() {
currentIndex = currentIndex > 0 ? currentIndex - 1 : allItems.length - 1;
}
{currentItem}
{isImage}
{isAudio}
+ {isVideo}
{isPdf}
{isText}
{displayPreview}
{displayTextContent}
{audioSrc}
+ {videoSrc}
{language}
{hasVisionModality}
{activeModelId}
<script lang="ts">
import type { ChatAttachmentDisplayItem } from '$lib/types';
- import { Image, Music, FileText, FileIcon } from '@lucide/svelte';
+ import { Image, Music, Video, FileText, FileIcon } from '@lucide/svelte';
import ChatAttachmentsPreviewCurrentItemPdf from './ChatAttachmentsPreviewCurrentItemPdf.svelte';
import ChatAttachmentsPreviewCurrentItemImage from './ChatAttachmentsPreviewCurrentItemImage.svelte';
import ChatAttachmentsPreviewCurrentItemAudio from './ChatAttachmentsPreviewCurrentItemAudio.svelte';
+ import ChatAttachmentsPreviewCurrentItemVideo from './ChatAttachmentsPreviewCurrentItemVideo.svelte';
import ChatAttachmentsPreviewCurrentItemText from './ChatAttachmentsPreviewCurrentItemText.svelte';
import ChatAttachmentsPreviewCurrentItemUnavailable from './ChatAttachmentsPreviewCurrentItemUnavailable.svelte';
currentItem: ChatAttachmentDisplayItem | null;
isImage: boolean;
isAudio: boolean;
+ isVideo: boolean;
isPdf: boolean;
isText: boolean;
displayPreview: string | undefined;
displayTextContent: string | undefined;
audioSrc: string | null;
+ videoSrc: string | null;
language: string;
hasVisionModality: boolean;
activeModelId?: string;
currentItem,
isImage,
isAudio,
+ isVideo,
isPdf,
isText,
displayPreview,
displayTextContent,
audioSrc,
+ videoSrc,
language,
hasVisionModality,
activeModelId
}: Props = $props();
let IconComponent = $derived(
- isImage ? Image : isText || isPdf ? FileText : isAudio ? Music : FileIcon
+ isImage ? Image : isText || isPdf ? FileText : isAudio ? Music : isVideo ? Video : FileIcon
);
- let isUnavailable = $derived(!isPdf && !isImage && !(isText && displayTextContent) && !isAudio);
+ let isUnavailable = $derived(
+ !isPdf && !isImage && !(isText && displayTextContent) && !isAudio && !isVideo
+ );
</script>
{#if currentItem}
<ChatAttachmentsPreviewCurrentItemText {displayTextContent} {language} />
{:else if isAudio}
<ChatAttachmentsPreviewCurrentItemAudio {currentItem} {audioSrc} />
+ {:else if isVideo}
+ <ChatAttachmentsPreviewCurrentItemVideo {currentItem} {videoSrc} />
{:else if isUnavailable}
<ChatAttachmentsPreviewCurrentItemUnavailable {IconComponent} />
{/if}
--- /dev/null
+<script lang="ts">
+ import { Video } from '@lucide/svelte';
+
+ interface Props {
+ currentItem: { name?: string } | null;
+ videoSrc: string | null;
+ }
+
+ let { currentItem, videoSrc }: Props = $props();
+</script>
+
+<div class="flex flex-1 items-center justify-center p-8">
+ <div class="w-full max-w-md text-center">
+ <Video class="mx-auto mb-4 h-16 w-16 text-white/50" />
+
+ {#if videoSrc}
+ <video controls class="mb-4 w-full" src={videoSrc}>
+ Your browser does not support the video element.
+ </video>
+ {:else}
+ <p class="mb-4 text-white/70">Video preview not available</p>
+ {/if}
+
+ <p class="text-sm text-white/50">{currentItem?.name || 'Video'}</p>
+ </div>
+</div>
<script lang="ts">
- import { Music, FileText } from '@lucide/svelte';
+ import { Music, Video, FileText } from '@lucide/svelte';
import { HorizontalScrollCarousel } from '$lib/components/app/misc';
interface PreviewItem {
name: string;
isImage: boolean;
isAudio: boolean;
+ isVideo: boolean;
preview?: string;
}
>
{#if item.isAudio}
<Music class="h-4 w-4 text-white/70" />
+ {:else if item.isVideo}
+ <Video class="h-4 w-4 text-white/70" />
{:else}
<FileText class="h-4 w-4 text-white/70" />
{/if}
class?: string;
disabled?: boolean;
hasAudioModality?: boolean;
+ hasVideoModality?: boolean;
hasVisionModality?: boolean;
hasMcpPromptsSupport?: boolean;
hasMcpResourcesSupport?: boolean;
class: className = '',
disabled = false,
hasAudioModality = false,
+ hasVideoModality = false,
hasVisionModality = false,
hasMcpPromptsSupport = false,
hasMcpResourcesSupport = false,
() => ({
hasVisionModality,
hasAudioModality,
+ hasVideoModality,
hasMcpPromptsSupport,
hasMcpResourcesSupport
}),
class?: string;
disabled?: boolean;
hasAudioModality?: boolean;
+ hasVideoModality?: boolean;
hasVisionModality?: boolean;
hasMcpPromptsSupport?: boolean;
hasMcpResourcesSupport?: boolean;
disabled = false,
hasAudioModality = false,
hasVisionModality = false,
+ hasVideoModality = false,
hasMcpPromptsSupport = false,
hasMcpResourcesSupport = false,
onFileUpload,
() => ({
hasVisionModality,
hasAudioModality,
+ hasVideoModality,
hasMcpPromptsSupport,
hasMcpResourcesSupport
}),
interface Props {
disabled?: boolean;
hasAudioModality?: boolean;
+ hasVideoModality?: boolean;
hasMcpPromptsSupport?: boolean;
hasMcpResourcesSupport?: boolean;
hasVisionModality?: boolean;
let {
disabled = false,
hasAudioModality = false,
+ hasVideoModality = false,
hasMcpPromptsSupport = false,
hasMcpResourcesSupport = false,
hasVisionModality = false,
<ChatFormActionAddSheet
{disabled}
{hasAudioModality}
+ {hasVideoModality}
{hasVisionModality}
{hasMcpPromptsSupport}
{hasMcpResourcesSupport}
<ChatFormActionAddDropdown
{disabled}
{hasAudioModality}
+ {hasVideoModality}
{hasVisionModality}
{hasMcpPromptsSupport}
{hasMcpResourcesSupport}
disabled?: boolean;
forceForegroundText?: boolean;
hasAudioModality?: boolean;
+ hasVideoModality?: boolean;
hasVisionModality?: boolean;
hasModelSelected?: boolean;
isSelectedModelInCache?: boolean;
disabled = false,
forceForegroundText = false,
hasAudioModality = $bindable(false),
+ hasVideoModality = $bindable(false),
hasVisionModality = $bindable(false),
hasModelSelected = $bindable(false),
isSelectedModelInCache = $bindable(true),
hasAudioModality = activeModelId ? modelsStore.modelSupportsAudio(activeModelId) : false;
});
+ $effect(() => {
+ hasVideoModality = activeModelId ? modelsStore.modelSupportsVideo(activeModelId) : false;
+ });
+
$effect(() => {
void modelPropsVersion;
});
let hasAudioModality = $state(false);
+ let hasVideoModality = $state(false);
let hasVisionModality = $state(false);
let hasModelSelected = $state(false);
let isSelectedModelInCache = $state(true);
<ChatFormActionsAdd
{disabled}
{hasAudioModality}
+ {hasVideoModality}
{hasVisionModality}
{hasMcpPromptsSupport}
{hasMcpResourcesSupport}
{disabled}
bind:this={selectorModelRef}
bind:hasAudioModality
+ bind:hasVideoModality
bind:hasVisionModality
bind:hasModelSelected
bind:isSelectedModelInCache
return false;
});
+ let hasVideoModality = $derived.by(() => {
+ if (activeModelId) {
+ void modelPropsVersion;
+
+ return modelsStore.modelSupportsVideo(activeModelId);
+ }
+
+ return false;
+ });
+
let hasVisionModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion;
}
// Use model-specific capabilities for file validation
- const capabilities = { hasVision: hasVisionModality, hasAudio: hasAudioModality };
+ const capabilities = {
+ hasVision: hasVisionModality,
+ hasAudio: hasAudioModality,
+ hasVideo: hasVideoModality
+ };
const { supportedFiles, unsupportedFiles, modalityReasons } = filterFilesByModalities(
generallySupported,
capabilities
if (hasVisionModality) supportedTypes.push('images');
if (hasAudioModality) supportedTypes.push('audio files');
+ if (hasVideoModality) supportedTypes.push('video files');
fileErrorData = {
generallyUnsupported,
disabledTooltip: 'Audio files processing requires an audio model',
action: AttachmentAction.FILE_UPLOAD
},
+ {
+ id: AttachmentMenuItemId.VIDEO,
+ label: 'Video Files',
+ icon: FILE_TYPE_ICONS.video,
+ class: 'video-button',
+ enabledWhen: AttachmentItemEnabledWhen.HAS_VIDEO_MODALITY,
+ disabledTooltip: 'Video files processing requires a video model',
+ action: AttachmentAction.FILE_UPLOAD
+ },
{
id: AttachmentMenuItemId.TEXT,
label: 'Text Files',
FileText as FileTextIcon,
Image as ImageIcon,
Eye as VisionIcon,
- Mic as AudioIcon
+ Mic as AudioIcon,
+ Video as VideoIcon
} from '@lucide/svelte';
import { FileTypeCategory, ModelModality } from '$lib/enums';
export const FILE_TYPE_ICONS = {
[FileTypeCategory.IMAGE]: ImageIcon,
[FileTypeCategory.AUDIO]: AudioIcon,
+ [FileTypeCategory.VIDEO]: VideoIcon,
[FileTypeCategory.TEXT]: FileTextIcon,
[FileTypeCategory.PDF]: FileIcon
} as const;
export const MODALITY_ICONS = {
[ModelModality.VISION]: VisionIcon,
- [ModelModality.AUDIO]: AudioIcon
+ [ModelModality.AUDIO]: AudioIcon,
+ [ModelModality.VIDEO]: VideoIcon
} as const;
export const MODALITY_LABELS = {
[ModelModality.VISION]: 'Vision',
- [ModelModality.AUDIO]: 'Audio'
+ [ModelModality.AUDIO]: 'Audio',
+ [ModelModality.VIDEO]: 'Video'
} as const;
FileTypePdf,
FileTypeText,
MimeTypeAudio,
+ MimeTypeVideo,
MimeTypeImage,
MimeTypeApplication,
MimeTypeText
} from '$lib/enums';
+import { FileExtensionVideo, FileTypeVideo } from '$lib/enums/files';
// File type configuration using enums
export const AUDIO_FILE_TYPES = {
}
} as const;
+export const VIDEO_FILE_TYPES = {
+ [FileTypeVideo.MP4]: {
+ extensions: [FileExtensionVideo.MP4],
+ mimeTypes: [MimeTypeVideo.MP4]
+ },
+ [FileTypeVideo.OGG]: {
+ extensions: [FileExtensionVideo.OGG],
+ mimeTypes: [MimeTypeVideo.OGG]
+ }
+} as const;
+
export const IMAGE_FILE_TYPES = {
[FileTypeImage.JPEG]: {
extensions: [FileExtensionImage.JPG, FileExtensionImage.JPEG],
export enum AttachmentType {
AUDIO = 'AUDIO',
IMAGE = 'IMAGE',
+ VIDEO = 'VIDEO',
MCP_PROMPT = 'MCP_PROMPT',
MCP_RESOURCE = 'MCP_RESOURCE',
PDF = 'PDF',
export enum AttachmentMenuItemId {
IMAGES = 'images',
AUDIO = 'audio',
+ VIDEO = 'video',
TEXT = 'text',
PDF = 'pdf',
SYSTEM_MESSAGE = 'system-message',
export enum AttachmentItemEnabledWhen {
ALWAYS = 'always',
HAS_VISION_MODALITY = 'hasVisionModality',
- HAS_AUDIO_MODALITY = 'hasAudioModality'
+ HAS_AUDIO_MODALITY = 'hasAudioModality',
+ HAS_VIDEO_MODALITY = 'hasVideoModality'
}
/**
export enum ContentPartType {
TEXT = 'text',
IMAGE_URL = 'image_url',
- INPUT_AUDIO = 'input_audio'
+ INPUT_AUDIO = 'input_audio',
+ INPUT_VIDEO = 'input_video'
}
/**
export enum FileTypeCategory {
IMAGE = 'image',
AUDIO = 'audio',
+ VIDEO = 'video',
PDF = 'pdf',
TEXT = 'text'
}
WEBM = 'webm'
}
+export enum FileTypeVideo {
+ MP4 = 'mp4',
+ OGG = 'ogg'
+}
+
export enum FileTypePdf {
PDF = 'pdf'
}
WAV = '.wav'
}
+export enum FileExtensionVideo {
+ MP4 = '.mp4',
+ OGG = '.ogg'
+}
+
export enum FileExtensionPdf {
PDF = '.pdf'
}
WEBM_OPUS = 'audio/webm;codecs=opus'
}
+export enum MimeTypeVideo {
+ MP4 = 'video/mp4',
+ OGG = 'video/ogg'
+}
+
export enum MimeTypeImage {
JPEG = 'image/jpeg',
JPG = 'image/jpg',
UriPattern,
MimeTypeApplication,
MimeTypeAudio,
+ MimeTypeVideo,
MimeTypeImage,
MimeTypeText,
SpecialFileType
export enum ModelModality {
TEXT = 'TEXT',
AUDIO = 'AUDIO',
- VISION = 'VISION'
+ VISION = 'VISION',
+ VIDEO = 'VIDEO'
}
export interface AttachmentModalityFlags {
hasVisionModality: boolean;
hasAudioModality: boolean;
+ hasVideoModality: boolean;
hasMcpPromptsSupport: boolean;
hasMcpResourcesSupport: boolean;
}
});
}
+ const videoFiles = message.extra.filter(
+ (extra: DatabaseMessageExtra): extra is DatabaseMessageExtraVideoFile =>
+ extra.type === AttachmentType.VIDEO
+ );
+
+ for (const video of videoFiles) {
+ contentParts.push({
+ type: ContentPartType.INPUT_VIDEO,
+ input_video: {
+ data: video.base64Data,
+ format: video.mimeType.includes('mp4')
+ ? 'mp4'
+ : video.mimeType.includes('ogg')
+ ? 'ogg'
+ : 'auto'
+ }
+ });
+ }
+
const pdfFiles = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraPdfFile =>
extra.type === AttachmentType.PDF
if (props?.modalities) {
return {
vision: props.modalities.vision ?? false,
- audio: props.modalities.audio ?? false
+ audio: props.modalities.audio ?? false,
+ video: props.modalities.video ?? false
};
}
return this.getModelModalities(modelId)?.audio ?? false;
}
+ /**
+ * Check if a model supports video modality
+ */
+ modelSupportsVideo(modelId: string): boolean {
+ return this.getModelModalities(modelId)?.video ?? false;
+ }
+
/**
* Get model modalities as an array of ModelModality enum values
*/
if (modalities.vision) result.push(ModelModality.VISION);
if (modalities.audio) result.push(ModelModality.AUDIO);
+ if (modalities.video) result.push(ModelModality.VIDEO);
return result;
}
if (serverStore.isModelMode && this.models.length > 0 && serverProps?.modalities) {
const modalities: ModelModalities = {
vision: serverProps.modalities.vision ?? false,
- audio: serverProps.modalities.audio ?? false
+ audio: serverProps.modalities.audio ?? false,
+ video: serverProps.modalities.video ?? false
};
this.modelPropsCache.set(this.models[0].model, serverProps);
this.models = this.models.map((model, index) =>
const modalities: ModelModalities = {
vision: props.modalities.vision ?? false,
- audio: props.modalities.audio ?? false
+ audio: props.modalities.audio ?? false,
+ video: props.modalities.video ?? false
};
return { ...model, modalities };
const modalities: ModelModalities = {
vision: props.modalities.vision ?? false,
- audio: props.modalities.audio ?? false
+ audio: props.modalities.audio ?? false,
+ video: props.modalities.video ?? false
};
this.models = this.models.map((model) =>
data: string;
format: 'wav' | 'mp3';
};
+ input_video?: {
+ data: string;
+ format: 'mp4' | 'ogg' | 'auto';
+ };
}
export interface ApiContextSizeError {
modalities: {
vision: boolean;
audio: boolean;
+ video: boolean;
};
chat_template: string;
bos_token: string;
mcpPromptAttachments: ClipboardMcpPromptAttachment[];
}
-export type MimeTypeUnion = MimeTypeAudio | MimeTypeImage | MimeTypeApplication | MimeTypeText;
+export type MimeTypeUnion =
+ | MimeTypeAudio
+ | MimeTypeVideo
+ | MimeTypeImage
+ | MimeTypeApplication
+ | MimeTypeText;
mimeType: string;
}
+export interface DatabaseMessageExtraVideoFile {
+ type: AttachmentType.VIDEO;
+ name: string;
+ size?: number;
+ base64Data: string;
+ mimeType: string;
+}
+
export interface DatabaseMessageExtraImageFile {
type: AttachmentType.IMAGE;
name: string;
| DatabaseMessageExtraImageFile
| DatabaseMessageExtraTextFile
| DatabaseMessageExtraAudioFile
+ | DatabaseMessageExtraVideoFile
| DatabaseMessageExtraPdfFile
| DatabaseMessageExtraMcpPrompt
| DatabaseMessageExtraMcpResource
McpServerOverride,
DatabaseConversation,
DatabaseMessageExtraAudioFile,
+ DatabaseMessageExtraVideoFile,
DatabaseMessageExtraImageFile,
DatabaseMessageExtraLegacyContext,
DatabaseMessageExtraMcpPrompt,
export interface ModelModalities {
vision: boolean;
audio: boolean;
+ video: boolean;
}
export interface ModelOption {
export interface ModalityCapabilities {
hasVision: boolean;
hasAudio: boolean;
+ hasVideo: boolean;
}
return false;
}
+
+/**
+ * Determines if an attachment or uploaded file is a video file
+ * @param uploadedFile - Optional uploaded file
+ * @param attachment - Optional database attachment
+ * @returns true if the file is a video file
+ */
+export function isVideoFile(
+ attachment?: DatabaseMessageExtra,
+ uploadedFile?: ChatUploadedFile
+): boolean {
+ if (uploadedFile) {
+ return getUploadedFileCategory(uploadedFile) === FileTypeCategory.VIDEO;
+ }
+
+ if (attachment) {
+ return attachment.type === AttachmentType.VIDEO;
+ }
+
+ return false;
+}
} catch (error) {
console.error(`Failed to process audio file ${file.name}:`, error);
}
+ } else if (getFileTypeCategory(file.type) === FileTypeCategory.VIDEO) {
+ // Process video files (MP4, etc)
+ try {
+ const base64Data = await readFileAsBase64(file.file);
+
+ extras.push({
+ type: AttachmentType.VIDEO,
+ name: file.name,
+ size: file.size,
+ base64Data: base64Data,
+ mimeType: file.type
+ });
+ } catch (error) {
+ console.error(`Failed to process video file ${file.name}:`, error);
+ }
} else if (getFileTypeCategory(file.type) === FileTypeCategory.PDF) {
try {
// Always get base64 data for preview functionality
import {
AUDIO_FILE_TYPES,
+ VIDEO_FILE_TYPES,
IMAGE_FILE_TYPES,
PDF_FILE_TYPES,
TEXT_FILE_TYPES
FileTypeCategory,
MimeTypeApplication,
MimeTypeAudio,
+ MimeTypeVideo,
MimeTypeImage,
MimeTypeText
} from '$lib/enums';
case MimeTypeAudio.WEBM_OPUS:
return FileTypeCategory.AUDIO;
+ // Video
+ case MimeTypeVideo.MP4:
+ case MimeTypeVideo.OGG:
+ return FileTypeCategory.VIDEO;
+
// PDF
case MimeTypeApplication.PDF:
return FileTypeCategory.PDF;
}
}
+ for (const [key, type] of Object.entries(VIDEO_FILE_TYPES)) {
+ if ((type.extensions as readonly string[]).includes(extension)) {
+ return `${FileTypeCategory.VIDEO}:${key}`;
+ }
+ }
+
for (const [key, type] of Object.entries(PDF_FILE_TYPES)) {
if ((type.extensions as readonly string[]).includes(extension)) {
return `${FileTypeCategory.PDF}:${key}`;
// Attachment utilities
export { getAttachmentDisplayItems, isMcpPrompt, isMcpResource } from './attachment-display';
-export { isTextFile, isImageFile, isPdfFile, isAudioFile } from './attachment-type';
+export { isTextFile, isImageFile, isPdfFile, isAudioFile, isVideoFile } from './attachment-type';
// Textarea utilities
export { default as autoResizeTextarea } from './autoresize-textarea';
// Audio files require audio support
return capabilities.hasAudio;
+ case FileTypeCategory.VIDEO:
+ // Video files require video support
+ return capabilities.hasVideo;
+
default:
// Unknown categories - be conservative and allow
return true;
const unsupportedFiles: File[] = [];
const modalityReasons: Record<string, string> = {};
- const { hasVision, hasAudio } = capabilities;
+ const { hasVision, hasAudio, hasVideo } = capabilities;
for (const file of files) {
const category = getFileTypeCategory(file.type);
}
break;
+ case FileTypeCategory.VIDEO:
+ if (!hasVideo) {
+ isSupported = false;
+ reason = 'Video files require a video-capable model';
+ }
+ break;
+
case FileTypeCategory.TEXT:
case FileTypeCategory.PDF:
// Always supported
): string {
if (unsupportedFiles.length === 0) return '';
- const { hasVision, hasAudio } = capabilities;
+ const { hasVision, hasAudio, hasVideo } = capabilities;
let message = '';
const supportedTypes: string[] = ['text files', 'PDFs'];
if (hasVision) supportedTypes.push('images');
if (hasAudio) supportedTypes.push('audio files');
+ if (hasVideo) supportedTypes.push('video files');
message += ` This model supports: ${supportedTypes.join(', ')}.`;
// Generate preview URL for audio files
const preview = await readFileAsDataURL(file);
results.push({ ...base, preview });
+ } else if (getFileTypeCategory(file.type) === FileTypeCategory.VIDEO) {
+ // Generate preview URL for video files
+ const preview = await readFileAsDataURL(file);
+ results.push({ ...base, preview });
} else {
// Fallback: treat unknown files as text
try {
model_path: props.model_path || 'test-model',
modalities: {
vision: props.modalities?.vision ?? false,
- audio: props.modalities?.audio ?? false
+ audio: props.modalities?.audio ?? false,
+ video: props.modalities?.video ?? false
},
...props
} as ApiLlamaCppServerProps;
// Also mock modelsStore methods for modality checking
const vision = props.modalities?.vision ?? false;
const audio = props.modalities?.audio ?? false;
+ const video = props.modalities?.video ?? false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(modelsStore as any).modelSupportsVision = () => vision;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(modelsStore as any).modelSupportsAudio = () => audio;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (modelsStore as any).modelSupportsVideo = () => video;
// Mock models list with a test model so activeModelId can be resolved
// eslint-disable-next-line @typescript-eslint/no-explicit-any
model_path: '',
modalities: {
vision: false,
- audio: false
+ audio: false,
+ video: false
}
} as ApiLlamaCppServerProps;
(serverStore as unknown as { error: string }).error = '';
modalities: { vision: true, audio: true }
},
noModalities: {
- modalities: { vision: false, audio: false }
+ modalities: { vision: false, audio: false, video: false }
}
} as const;