From 43a85f8d24dcfee7dfb41f1ac75940a3aad8c4b8 Mon Sep 17 00:00:00 2001 From: techjarves Date: Sun, 14 Jun 2026 17:14:20 +0530 Subject: [PATCH] add cross-platform setup scripts and frontend boilerplate for local AI image generation --- .gitignore | 44 +-- README.md | 12 +- app/frontend/src/App.css | 233 ++++++++++++++++ app/frontend/src/App.jsx | 9 + app/frontend/src/components/Sidebar.jsx | 10 +- app/frontend/src/components/TextChat.jsx | 271 ++++++++++++++++++ app/frontend/src/services/api.js | 83 +++++- linux.sh | 6 + mac.sh | 9 + scripts/reset.ps1 | 51 ++-- scripts/reset.sh | 45 +-- scripts/serve.cjs | 340 ++++++++++++++++++++++- scripts/setup-llama.ps1 | 42 +++ scripts/setup-llama.sh | 55 ++++ scripts/setup.ps1 | 13 +- scripts/setup.sh | 9 +- windows.bat | 7 + 17 files changed, 1163 insertions(+), 76 deletions(-) create mode 100644 app/frontend/src/components/TextChat.jsx create mode 100644 scripts/setup-llama.ps1 create mode 100644 scripts/setup-llama.sh diff --git a/.gitignore b/.gitignore index 291422d..d2265da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,39 @@ -# Ignore large Stable Diffusion model weights (exceeds GitHub file limits) -app/models/*.safetensors -app/models/*.gguf -app/models/*.ckpt -app/models +# Downloaded AI models +app/models/ app/openvino-models/ +app/llm-models/ + +# Downloaded portable runtimes/backends +app/backend/ +app/llm-backend/ +app/tools/ + +# Generated files +app/dist/ app/outputs/ -# Ignore generated frontend build output -app/dist/ - -# Ignore portable Node.js binaries and packages -app/tools/ +# Dependencies and platform-specific copies **/node_modules/ -**/node_modules_mac/ +**/node_modules_*/ +**/node_modules_windows/ **/node_modules_linux/ -node_modules/ +**/node_modules_mac/ **/.active_modules_os -# Ignore backend folder completely (contains massive DLLs, EXEs, and ZIPs >1GB) -app/backend/ +# Partial downloads and archives +*.part +*.tmp +app/tools/*.zip +app/tools/*.tar.gz +app/tools/*.tar.xz -# Ignore local IDE and OS temp files +# Logs and caches +*.log +__pycache__/ +*.pyc + +# OS/editor files .vscode/ .idea/ .DS_Store Thumbs.db -*.log -*.tmp diff --git a/README.md b/README.md index 6fb8d55..ed7a944 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # 🖼️ Local AI Image Generator -### An easy, zero-setup Stable Diffusion GUI for Windows, Linux, and macOS. Run GGUF & Safetensors models offline without Python configuration. +### An easy, zero-setup local image and text AI studio for Windows, Linux, and macOS. + +## Text Chat + +The Text Chat workspace runs official `llama.cpp` binaries and keeps language models separate from image models: + +- Image models: `app/models` +- Text GGUF models: `app/llm-models` +- Portable llama.cpp binaries: `app/llm-backend/` + +The normal platform setup installs both image and text backends. Text and image engines are mutually exclusive by default to avoid exhausting shared RAM or VRAM. A small Qwen2.5 Coder starter model is available from the Text Chat download panel. diff --git a/app/frontend/src/App.css b/app/frontend/src/App.css index e592830..dc26e98 100644 --- a/app/frontend/src/App.css +++ b/app/frontend/src/App.css @@ -219,6 +219,239 @@ body { overflow: hidden; } +.text-chat-layout { + flex: 1; + display: grid; + grid-template-columns: minmax(0, 1fr) 380px; + gap: 18px; + padding: 20px; + min-height: 0; +} + +.text-chat-main, +.text-chat-settings { + min-height: 0; +} + +.text-chat-main { + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--border-color); + border-radius: var(--md-shape-corner-large); + background: var(--md-sys-color-surface); + box-shadow: var(--glass-shadow); +} + +.text-chat-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 20px; + border-bottom: 1px solid var(--border-color); +} + +.text-chat-header h2 { + display: flex; + align-items: center; + gap: 9px; + font-size: 1.2rem; +} + +.text-chat-header p, +.chat-empty p { + margin-top: 5px; + color: var(--md-sys-color-on-surface-variant); + font-size: 0.86rem; +} + +.text-engine-badge { + max-width: 240px; + padding: 7px 10px; + border-radius: var(--md-shape-corner-full); + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface-variant); + font-size: 0.75rem; + text-align: center; +} + +.text-engine-badge.ready { + color: var(--md-sys-color-success); + border: 1px solid color-mix(in srgb, var(--md-sys-color-success) 45%, transparent); +} + +.chat-messages { + flex: 1; + overflow: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.chat-empty { + margin: auto; + max-width: 440px; + text-align: center; + color: var(--md-sys-color-on-surface-variant); +} + +.chat-empty h3 { + margin-top: 10px; + color: var(--md-sys-color-on-surface); +} + +.chat-message { + align-self: flex-start; + max-width: min(780px, 88%); + padding: 12px 14px; + border-radius: 4px var(--md-shape-corner-large) var(--md-shape-corner-large) var(--md-shape-corner-large); + background: var(--md-sys-color-surface-variant); + line-height: 1.55; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.chat-message.user { + align-self: flex-end; + border-radius: var(--md-shape-corner-large) 4px var(--md-shape-corner-large) var(--md-shape-corner-large); + background: var(--md-sys-color-primary-container); +} + +.chat-message.error { + color: var(--md-sys-color-error); +} + +.chat-message strong { + display: block; + margin-bottom: 5px; + color: var(--md-sys-color-primary); + font-size: 0.78rem; +} + +.chat-thinking, +.text-progress { + display: flex; + align-items: center; + gap: 7px; + color: var(--md-sys-color-on-surface-variant); + font-size: 0.8rem; +} + +.text-progress.error { + color: var(--md-sys-color-error); +} + +.chat-composer { + display: flex; + gap: 10px; + padding: 14px; + border-top: 1px solid var(--border-color); +} + +.chat-composer textarea, +.system-prompt, +.text-chat-settings input, +.text-chat-settings select { + width: 100%; + border: 1px solid var(--border-color); + border-radius: var(--md-shape-corner-medium); + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface); + padding: 10px 12px; + font: inherit; + outline: none; +} + +.chat-composer textarea { + min-height: 48px; + max-height: 160px; + resize: vertical; +} + +.text-chat-settings { + display: flex; + flex-direction: column; + gap: 14px; + overflow: auto; + padding-right: 3px; +} + +.text-chat-settings .m3-card { + padding: 16px; +} + +.text-model-actions, +.text-url-row { + display: flex; + gap: 8px; + margin-top: 10px; +} + +.text-local-model, +.text-download-option { + width: 100%; + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 9px 10px; + border: 1px solid var(--border-color); + border-radius: var(--md-shape-corner-small); + background: transparent; + color: var(--md-sys-color-on-surface); + text-align: left; +} + +.text-local-model span, +.text-download-option span { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.text-local-model button { + border: 0; + background: transparent; + color: var(--md-sys-color-error); + cursor: pointer; +} + +.text-download-option { + cursor: pointer; +} + +.text-download-option:hover { + background: var(--md-sys-color-surface-variant); +} + +.text-download-option small { + color: var(--md-sys-color-on-surface-variant); +} + +.system-prompt { + min-height: 85px; + resize: vertical; + margin-top: 9px; +} + +@media (max-width: 1050px) { + .text-chat-layout { + grid-template-columns: 1fr; + overflow: auto; + } + + .text-chat-main { + min-height: 620px; + } + + .text-chat-settings { + overflow: visible; + } +} + /* Persistent Top Status Bar */ .top-status-bar { height: 64px; diff --git a/app/frontend/src/App.jsx b/app/frontend/src/App.jsx index fcb9f9d..1589964 100644 --- a/app/frontend/src/App.jsx +++ b/app/frontend/src/App.jsx @@ -4,6 +4,7 @@ import TopStatusBar from "./components/TopStatusBar"; import Generator from "./components/Generator"; import ModelManager from "./components/ModelManager"; import ImageConstraints from "./components/ImageConstraints"; +import TextChat from "./components/TextChat"; import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, stopServer } from "./services/api"; import "./App.css"; @@ -476,6 +477,14 @@ function App() { /> +
+ +
+
Image Generator
+
setActiveTab("chat")} + > + + Text Chat +
+
setActiveTab("models")} diff --git a/app/frontend/src/components/TextChat.jsx b/app/frontend/src/components/TextChat.jsx new file mode 100644 index 0000000..ac5f4e1 --- /dev/null +++ b/app/frontend/src/components/TextChat.jsx @@ -0,0 +1,271 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Bot, Download, LoaderCircle, MessageSquare, Play, Send, Square, Trash2, Upload } from "lucide-react"; +import { + chatWithLlm, + deleteLlmModel, + downloadLlmModel, + getDownloadProgress, + getLlmStatus, + importLlmModel, + listLlmModels, + startLlm, + stopLlm, +} from "../services/api"; + +const RECOMMENDED_MODELS = [ + { + name: "Qwen2.5 Coder 0.5B Instruct Q4_K_M", + size: "491 MB", + url: "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf", + }, + { + name: "SmolLM2 1.7B Instruct Q4_K_M", + size: "1.1 GB", + url: "https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF/resolve/main/smollm2-1.7b-instruct-q4_k_m.gguf", + }, +]; + +function TextChat({ specs, showAlert, showConfirm }) { + const [models, setModels] = useState([]); + const [status, setStatus] = useState({ ready: false, running: false, settings: {} }); + const [selectedModel, setSelectedModel] = useState(""); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [systemPrompt, setSystemPrompt] = useState("You are a helpful local AI assistant."); + const [isBusy, setIsBusy] = useState(false); + const [download, setDownload] = useState(null); + const [customUrl, setCustomUrl] = useState(""); + const [importProgress, setImportProgress] = useState(null); + const fileInputRef = useRef(null); + const bottomRef = useRef(null); + const completedDownloadRef = useRef(""); + + const refresh = useCallback(async () => { + const [nextModels, nextStatus] = await Promise.all([listLlmModels(), getLlmStatus()]); + setModels(nextModels); + setStatus(nextStatus); + const active = nextStatus.settings?.model; + setSelectedModel((current) => active || current || nextModels[0]?.filename || ""); + }, []); + + useEffect(() => { + refresh().catch(() => {}); + const timer = setInterval(() => { + getLlmStatus().then(setStatus).catch(() => {}); + getDownloadProgress().then((state) => { + if (state.kind === "text" && (state.active || state.error || state.progress === 100)) { + setDownload(state); + const completionKey = `${state.filename || ""}:${state.downloadedBytes || 0}`; + if (!state.active && !state.error && completedDownloadRef.current !== completionKey) { + completedDownloadRef.current = completionKey; + refresh().catch(() => {}); + } + } + }).catch(() => {}); + }, 1500); + return () => clearInterval(timer); + }, [refresh]); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, isBusy]); + + const loadModel = async () => { + if (!selectedModel || isBusy) return; + setIsBusy(true); + try { + const result = await startLlm(selectedModel, { + threads: specs.cpu_cores_physical || 4, + contextSize: 4096, + gpuLayers: -1, + }); + setStatus({ ...status, ...result, ready: true, running: true, settings: result.settings }); + } catch (err) { + showAlert({ title: "Text Model Load Failed", message: err.message, danger: true }); + } finally { + setIsBusy(false); + } + }; + + const unloadModel = async () => { + setIsBusy(true); + try { + await stopLlm(); + setStatus((current) => ({ ...current, ready: false, running: false })); + } finally { + setIsBusy(false); + } + }; + + const sendMessage = async () => { + const text = input.trim(); + if (!text || isBusy || !status.ready) return; + const nextMessages = [...messages, { role: "user", content: text }]; + setMessages(nextMessages); + setInput(""); + setIsBusy(true); + try { + const requestMessages = [ + ...(systemPrompt.trim() ? [{ role: "system", content: systemPrompt.trim() }] : []), + ...nextMessages, + ]; + const content = await chatWithLlm(requestMessages, { temperature: 0.7, maxTokens: 768 }); + setMessages([...nextMessages, { role: "assistant", content }]); + } catch (err) { + setMessages([...nextMessages, { role: "assistant", content: `Error: ${err.message}`, error: true }]); + } finally { + setIsBusy(false); + } + }; + + const startDownload = async (url) => { + if (!url || download?.active) return; + try { + const result = await downloadLlmModel(url); + setDownload({ active: true, filename: result.filename, progress: 0, kind: "text" }); + setCustomUrl(""); + } catch (err) { + showAlert({ title: "Text Model Download Failed", message: err.message, danger: true }); + } + }; + + const importFile = async (event) => { + const file = event.target.files?.[0]; + event.target.value = ""; + if (!file) return; + if (!file.name.toLowerCase().endsWith(".gguf")) { + showAlert({ title: "Unsupported File", message: "Text models must use the .gguf format.", danger: true }); + return; + } + try { + await importLlmModel(file, (progress) => setImportProgress(progress)); + setImportProgress(null); + await refresh(); + setSelectedModel(file.name); + } catch (err) { + setImportProgress(null); + showAlert({ title: "Text Model Import Failed", message: err.message, danger: true }); + } + }; + + const removeModel = async (filename) => { + const confirmed = await showConfirm({ + title: "Delete Text Model?", + message: `Delete "${filename}" from app/llm-models?`, + confirmLabel: "Delete", + danger: true, + }); + if (!confirmed) return; + await deleteLlmModel(filename); + if (selectedModel === filename) setSelectedModel(""); + await refresh(); + }; + + return ( +
+
+
+
+

Text Chat

+

Private chat powered by llama.cpp and local GGUF models.

+
+ + {status.ready ? `${status.settings?.backendMode || status.backendMode || "llama.cpp"} ready` : status.backendInstalled ? "Backend installed" : "Run setup to install llama.cpp"} + +
+ +
+ {messages.length === 0 && ( +
+ +

Local text generation

+

Choose a GGUF model, load it, and start chatting. Loading text automatically unloads the image engine.

+
+ )} + {messages.map((message, index) => ( +
+ {message.role === "user" ? "You" : "Local AI"} +
{message.content}
+
+ ))} + {isBusy && status.ready &&
Generating...
} +
+
+ +
+