updating to use stable-diffusion.cpp for the image generation as ollama has since removed it
This commit is contained in:
@@ -26,7 +26,7 @@ export function FirstRunWizard({ onComplete }: { onComplete: () => void }) {
|
||||
// Test connection to the default Ollama URL
|
||||
const result = await invoke<{ ok: boolean; models: string[]; error: string }>(
|
||||
"test_connection",
|
||||
{ config: { api_url: apiUrl, api_key: apiKey, model: "", temperature: 0.7, max_tokens: 512, top_p: 0.9, image_model: "", embed_model: "" } }
|
||||
{ config: { api_url: apiUrl, api_key: apiKey, model: "", temperature: 0.7, max_tokens: 512, top_p: 0.9, image_api_url: "", embed_model: "" } }
|
||||
);
|
||||
if (result.ok) {
|
||||
setModels(result.models);
|
||||
@@ -58,7 +58,7 @@ export function FirstRunWizard({ onComplete }: { onComplete: () => void }) {
|
||||
temperature: 0.7,
|
||||
max_tokens: 512,
|
||||
top_p: 0.9,
|
||||
image_model: "x/flux2-klein:4b",
|
||||
image_api_url: "http://localhost:1234",
|
||||
embed_model: "nomic-embed-text",
|
||||
}
|
||||
});
|
||||
@@ -244,7 +244,7 @@ export function FirstRunWizard({ onComplete }: { onComplete: () => void }) {
|
||||
</h2>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mb-4">
|
||||
DM-Pal is now configured to use {selectedModel} for text generation.
|
||||
Image generation uses x/flux2-klein:4b (macOS-only via Ollama).
|
||||
Image generation runs via a stable-diffusion.cpp `sd-server` at http://localhost:1234 (cross-platform).
|
||||
Embedding model for lore search is set to nomic-embed-text.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { invoke, Channel } from "@tauri-apps/api/core";
|
||||
import { ImageOff } from "lucide-react";
|
||||
import { useToast } from "./Toast";
|
||||
|
||||
interface Props {
|
||||
@@ -20,22 +19,20 @@ type ImageEvent =
|
||||
| { type: "error"; data: string };
|
||||
|
||||
/**
|
||||
* Generates and caches an image for `prompt` via the Ollama image model.
|
||||
* Results are cached on disk by the backend, so re-renders are instant.
|
||||
* macOS-only: on other OSes the backend returns an error and we show a
|
||||
* placeholder instead of a confusing timeout.
|
||||
* Generates and caches an image for `prompt` via a stable-diffusion.cpp
|
||||
* `sd-server` (A1111-compatible API). Results are cached on disk by the
|
||||
* backend, so re-renders are instant. Cross-platform.
|
||||
*
|
||||
* Uses the streaming command so the DM sees a real progress bar (driven from
|
||||
* Ollama's NDJSON step/total) during the multi-second wait. The image still
|
||||
* loads even if no progress events arrive — Done carries the data URL.
|
||||
* `/sdapi/v1/progress`) during the multi-second wait. The image still loads
|
||||
* even if no progress events arrive — Done carries the data URL.
|
||||
*/
|
||||
export function GeneratedImage({ prompt, nonce = 0, className = "", aspect = "aspect-square" }: Props) {
|
||||
const [dataUrl, setDataUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [unsupported, setUnsupported] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
// ponytail: progress for the in-flight generation. step/total may be null
|
||||
// if Ollama omits them; we show an indeterminate shimmer then.
|
||||
// if the server omits them; we show an indeterminate shimmer then.
|
||||
const [progress, setProgress] = useState<{ step: number | null; total: number | null } | null>(null);
|
||||
const { addToast } = useToast();
|
||||
|
||||
@@ -43,7 +40,6 @@ export function GeneratedImage({ prompt, nonce = 0, className = "", aspect = "as
|
||||
if (!prompt) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setUnsupported(false);
|
||||
setProgress(null);
|
||||
|
||||
const channel = new Channel<ImageEvent>();
|
||||
@@ -56,9 +52,7 @@ export function GeneratedImage({ prompt, nonce = 0, className = "", aspect = "as
|
||||
setLoading(false);
|
||||
setProgress(null);
|
||||
} else if (event.type === "error") {
|
||||
const msg = String(event.data);
|
||||
if (msg.includes("macOS-only")) setUnsupported(true);
|
||||
else addToast(`Image failed: ${msg}`, "error");
|
||||
addToast(`Image failed: ${event.data}`, "error");
|
||||
setLoading(false);
|
||||
setProgress(null);
|
||||
}
|
||||
@@ -66,9 +60,7 @@ export function GeneratedImage({ prompt, nonce = 0, className = "", aspect = "as
|
||||
|
||||
invoke("generate_image_stream", { req: { prompt }, channel }).catch((e) => {
|
||||
if (!cancelled) {
|
||||
const msg = String(e);
|
||||
if (msg.includes("macOS-only")) setUnsupported(true);
|
||||
else addToast(`Image failed: ${msg}`, "error");
|
||||
addToast(`Image failed: ${e}`, "error");
|
||||
setLoading(false);
|
||||
setProgress(null);
|
||||
}
|
||||
@@ -83,17 +75,6 @@ export function GeneratedImage({ prompt, nonce = 0, className = "", aspect = "as
|
||||
|
||||
const box = `relative ${aspect} w-full rounded-lg overflow-hidden border border-[var(--color-border-glass)] bg-[var(--color-bg-deep)] flex items-center justify-center ${className}`;
|
||||
|
||||
if (unsupported) {
|
||||
return (
|
||||
<div className={box}>
|
||||
<div className="flex flex-col items-center gap-1 text-[var(--color-text-dim)]">
|
||||
<ImageOff size={20} />
|
||||
<span className="text-[10px] text-center px-2">Image gen is macOS-only via Ollama</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
// ponytail: determinate bar when step/total are known, indeterminate shimmer otherwise.
|
||||
const pct = progress && progress.step != null && progress.total ? Math.round((progress.step / progress.total) * 100) : null;
|
||||
|
||||
@@ -4,8 +4,7 @@ import { ImagePlus, Sparkles } from "lucide-react";
|
||||
import { useToast } from "./Toast";
|
||||
import { addGeneration, type Generation } from "../lib/generations";
|
||||
import { usePrefillEffect } from "../lib/usePrefill";
|
||||
|
||||
const DEFAULT_MODEL = "x/flux2-klein:4b";
|
||||
import { ImageSetupGuide } from "./ImageSetupGuide";
|
||||
|
||||
interface Props {
|
||||
prefill?: Generation | null;
|
||||
@@ -14,12 +13,11 @@ interface Props {
|
||||
|
||||
export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [model, setModel] = useState("");
|
||||
const [dataUrl, setDataUrl] = useState<string | null>(null);
|
||||
const [variants, setVariants] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [variant, setVariant] = useState(0);
|
||||
const [unsupported, setUnsupported] = useState(false);
|
||||
const [guideOpen, setGuideOpen] = useState(false);
|
||||
const { addToast } = useToast();
|
||||
|
||||
usePrefillEffect(prefill ?? null, "image", () => onPrefillConsumed?.(), (g) => {
|
||||
@@ -38,23 +36,21 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
// so "regenerate" actually produces a new image instead of the cached one.
|
||||
const v = variant > 0 ? `\n\n(variation ${variant})` : "";
|
||||
const url = await invoke<string>("generate_image", {
|
||||
req: { prompt: `${prompt}${v}`, model: model.trim() || null },
|
||||
req: { prompt: `${prompt}${v}` },
|
||||
});
|
||||
setDataUrl(url);
|
||||
setUnsupported(false);
|
||||
addToast("Image generated", "success");
|
||||
// ponytail: persist the PNG data URL so the DM can re-open past renders.
|
||||
void addGeneration({ kind: "image", title: prompt, data: url, source: model.trim() || DEFAULT_MODEL });
|
||||
void addGeneration({ kind: "image", title: prompt, data: url, source: "sd-server" });
|
||||
} catch (e) {
|
||||
const msg = String(e);
|
||||
if (msg.toLowerCase().includes("macos-only")) setUnsupported(true);
|
||||
addToast(`Image generation failed: ${msg}`, "error");
|
||||
addToast(`Image generation failed: ${String(e)}`, "error");
|
||||
setGuideOpen(true); // ponytail: a failed generate usually means sd-server isn't running — pop the guide.
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// ponytail: batch = 4 concurrent generates with distinct variation tags so
|
||||
// the backend cache yields 4 different images. Ollama serializes them
|
||||
// the backend cache yields 4 different images. sd-server processes them
|
||||
// server-side anyway, so concurrency is just cleaner code than a loop with
|
||||
// awaits. Each variant is persisted so the gallery gets all four.
|
||||
async function generateBatch() {
|
||||
@@ -62,12 +58,11 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
setLoading(true);
|
||||
setVariants([]);
|
||||
setDataUrl(null);
|
||||
setUnsupported(false);
|
||||
const tags = [1, 2, 3, 4];
|
||||
const results = await Promise.allSettled(
|
||||
tags.map((t) =>
|
||||
invoke<string>("generate_image", {
|
||||
req: { prompt: `${prompt}\n\n(variation ${t})`, model: model.trim() || null },
|
||||
req: { prompt: `${prompt}\n\n(variation ${t})` },
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -75,14 +70,14 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
for (const r of results) {
|
||||
if (r.status === "fulfilled" && r.value) {
|
||||
ok.push(r.value);
|
||||
void addGeneration({ kind: "image", title: prompt, data: r.value, source: model.trim() || DEFAULT_MODEL });
|
||||
void addGeneration({ kind: "image", title: prompt, data: r.value, source: "sd-server" });
|
||||
}
|
||||
}
|
||||
if (ok.length === 0) {
|
||||
const firstErr = results.find((r) => r.status === "rejected");
|
||||
const msg = firstErr ? String((firstErr as PromiseRejectedResult).reason) : "";
|
||||
if (msg.toLowerCase().includes("macos-only")) setUnsupported(true);
|
||||
addToast(`Batch failed: ${msg || "no images returned"}`, "error");
|
||||
setGuideOpen(true); // ponytail: likely sd-server not running — show the guide.
|
||||
} else {
|
||||
setVariants(ok);
|
||||
setDataUrl(ok[0]);
|
||||
@@ -99,6 +94,7 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<ImageSetupGuide forcedOpen={guideOpen} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">Prompt</label>
|
||||
<textarea
|
||||
@@ -110,18 +106,6 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||
Model (optional — defaults to {DEFAULT_MODEL})
|
||||
</label>
|
||||
<input
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder="x/flux2-klein:4b · x/z-image-turbo for speed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={generate}
|
||||
@@ -155,14 +139,6 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unsupported && (
|
||||
<div className="rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/30 p-3 text-[var(--color-danger)] text-xs">
|
||||
Image generation is macOS-only via Ollama (for now). On other platforms the
|
||||
✨ buttons elsewhere fall back to a placeholder.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{variants.length > 1 && !loading && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[var(--color-text-secondary)] text-xs font-medium">Variants — click to select</span>
|
||||
@@ -215,7 +191,7 @@ export function ImageGenerator({ prefill, onPrefillConsumed }: Props = {}) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!dataUrl && !loading && !unsupported && (
|
||||
{!dataUrl && !loading && (
|
||||
<div className="flex flex-col items-center justify-center aspect-square max-w-md mx-auto text-center text-[var(--color-text-dim)] text-xs">
|
||||
<span>No image yet.</span>
|
||||
<span className="mt-1">Describe a scene or portrait, then Generate.</span>
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { ChevronDown, ExternalLink, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { useToast } from "./Toast";
|
||||
|
||||
// ponytail: a non-technical DM won't read the README or build sd-server from
|
||||
// source. This is the one place they'll look — the Image tab — so the full
|
||||
// step-by-step lives here as a collapsible panel. The "Test" button mirrors
|
||||
// Settings so they get a green check without leaving the tab.
|
||||
|
||||
interface Props {
|
||||
/** Control from the parent so a failed generate can pop this open. */
|
||||
forcedOpen?: boolean;
|
||||
}
|
||||
|
||||
export function ImageSetupGuide({ forcedOpen = false }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [result, setResult] = useState<{ ok: boolean; error: string } | null>(null);
|
||||
const { addToast } = useToast();
|
||||
|
||||
// ponytail: when the parent reports a connection failure, expand so the DM
|
||||
// sees the fix immediately instead of staring at a toast.
|
||||
const isOpen = open || forcedOpen;
|
||||
|
||||
async function test() {
|
||||
setTesting(true);
|
||||
setResult(null);
|
||||
try {
|
||||
const r = await invoke<{ ok: boolean; error: string }>("test_image_connection");
|
||||
setResult(r);
|
||||
if (r.ok) addToast("sd-server reachable — image generation is ready", "success");
|
||||
else addToast(`Not reachable: ${r.error}`, "error");
|
||||
} catch (e) {
|
||||
setResult({ ok: false, error: String(e) });
|
||||
addToast(`Not reachable: ${e}`, "error");
|
||||
}
|
||||
setTesting(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-bg-deep)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-xs font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-gold-bright)] cursor-pointer select-none"
|
||||
>
|
||||
<span>⚙️ First time? Set up image generation (5-minute guide)</span>
|
||||
<ChevronDown size={14} className={`transition-transform ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="px-3 pb-3 pt-1 border-t border-[var(--color-border-subtle)] flex flex-col gap-3 text-xs leading-relaxed text-[var(--color-text-secondary)]">
|
||||
<p>
|
||||
DM-Pal makes images through a small free helper program called{" "}
|
||||
<strong className="text-[var(--color-text-primary)]">sd-server</strong> (part of{" "}
|
||||
<a href="https://github.com/leejet/stable-diffusion.cpp" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
stable-diffusion.cpp
|
||||
</a>
|
||||
). It runs on your computer alongside Ollama. Set it up once and you're done.
|
||||
</p>
|
||||
|
||||
<Step n={1} title="Download sd-server">
|
||||
Go to the{" "}
|
||||
<a href="https://github.com/leejet/stable-diffusion.cpp/releases" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
releases page <ExternalLink size={10} className="inline" />
|
||||
</a>{" "}
|
||||
and download the latest build for your system:
|
||||
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
||||
<li><strong className="text-[var(--color-text-primary)]">Windows</strong> — the <code className="font-mono">sd-server-win</code> zip</li>
|
||||
<li><strong className="text-[var(--color-text-primary)]">Mac (Apple Silicon)</strong> — the <code className="font-mono">sd-server-macos</code> zip</li>
|
||||
<li><strong className="text-[var(--color-text-primary)]">Linux</strong> — the <code className="font-mono">sd-server-linux</code> build</li>
|
||||
</ul>
|
||||
Unzip it into a permanent folder, e.g. <code className="font-mono">sd-server</code> in your home folder.
|
||||
</Step>
|
||||
|
||||
<Step n={2} title="Download a model (recommended: SDXL — great fantasy art, 2 files)">
|
||||
Put these two files in your <code className="font-mono">sd-server</code> folder:
|
||||
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
||||
<li>
|
||||
Model (~6.5 GB):{" "}
|
||||
<a href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/sd_xl_base_1.0.safetensors" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
sd_xl_base_1.0.safetensors <ExternalLink size={10} className="inline" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
VAE (~0.6 GB):{" "}
|
||||
<a href="https://huggingface.co/madebyollin/sdxl-vae-fp16-fix/blob/main/sdxl_vae-fp16-fix.safetensors" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
sdxl_vae-fp16-fix.safetensors <ExternalLink size={10} className="inline" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-1 text-[var(--color-text-dim)]">
|
||||
On the HuggingFace page click the ↓ next to the file to download it.
|
||||
</p>
|
||||
</Step>
|
||||
|
||||
<Step n={3} title="Start the server (leave the window open)">
|
||||
Open a terminal — <em>Mac:</em> Terminal (Applications → Utilities); <em>Windows:</em> Command Prompt.
|
||||
Move into your folder, then run:
|
||||
<pre className="mt-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[10px] font-mono overflow-x-auto whitespace-pre">
|
||||
{`# Mac / Linux
|
||||
./sd-server -m sd_xl_base_1.0.safetensors \\
|
||||
--vae sdxl_vae-fp16-fix.safetensors --offload-to-cpu
|
||||
|
||||
# Windows
|
||||
sd-server.exe -m sd_xl_base_1.0.safetensors \\
|
||||
--vae sdxl_vae-fp16-fix.safetensors --offload-to-cpu`}</pre>
|
||||
<p className="mt-1 text-[var(--color-text-dim)]">
|
||||
<code className="font-mono">--offload-to-cpu</code> lets it run on machines without a strong GPU (slower). You'll see it listening on <code className="font-mono">http://127.0.0.1:1234</code>.
|
||||
</p>
|
||||
</Step>
|
||||
|
||||
<Step n={4} title="Point DM-Pal at it & test">
|
||||
In <strong className="text-[var(--color-text-primary)]">Settings → Image Server</strong>, make sure the URL is{" "}
|
||||
<code className="font-mono">http://localhost:1234</code>. Then test it right here:
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={test}
|
||||
disabled={testing}
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:border-[var(--color-gold-bright)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{testing ? "Testing…" : "Test sd-server connection"}
|
||||
</button>
|
||||
{result && (
|
||||
<span className={`flex items-center gap-1 text-xs ${result.ok ? "text-[var(--color-success)]" : "text-[var(--color-danger)]"}`}>
|
||||
{result.ok ? <CheckCircle2 size={14} /> : <XCircle size={14} />}
|
||||
{result.ok ? "Connected — ready to generate" : result.error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Step>
|
||||
|
||||
<details className="rounded-lg border border-[var(--color-border-subtle)] px-3 py-2">
|
||||
<summary className="text-[var(--color-text-dim)] cursor-pointer hover:text-[var(--color-text-secondary)] select-none">
|
||||
Slower laptop? Try the lightweight option (Z-Image-Turbo, ~4 GB VRAM, 3 files)
|
||||
</summary>
|
||||
<div className="mt-2 flex flex-col gap-1.5">
|
||||
<p>Download these three files into your <code className="font-mono">sd-server</code> folder:</p>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
<li>Diffusion model:{" "}
|
||||
<a href="https://huggingface.co/leejet/Z-Image-Turbo-GGUF" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
z_image_turbo-Q4_K_M.gguf <ExternalLink size={10} className="inline" />
|
||||
</a>
|
||||
</li>
|
||||
<li>VAE:{" "}
|
||||
<a href="https://huggingface.co/black-forest-labs/FLUX.1-schnell/blob/main/ae.sft" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
ae.sft <ExternalLink size={10} className="inline" />
|
||||
</a>
|
||||
</li>
|
||||
<li>Text encoder:{" "}
|
||||
<a href="https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF" target="_blank" rel="noreferrer" className="text-[var(--color-gold-bright)] underline">
|
||||
Qwen3-4B-Instruct-2507-Q4_K_M.gguf <ExternalLink size={10} className="inline" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<pre className="mt-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[10px] font-mono overflow-x-auto whitespace-pre">
|
||||
{`./sd-server --diffusion-model z_image_turbo-Q4_K_M.gguf \\
|
||||
--vae ae.sft --llm Qwen3-4B-Instruct-2507-Q4_K_M.gguf \\
|
||||
--cfg-scale 1.0 --diffusion-fa --offload-to-cpu`}</pre>
|
||||
<p className="text-[var(--color-text-dim)]">
|
||||
Z-Image-Turbo is built for speed (few steps), so it generates faster than SDXL on modest hardware.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({ n, title, children }: { n: number; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium text-[var(--color-text-primary)]">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] text-[10px] font-bold mr-1.5 align-middle">
|
||||
{n}
|
||||
</span>
|
||||
{title}
|
||||
</p>
|
||||
<div className="pl-[26px]">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ interface LlmConfig {
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
top_p: number;
|
||||
image_model: string;
|
||||
image_api_url: string;
|
||||
embed_model: string;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ export function SettingsPanel() {
|
||||
const [conn, setConn] = useState<{ ok: boolean; models: string[]; error: string } | null>(null);
|
||||
const [dataDir, setDataDir] = useState<DataDirInfo | null>(null);
|
||||
const [relocating, setRelocating] = useState(false);
|
||||
// ponytail: probe the sd-server so a non-technical DM gets a green check, same pattern as the LLM test.
|
||||
const [imgTesting, setImgTesting] = useState(false);
|
||||
const [imgConn, setImgConn] = useState<{ ok: boolean; error: string } | null>(null);
|
||||
// ponytail: advanced SQLite viewer — read-only, two DBs (lore.db / generations.db).
|
||||
const [sqlDb, setSqlDb] = useState<"lore" | "generations">("generations");
|
||||
const [sqlInput, setSqlInput] = useState("SELECT name, type FROM sqlite_master WHERE type='table' ORDER BY name");
|
||||
@@ -153,6 +156,24 @@ export function SettingsPanel() {
|
||||
setTesting(false);
|
||||
}
|
||||
|
||||
// ponytail: probe the configured sd-server. Save first so it uses the latest URL.
|
||||
async function testImage() {
|
||||
if (!config) return;
|
||||
setImgTesting(true);
|
||||
setImgConn(null);
|
||||
try {
|
||||
await invoke("set_llm_config", { config });
|
||||
const r = await invoke<{ ok: boolean; error: string }>("test_image_connection");
|
||||
setImgConn(r);
|
||||
if (r.ok) addToast("sd-server reachable", "success");
|
||||
else addToast(`Image server not reachable: ${r.error}`, "error");
|
||||
} catch (e) {
|
||||
setImgConn({ ok: false, error: String(e) });
|
||||
addToast(`Image server not reachable: ${e}`, "error");
|
||||
}
|
||||
setImgTesting(false);
|
||||
}
|
||||
|
||||
// ponytail: provider presets fill in the API URL pattern + default model.
|
||||
const PRESETS: { label: string; url: string; model: string; key?: boolean }[] = [
|
||||
{ label: "Ollama", url: "http://localhost:11434", model: "llama3.2" },
|
||||
@@ -177,7 +198,7 @@ export function SettingsPanel() {
|
||||
temperature: 0.7,
|
||||
max_tokens: 512,
|
||||
top_p: 0.9,
|
||||
image_model: "x/flux2-klein:4b",
|
||||
image_api_url: "http://localhost:1234",
|
||||
embed_model: "nomic-embed-text",
|
||||
};
|
||||
function resetDefaults() {
|
||||
@@ -386,17 +407,39 @@ export function SettingsPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">Image Model</h3>
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">Image Server</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||
Image model
|
||||
sd-server URL
|
||||
</label>
|
||||
<p className="text-[10px] text-[var(--color-text-dim)] leading-relaxed">
|
||||
Base URL of a <a href="https://github.com/leejet/stable-diffusion.cpp" target="_blank" rel="noreferrer" className="underline">stable-diffusion.cpp</a> `sd-server` (A1111-compatible API). Start it with your chosen model, e.g.<br /><code className="font-mono">sd-server -m model.safetensors</code>
|
||||
</p>
|
||||
<input
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
|
||||
value={config.image_model}
|
||||
onChange={(e) => setConfig({ ...config, image_model: e.target.value })}
|
||||
placeholder="x/flux2-klein:4b"
|
||||
value={config.image_api_url}
|
||||
onChange={(e) => setConfig({ ...config, image_api_url: e.target.value })}
|
||||
placeholder="http://localhost:1234"
|
||||
/>
|
||||
{/* ponytail: mirror the LLM test button so a non-technical DM gets a green check */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={testImage}
|
||||
disabled={imgTesting}
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:border-[var(--color-gold-bright)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{imgTesting ? "Testing…" : "Test connection"}
|
||||
</button>
|
||||
{imgConn && (
|
||||
<span className={`text-xs ${imgConn.ok ? "text-[var(--color-success)]" : "text-[var(--color-danger)]"}`}>
|
||||
{imgConn.ok ? "✓ sd-server reachable" : `✕ ${imgConn.error}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-[var(--color-text-dim)]">
|
||||
New to this? Open the <strong>Image</strong> tab → "First time? Set up image generation" for a full step-by-step.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">Embedding Model (Lore RAG)</h3>
|
||||
|
||||
Reference in New Issue
Block a user