Ideen 2.0: schreiben zuerst, die Box waehlt vor
Ampel / ampel (push) Successful in 24s

Die Seite verlangte eine technische Weichenstellung, BEVOR man gesagt hat was man
will — mit Begriffen aus dem Maschinenraum (BOX / IDE: SIMPEL / IDE: GRUENDLICH).
Deshalb klebten 116 Woerter Beipackzettel unter einem einzigen Eingabefeld.

Zwei unabhaengige Entscheidungen, vorher in vier sich ausschliessende Knoepfe gepresst:
  wer       — die Box allein, oder ein Repo fuer Zed
  nurDenken — erst nur ein Konzept (passt zu beidem, jetzt ein Haken)
Die Knoepfe nennen das ERGEBNIS statt des Werkzeugs, und der erklaerende Satz steht
an der Option statt in einer Legende darunter.

Neu: POST /api/ideen/einordnen — das dauerwarme Hirn liest den getippten Text und
waehlt vor (gemessen ~500 ms, 5 von 5 Testfaellen richtig, inkl. "Lohnt sich...?"
-> nur denken). Bewusst ein VORSCHLAG mit sichtbarem Etikett: er setzt nur die
Auswahl, die ohnehin dasteht. Faellt er aus, bleibt schlicht die Voreinstellung —
das Tippen wird nie blockiert, es gibt keinen Ladebalken und keine Fehlermeldung.

Tiefe und Ziel erscheinen nur noch beim Repo-Weg, weil sie nur dort wirken. Ein
Schalter der nichts tut ist schlimmer als keiner.

Leerer Zustand zeigt drei anklickbare Beispiele statt "wirf der Box eine Idee zu".
Kopfzeile nennt nur noch den Zustand — "Ideen" stand dreimal uebereinander.

