diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index 260fdcfecaee8..d7363e13eb25a 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/components/useChatExtraContext.tsx b/tools/server/webui/src/components/useChatExtraContext.tsx
index 866401db9a7f2..7eeff61f5e088 100644
--- a/tools/server/webui/src/components/useChatExtraContext.tsx
+++ b/tools/server/webui/src/components/useChatExtraContext.tsx
@@ -37,19 +37,26 @@ export function useChatExtraContext(): ChatExtraContextApi {
break;
}
- if (mimeType.startsWith('image/') && mimeType !== 'image/svg+xml') {
- if (!serverProps?.has_multimodal) {
+ if (mimeType.startsWith('image/')) {
+ if (!serverProps?.modalities?.vision) {
toast.error('Multimodal is not supported by this server or model.');
break;
}
const reader = new FileReader();
- reader.onload = (event) => {
+ reader.onload = async (event) => {
if (event.target?.result) {
+ let base64Url = event.target.result as string;
+
+ if (mimeType === 'image/svg+xml') {
+ // Convert SVG to PNG
+ base64Url = await svgBase64UrlToPngDataURL(base64Url);
+ }
+
addItems([
{
type: 'imageFile',
name: file.name,
- base64Url: event.target.result as string,
+ base64Url,
},
]);
}
@@ -172,3 +179,56 @@ export function isLikelyNotBinary(str: string): boolean {
const ratio = suspiciousCharCount / sampleLength;
return ratio <= options.suspiciousCharThresholdRatio;
}
+
+// WARN: vibe code below
+// Converts a Base64URL encoded SVG string to a PNG Data URL using browser Canvas API.
+function svgBase64UrlToPngDataURL(base64UrlSvg: string): Promise {
+ const backgroundColor = 'white'; // Default background color for PNG
+
+ return new Promise((resolve, reject) => {
+ try {
+ const img = new Image();
+
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ if (!ctx) {
+ reject(new Error('Failed to get 2D canvas context.'));
+ return;
+ }
+
+ // Use provided dimensions or SVG's natural dimensions, with fallbacks
+ // Fallbacks (e.g., 300x300) are for SVGs without explicit width/height
+ // or when naturalWidth/Height might be 0 before full processing.
+ const targetWidth = img.naturalWidth || 300;
+ const targetHeight = img.naturalHeight || 300;
+
+ canvas.width = targetWidth;
+ canvas.height = targetHeight;
+
+ if (backgroundColor) {
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
+ resolve(canvas.toDataURL('image/png'));
+ };
+
+ img.onerror = () => {
+ reject(
+ new Error('Failed to load SVG image. Ensure the SVG data is valid.')
+ );
+ };
+
+ // Load SVG string into an Image element
+ img.src = base64UrlSvg;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ const errorMessage = `Error converting SVG to PNG: ${message}`;
+ toast.error(errorMessage);
+ reject(new Error(errorMessage));
+ }
+ });
+}
diff --git a/tools/server/webui/src/utils/types.ts b/tools/server/webui/src/utils/types.ts
index add48be4cd2c1..ba673dd9432da 100644
--- a/tools/server/webui/src/utils/types.ts
+++ b/tools/server/webui/src/utils/types.ts
@@ -118,6 +118,8 @@ export interface LlamaCppServerProps {
build_info: string;
model_path: string;
n_ctx: number;
- has_multimodal: boolean;
+ modalities?: {
+ vision: boolean;
+ };
// TODO: support params
}