From 2e24396b35b5d37665c114672839bf3e61bef6ed Mon Sep 17 00:00:00 2001 From: techjarves Date: Mon, 15 Jun 2026 06:41:15 +0530 Subject: [PATCH] feat: add static file server, backend process manager, and base frontend components for local AI image generation --- app/frontend/src/components/ModelManager.jsx | 137 +++++-- app/frontend/src/components/TextChat.jsx | 239 +++++++++++-- scripts/serve.cjs | 353 +++++++++++++++++-- 3 files changed, 657 insertions(+), 72 deletions(-) diff --git a/app/frontend/src/components/ModelManager.jsx b/app/frontend/src/components/ModelManager.jsx index 1c0f44c..e62d14e 100644 --- a/app/frontend/src/components/ModelManager.jsx +++ b/app/frontend/src/components/ModelManager.jsx @@ -35,6 +35,7 @@ const MODEL_LIBRARY = [ resolution: "1024x1024", notes: "Photorealistic SDXL model tuned for 4-8 step Lightning generation.", url: "https://huggingface.co/RunDiffusion/Juggernaut-XL-Lightning/resolve/main/Juggernaut_RunDiffusionPhoto2_Lightning_4Steps.safetensors", + recommendedTiers: ["Mid", "High"], }, { name: "DreamShaper XL Lightning", @@ -44,6 +45,7 @@ const MODEL_LIBRARY = [ resolution: "1024x1024", notes: "All-around SDXL model for fantasy art, renders, illustration, and stylized images.", url: "https://huggingface.co/Lykon/dreamshaper-xl-lightning/resolve/main/DreamShaperXL_Lightning.safetensors", + recommendedTiers: ["Mid", "High"], }, ], }, @@ -58,6 +60,7 @@ const MODEL_LIBRARY = [ resolution: "512x512", notes: "General-purpose SD 1.5 model for illustration, anime, and semi-realistic portraits.", url: "https://huggingface.co/Lykon/DreamShaper/resolve/main/DreamShaper_8_pruned.safetensors", + recommendedTiers: ["Low"], }, { name: "CyberRealistic V8", @@ -67,6 +70,7 @@ const MODEL_LIBRARY = [ resolution: "512x512", notes: "High-fidelity realism and photorealism in the SD 1.5 ecosystem.", url: "https://huggingface.co/cyberdelia/CyberRealistic/resolve/main/CyberRealistic_V8_FP16.safetensors", + recommendedTiers: ["Low"], }, { name: "ReV Animated v1.2.2", @@ -76,6 +80,7 @@ const MODEL_LIBRARY = [ resolution: "512x512", notes: "Semi-realistic, 2.5D, anime, fantasy, and stylized digital art.", url: "https://huggingface.co/danbrown/RevAnimated-v1-2-2/resolve/main/rev-animated-v1-2-2.safetensors", + recommendedTiers: ["Low"], }, ], }, @@ -93,6 +98,7 @@ const TEXT_MODEL_LIBRARY = [ resolution: "N/A", notes: "Extremely fast, lightweight assistant, perfect for low RAM/VRAM machines.", url: "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf", + recommendedTiers: ["Low"], }, { name: "SmolLM2 1.7B Instruct Q4_K_M", @@ -102,6 +108,42 @@ const TEXT_MODEL_LIBRARY = [ resolution: "N/A", notes: "Excellent lightweight assistant with strong logic, reasoning, and prompt expansion capabilities.", url: "https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF/resolve/main/smollm2-1.7b-instruct-q4_k_m.gguf", + recommendedTiers: ["Low"], + }, + { + name: "Qwen2.5-Coder-7B-Instruct", + filename: "qwen2.5-coder-7b-instruct-q4_k_m.gguf", + format: "GGUF", + size: "4.7 GB", + approxSize: "4.7 GB", + resolution: "N/A", + notes: "Highly intelligent coding and text assistant. Recommended for mid/high tier systems.", + url: "https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_k_m.gguf", + recommendedTiers: ["Mid", "High"], + }, + { + name: "Llama-3.1-8B-Instruct", + filename: "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", + format: "GGUF", + size: "4.9 GB", + approxSize: "4.9 GB", + resolution: "N/A", + notes: "Excellent general-purpose text model. Recommended for high-tier systems.", + url: "https://huggingface.co/lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", + recommendedTiers: ["High"], + }, + { + name: "LLaVA 1.5 7B", + filename: "ggml-model-q4_k.gguf", + format: "GGUF", + size: "4.5 GB", + approxSize: "4.5 GB", + resolution: "N/A", + notes: "Multimodal model capable of understanding images. Downloads companion vision projector automatically.", + url: "https://huggingface.co/mys/ggml_llava-v1.5-7b/resolve/main/ggml-model-q4_k.gguf", + projectorUrl: "https://huggingface.co/mys/ggml_llava-v1.5-7b/resolve/main/mmproj-model-f16.gguf", + projectorFilename: "mmproj-model-f16.gguf", + recommendedTiers: ["Mid", "High"], }, ], }, @@ -143,6 +185,20 @@ const COREML_MODEL_LIBRARY = [ }, ]; +const getHardwareTier = (specs) => { + if (!specs) return "Low"; + const ram = Number(specs.ram_total_gb) || 0; + const vram = Number(specs.gpu_vram_gb) || 0; + + if (vram >= 12 || (vram >= 8 && ram >= 32)) { + return "High"; + } else if (vram >= 6 || ram >= 16) { + return "Mid"; + } else { + return "Low"; + } +}; + function ModelManager({ activeModel, setActiveModel, @@ -288,32 +344,6 @@ function ModelManager({ } }; - const startProgressPolling = (modelId) => { - setDownloadingModelId(modelId); - - const interval = setInterval(async () => { - try { - const status = await getDownloadProgress(); - if (status.active) { - setDownloadProgress(status.progress === -1 ? 0 : status.progress); - setDownloadSpeed(`${status.speed} • ETA ${status.eta}s`); - } else { - clearInterval(interval); - setDownloadingModelId(null); - if (status.error) { - if (!String(status.error).toLowerCase().includes("cancelled")) { - showAlert({ title: "Download Failed", message: status.error, danger: true }); - } - } else { - fetchModels(); - } - } - } catch (e) { - console.error("Polling error:", e); - } - }, 1000); - }; - const downloadByUrl = async (url, expectedFilename) => { if (downloadingModelId) return; @@ -352,6 +382,40 @@ function ModelManager({ } }; + const startProgressPolling = (modelId) => { + setDownloadingModelId(modelId); + + const interval = setInterval(async () => { + try { + const status = await getDownloadProgress(); + if (status.active) { + setDownloadProgress(status.progress === -1 ? 0 : status.progress); + setDownloadSpeed(`${status.speed} • ETA ${status.eta}s`); + } else { + clearInterval(interval); + setDownloadingModelId(null); + if (status.error) { + if (!String(status.error).toLowerCase().includes("cancelled")) { + showAlert({ title: "Download Failed", message: status.error, danger: true }); + } + } else { + fetchModels(); + if (modelId === "ggml-model-q4_k.gguf") { + setTimeout(() => { + downloadByUrl( + "https://huggingface.co/mys/ggml_llava-v1.5-7b/resolve/main/mmproj-model-f16.gguf", + "mmproj-model-f16.gguf" + ); + }, 500); + } + } + } + } catch (e) { + console.error("Polling error:", e); + } + }, 1000); + }; + const handleUrlDownloadSubmit = async (e) => { e.preventDefault(); if (!downloadUrl.trim()) return; @@ -809,6 +873,8 @@ function ModelManager({ + + {/* Active Model Status Tonal Box */} {activeModelType === "image" && activeModel && (
@@ -1006,10 +1072,27 @@ function ModelManager({ {section.items.map((model) => { const installed = modelNames.includes(model.filename); const downloading = downloadingModelId === model.filename || downloadingModelId === `${model.filename}.zip`; + const systemTier = getHardwareTier(specs); + const isRecommended = model.recommendedTiers && model.recommendedTiers.includes(systemTier); return (
- {model.name} + + {model.name} + {isRecommended && ( + + [Recommended Fit] + + )} + {model.format} • approx. {model.approxSize} {model.resolution !== "N/A" && `• ${model.resolution}`} diff --git a/app/frontend/src/components/TextChat.jsx b/app/frontend/src/components/TextChat.jsx index c03edc3..a697dfa 100644 --- a/app/frontend/src/components/TextChat.jsx +++ b/app/frontend/src/components/TextChat.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Bot, LoaderCircle, Send, Trash2, Square, History, Plus } from "lucide-react"; +import { Bot, LoaderCircle, Send, Trash2, Square, History, Plus, Paperclip, X } from "lucide-react"; import { chatWithLlm, getDownloadProgress, @@ -30,6 +30,64 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings const completedDownloadRef = useRef(""); const loadingModelRef = useRef(null); + const [attachments, setAttachments] = useState([]); + const fileInputRef = useRef(null); + + const isImage = (file) => { + return /\.(jpe?g|png|webp)$/i.test(file.name) || file.type.startsWith("image/"); + }; + + const isTextFile = (file) => { + return /\.(txt|md|csv|js|jsx|ts|tsx|py|json|css|html|java|cpp|c|h|rs|go|sh|bat|xml|yaml|yml)$/i.test(file.name) || file.type.startsWith("text/"); + }; + + const handleFileChange = (e) => { + const files = Array.from(e.target.files); + if (!files || files.length === 0) return; + + files.forEach((file) => { + if (isImage(file)) { + const reader = new FileReader(); + reader.onload = (event) => { + setAttachments((prev) => [ + ...prev, + { + id: Math.random().toString(36).substring(7), + file, + type: "image", + name: file.name, + dataUrl: event.target.result, + }, + ]); + }; + reader.readAsDataURL(file); + } else if (isTextFile(file)) { + const reader = new FileReader(); + reader.onload = (event) => { + setAttachments((prev) => [ + ...prev, + { + id: Math.random().toString(36).substring(7), + file, + type: "document", + name: file.name, + content: event.target.result, + }, + ]); + }; + reader.readAsText(file); + } else { + showAlert({ + title: "Unsupported File", + message: `File "${file.name}" is not supported. Please select an image (JPEG, PNG, WEBP) or a text/code file.`, + danger: true, + }); + } + }); + + e.target.value = ""; + }; + useEffect(() => { loadingModelRef.current = loadingModel; }, [loadingModel]); @@ -179,7 +237,12 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings setSelectedModel(conv.model); } // Try to compute approximate token usage from messages - const total = conv.messages.reduce((sum, m) => sum + m.content.split(/\s+/).length, 0); + const total = conv.messages.reduce((sum, m) => { + const text = Array.isArray(m.content) + ? m.content.map(c => c.text || "").join(" ") + : (m.content || ""); + return sum + text.split(/\s+/).length; + }, 0); setTokenUsage({ prompt_tokens: Math.round(total * 0.7), completion_tokens: Math.round(total * 0.3), @@ -204,7 +267,8 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings const sendMessage = async () => { const text = input.trim(); - if (!text || isBusy || !status.ready) return; + const hasAttachments = attachments.length > 0; + if ((!text && !hasAttachments) || isBusy || !status.ready) return; let convId = activeConversationId; let isNew = false; @@ -214,12 +278,40 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings isNew = true; } - const nextMessages = [...messages, { role: "user", content: text }]; + // Delimited context blocks for document attachments + let documentContext = ""; + attachments.filter(att => att.type === "document").forEach((att) => { + documentContext += `\n[Attached File: ${att.name}]\n${att.content}\n`; + }); + + const combinedText = documentContext ? `${text}\n\n${documentContext}`.trim() : text; + const imageAttachments = attachments.filter(att => att.type === "image"); + + let userMessageContent; + if (imageAttachments.length > 0) { + userMessageContent = [ + { + type: "text", + text: combinedText + }, + ...imageAttachments.map((img) => ({ + type: "image_url", + image_url: { + url: img.dataUrl + } + })) + ]; + } else { + userMessageContent = combinedText; + } + + const nextMessages = [...messages, { role: "user", content: userMessageContent }]; setMessages(nextMessages); setInput(""); setIsBusy(true); - const firstTitle = isNew ? (text.slice(0, 26) + (text.length > 26 ? "..." : "")) : null; + const displayTitleText = text || (imageAttachments.length > 0 ? "Sent Image" : "Sent File"); + const firstTitle = isNew ? (displayTitleText.slice(0, 26) + (displayTitleText.length > 26 ? "..." : "")) : null; saveConversationState(convId, nextMessages, selectedModel, firstTitle); try { @@ -238,6 +330,9 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings setTokenUsage(response.usage); } saveConversationState(convId, finalMessages, selectedModel); + + // Clean up attached files on success + setAttachments([]); } catch (err) { const finalMessages = [...nextMessages, { role: "assistant", content: `Error: ${err.message}`, error: true }]; setMessages(finalMessages); @@ -510,7 +605,30 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings {messages.map((message, index) => (
{message.role === "user" ? "You" : "Local AI"} -
{message.content}
+
+ {Array.isArray(message.content) ? ( +
+ {message.content.map((item, idx) => { + if (item.type === "text") { + return {item.text}; + } + if (item.type === "image_url") { + return ( + Attached image + ); + } + return null; + })} +
+ ) : ( + {message.content} + )} +
))} {isBusy && status.ready &&
Generating...
} @@ -519,22 +637,99 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings
-
-