diff --git a/app/frontend/src/App.css b/app/frontend/src/App.css index dc26e98..ab7aa7c 100644 --- a/app/frontend/src/App.css +++ b/app/frontend/src/App.css @@ -222,8 +222,8 @@ body { .text-chat-layout { flex: 1; display: grid; - grid-template-columns: minmax(0, 1fr) 380px; - gap: 18px; + grid-template-columns: minmax(0, 1fr); + gap: 0px; padding: 20px; min-height: 0; } diff --git a/app/frontend/src/App.jsx b/app/frontend/src/App.jsx index 1589964..d354dc9 100644 --- a/app/frontend/src/App.jsx +++ b/app/frontend/src/App.jsx @@ -3,7 +3,7 @@ import Sidebar from "./components/Sidebar"; import TopStatusBar from "./components/TopStatusBar"; import Generator from "./components/Generator"; import ModelManager from "./components/ModelManager"; -import ImageConstraints from "./components/ImageConstraints"; +import Settings from "./components/Settings"; import TextChat from "./components/TextChat"; import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, stopServer } from "./services/api"; import "./App.css"; @@ -114,6 +114,25 @@ function App() { backendType: "auto", }); + const [textSettings, setTextSettings] = useState(() => { + const saved = localStorage.getItem("textSettings"); + if (saved) { + try { + return JSON.parse(saved); + } catch (_) {} + } + return { + contextSize: 4096, + temperature: 0.7, + systemPrompt: "You are a helpful local AI assistant.", + threads: 4 + }; + }); + + useEffect(() => { + localStorage.setItem("textSettings", JSON.stringify(textSettings)); + }, [textSettings]); + // Load hardware specifications on mount useEffect(() => { async function loadSpecs() { @@ -129,6 +148,10 @@ function App() { backendType: prev.backendType === "auto" ? backendInfo.defaultBackendType || "cpu" : prev.backendType, useGpu: (prev.backendType === "auto" ? backendInfo.defaultBackendType : prev.backendType) !== "cpu", })); + setTextSettings((prev) => ({ + ...prev, + threads: prev.threads || hardware.cpu_cores_physical || 4 + })); } catch (err) { console.error("Error fetching hardware specs:", err); } @@ -458,6 +481,8 @@ function App() { setActiveTab={setActiveTab} showAlert={showAlert} showConfirm={showConfirm} + specs={specs} + textSettings={textSettings} /> @@ -474,6 +499,8 @@ function App() { showAlert={showAlert} showConfirm={showConfirm} activeTab={activeTab} + specs={specs} + textSettings={textSettings} /> @@ -482,11 +509,15 @@ function App() { specs={specs} showAlert={showAlert} showConfirm={showConfirm} + textSettings={textSettings} + setTextSettings={setTextSettings} + setActiveModel={setActiveModel} + setServerRunning={setServerRunning} /> -
- +
diff --git a/app/frontend/src/components/Generator.jsx b/app/frontend/src/components/Generator.jsx index c257aa3..7929037 100644 --- a/app/frontend/src/components/Generator.jsx +++ b/app/frontend/src/components/Generator.jsx @@ -1,6 +1,19 @@ import React, { memo, useState, useRef, useCallback } from "react"; import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Trash2, ImagePlus } from "lucide-react"; -import { generateImage, startServer, stopServer, waitForServerReady, getBackendStatus, getGenerationProgress, saveGeneratedOutput, deleteGeneratedOutputs } from "../services/api"; +import { + generateImage, + startServer, + stopServer, + waitForServerReady, + getBackendStatus, + getGenerationProgress, + saveGeneratedOutput, + deleteGeneratedOutputs, + getLlmStatus, + listLlmModels, + startLlm, + chatWithLlm +} from "../services/api"; const GalleryItem = memo(({ img, idx, isSelected, onClick }) => { const handleClick = (e) => { @@ -39,6 +52,8 @@ function Generator({ setActiveTab, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message), + specs, + textSettings, }) { const [outputImage, setOutputImage] = useState(null); const [outputSeed, setOutputSeed] = useState(null); @@ -56,10 +71,81 @@ function Generator({ const [isDecoding, setIsDecoding] = useState(false); const [selectedGalleryIndexes, setSelectedGalleryIndexes] = useState([]); const [baseImage, setBaseImage] = useState(null); + const [isEnhancing, setIsEnhancing] = useState(false); const timerRef = useRef(null); const abortControllerRef = useRef(null); const hasRealGenerationStepRef = useRef(false); + const handleEnhancePrompt = async () => { + if (!prompt.trim() || isEnhancing || isGenerating) return; + setIsEnhancing(true); + try { + const status = await getLlmStatus(); + let targetModel = status.settings?.model; + + if (!status.ready) { + const models = await listLlmModels(); + if (models.length === 0) { + showAlert({ + title: "No Text Models", + message: "You haven't downloaded any text models yet. Please go to the 'Model Manager' or 'Text Chat' tab to download a GGUF model first.", + danger: true + }); + setIsEnhancing(false); + return; + } + + const savedLlm = localStorage.getItem("selectedLlmModel") || models[0]?.filename; + targetModel = models.some(m => m.filename === savedLlm) ? savedLlm : models[0]?.filename; + + const confirm = await showConfirm({ + title: "Load Text Model?", + message: `Enhancing your prompt requires loading the local text model "${targetModel}". This will temporarily unload the image model from memory. Do you want to proceed?`, + confirmLabel: "Load & Enhance", + cancelLabel: "Cancel" + }); + + if (!confirm) { + setIsEnhancing(false); + return; + } + + const cores = textSettings?.threads || specs?.cpu_cores_physical || 4; + const context = textSettings?.contextSize || 2048; + await startLlm(targetModel, { + threads: cores, + contextSize: context, + gpuLayers: -1 + }); + } + + const systemPrompt = "You are a helpful assistant. Rewrite the user's image prompt to be more descriptive and detailed for image generation. Keep it under 80 words. Output ONLY the rewritten prompt, no explanation or introductory text."; + const messages = [ + { role: "system", content: systemPrompt }, + { role: "user", content: `Please expand this prompt: "${prompt}"` } + ]; + + const temp = textSettings?.temperature || 0.7; + const response = await chatWithLlm(messages, { temperature: temp, maxTokens: 128 }); + let cleanResponse = response.content.trim(); + if (cleanResponse.startsWith('"') && cleanResponse.endsWith('"')) { + cleanResponse = cleanResponse.slice(1, -1); + } + cleanResponse = cleanResponse.replace(/^Here is the expanded prompt:?\s*/i, ""); + cleanResponse = cleanResponse.replace(/^Enhanced prompt:?\s*/i, ""); + + setPrompt(cleanResponse); + } catch (err) { + showAlert({ + title: "Prompt Enhancement Failed", + message: err.message || String(err), + danger: true + }); + } finally { + setIsEnhancing(false); + } + }; + React.useEffect(() => { if (constraints.backendType === "apple-npu" || constraints.backendType === "openvino-npu") { setBaseImage(null); @@ -592,7 +678,28 @@ function Generator({
{/* Prompt Textarea */}
- +
+ + +