refactor: Clean up unused code

This commit is contained in:
Aunali321 2025-09-01 00:24:51 +05:30
parent 12b4fef96d
commit bca38fa221
6 changed files with 64 additions and 378 deletions

View file

@ -15,9 +15,6 @@
supportsReasoning,
supportsStreaming,
supportsToolCalls,
supportsVideo,
supportsAudio,
supportsDocuments,
} from '$lib/utils/model-capabilities';
import { capitalize } from '$lib/utils/strings';
import { cn } from '$lib/utils/utils';
@ -53,7 +50,7 @@
type Props = {
class?: string;
/* When attachments are present, restrict to models that support all attachment types */
/* Required capabilities that the selected model must support */
requiredCapabilities?: Array<'vision' | 'audio' | 'video' | 'documents'>;
};
@ -61,25 +58,6 @@
const client = useConvexClient();
function meetsRequiredCapabilities(model: any): boolean {
if (requiredCapabilities.length === 0) return true;
return requiredCapabilities.every((capability) => {
switch (capability) {
case 'vision':
return supportsImages(model);
case 'video':
return supportsVideo(model);
case 'audio':
return supportsAudio(model);
case 'documents':
return supportsDocuments(model);
default:
return true;
}
});
}
const enabledModelsQuery = useCachedQuery(api.user_enabled_models.get_enabled, {
session_token: session.current?.session.token ?? '',
});
@ -285,6 +263,18 @@
// until we migrate the pinning system to work with the new ModelInfo structure
const enabledModelsData = $derived(Object.values(enabledModelsQuery.data ?? {}));
const pinnedModels = $derived(enabledModelsData.filter((m) => isPinned(m)));
function modelSupportsRequiredCapabilities(model: typeof enabledArr[number], required: Array<'vision' | 'audio' | 'video' | 'documents'>): boolean {
return required.every(capability => {
switch (capability) {
case 'vision': return model.capabilities.vision;
case 'audio': return model.capabilities.audio;
case 'video': return model.capabilities.video;
case 'documents': return model.capabilities.documents;
default: return false;
}
});
}
</script>
<svelte:window
@ -374,7 +364,7 @@
{@const formatted = modelInfo
? formatModelName(modelInfo)
: { full: model.model_id, primary: model.model_id, secondary: '' }}
{@const disabled = modelInfo && !meetsRequiredCapabilities(modelInfo)}
{@const disabled = requiredCapabilities.length > 0 && modelInfo && !modelSupportsRequiredCapabilities(modelInfo, requiredCapabilities)}
<Command.Item
value={model.model_id}
@ -534,7 +524,7 @@
{#snippet modelCard(model: (typeof enabledArr)[number])}
{@const formatted = formatModelName(model)}
{@const disabled = !meetsRequiredCapabilities(model)}
{@const disabled = requiredCapabilities.length > 0 && !modelSupportsRequiredCapabilities(model, requiredCapabilities)}
{@const enabledModelData = enabledModelsData.find((m) => m.model_id === model.id)}
<Command.Item

View file

@ -18,13 +18,6 @@ export type ProviderMeta = {
apiKeyName: string;
placeholder: string;
docsLink: string;
supportsStreaming: boolean;
supportsTools: boolean;
supportsVision: boolean;
supportsVideo: boolean;
supportsAudio: boolean;
supportsDocuments: boolean;
supportsEmbeddings: boolean;
};
export const UrlCitationSchema = z.object({
@ -53,13 +46,6 @@ export const PROVIDER_META: Record<Provider, ProviderMeta> = {
apiKeyName: 'OpenAI API Key',
placeholder: 'sk-...',
docsLink: 'https://platform.openai.com/docs',
supportsStreaming: true,
supportsTools: true,
supportsVision: true,
supportsVideo: false,
supportsAudio: true,
supportsDocuments: false,
supportsEmbeddings: true,
},
[Provider.Anthropic]: {
title: 'Anthropic',
@ -68,13 +54,6 @@ export const PROVIDER_META: Record<Provider, ProviderMeta> = {
apiKeyName: 'Anthropic API Key',
placeholder: 'sk-ant-...',
docsLink: 'https://docs.anthropic.com',
supportsStreaming: true,
supportsTools: true,
supportsVision: true,
supportsVideo: false,
supportsAudio: false,
supportsDocuments: true,
supportsEmbeddings: false,
},
[Provider.Gemini]: {
title: 'Google Gemini',
@ -83,13 +62,6 @@ export const PROVIDER_META: Record<Provider, ProviderMeta> = {
apiKeyName: 'Google AI API Key',
placeholder: 'AIza...',
docsLink: 'https://ai.google.dev/docs',
supportsStreaming: true,
supportsTools: true,
supportsVision: true,
supportsVideo: true,
supportsAudio: true,
supportsDocuments: true,
supportsEmbeddings: true,
},
[Provider.Mistral]: {
title: 'Mistral',
@ -98,13 +70,6 @@ export const PROVIDER_META: Record<Provider, ProviderMeta> = {
apiKeyName: 'Mistral API Key',
placeholder: 'mistral-...',
docsLink: 'https://docs.mistral.ai',
supportsStreaming: true,
supportsTools: true,
supportsVision: false,
supportsVideo: false,
supportsAudio: false,
supportsDocuments: false,
supportsEmbeddings: true,
},
[Provider.Cohere]: {
title: 'Cohere',
@ -113,13 +78,6 @@ export const PROVIDER_META: Record<Provider, ProviderMeta> = {
apiKeyName: 'Cohere API Key',
placeholder: 'co_...',
docsLink: 'https://docs.cohere.com',
supportsStreaming: true,
supportsTools: true,
supportsVision: false,
supportsVideo: false,
supportsAudio: false,
supportsDocuments: false,
supportsEmbeddings: true,
},
[Provider.OpenRouter]: {
title: 'OpenRouter',
@ -128,12 +86,5 @@ export const PROVIDER_META: Record<Provider, ProviderMeta> = {
apiKeyName: 'OpenRouter API Key',
placeholder: 'sk-or-...',
docsLink: 'https://openrouter.ai/docs',
supportsStreaming: true,
supportsTools: true,
supportsVision: true,
supportsVideo: false,
supportsAudio: false,
supportsDocuments: false,
supportsEmbeddings: false,
},
};

View file

@ -1,15 +1,7 @@
import type { ModelInfo } from '@keplersystems/kepler-ai-sdk';
import { supportsImages, supportsVideo, supportsAudio, supportsDocuments } from './model-capabilities';
export type AttachmentType = 'image' | 'video' | 'audio' | 'document';
export interface AttachmentInfo {
type: AttachmentType;
mimeType: string;
maxSize: number; // in bytes
extensions: string[];
}
export interface ProcessedAttachment {
type: AttachmentType;
url: string;
@ -19,205 +11,30 @@ export interface ProcessedAttachment {
size: number;
}
export class AttachmentManager {
private static readonly SUPPORTED_ATTACHMENTS: Record<AttachmentType, AttachmentInfo> = {
image: {
type: 'image',
mimeType: 'image/*',
maxSize: 10 * 1024 * 1024, // 10MB
extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg']
},
video: {
type: 'video',
mimeType: 'video/*',
maxSize: 100 * 1024 * 1024, // 100MB
extensions: ['.mp4', '.webm', '.ogg', '.avi', '.mov', '.wmv', '.flv', '.mkv']
},
audio: {
type: 'audio',
mimeType: 'audio/*',
maxSize: 50 * 1024 * 1024, // 50MB
extensions: ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma']
},
document: {
type: 'document',
mimeType: 'application/pdf,text/*',
maxSize: 20 * 1024 * 1024, // 20MB
extensions: ['.pdf', '.txt', '.md', '.doc', '.docx', '.rtf', '.csv', '.json', '.xml', '.html']
}
};
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
private static readonly MIME_TYPE_MAPPING: Record<string, AttachmentType> = {
// Images
'image/jpeg': 'image',
'image/jpg': 'image',
'image/png': 'image',
'image/gif': 'image',
'image/webp': 'image',
'image/bmp': 'image',
'image/svg+xml': 'image',
// Videos
'video/mp4': 'video',
'video/webm': 'video',
'video/ogg': 'video',
'video/avi': 'video',
'video/quicktime': 'video',
'video/x-msvideo': 'video',
'video/x-flv': 'video',
'video/x-matroska': 'video',
// Audio
'audio/mpeg': 'audio',
'audio/mp3': 'audio',
'audio/wav': 'audio',
'audio/ogg': 'audio',
'audio/mp4': 'audio',
'audio/aac': 'audio',
'audio/flac': 'audio',
'audio/x-ms-wma': 'audio',
// Documents
'application/pdf': 'document',
'text/plain': 'document',
'text/markdown': 'document',
'text/csv': 'document',
'text/html': 'document',
'text/xml': 'document',
'application/json': 'document',
'application/msword': 'document',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document',
'application/rtf': 'document'
};
/**
* Get supported attachment types for a given model
*/
static getSupportedAttachmentTypes(model: ModelInfo): AttachmentType[] {
const types: AttachmentType[] = [];
if (supportsImages(model)) types.push('image');
if (supportsVideo(model)) types.push('video');
if (supportsAudio(model)) types.push('audio');
if (supportsDocuments(model)) types.push('document');
return types;
}
/**
* Get attachment info for supported types
*/
static getAttachmentInfo(types: AttachmentType[]): AttachmentInfo[] {
return types.map(type => this.SUPPORTED_ATTACHMENTS[type]);
}
/**
* Validate if a file is supported by the given model
*/
static validateFile(file: File, model: ModelInfo): { valid: boolean; error?: string; type?: AttachmentType } {
const supportedTypes = this.getSupportedAttachmentTypes(model);
const fileType = this.getFileType(file);
if (!fileType) {
return { valid: false, error: `Unsupported file type: ${file.type}` };
}
if (!supportedTypes.includes(fileType)) {
return { valid: false, error: `${fileType} files are not supported by this model` };
}
const attachmentInfo = this.SUPPORTED_ATTACHMENTS[fileType];
if (file.size > attachmentInfo.maxSize) {
return {
valid: false,
error: `File size (${this.formatFileSize(file.size)}) exceeds maximum allowed size (${this.formatFileSize(attachmentInfo.maxSize)}) for ${fileType} files`
};
}
return { valid: true, type: fileType };
}
/**
* Get file type from File object
*/
static getFileType(file: File): AttachmentType | null {
if (this.MIME_TYPE_MAPPING[file.type]) {
return this.MIME_TYPE_MAPPING[file.type];
}
// Fallback to extension-based detection
const extension = this.getFileExtension(file.name);
for (const [type, info] of Object.entries(this.SUPPORTED_ATTACHMENTS)) {
if (info.extensions.includes(extension)) {
return type as AttachmentType;
}
}
return null;
}
/**
* Get file extension from filename
*/
private static getFileExtension(filename: string): string {
return '.' + filename.split('.').pop()?.toLowerCase() || '';
}
/**
* Format file size for human reading
*/
static formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
}
/**
* Get accept string for HTML input element
*/
static getAcceptString(types: AttachmentType[]): string {
const mimeTypes = types.map(type => this.SUPPORTED_ATTACHMENTS[type].mimeType);
return mimeTypes.join(',');
}
/**
* Get icon name for attachment type
*/
static getTypeIcon(type: AttachmentType): string {
const icons = {
image: 'image',
video: 'video',
audio: 'music',
document: 'file-text'
};
return icons[type];
}
/**
* Get display name for attachment type
*/
static getTypeDisplayName(type: AttachmentType): string {
const names = {
image: 'Image',
video: 'Video',
audio: 'Audio',
document: 'Document'
};
return names[type];
}
/**
* Create accept string for multiple types
*/
static createFileInputAccept(supportedTypes: AttachmentType[]): string {
if (supportedTypes.length === 0) return '';
const extensions: string[] = [];
supportedTypes.forEach(type => {
extensions.push(...this.SUPPORTED_ATTACHMENTS[type].extensions);
});
return extensions.join(',');
}
export function getSupportedAttachmentTypes(model: ModelInfo): AttachmentType[] {
const types: AttachmentType[] = [];
if (model.capabilities.vision) types.push('image');
if (model.capabilities.video) types.push('video');
if (model.capabilities.audio) types.push('audio');
if (model.capabilities.documents) types.push('document');
return types;
}
export function getFileType(file: File): AttachmentType | null {
const { type } = file;
if (type.startsWith('image/')) return 'image';
if (type.startsWith('video/')) return 'video';
if (type.startsWith('audio/')) return 'audio';
if (type === 'application/pdf' || type.startsWith('text/')) return 'document';
return null;
}
export function getAcceptString(types: AttachmentType[]): string {
const mimeTypes = types.map(type => `${type}/*`);
if (types.includes('document')) {
mimeTypes.push('application/pdf', 'text/*');
}
return mimeTypes.join(',');
}

View file

@ -16,34 +16,10 @@ export function supportsToolCalls(model: ModelInfo): boolean {
return model.capabilities.functionCalling;
}
export function supportsVideo(model: ModelInfo): boolean {
return model.capabilities.video ?? false;
}
export function supportsAudio(model: ModelInfo): boolean {
return model.capabilities.audio;
}
export function supportsDocuments(model: ModelInfo): boolean {
return model.capabilities.documents ?? false;
}
export function getImageSupportedModels(models: ModelInfo[]): ModelInfo[] {
return models.filter(supportsImages);
}
export function getVideoSupportedModels(models: ModelInfo[]): ModelInfo[] {
return models.filter(supportsVideo);
}
export function getAudioSupportedModels(models: ModelInfo[]): ModelInfo[] {
return models.filter(supportsAudio);
}
export function getDocumentSupportedModels(models: ModelInfo[]): ModelInfo[] {
return models.filter(supportsDocuments);
}
export function getReasoningModels(models: ModelInfo[]): ModelInfo[] {
return models.filter(supportsReasoning);
}

View file

@ -356,55 +356,17 @@ async function generateAIResponse({
log(`Background: ${attachedRules.length} rules attached`, startTime);
const formattedMessages = messages.map((m) => {
// Handle attachments format
if (m.attachments && m.attachments.length > 0 && m.role === 'user') {
const contentParts: Array<{
type: string;
text?: string;
imageUrl?: string;
videoUrl?: string;
audioUrl?: string;
documentUrl?: string;
mimeType?: string;
}> = [{ type: 'text', text: m.content }];
const contentParts = [
{ type: 'text', text: m.content },
...m.attachments.map(attachment => ({
type: attachment.type,
[`${attachment.type}Url`]: attachment.url,
mimeType: attachment.mimeType,
}))
];
for (const attachment of m.attachments) {
switch (attachment.type) {
case 'image':
contentParts.push({
type: 'image',
imageUrl: attachment.url,
mimeType: attachment.mimeType,
});
break;
case 'video':
contentParts.push({
type: 'video',
videoUrl: attachment.url,
mimeType: attachment.mimeType,
});
break;
case 'audio':
contentParts.push({
type: 'audio',
audioUrl: attachment.url,
mimeType: attachment.mimeType,
});
break;
case 'document':
contentParts.push({
type: 'document',
documentUrl: attachment.url,
mimeType: attachment.mimeType,
});
break;
}
}
return {
role: 'user' as const,
content: contentParts,
};
return { role: 'user' as const, content: contentParts };
}
return {

View file

@ -20,8 +20,8 @@
import { settings } from '$lib/state/settings.svelte.js';
import { Provider } from '$lib/types';
import { compressImage } from '$lib/utils/image-compression';
import { supportsImages, supportsReasoning, supportsVideo, supportsAudio, supportsDocuments } from '$lib/utils/model-capabilities';
import { AttachmentManager, type AttachmentType, type ProcessedAttachment } from '$lib/utils/attachment-manager';
import { supportsImages, supportsReasoning } from '$lib/utils/model-capabilities';
import { getSupportedAttachmentTypes, getFileType, getAcceptString, type ProcessedAttachment } from '$lib/utils/attachment-manager';
import { omit, pick } from '$lib/utils/object.js';
import { cn } from '$lib/utils/utils.js';
import { useConvexClient } from 'convex-svelte';
@ -218,19 +218,12 @@
const currentModel = $derived.by(() => {
if (!settings.modelId) return null;
const allModels = models.all();
return allModels.find((m) => m.id === settings.modelId) || null;
return models.all().find((m) => m.id === settings.modelId) || null;
});
const currentModelSupportsImages = $derived(currentModel ? supportsImages(currentModel) : false);
const currentModelSupportsVideo = $derived(currentModel ? supportsVideo(currentModel) : false);
const currentModelSupportsAudio = $derived(currentModel ? supportsAudio(currentModel) : false);
const currentModelSupportsDocuments = $derived(currentModel ? supportsDocuments(currentModel) : false);
const supportedAttachmentTypes = $derived.by(() => {
if (!currentModel) return [];
return AttachmentManager.getSupportedAttachmentTypes(currentModel);
});
const supportedAttachmentTypes = $derived(
currentModel ? getSupportedAttachmentTypes(currentModel) : []
);
const currentModelSupportsReasoning = $derived.by(() => {
if (!settings.modelId) return false;
@ -245,11 +238,10 @@
maxSize: 100 * 1024 * 1024, // 100MB max for any file type
});
// Update the file input accept attribute reactively
// Update file input accept attribute reactively
$effect(() => {
if (fileInput) {
const acceptString = AttachmentManager.getAcceptString(supportedAttachmentTypes);
fileInput.accept = acceptString;
fileInput.accept = getAcceptString(supportedAttachmentTypes);
}
});
@ -261,18 +253,16 @@
try {
for (const file of files) {
// Validate file against current model capabilities
const validation = AttachmentManager.validateFile(file, currentModel);
if (!validation.valid) {
console.warn(`Skipping invalid file ${file.name}: ${validation.error}`);
// TODO: Show error toast to user
// Simple file validation
const fileType = getFileType(file);
if (!fileType || !supportedAttachmentTypes.includes(fileType)) {
console.warn(`Unsupported file type: ${file.name}`);
continue;
}
// Compress images for better performance
let fileToUpload = file;
// Compress images to max 1MB for better performance
if (validation.type === 'image') {
if (fileType === 'image') {
fileToUpload = await compressImage(file, 1024 * 1024);
}
@ -301,7 +291,7 @@
if (url) {
uploadedFiles.push({
type: validation.type!,
type: fileType,
url,
storage_id: storageId,
fileName: file.name,
@ -838,7 +828,7 @@
{/if}
<span class="hidden whitespace-nowrap sm:inline">
{#if supportedAttachmentTypes.length === 1 && supportedAttachmentTypes[0]}
Attach {AttachmentManager.getTypeDisplayName(supportedAttachmentTypes[0]).toLowerCase()}
Attach {supportedAttachmentTypes[0]}
{:else}
Attach files
{/if}
@ -899,7 +889,7 @@
<UploadIcon class="text-primary mx-auto mb-4 h-16 w-16" />
<p class="text-xl font-semibold">
{#if supportedAttachmentTypes.length === 1 && supportedAttachmentTypes[0]}
Add {AttachmentManager.getTypeDisplayName(supportedAttachmentTypes[0]).toLowerCase()}
Add {supportedAttachmentTypes[0]}
{:else}
Add files
{/if}