Die API bleibt unveraendert; die Abbildung passiert in submit().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 23:58:51 +02:00
parent d18aac0e1b
commit cb649aac34
30 changed files with 289 additions and 165 deletions
+153 -105
View File
@@ -67,11 +67,20 @@ function IdeenSection() {
const qc = useQueryClient()
const { showAlert, showConfirm, dialogElement } = useDialog()
const [text, setText] = useState("")
// Intake-Modus: Box baut · Idee (nur durchdenken) · IDE simpel/gründlich (Projekt vorbereiten).
const [modus, setModus] = useState<"box" | "idee" | "simpel" | "gruendlich">("box")
const isIdee = modus === "idee" // Produkt-Idee prüfen
const isProjekt = modus === "simpel" || modus === "gruendlich" // Projekt für Zed
const isIde = isIdee || isProjekt // beides → projektstart
// Zwei UNABHÄNGIGE Entscheidungen statt vier sich ausschliessender Modi (25.07.2026):
// wer — baut die Box selbst, oder bekommst du ein Repo für Zed?
// nurDenken — soll erst nur ein Konzept entstehen? (passt zu beidem)
// Vorher hiessen die Knöpfe BOX/IDEE/IDE:SIMPEL/IDE:GRÜNDLICH und pressten beides in eine
// Reihe — das brauchte 116 Wörter Beipackzettel darunter. Die Abbildung auf die
// unveränderte API passiert in submit().
const [wer, setWer] = useState<"box" | "zed">("box")
const [tiefe, setTiefe] = useState<"schlank" | "gruendlich">("schlank")
const [nurDenken, setNurDenken] = useState(false)
// Kam die aktuelle Auswahl von der Box? Nur fürs Etikett „Vorschlag" — sobald du selbst
// klickst, verschwindet es, damit nie unklar ist, wessen Entscheidung da steht.
const [vorgeschlagen, setVorgeschlagen] = useState(false)
const isIde = wer === "zed" || nurDenken // beides läuft über das projektstart-Profil
const isProjekt = wer === "zed" && !nurDenken // nur DANN sind Tiefe und Ziel wirksam
// Ziel-Plattform des Projekts (nur IDE-Welt): läuft es als LXC-Dienst oder in Docker?
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
const [sending, setSending] = useState(false)
@@ -83,6 +92,28 @@ function IdeenSection() {
const items = data?.items ?? []
const reload = () => qc.invalidateQueries({ queryKey: qk.ideen })
// Die Box liest mit und wählt vor. Gemessen ~500 ms auf dem dauerwarmen Hirn, deshalb
// reicht eine kurze Tipp-Pause. Bewusst ein VORSCHLAG: er setzt nur die Auswahl, die du
// ohnehin siehst und mit einem Klick überschreibst. Scheitert er, passiert schlicht nichts
// — kein Ladebalken, keine Fehlermeldung, das Tippen wird nie blockiert.
useEffect(() => {
const t = text.trim()
if (t.length < 12) { setVorgeschlagen(false); return }
let verworfen = false
const timer = setTimeout(async () => {
try {
const r = await api<{ ok: boolean; wer?: "box" | "zed"; tiefe?: "schlank" | "gruendlich"; nur_denken?: boolean }>(
"/api/ideen/einordnen", { method: "POST", body: JSON.stringify({ text: t }) })
if (verworfen || !r?.ok) return
if (r.wer) setWer(r.wer)
if (r.tiefe) setTiefe(r.tiefe)
setNurDenken(!!r.nur_denken)
setVorgeschlagen(true)
} catch { /* stumm — die Voreinstellung bleibt einfach stehen */ }
}, 700)
return () => { verworfen = true; clearTimeout(timer) }
}, [text])
// Polling-Log nur aktivieren, wenn Idee triage/running und geöffnet
const logEnabled = openLog !== null && (data?.items.find((i) => i.id === openLog)?.status === "triage" || data?.items.find((i) => i.id === openLog)?.status === "running")
const logData = useIdeenLog(openLog ?? "", logEnabled)
@@ -97,12 +128,15 @@ function IdeenSection() {
const titel = t.length > 160 ? t.slice(0, 157) + "…" : t
await api("/api/ideen", { method: "POST", body: JSON.stringify({
titel, notiz: t.length > 160 ? t : "",
// Abbildung auf die unveränderte API: „nur denken" läuft immer über die IDE-Welt,
// weil dort das Profil sitzt, das denkt statt baut.
welt: isIde ? "ide" : "box",
tiefe: isProjekt ? modus : "",
tiefe: isProjekt ? (tiefe === "gruendlich" ? "gruendlich" : "simpel") : "",
ziel: isProjekt ? ziel : "", // eine Idee braucht noch keine Plattform
art: isIdee ? "idee" : "projekt",
art: nurDenken ? "idee" : "projekt",
}) })
setText("")
setVorgeschlagen(false)
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
@@ -392,7 +426,9 @@ function IdeenSection() {
n(["todo", "scheduled", "ready"]) > 0 ? `${n(["todo", "scheduled", "ready"])} wartet` : "",
n(["done"]) > 0 ? `${n(["done"])} fertig` : "",
].filter(Boolean)
return `Ideen-Queue${teile.length ? " — " + teile.join(" · ") : ""}`
// Nur der Zustand, nicht nochmal der Seitenname: „Ideen" steht bereits als
// Überschrift darüber und ein drittes Mal in der Kopfzeile der App.
return teile.length ? teile.join(" · ") : "noch nichts in der Queue"
})()}
</SectionLabel>
{(() => {
@@ -410,120 +446,116 @@ function IdeenSection() {
)
})()}
</div>
<div className="mc-card p-4 space-y-3">
<div className="mc-card p-4 space-y-4">
{/* Schreiben zuerst. Alles darunter ist eine Entscheidung ZU dem, was hier steht —
nicht davor. Der Knopf heisst immer gleich; was passiert, steht bei den Karten. */}
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onChange={(e) => { setText(e.target.value); setVorgeschlagen(false) }}
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
placeholder="Neue Idee — einfach hinschreiben, die Box macht den Rest"
className="h-9 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
placeholder="Was soll entstehen?"
className="h-10 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
/>
<button
onClick={submit}
disabled={sending || text.trim().length < 3}
className={cn("flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-3 text-[11px] font-bold uppercase tracking-wide transition-all cursor-pointer disabled:opacity-50",
modus === "gruendlich" ? "border-amber-500/40 bg-amber-500/10 text-amber-300 hover:bg-amber-500/20"
: modus === "simpel" ? "border-violet-500/40 bg-violet-500/10 text-violet-300 hover:bg-violet-500/20"
: isIdee ? "border-sky-500/40 bg-sky-500/10 text-sky-300 hover:bg-sky-500/20"
: "border-primary/40 bg-primary/10 text-primary hover:bg-primary/20")}>
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : isIdee ? <Lightbulb className="h-3.5 w-3.5" /> : isProjekt ? <Code2 className="h-3.5 w-3.5" /> : <Send className="h-3.5 w-3.5" />}
{isIdee ? "Durchdenken" : isProjekt ? "Vorbereiten" : "In die Queue"}
className="flex h-10 shrink-0 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-4 text-xs font-semibold text-primary transition-all hover:bg-primary/20 disabled:opacity-40 cursor-pointer">
{sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
Los
</button>
</div>
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-muted-foreground/70">Modus</span>
<div className="flex rounded-lg border border-border/60 p-0.5">
<button
onClick={() => setModus("box")}
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "box" ? "bg-primary/15 text-primary" : "text-muted-foreground/60 hover:text-foreground")}>
<Hammer className="h-3 w-3" /> Box
</button>
<button
onClick={() => setModus("idee")}
title="Produkt-Idee erst einmal durchdenken lassen: Was ist es, für wen, was gibt es schon, was spricht dagegen? Ergebnis ist ein Konzept — kein Projekt, kein Bau-Druck."
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "idee" ? "bg-sky-500/15 text-sky-300" : "text-muted-foreground/60 hover:text-foreground")}>
<Lightbulb className="h-3 w-3" /> Idee
</button>
<button
onClick={() => setModus("simpel")}
title="IDE-Projekt, schnell vorbereitet: knappes Konzept, wenige Denk-Runden. Für kleine, klar umrissene Sachen."
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "simpel" ? "bg-violet-500/15 text-violet-300" : "text-muted-foreground/60 hover:text-foreground")}>
<Code2 className="h-3 w-3" /> IDE: simpel
</button>
<button
onClick={() => setModus("gruendlich")}
title="IDE-Projekt, gründlich durchdacht: volle Kaskade mit mehreren Rollen und Härtetests. Für große/ambitionierte Projekte."
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "gruendlich" ? "bg-amber-500/15 text-amber-300" : "text-muted-foreground/60 hover:text-foreground")}>
<Code2 className="h-3 w-3" /> IDE: gründlich
</button>
{/* Wer baut das? Zwei Karten statt vier Kürzel — jede sagt, was DU danach bekommst. */}
<div>
<p className="mb-1.5 text-[11px] text-muted-foreground/70">Wer baut das?</p>
<div className="grid gap-2 sm:grid-cols-2">
{([
{ id: "box" as const, icon: Hammer, titel: "Die Box, allein",
text: "Sie baut es und legt dir das Ergebnis als Vorschlag hin — nichts geht ohne deinen Klick live." },
{ id: "zed" as const, icon: Code2, titel: "Ich, in Zed",
text: "Du bekommst ein fertiges Repo mit Konzept und Doku. Den Code schreibst du selbst." },
]).map((k) => {
const aktiv = wer === k.id
return (
<button key={k.id}
onClick={() => { setWer(k.id); setVorgeschlagen(false) }}
disabled={nurDenken}
className={cn("rounded-xl border p-3 text-left transition-all cursor-pointer disabled:cursor-default",
nurDenken ? "border-border/30 opacity-40"
: aktiv ? "border-primary/60 bg-primary/5" : "border-border/50 hover:border-border")}>
<div className="flex items-center gap-2">
<k.icon className={cn("h-4 w-4 shrink-0", aktiv && !nurDenken ? "text-primary" : "text-muted-foreground")} />
<span className="text-xs font-semibold text-foreground">{k.titel}</span>
{aktiv && vorgeschlagen && !nurDenken && (
<span className="ml-auto shrink-0 rounded border border-primary/40 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold text-primary">
Vorschlag
</span>
)}
</div>
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{k.text}</p>
</button>
)
})}
</div>
{nurDenken && (
<p className="mt-1.5 text-[10px] text-muted-foreground/60">
Wer baut, entscheidest du, wenn das Konzept da ist.
</p>
)}
</div>
{/* Immer sichtbare Orientierung — der Text darunter erklärt nur den GEWÄHLTEN
Modus, die Wahl selbst muss man aber vorher treffen können (User 21.07.). */}
<p className="text-[10px] leading-relaxed text-muted-foreground/60">
<b className="text-primary/80">Box</b>, wenn die Box es selbst bauen soll (dein Heimlabor,
MC2, Lucy Ergebnis kommt als Patch-Vorschlag zurück). ·{" "}
<b className="text-sky-300/80">Idee</b>, wenn du noch nicht weißt, ob es das überhaupt wert
ist erst durchdenken, entscheiden kannst du danach. ·{" "}
<b className="text-violet-300/80">IDE</b>, wenn du es selbst in Zed bauen willst und dafür
ein startklares Repo brauchst.
</p>
{/* Tiefe und Ziel wirken NUR beim Repo-Weg — deshalb erscheinen sie auch nur dort.
Ein Schalter, der nichts tut, ist schlimmer als keiner. */}
{isProjekt && (
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-muted-foreground/70">Ziel</span>
<div className="flex rounded-lg border border-border/40 p-0.5">
<button
onClick={() => setZiel("lxc")}
title="Läuft als Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker). Wird vom Backup-Server automatisch mitgesichert."
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
ziel === "lxc" ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
LXC
</button>
<button
onClick={() => setZiel("docker")}
title="Natives Docker nach Standard-Praxis: Dockerfile + compose, portabel, über Gitea-/GitLab-Pipelines baubar. Für Projekte, die bewusst außerhalb der LXC-Welt leben."
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
ziel === "docker" ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
Docker
</button>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<span className="text-[11px] text-muted-foreground/70">Wie gründlich?</span>
<div className="flex rounded-lg border border-border/60 p-0.5">
{([["schlank", "schlank"], ["gruendlich", "gründlich"]] as const).map(([id, label]) => (
<button key={id} onClick={() => { setTiefe(id); setVorgeschlagen(false) }}
className={cn("rounded px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
tiefe === id ? "bg-primary/15 text-primary" : "text-muted-foreground/60 hover:text-foreground")}>
{label}
</button>
))}
</div>
<span className="text-[10px] text-muted-foreground/50">
{ziel === "lxc" ? "Dienst im Proxmox-Container (deine Heimat)" : "natives Docker, portabel & pipeline-tauglich"}
<span className="text-[10px] text-muted-foreground/60">
{tiefe === "gruendlich"
? "mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Härtetest — dauert entsprechend"
: "2 Rollen, eine Denk-Runde — ein paar Minuten"}
</span>
<span className="ml-auto flex items-center gap-1.5">
<span className="text-[11px] text-muted-foreground/70">Läuft als</span>
<div className="flex rounded-lg border border-border/40 p-0.5">
{([["lxc", "LXC"], ["docker", "Docker"]] as const).map(([id, label]) => (
<button key={id} onClick={() => setZiel(id)}
title={id === "lxc"
? "Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker). Wird vom Backup-Server automatisch mitgesichert."
: "Natives Docker: Dockerfile + compose, portabel und pipeline-tauglich."}
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
ziel === id ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
{label}
</button>
))}
</div>
</span>
</div>
)}
<p className="text-[11px] text-muted-foreground">
{isIdee ? (
<>
<b className="text-sky-300/90">Idee durchdenken:</b> Die Box arbeitet deine Produkt-Idee
mit Produkt-Blick aus was ist es genau, für wen, was gibt es dafür schon, was wäre der
kleinste sinnvolle Wurf und was spricht dagegen. Volle Denk-Kaskade mit Kritiker, Ergebnis
ist <b>nur ein Konzept</b> (kein Gerüst, keine Roadmap, kein Bau-Druck) du liest es hier
und entscheidest danach, ob ein Projekt draus wird.
</>
) : isProjekt ? (
<>
<b className={modus === "gruendlich" ? "text-amber-300/90" : "text-violet-300/90"}>IDE-Welt{modus === "gruendlich" ? " · gründlich" : " · simpel"}:</b>{" "}
Die Box denkt das Konzept durch{modus === "gruendlich"
? " — volle Kaskade: mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Kritiker-Härtetest (für große/ambitionierte Projekte, dauert entsprechend)"
: " — schlank: 2 Rollen, 1 Denk-Runde (für kleine, klar umrissene Projekte, ein paar Minuten)"}, legt
ein leeres Gitea-Repo an und füllt es nur mit den wichtigen Dokumenten (Konzept, Roadmap, AGENTS.md,
.aiexclude) <b>kein Code</b>. Danach öffnest du das Repo in Zed und baust selbst.
</>
) : (
<>
Eine Queue, alle Türen: hier, per Telegram oder im Gespräch mit Lucy. Die Box arbeitet Ideen selbst
aus und baut Code-Ergebnisse erscheinen als Karte unter Patch-Vorschläge, nichts davon geht ohne
deinen Klick live. Nur reine Doku-Bagatellen zieht die Box nachts selbst ein (mit Fangnetz und Meldung).
</>
)}
</p>
{/* Orthogonal zu allem darüber: erst denken, bauen später entscheiden. */}
<label className="flex cursor-pointer items-start gap-2 border-t border-border/30 pt-3">
<input type="checkbox" checked={nurDenken}
onChange={(e) => { setNurDenken(e.target.checked); setVorgeschlagen(false) }}
className="mt-0.5 h-3.5 w-3.5 shrink-0 accent-sky-400 cursor-pointer" />
<span className="min-w-0">
<span className="text-xs font-semibold text-foreground">Erst nur durchdenken, noch nichts anlegen</span>
<span className="mt-0.5 block text-[11px] leading-relaxed text-muted-foreground">
Die Box arbeitet die Idee mit Produkt-Blick aus was ist es, für wen, was gibt es
schon, was wäre der kleinste sinnvolle Wurf, was spricht dagegen. Ergebnis ist ein
Konzept: kein Repo, kein Code, kein Bau-Druck.
</span>
</span>
</label>
{isLoading && (
<p className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin" /> Queue wird geladen </p>
)}
@@ -577,7 +609,23 @@ function IdeenSection() {
</div>
)}
{data?.available && !isLoading && items.length === 0 && (
<p className="text-xs text-muted-foreground/70">Queue ist leer wirf der Box eine Idee zu.</p>
// Leerer Zustand als Einladung statt als Feststellung: genau jetzt möchte man
// wissen, was man überhaupt hineinschreiben kann. Ein Klick füllt das Feld —
// die Box ordnet den Beispieltext dann selbst ein.
<div className="space-y-2">
<p className="text-[11px] text-muted-foreground/70">Noch nichts hier. Zum Beispiel:</p>
{[
"Ein kleines Werkzeug, das meine Fotos nach Aufnahmedatum in Ordner sortiert",
"Rippy soll die Untertitel-Spur automatisch auswählen statt zu fragen",
"Lohnt sich ein Dashboard für die Stromzähler, oder reicht Home Assistant?",
].map((b) => (
<button key={b} onClick={() => setText(b)}
className="flex w-full items-center gap-2 rounded-lg border border-border/40 bg-background/20 px-3 py-2 text-left text-xs text-muted-foreground transition-all hover:border-primary/40 hover:text-foreground cursor-pointer">
<Lightbulb className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
{b}
</button>
))}
</div>
)}
</div>
{dialogElement}