feat: single-image transform, NAS/FTP in delivery, backup-all any target, metadata sidecar, prompt view, preset manager, API token + MCP server

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
2026-07-23 21:29:43 +00:00
parent 5cfafb456c
commit 295f0ce91e
19 changed files with 479 additions and 69 deletions
+16 -1
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
interface Item { id: string; filename: string; output_px: string; has_alpha: boolean;
folder_id: string | null; by_name: string; tasks: string[]; delivery_status: string; nas_status: string;
model_used: string; variant_of: string | null; mode: string; thumb_path: string | null; created_at: string; }
model_used: string; variant_of: string | null; mode: string; thumb_path: string | null; prompt_used: string | null; created_at: string; }
export default function LibraryApp() {
const [items, setItems] = useState<Item[]>([]);
@@ -12,6 +12,7 @@ export default function LibraryApp() {
const [pick, setPick] = useState<Set<string>>(new Set());
const [variantSel, setVariantSel] = useState<Record<string, string>>({});
const [compare, setCompare] = useState<Item | null>(null);
const [promptOf, setPromptOf] = useState<Item | null>(null);
const [toast, setToast] = useState<string | null>(null);
const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2400); };
@@ -176,6 +177,7 @@ export default function LibraryApp() {
{v.nas_status === 'failed' && <span className="dbadge err">NAS </span>}
</div>
<div className="reihe knapp">
{v.prompt_used && <button className="mini" onClick={() => setPromptOf(v)}>Prompt</button>}
{v.mode !== 'generate' && <button className="mini" onClick={() => setCompare(v)}>Vorher/Nachher</button>}
<a className="mini" href={`/api/items/${v.id}/file?download=1`}>Laden</a>
<button className="mini stark2" onClick={() => reuse(v.id)} title="Ergebnis im Studio weiterbearbeiten">Weiterbearbeiten</button>
@@ -203,6 +205,16 @@ export default function LibraryApp() {
<div className="lupe-fuss">{compare.filename} · {compare.output_px}px Regler: links {compare.mode === 'compose' ? 'Vorlage' : 'Original'}, rechts Ergebnis</div>
</div>
)}
{promptOf && (
<div className="lupe" onClick={() => setPromptOf(null)}>
<div className="promptbox" onClick={(e) => e.stopPropagation()}>
<div className="pb-kopf"><b>{promptOf.filename}</b>
<button className="mini" onClick={() => { navigator.clipboard?.writeText(promptOf.prompt_used || ''); notify('Prompt kopiert.'); }}>Kopieren</button></div>
<pre className="pb-text">{promptOf.prompt_used}</pre>
<div className="fein">{promptOf.model_used ? `Modell: ${modelName(promptOf.model_used)} · ` : ''}{promptOf.output_px}px</div>
</div>
</div>
)}
{toast && <div className="toast">{toast}</div>}
<LibStyles />
</div>
@@ -254,6 +266,9 @@ function LibStyles() {
.vergleich .oben{position:absolute;inset:0;clip-path:inset(0 0 0 50%);}
.vergleich input[type=range]{position:absolute;left:0;right:0;bottom:-34px;width:100%;}
.lupe-fuss{color:#EAEAE3;font-family:var(--font-mono);font-size:12px;margin-top:30px;text-align:center;}
.promptbox{background:var(--card);border-radius:var(--radius);max-width:min(92vw,620px);width:100%;padding:16px;display:flex;flex-direction:column;gap:10px;}
.pb-kopf{display:flex;justify-content:space-between;align-items:center;gap:10px;}
.pb-text{white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:12.5px;line-height:1.5;background:var(--paper);border:1px solid var(--line);border-radius:4px;padding:12px;max-height:50vh;overflow:auto;margin:0;}
.toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);background:var(--ink);color:#FBFBF7;padding:10px 18px;border-radius:3px;font-size:13.5px;z-index:60;}
`}</style>;
}