server: introduce API for serving / loading / unloading multiple models (#17470)
* server: add model management and proxy * fix compile error * does this fix windows? * fix windows build * use subprocess.h, better logging * add test * fix windows * feat: Model/Router server architecture WIP * more stable * fix unsafe pointer * also allow terminate loading model * add is_active() * refactor: Architecture improvements * tmp apply upstream fix * address most problems * address thread safety issue * address review comment * add docs (first version) * address review comment * feat: Improved UX for model information, modality interactions etc * chore: update webui build output * refactor: Use only the message data `model` property for displaying model used info * chore: update webui build output * add --models-dir param * feat: New Model Selection UX WIP * chore: update webui build output * feat: Add auto-mic setting * feat: Attachments UX improvements * implement LRU * remove default model path * better --models-dir * add env for args * address review comments * fix compile * refactor: Chat Form Submit component * ad endpoint docs * Merge remote-tracking branch 'webui/allozaur/server_model_management_v1_2' into xsn/server_model_maagement_v1_2 Co-authored-by: Aleksander <aleksander.grygier@gmail.com> * feat: Add copy to clipboard to model name in model info dialog * feat: Model unavailable UI state for model selector * feat: Chat Form Actions UI logic improvements * feat: Auto-select model from last assistant response * chore: update webui build output * expose args and exit_code in API * add note * support extra_args on loading model * allow reusing args if auto_load * typo docs * oai-compat /models endpoint * cleaner * address review comments * feat: Use `model` property for displaying the `repo/model-name` naming format * refactor: Attachments data * chore: update webui build output * refactor: Enum imports * feat: Improve Model Selector responsiveness * chore: update webui build output * refactor: Cleanup * refactor: Cleanup * refactor: Formatters * chore: update webui build output * refactor: Copy To Clipboard Icon component * chore: update webui build output * refactor: Cleanup * chore: update webui build output * refactor: UI badges * chore: update webui build output * refactor: Cleanup * refactor: Cleanup * chore: update webui build output * add --models-allow-extra-args for security * nits * add stdin_file * fix merge * fix: Retrieve lost setting after resolving merge conflict * refactor: DatabaseStore -> DatabaseService * refactor: Database, Conversations & Chat services + stores architecture improvements (WIP) * refactor: Remove redundant settings * refactor: Multi-model business logic WIP * chore: update webui build output * feat: Switching models logic for ChatForm or when regenerating messges + modality detection logic * chore: update webui build output * fix: Add `untrack` inside chat processing info data logic to prevent infinite effect * fix: Regenerate * feat: Remove redundant settigns + rearrange * fix: Audio attachments * refactor: Icons * chore: update webui build output * feat: Model management and selection features WIP * chore: update webui build output * refactor: Improve server properties management * refactor: Icons * chore: update webui build output * feat: Improve model loading/unloading status updates * chore: update webui build output * refactor: Improve API header management via utility functions * remove support for extra args * set hf_repo/docker_repo as model alias when posible * refactor: Remove ConversationsService * refactor: Chat requests abort handling * refactor: Server store * tmp webui build * refactor: Model modality handling * chore: update webui build output * refactor: Processing state reactivity * fix: UI * refactor: Services/Stores syntax + logic improvements Refactors components to access stores directly instead of using exported getter functions. This change centralizes store access and logic, simplifying component code and improving maintainability by reducing the number of exported functions and promoting direct store interaction. Removes exported getter functions from `chat.svelte.ts`, `conversations.svelte.ts`, `models.svelte.ts` and `settings.svelte.ts`. * refactor: Architecture cleanup * feat: Improve statistic badges * feat: Condition available models based on modality + better model loading strategy & UX * docs: Architecture documentation * feat: Update logic for PDF as Image * add TODO for http client * refactor: Enhance model info and attachment handling * chore: update webui build output * refactor: Components naming * chore: update webui build output * refactor: Cleanup * refactor: DRY `getAttachmentDisplayItems` function + fix UI * chore: update webui build output * fix: Modality detection improvement for text-based PDF attachments * refactor: Cleanup * docs: Add info comment * refactor: Cleanup * re * refactor: Cleanup * refactor: Cleanup * feat: Attachment logic & UI improvements * refactor: Constants * feat: Improve UI sidebar background color * chore: update webui build output * refactor: Utils imports + move types to `app.d.ts` * test: Fix Storybook mocks * chore: update webui build output * test: Update Chat Form UI tests * refactor: Tooltip Provider from core layout * refactor: Tests to separate location * decouple server_models from server_routes * test: Move demo test to tests/server * refactor: Remove redundant method * chore: update webui build output * also route anthropic endpoints * fix duplicated arg * fix invalid ptr to shutdown_handler * server : minor * rm unused fn * add ?autoload=true|false query param * refactor: Remove redundant code * docs: Update README documentations + architecture & data flow diagrams * fix: Disable autoload on calling server props for the model * chore: update webui build output * fix ubuntu build * fix: Model status reactivity * fix: Modality detection for MODEL mode * chore: update webui build output --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
||||
/**
|
||||
* Get authorization headers for API requests
|
||||
* Includes Bearer token if API key is configured
|
||||
*/
|
||||
export function getAuthHeaders(): Record<string, string> {
|
||||
const currentConfig = config();
|
||||
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get standard JSON headers with optional authorization
|
||||
*/
|
||||
export function getJsonHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { FileTypeCategory } from '$lib/enums';
|
||||
import { getFileTypeCategory, getFileTypeCategoryByExtension, isImageFile } from '$lib/utils';
|
||||
|
||||
export interface AttachmentDisplayItemsOptions {
|
||||
uploadedFiles?: ChatUploadedFile[];
|
||||
attachments?: DatabaseMessageExtra[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file type category from an uploaded file, checking both MIME type and extension
|
||||
*/
|
||||
function getUploadedFileCategory(file: ChatUploadedFile): FileTypeCategory | null {
|
||||
const categoryByMime = getFileTypeCategory(file.type);
|
||||
|
||||
if (categoryByMime) {
|
||||
return categoryByMime;
|
||||
}
|
||||
|
||||
return getFileTypeCategoryByExtension(file.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unified list of display items from uploaded files and stored attachments.
|
||||
* Items are returned in reverse order (newest first).
|
||||
*/
|
||||
export function getAttachmentDisplayItems(
|
||||
options: AttachmentDisplayItemsOptions
|
||||
): ChatAttachmentDisplayItem[] {
|
||||
const { uploadedFiles = [], attachments = [] } = options;
|
||||
const items: ChatAttachmentDisplayItem[] = [];
|
||||
|
||||
// Add uploaded files (ChatForm)
|
||||
for (const file of uploadedFiles) {
|
||||
items.push({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
preview: file.preview,
|
||||
isImage: getUploadedFileCategory(file) === FileTypeCategory.IMAGE,
|
||||
uploadedFile: file,
|
||||
textContent: file.textContent
|
||||
});
|
||||
}
|
||||
|
||||
// Add stored attachments (ChatMessage)
|
||||
for (const [index, attachment] of attachments.entries()) {
|
||||
const isImage = isImageFile(attachment);
|
||||
|
||||
items.push({
|
||||
id: `attachment-${index}`,
|
||||
name: attachment.name,
|
||||
preview: isImage && 'base64Url' in attachment ? attachment.base64Url : undefined,
|
||||
isImage,
|
||||
attachment,
|
||||
attachmentIndex: index,
|
||||
textContent: 'content' in attachment ? attachment.content : undefined
|
||||
});
|
||||
}
|
||||
|
||||
return items.reverse();
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { AttachmentType, FileTypeCategory } from '$lib/enums';
|
||||
import { getFileTypeCategory, getFileTypeCategoryByExtension } from '$lib/utils';
|
||||
|
||||
/**
|
||||
* Gets the file type category from an uploaded file, checking both MIME type and extension
|
||||
* @param uploadedFile - The uploaded file to check
|
||||
* @returns The file type category or null if not recognized
|
||||
*/
|
||||
function getUploadedFileCategory(uploadedFile: ChatUploadedFile): FileTypeCategory | null {
|
||||
// First try MIME type
|
||||
const categoryByMime = getFileTypeCategory(uploadedFile.type);
|
||||
|
||||
if (categoryByMime) {
|
||||
return categoryByMime;
|
||||
}
|
||||
|
||||
// Fallback to extension (browsers don't always provide correct MIME types)
|
||||
return getFileTypeCategoryByExtension(uploadedFile.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an attachment or uploaded file is a text file
|
||||
* @param uploadedFile - Optional uploaded file
|
||||
* @param attachment - Optional database attachment
|
||||
* @returns true if the file is a text file
|
||||
*/
|
||||
export function isTextFile(
|
||||
attachment?: DatabaseMessageExtra,
|
||||
uploadedFile?: ChatUploadedFile
|
||||
): boolean {
|
||||
if (uploadedFile) {
|
||||
return getUploadedFileCategory(uploadedFile) === FileTypeCategory.TEXT;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
return (
|
||||
attachment.type === AttachmentType.TEXT || attachment.type === AttachmentType.LEGACY_CONTEXT
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an attachment or uploaded file is an image
|
||||
* @param uploadedFile - Optional uploaded file
|
||||
* @param attachment - Optional database attachment
|
||||
* @returns true if the file is an image
|
||||
*/
|
||||
export function isImageFile(
|
||||
attachment?: DatabaseMessageExtra,
|
||||
uploadedFile?: ChatUploadedFile
|
||||
): boolean {
|
||||
if (uploadedFile) {
|
||||
return getUploadedFileCategory(uploadedFile) === FileTypeCategory.IMAGE;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
return attachment.type === AttachmentType.IMAGE;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an attachment or uploaded file is a PDF
|
||||
* @param uploadedFile - Optional uploaded file
|
||||
* @param attachment - Optional database attachment
|
||||
* @returns true if the file is a PDF
|
||||
*/
|
||||
export function isPdfFile(
|
||||
attachment?: DatabaseMessageExtra,
|
||||
uploadedFile?: ChatUploadedFile
|
||||
): boolean {
|
||||
if (uploadedFile) {
|
||||
return getUploadedFileCategory(uploadedFile) === FileTypeCategory.PDF;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
return attachment.type === AttachmentType.PDF;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an attachment or uploaded file is an audio file
|
||||
* @param uploadedFile - Optional uploaded file
|
||||
* @param attachment - Optional database attachment
|
||||
* @returns true if the file is an audio file
|
||||
*/
|
||||
export function isAudioFile(
|
||||
attachment?: DatabaseMessageExtra,
|
||||
uploadedFile?: ChatUploadedFile
|
||||
): boolean {
|
||||
if (uploadedFile) {
|
||||
return getUploadedFileCategory(uploadedFile) === FileTypeCategory.AUDIO;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
return attachment.type === AttachmentType.AUDIO;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MimeTypeAudio } from '$lib/enums/files';
|
||||
import { MimeTypeAudio } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* AudioRecorder - Browser-based audio recording with MediaRecorder API
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Browser-only utility exports
|
||||
*
|
||||
* These utilities require browser APIs (DOM, Canvas, MediaRecorder, etc.)
|
||||
* and cannot be imported during SSR. Import from '$lib/utils/browser-only'
|
||||
* only in client-side code or components that are not server-rendered.
|
||||
*/
|
||||
|
||||
// Audio utilities (MediaRecorder API)
|
||||
export {
|
||||
AudioRecorder,
|
||||
convertToWav,
|
||||
createAudioFile,
|
||||
isAudioRecordingSupported
|
||||
} from './audio-recording';
|
||||
|
||||
// PDF processing utilities (pdfjs-dist with DOMMatrix)
|
||||
export {
|
||||
convertPDFToText,
|
||||
convertPDFToImage,
|
||||
isPdfFile as isPdfFileFromFile,
|
||||
isApplicationMimeType
|
||||
} from './pdf-processing';
|
||||
|
||||
// File conversion utilities (depends on pdf-processing)
|
||||
export { parseFilesToMessageExtras, type FileProcessingResult } from './convert-files-to-extra';
|
||||
|
||||
// File upload processing utilities (depends on pdf-processing, svg-to-png, webp-to-png)
|
||||
export { processFilesToChatUploaded } from './process-uploaded-files';
|
||||
|
||||
// SVG utilities (Canvas/Image API)
|
||||
export { svgBase64UrlToPngDataURL, isSvgFile, isSvgMimeType } from './svg-to-png';
|
||||
|
||||
// WebP utilities (Canvas/Image API)
|
||||
export { webpBase64UrlToPngDataURL, isWebpFile, isWebpMimeType } from './webp-to-png';
|
||||
@@ -5,8 +5,6 @@
|
||||
* with dynamic keys while maintaining TypeScript type safety.
|
||||
*/
|
||||
|
||||
import type { SettingsConfigType } from '$lib/types/settings';
|
||||
|
||||
/**
|
||||
* Type-safe helper to access config properties dynamically
|
||||
* Provides better type safety than direct casting to Record
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { convertPDFToImage, convertPDFToText } from './pdf-processing';
|
||||
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||
import { FileTypeCategory } from '$lib/enums/files';
|
||||
import { FileTypeCategory, AttachmentType } from '$lib/enums';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { supportsVision } from '$lib/stores/server.svelte';
|
||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { getFileTypeCategory } from '$lib/utils';
|
||||
import { readFileAsText, isLikelyTextFile } from './text-files';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -31,7 +31,8 @@ export interface FileProcessingResult {
|
||||
}
|
||||
|
||||
export async function parseFilesToMessageExtras(
|
||||
files: ChatUploadedFile[]
|
||||
files: ChatUploadedFile[],
|
||||
activeModelId?: string
|
||||
): Promise<FileProcessingResult> {
|
||||
const extras: DatabaseMessageExtra[] = [];
|
||||
const emptyFiles: string[] = [];
|
||||
@@ -56,7 +57,7 @@ export async function parseFilesToMessageExtras(
|
||||
}
|
||||
|
||||
extras.push({
|
||||
type: 'imageFile',
|
||||
type: AttachmentType.IMAGE,
|
||||
name: file.name,
|
||||
base64Url
|
||||
});
|
||||
@@ -67,7 +68,7 @@ export async function parseFilesToMessageExtras(
|
||||
const base64Data = await readFileAsBase64(file.file);
|
||||
|
||||
extras.push({
|
||||
type: 'audioFile',
|
||||
type: AttachmentType.AUDIO,
|
||||
name: file.name,
|
||||
base64Data: base64Data,
|
||||
mimeType: file.type
|
||||
@@ -80,7 +81,10 @@ export async function parseFilesToMessageExtras(
|
||||
// Always get base64 data for preview functionality
|
||||
const base64Data = await readFileAsBase64(file.file);
|
||||
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
|
||||
let shouldProcessAsImages = Boolean(currentConfig.pdfAsImage) && hasVisionSupport;
|
||||
@@ -117,7 +121,7 @@ export async function parseFilesToMessageExtras(
|
||||
);
|
||||
|
||||
extras.push({
|
||||
type: 'pdfFile',
|
||||
type: AttachmentType.PDF,
|
||||
name: file.name,
|
||||
content: `PDF file with ${images.length} pages`,
|
||||
images: images,
|
||||
@@ -134,7 +138,7 @@ export async function parseFilesToMessageExtras(
|
||||
const content = await convertPDFToText(file.file);
|
||||
|
||||
extras.push({
|
||||
type: 'pdfFile',
|
||||
type: AttachmentType.PDF,
|
||||
name: file.name,
|
||||
content: content,
|
||||
processedAsImages: false,
|
||||
@@ -151,7 +155,7 @@ export async function parseFilesToMessageExtras(
|
||||
});
|
||||
|
||||
extras.push({
|
||||
type: 'pdfFile',
|
||||
type: AttachmentType.PDF,
|
||||
name: file.name,
|
||||
content: content,
|
||||
processedAsImages: false,
|
||||
@@ -171,7 +175,7 @@ export async function parseFilesToMessageExtras(
|
||||
emptyFiles.push(file.name);
|
||||
} else if (isLikelyTextFile(content)) {
|
||||
extras.push({
|
||||
type: 'textFile',
|
||||
type: AttachmentType.TEXT,
|
||||
name: file.name,
|
||||
content: content
|
||||
});
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
/**
|
||||
* Formats file size in bytes to human readable format
|
||||
* @param bytes - File size in bytes
|
||||
* @returns Formatted file size string
|
||||
* Gets a display label for a file type from various input formats
|
||||
*
|
||||
* 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 formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
export function getFileTypeLabel(input: string | undefined): string {
|
||||
if (!input) return 'FILE';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
// Handle file names (contains '.')
|
||||
if (input.includes('.')) {
|
||||
const ext = input.split('.').pop();
|
||||
if (ext) return ext.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a display label for a file type
|
||||
* @param fileType - The file type/mime type
|
||||
* @returns Formatted file type label
|
||||
*/
|
||||
export function getFileTypeLabel(fileType: string): string {
|
||||
return fileType.split('/').pop()?.toUpperCase() || 'FILE';
|
||||
// Handle AttachmentType or other plain strings
|
||||
return input.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,42 +4,151 @@ import {
|
||||
PDF_FILE_TYPES,
|
||||
TEXT_FILE_TYPES
|
||||
} from '$lib/constants/supported-file-types';
|
||||
import { FileTypeCategory } from '$lib/enums/files';
|
||||
import {
|
||||
FileExtensionAudio,
|
||||
FileExtensionImage,
|
||||
FileExtensionPdf,
|
||||
FileExtensionText,
|
||||
FileTypeCategory,
|
||||
MimeTypeApplication,
|
||||
MimeTypeAudio,
|
||||
MimeTypeImage,
|
||||
MimeTypeText
|
||||
} from '$lib/enums';
|
||||
|
||||
export function getFileTypeCategory(mimeType: string): FileTypeCategory | null {
|
||||
if (
|
||||
Object.values(IMAGE_FILE_TYPES).some((type) =>
|
||||
(type.mimeTypes as readonly string[]).includes(mimeType)
|
||||
)
|
||||
) {
|
||||
return FileTypeCategory.IMAGE;
|
||||
}
|
||||
switch (mimeType) {
|
||||
// Images
|
||||
case MimeTypeImage.JPEG:
|
||||
case MimeTypeImage.PNG:
|
||||
case MimeTypeImage.GIF:
|
||||
case MimeTypeImage.WEBP:
|
||||
case MimeTypeImage.SVG:
|
||||
return FileTypeCategory.IMAGE;
|
||||
|
||||
if (
|
||||
Object.values(AUDIO_FILE_TYPES).some((type) =>
|
||||
(type.mimeTypes as readonly string[]).includes(mimeType)
|
||||
)
|
||||
) {
|
||||
return FileTypeCategory.AUDIO;
|
||||
}
|
||||
// Audio
|
||||
case MimeTypeAudio.MP3_MPEG:
|
||||
case MimeTypeAudio.MP3:
|
||||
case MimeTypeAudio.MP4:
|
||||
case MimeTypeAudio.WAV:
|
||||
case MimeTypeAudio.WEBM:
|
||||
case MimeTypeAudio.WEBM_OPUS:
|
||||
return FileTypeCategory.AUDIO;
|
||||
|
||||
if (
|
||||
Object.values(PDF_FILE_TYPES).some((type) =>
|
||||
(type.mimeTypes as readonly string[]).includes(mimeType)
|
||||
)
|
||||
) {
|
||||
return FileTypeCategory.PDF;
|
||||
}
|
||||
// PDF
|
||||
case MimeTypeApplication.PDF:
|
||||
return FileTypeCategory.PDF;
|
||||
|
||||
if (
|
||||
Object.values(TEXT_FILE_TYPES).some((type) =>
|
||||
(type.mimeTypes as readonly string[]).includes(mimeType)
|
||||
)
|
||||
) {
|
||||
return FileTypeCategory.TEXT;
|
||||
}
|
||||
// Text
|
||||
case MimeTypeText.PLAIN:
|
||||
case MimeTypeText.MARKDOWN:
|
||||
case MimeTypeText.ASCIIDOC:
|
||||
case MimeTypeText.JAVASCRIPT:
|
||||
case MimeTypeText.JAVASCRIPT_APP:
|
||||
case MimeTypeText.TYPESCRIPT:
|
||||
case MimeTypeText.JSX:
|
||||
case MimeTypeText.TSX:
|
||||
case MimeTypeText.CSS:
|
||||
case MimeTypeText.HTML:
|
||||
case MimeTypeText.JSON:
|
||||
case MimeTypeText.XML_TEXT:
|
||||
case MimeTypeText.XML_APP:
|
||||
case MimeTypeText.YAML_TEXT:
|
||||
case MimeTypeText.YAML_APP:
|
||||
case MimeTypeText.CSV:
|
||||
case MimeTypeText.PYTHON:
|
||||
case MimeTypeText.JAVA:
|
||||
case MimeTypeText.CPP_SRC:
|
||||
case MimeTypeText.C_SRC:
|
||||
case MimeTypeText.C_HDR:
|
||||
case MimeTypeText.PHP:
|
||||
case MimeTypeText.RUBY:
|
||||
case MimeTypeText.GO:
|
||||
case MimeTypeText.RUST:
|
||||
case MimeTypeText.SHELL:
|
||||
case MimeTypeText.BAT:
|
||||
case MimeTypeText.SQL:
|
||||
case MimeTypeText.R:
|
||||
case MimeTypeText.SCALA:
|
||||
case MimeTypeText.KOTLIN:
|
||||
case MimeTypeText.SWIFT:
|
||||
case MimeTypeText.DART:
|
||||
case MimeTypeText.VUE:
|
||||
case MimeTypeText.SVELTE:
|
||||
case MimeTypeText.LATEX:
|
||||
case MimeTypeText.BIBTEX:
|
||||
return FileTypeCategory.TEXT;
|
||||
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileTypeCategoryByExtension(filename: string): FileTypeCategory | null {
|
||||
const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
|
||||
|
||||
switch (extension) {
|
||||
// Images
|
||||
case FileExtensionImage.JPG:
|
||||
case FileExtensionImage.JPEG:
|
||||
case FileExtensionImage.PNG:
|
||||
case FileExtensionImage.GIF:
|
||||
case FileExtensionImage.WEBP:
|
||||
case FileExtensionImage.SVG:
|
||||
return FileTypeCategory.IMAGE;
|
||||
|
||||
// Audio
|
||||
case FileExtensionAudio.MP3:
|
||||
case FileExtensionAudio.WAV:
|
||||
return FileTypeCategory.AUDIO;
|
||||
|
||||
// PDF
|
||||
case FileExtensionPdf.PDF:
|
||||
return FileTypeCategory.PDF;
|
||||
|
||||
// Text
|
||||
case FileExtensionText.TXT:
|
||||
case FileExtensionText.MD:
|
||||
case FileExtensionText.ADOC:
|
||||
case FileExtensionText.JS:
|
||||
case FileExtensionText.TS:
|
||||
case FileExtensionText.JSX:
|
||||
case FileExtensionText.TSX:
|
||||
case FileExtensionText.CSS:
|
||||
case FileExtensionText.HTML:
|
||||
case FileExtensionText.HTM:
|
||||
case FileExtensionText.JSON:
|
||||
case FileExtensionText.XML:
|
||||
case FileExtensionText.YAML:
|
||||
case FileExtensionText.YML:
|
||||
case FileExtensionText.CSV:
|
||||
case FileExtensionText.LOG:
|
||||
case FileExtensionText.PY:
|
||||
case FileExtensionText.JAVA:
|
||||
case FileExtensionText.CPP:
|
||||
case FileExtensionText.C:
|
||||
case FileExtensionText.H:
|
||||
case FileExtensionText.PHP:
|
||||
case FileExtensionText.RB:
|
||||
case FileExtensionText.GO:
|
||||
case FileExtensionText.RS:
|
||||
case FileExtensionText.SH:
|
||||
case FileExtensionText.BAT:
|
||||
case FileExtensionText.SQL:
|
||||
case FileExtensionText.R:
|
||||
case FileExtensionText.SCALA:
|
||||
case FileExtensionText.KT:
|
||||
case FileExtensionText.SWIFT:
|
||||
case FileExtensionText.DART:
|
||||
case FileExtensionText.VUE:
|
||||
case FileExtensionText.SVELTE:
|
||||
case FileExtensionText.TEX:
|
||||
case FileExtensionText.BIB:
|
||||
return FileTypeCategory.TEXT;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileTypeByExtension(filename: string): string | null {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Formats file size in bytes to human readable format
|
||||
* Supports Bytes, KB, MB, and GB
|
||||
*
|
||||
* @param bytes - File size in bytes (or unknown for safety)
|
||||
* @returns Formatted file size string
|
||||
*/
|
||||
export function formatFileSize(bytes: number | unknown): string {
|
||||
if (typeof bytes !== 'number') return 'Unknown';
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format parameter count to human-readable format (B, M, K)
|
||||
*
|
||||
* @param params - Parameter count
|
||||
* @returns Human-readable parameter count
|
||||
*/
|
||||
export 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with locale-specific thousands separators
|
||||
*
|
||||
* @param num - Number to format
|
||||
* @returns Human-readable number
|
||||
*/
|
||||
export function formatNumber(num: number | unknown): string {
|
||||
if (typeof num !== 'number') return 'Unknown';
|
||||
|
||||
return num.toLocaleString();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Unified exports for all utility functions
|
||||
* Import utilities from '$lib/utils' for cleaner imports
|
||||
*
|
||||
* For browser-only utilities (pdf-processing, audio-recording, svg-to-png,
|
||||
* webp-to-png, process-uploaded-files, convert-files-to-extra), use:
|
||||
* import { ... } from '$lib/utils/browser-only'
|
||||
*/
|
||||
|
||||
// API utilities
|
||||
export { getAuthHeaders, getJsonHeaders } from './api-headers';
|
||||
export { validateApiKey } from './api-key-validation';
|
||||
|
||||
// Attachment utilities
|
||||
export {
|
||||
getAttachmentDisplayItems,
|
||||
type AttachmentDisplayItemsOptions
|
||||
} from './attachment-display';
|
||||
export { isTextFile, isImageFile, isPdfFile, isAudioFile } from './attachment-type';
|
||||
|
||||
// Textarea utilities
|
||||
export { default as autoResizeTextarea } from './autoresize-textarea';
|
||||
|
||||
// Branching utilities
|
||||
export {
|
||||
filterByLeafNodeId,
|
||||
findLeafNode,
|
||||
findDescendantMessages,
|
||||
getMessageSiblings,
|
||||
getMessageDisplayList,
|
||||
hasMessageSiblings,
|
||||
getNextSibling,
|
||||
getPreviousSibling
|
||||
} from './branching';
|
||||
|
||||
// Config helpers
|
||||
export { setConfigValue, getConfigValue, configToParameterRecord } from './config-helpers';
|
||||
|
||||
// Conversation utilities
|
||||
export { createMessageCountMap, getMessageCount } from './conversation-utils';
|
||||
|
||||
// Clipboard utilities
|
||||
export { copyToClipboard, copyCodeToClipboard } from './copy';
|
||||
|
||||
// File preview utilities
|
||||
export { getFileTypeLabel, getPreviewText } from './file-preview';
|
||||
|
||||
// File type utilities
|
||||
export {
|
||||
getFileTypeCategory,
|
||||
getFileTypeCategoryByExtension,
|
||||
getFileTypeByExtension,
|
||||
isFileTypeSupported
|
||||
} from './file-type';
|
||||
|
||||
// Formatting utilities
|
||||
export { formatFileSize, formatParameters, formatNumber } from './formatters';
|
||||
|
||||
// IME utilities
|
||||
export { isIMEComposing } from './is-ime-composing';
|
||||
|
||||
// LaTeX utilities
|
||||
export { maskInlineLaTeX, preprocessLaTeX } from './latex-protection';
|
||||
|
||||
// Modality file validation utilities
|
||||
export {
|
||||
isFileTypeSupportedByModel,
|
||||
filterFilesByModalities,
|
||||
generateModalityErrorMessage,
|
||||
generateModalityAwareAcceptString,
|
||||
type ModalityCapabilities
|
||||
} from './modality-file-validation';
|
||||
|
||||
// Model name utilities
|
||||
export { normalizeModelName, isValidModelName } from './model-names';
|
||||
|
||||
// Portal utilities
|
||||
export { portalToBody } from './portal-to-body';
|
||||
|
||||
// Precision utilities
|
||||
export { normalizeFloatingPoint, normalizeNumber } from './precision';
|
||||
|
||||
// Syntax highlighting utilities
|
||||
export { getLanguageFromFilename } from './syntax-highlight-language';
|
||||
|
||||
// Text file utilities
|
||||
export { isTextFileByName, readFileAsText, isLikelyTextFile } from './text-files';
|
||||
@@ -3,8 +3,7 @@
|
||||
* Ensures only compatible file types are processed based on model capabilities
|
||||
*/
|
||||
|
||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||
import { supportsVision, supportsAudio } from '$lib/stores/server.svelte';
|
||||
import { getFileTypeCategory } from '$lib/utils';
|
||||
import {
|
||||
FileExtensionAudio,
|
||||
FileExtensionImage,
|
||||
@@ -15,15 +14,26 @@ import {
|
||||
MimeTypeApplication,
|
||||
MimeTypeText,
|
||||
FileTypeCategory
|
||||
} from '$lib/enums/files';
|
||||
} from '$lib/enums';
|
||||
|
||||
/** Modality capabilities for file validation */
|
||||
export interface ModalityCapabilities {
|
||||
hasVision: boolean;
|
||||
hasAudio: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file type is supported by the current model's modalities
|
||||
* Check if a file type is supported by the given modalities
|
||||
* @param filename - The filename to check
|
||||
* @param mimeType - The MIME type of the file
|
||||
* @returns true if the file type is supported by the current model
|
||||
* @param capabilities - The modality capabilities to check against
|
||||
* @returns true if the file type is supported
|
||||
*/
|
||||
export function isFileTypeSupportedByModel(filename: string, mimeType?: string): boolean {
|
||||
export function isFileTypeSupportedByModel(
|
||||
filename: string,
|
||||
mimeType: string | undefined,
|
||||
capabilities: ModalityCapabilities
|
||||
): boolean {
|
||||
const category = mimeType ? getFileTypeCategory(mimeType) : null;
|
||||
|
||||
// If we can't determine the category from MIME type, fall back to general support check
|
||||
@@ -44,11 +54,11 @@ export function isFileTypeSupportedByModel(filename: string, mimeType?: string):
|
||||
|
||||
case FileTypeCategory.IMAGE:
|
||||
// Images require vision support
|
||||
return supportsVision();
|
||||
return capabilities.hasVision;
|
||||
|
||||
case FileTypeCategory.AUDIO:
|
||||
// Audio files require audio support
|
||||
return supportsAudio();
|
||||
return capabilities.hasAudio;
|
||||
|
||||
default:
|
||||
// Unknown categories - be conservative and allow
|
||||
@@ -59,9 +69,13 @@ export function isFileTypeSupportedByModel(filename: string, mimeType?: string):
|
||||
/**
|
||||
* Filter files based on model modalities and return supported/unsupported lists
|
||||
* @param files - Array of files to filter
|
||||
* @param capabilities - The modality capabilities to check against
|
||||
* @returns Object with supportedFiles and unsupportedFiles arrays
|
||||
*/
|
||||
export function filterFilesByModalities(files: File[]): {
|
||||
export function filterFilesByModalities(
|
||||
files: File[],
|
||||
capabilities: ModalityCapabilities
|
||||
): {
|
||||
supportedFiles: File[];
|
||||
unsupportedFiles: File[];
|
||||
modalityReasons: Record<string, string>;
|
||||
@@ -70,8 +84,7 @@ export function filterFilesByModalities(files: File[]): {
|
||||
const unsupportedFiles: File[] = [];
|
||||
const modalityReasons: Record<string, string> = {};
|
||||
|
||||
const hasVision = supportsVision();
|
||||
const hasAudio = supportsAudio();
|
||||
const { hasVision, hasAudio } = capabilities;
|
||||
|
||||
for (const file of files) {
|
||||
const category = getFileTypeCategory(file.type);
|
||||
@@ -119,16 +132,17 @@ export function filterFilesByModalities(files: File[]): {
|
||||
* Generate a user-friendly error message for unsupported files
|
||||
* @param unsupportedFiles - Array of unsupported files
|
||||
* @param modalityReasons - Reasons why files are unsupported
|
||||
* @param capabilities - The modality capabilities to check against
|
||||
* @returns Formatted error message
|
||||
*/
|
||||
export function generateModalityErrorMessage(
|
||||
unsupportedFiles: File[],
|
||||
modalityReasons: Record<string, string>
|
||||
modalityReasons: Record<string, string>,
|
||||
capabilities: ModalityCapabilities
|
||||
): string {
|
||||
if (unsupportedFiles.length === 0) return '';
|
||||
|
||||
const hasVision = supportsVision();
|
||||
const hasAudio = supportsAudio();
|
||||
const { hasVision, hasAudio } = capabilities;
|
||||
|
||||
let message = '';
|
||||
|
||||
@@ -152,12 +166,12 @@ export function generateModalityErrorMessage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate file input accept string based on current model modalities
|
||||
* Generate file input accept string based on model modalities
|
||||
* @param capabilities - The modality capabilities to check against
|
||||
* @returns Accept string for HTML file input element
|
||||
*/
|
||||
export function generateModalityAwareAcceptString(): string {
|
||||
const hasVision = supportsVision();
|
||||
const hasAudio = supportsAudio();
|
||||
export function generateModalityAwareAcceptString(capabilities: ModalityCapabilities): string {
|
||||
const { hasVision, hasAudio } = capabilities;
|
||||
|
||||
const acceptedExtensions: string[] = [];
|
||||
const acceptedMimeTypes: string[] = [];
|
||||
|
||||
@@ -2,12 +2,19 @@ import { describe, expect, it } from 'vitest';
|
||||
import { isValidModelName, normalizeModelName } from './model-names';
|
||||
|
||||
describe('normalizeModelName', () => {
|
||||
it('extracts filename from forward slash path', () => {
|
||||
expect(normalizeModelName('models/model-name-1')).toBe('model-name-1');
|
||||
expect(normalizeModelName('path/to/model/model-name-2')).toBe('model-name-2');
|
||||
it('preserves Hugging Face org/model format (single slash)', () => {
|
||||
// Single slash is treated as Hugging Face format and preserved
|
||||
expect(normalizeModelName('meta-llama/Llama-3.1-8B')).toBe('meta-llama/Llama-3.1-8B');
|
||||
expect(normalizeModelName('models/model-name-1')).toBe('models/model-name-1');
|
||||
});
|
||||
|
||||
it('extracts filename from backslash path', () => {
|
||||
it('extracts filename from multi-segment paths', () => {
|
||||
// Multiple slashes -> extract just the filename
|
||||
expect(normalizeModelName('path/to/model/model-name-2')).toBe('model-name-2');
|
||||
expect(normalizeModelName('/absolute/path/to/model')).toBe('model');
|
||||
});
|
||||
|
||||
it('extracts filename from backslash paths', () => {
|
||||
expect(normalizeModelName('C\\Models\\model-name-1')).toBe('model-name-1');
|
||||
expect(normalizeModelName('path\\to\\model\\model-name-2')).toBe('model-name-2');
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
* 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
|
||||
* @returns The normalized model name (filename only)
|
||||
* @returns The normalized model name
|
||||
*
|
||||
* @example
|
||||
* normalizeModelName('models/llama-3.1-8b') // Returns: 'llama-3.1-8b'
|
||||
* normalizeModelName('C:\\Models\\gpt-4') // Returns: 'gpt-4'
|
||||
* normalizeModelName('simple-model') // Returns: 'simple-model'
|
||||
* normalizeModelName('models/llama-3.1-8b') // Returns: 'llama-3.1-8b' (multiple slashes -> filename)
|
||||
* normalizeModelName('C:\\Models\\gpt-4') // Returns: 'gpt-4' (multiple slashes -> filename)
|
||||
* 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('') // Returns: ''
|
||||
*/
|
||||
@@ -22,6 +25,20 @@ export function normalizeModelName(modelName: string): string {
|
||||
}
|
||||
|
||||
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 normalized = candidate?.trim();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { MimeTypeApplication, MimeTypeImage } from '$lib/enums/files';
|
||||
import { MimeTypeApplication, MimeTypeImage } from '$lib/enums';
|
||||
import * as pdfjs from 'pdfjs-dist';
|
||||
|
||||
type TextContent = {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
||||
import { isTextFileByName } from './text-files';
|
||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||
import { FileTypeCategory } from '$lib/enums/files';
|
||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||
import { supportsVision } from '$lib/stores/server.svelte';
|
||||
import { FileTypeCategory } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getFileTypeCategory } from '$lib/utils';
|
||||
import { convertPDFToText } from './pdf-processing';
|
||||
|
||||
/**
|
||||
* Read a file as a data URL (base64 encoded)
|
||||
@@ -47,7 +48,10 @@ function readFileAsUTF8(file: File): Promise<string> {
|
||||
* @param files - Array of File objects to process
|
||||
* @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[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
@@ -92,11 +96,19 @@ export async function processFilesToChatUploaded(files: File[]): Promise<ChatUpl
|
||||
results.push(base);
|
||||
}
|
||||
} else if (getFileTypeCategory(file.type) === FileTypeCategory.PDF) {
|
||||
// PDFs handled later when building extras; keep metadata only
|
||||
results.push(base);
|
||||
// Extract text content from PDF for preview
|
||||
try {
|
||||
const textContent = await convertPDFToText(file);
|
||||
results.push({ ...base, textContent });
|
||||
} catch (err) {
|
||||
console.warn('Failed to extract text from PDF, adding without content:', err);
|
||||
results.push(base);
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (hasVisionSupport && !currentConfig.pdfAsImage) {
|
||||
toast.info(`You can enable parsing PDF as images with vision models.`, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MimeTypeImage } from '$lib/enums/files';
|
||||
import { MimeTypeImage } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* Convert an SVG base64 data URL to a PNG data URL
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Maps file extensions to highlight.js language identifiers
|
||||
*/
|
||||
export function getLanguageFromFilename(filename: string): string {
|
||||
const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
|
||||
|
||||
switch (extension) {
|
||||
// JavaScript / TypeScript
|
||||
case '.js':
|
||||
case '.mjs':
|
||||
case '.cjs':
|
||||
return 'javascript';
|
||||
case '.ts':
|
||||
case '.mts':
|
||||
case '.cts':
|
||||
return 'typescript';
|
||||
case '.jsx':
|
||||
return 'javascript';
|
||||
case '.tsx':
|
||||
return 'typescript';
|
||||
|
||||
// Web
|
||||
case '.html':
|
||||
case '.htm':
|
||||
return 'html';
|
||||
case '.css':
|
||||
return 'css';
|
||||
case '.scss':
|
||||
return 'scss';
|
||||
case '.less':
|
||||
return 'less';
|
||||
case '.vue':
|
||||
return 'html';
|
||||
case '.svelte':
|
||||
return 'html';
|
||||
|
||||
// Data formats
|
||||
case '.json':
|
||||
return 'json';
|
||||
case '.xml':
|
||||
return 'xml';
|
||||
case '.yaml':
|
||||
case '.yml':
|
||||
return 'yaml';
|
||||
case '.toml':
|
||||
return 'ini';
|
||||
case '.csv':
|
||||
return 'plaintext';
|
||||
|
||||
// Programming languages
|
||||
case '.py':
|
||||
return 'python';
|
||||
case '.java':
|
||||
return 'java';
|
||||
case '.kt':
|
||||
case '.kts':
|
||||
return 'kotlin';
|
||||
case '.scala':
|
||||
return 'scala';
|
||||
case '.cpp':
|
||||
case '.cc':
|
||||
case '.cxx':
|
||||
case '.c++':
|
||||
return 'cpp';
|
||||
case '.c':
|
||||
return 'c';
|
||||
case '.h':
|
||||
case '.hpp':
|
||||
return 'cpp';
|
||||
case '.cs':
|
||||
return 'csharp';
|
||||
case '.go':
|
||||
return 'go';
|
||||
case '.rs':
|
||||
return 'rust';
|
||||
case '.rb':
|
||||
return 'ruby';
|
||||
case '.php':
|
||||
return 'php';
|
||||
case '.swift':
|
||||
return 'swift';
|
||||
case '.dart':
|
||||
return 'dart';
|
||||
case '.r':
|
||||
return 'r';
|
||||
case '.lua':
|
||||
return 'lua';
|
||||
case '.pl':
|
||||
case '.pm':
|
||||
return 'perl';
|
||||
|
||||
// Shell
|
||||
case '.sh':
|
||||
case '.bash':
|
||||
case '.zsh':
|
||||
return 'bash';
|
||||
case '.bat':
|
||||
case '.cmd':
|
||||
return 'dos';
|
||||
case '.ps1':
|
||||
return 'powershell';
|
||||
|
||||
// Database
|
||||
case '.sql':
|
||||
return 'sql';
|
||||
|
||||
// Markup / Documentation
|
||||
case '.md':
|
||||
case '.markdown':
|
||||
return 'markdown';
|
||||
case '.tex':
|
||||
case '.latex':
|
||||
return 'latex';
|
||||
case '.adoc':
|
||||
case '.asciidoc':
|
||||
return 'asciidoc';
|
||||
|
||||
// Config
|
||||
case '.ini':
|
||||
case '.cfg':
|
||||
case '.conf':
|
||||
return 'ini';
|
||||
case '.dockerfile':
|
||||
return 'dockerfile';
|
||||
case '.nginx':
|
||||
return 'nginx';
|
||||
|
||||
// Other
|
||||
case '.graphql':
|
||||
case '.gql':
|
||||
return 'graphql';
|
||||
case '.proto':
|
||||
return 'protobuf';
|
||||
case '.diff':
|
||||
case '.patch':
|
||||
return 'diff';
|
||||
case '.log':
|
||||
return 'plaintext';
|
||||
case '.txt':
|
||||
return 'plaintext';
|
||||
|
||||
default:
|
||||
return 'plaintext';
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DEFAULT_BINARY_DETECTION_OPTIONS,
|
||||
type BinaryDetectionOptions
|
||||
} from '$lib/constants/binary-detection';
|
||||
import { FileExtensionText } from '$lib/enums/files';
|
||||
import { FileExtensionText } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* Check if a filename indicates a text file based on its extension
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FileExtensionImage, MimeTypeImage } from '$lib/enums/files';
|
||||
import { FileExtensionImage, MimeTypeImage } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* Convert a WebP base64 data URL to a PNG data URL
|
||||
|
||||
Reference in New Issue
Block a user