From 55e01f137ebe5090a3db1f4518463638e47a1be0 Mon Sep 17 00:00:00 2001 From: techjarves Date: Sun, 21 Jun 2026 09:09:40 +0530 Subject: [PATCH] feat: add portable backend manager script and implement text chat UI component --- app/frontend/src/components/TextChat.jsx | 3 ++- scripts/serve.cjs | 18 +++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/frontend/src/components/TextChat.jsx b/app/frontend/src/components/TextChat.jsx index cb85501..9b08da8 100644 --- a/app/frontend/src/components/TextChat.jsx +++ b/app/frontend/src/components/TextChat.jsx @@ -169,6 +169,7 @@ function TextChat({ const fileInputRef = useRef(null); const supportsVision = Boolean(status.ready && status.settings?.supportsVision); const supportsThinking = Boolean(status.ready && status.settings?.supportsThinking); + const visionStatus = status.settings?.visionStatus || "Image input requires a matching mmproj projector file."; const resizeComposerInput = useCallback(() => { const textarea = textareaRef.current; @@ -1040,7 +1041,7 @@ function TextChat({ className="chat-composer-attach-btn" onClick={() => fileInputRef.current?.click()} disabled={!supportsVision || isBusy} - title={supportsVision ? "Attach files or images" : "Image attachment requires a vision model or matching projector"} + title={supportsVision ? "Attach files or images" : visionStatus} style={{ marginBottom: "2px" }} > diff --git a/scripts/serve.cjs b/scripts/serve.cjs index 88efdb6..769d17a 100644 --- a/scripts/serve.cjs +++ b/scripts/serve.cjs @@ -140,6 +140,7 @@ let llmSettings = { backendBinary: "", supportsVision: false, visionMode: "none", + visionStatus: "Projector not loaded", flashAttn: true, cacheTypeK: "q8_0", cacheTypeV: "q8_0", @@ -1385,6 +1386,7 @@ function killLlm() { llmReady = false; llmSettings.supportsVision = false; llmSettings.visionMode = "none"; + llmSettings.visionStatus = "Projector not loaded"; llmSettings.mmproj = null; if (!llmProc) { resolve(); @@ -2476,14 +2478,6 @@ function chooseAutoContext(modelFilename, isGpu) { return 2048; } -function modelHasEmbeddedVisionSupport(filename) { - const lower = String(filename || "").toLowerCase(); - return /gemma[-_]?4/.test(lower) || - /gemma[-_]?3n/.test(lower) || - /(?:^|[-_])e[24]b(?:[-_]|$)/.test(lower) || - /paligemma/.test(lower); -} - async function startLlm(settings = {}) { const filename = path.basename(String(settings.model || "")); const modelPath = path.join(LLM_MODELS, filename); @@ -2550,7 +2544,6 @@ async function startLlm(settings = {}) { let mmprojPath = null; const lowerFilename = filename.toLowerCase(); - const hasEmbeddedVision = modelHasEmbeddedVisionSupport(filename); const isMultimodal = lowerFilename.includes("llava") || lowerFilename.includes("vision") || lowerFilename.includes("qwen2vl") || @@ -2666,9 +2659,12 @@ async function startLlm(settings = {}) { if (mmprojPath) { args.push("--mmproj", mmprojPath); } - llmSettings.supportsVision = Boolean(mmprojPath || hasEmbeddedVision); + llmSettings.supportsVision = Boolean(mmprojPath); llmSettings.mmproj = mmprojPath ? path.basename(mmprojPath) : null; - llmSettings.visionMode = mmprojPath ? "mmproj" : (hasEmbeddedVision ? "embedded" : "none"); + llmSettings.visionMode = mmprojPath ? "mmproj" : "none"; + llmSettings.visionStatus = mmprojPath + ? `Using projector ${path.basename(mmprojPath)}` + : (isMultimodal ? "Matching mmproj projector not found" : "Model is text-only"); const spawnEnv = { ...process.env }; const backendDir = path.dirname(backend.path);