Ideen: Entscheidungsbaum zugeklappt statt Assistent
Ampel / ampel (push) Successful in 24s

Der vorgeschlagene Wizard hatte den richtigen BAUM — Wer baut das?, und nur im
Zed-Ast: wie startest du? — aber den falschen Mechanismus fuer eine taegliche
Zehn-Sekunden-Handlung. Ein Assistent haette gefragt, was der Einordner in
~500 ms schon weiss.

Jetzt: derselbe Baum, zugeklappt.
  Normalfall  Textfeld + EINE Zeile ("Du baust selbst — ich arbeite erst ein
              Konzept aus.") + Enter. Nichts steht zwischen dir und der Idee.
  "anders"    klappt genau die Wizard-Reihenfolge auf: erst Wer baut das?, und
              NUR im Zed-Ast eingerueckt darunter Wie startest du?
              Bei "die Box" folgt keine Rueckfrage — dann geht es direkt los.

Das Etikett heisst "gelesen" und erscheint nur, wenn die Box den Text wirklich
eingeordnet hat. Ohne Einordnung steht dort die Voreinstellung, und die darf sich
nicht als Verstaendnis ausgeben.

Der Entwurf steht und faellt damit, dass der Einordner meistens richtig liegt —
5 von 5 im Test ist ein huebsches, aber winziges Ergebnis. Deshalb zaehlt die
Seite jetzt mit, wie oft "anders" geklickt wird (localStorage, zwei Zahlen, kein
Endpunkt) und zeigt die Quote beim Aufklappen. Bleibt sie unter etwa jedem
fuenften Mal, war der Weg richtig; liegt sie darueber, bauen wir den Assistenten
mit Daten statt mit Meinung.

Nebenbei: das jetzt tote `vorgeschlagen`-Prop aus <Wahl> entfernt (4 Aufrufer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 00:16:17 +02:00
parent 20d4f40ed9
commit cd60974ff9
27 changed files with 194 additions and 149 deletions
+136 -91
View File
@@ -62,6 +62,27 @@ const FRAGE_HINT: Record<string, string> = {
transient: "Die Aufgabe hakt:",
}
// Wie oft lag die Einordnung daneben? Der ganze zugeklappte Entwurf steht und fällt damit.
// Bewusst im Browser statt im Backend: zwei Zahlen brauchen keine Tabelle, keinen Endpunkt
// und keine Migration — und es geht niemanden ausser dem Nutzer etwas an. Sichtbar wird die
// Quote nur beim Aufklappen, damit sie beweisbar ist statt behauptet.
const QUOTE_KEY = "mc_ideen_quote"
function quoteLesen(): { gesendet: number; geaendert: number } {
try {
const d = JSON.parse(localStorage.getItem(QUOTE_KEY) || "{}")
return { gesendet: Number(d.gesendet) || 0, geaendert: Number(d.geaendert) || 0 }
} catch { return { gesendet: 0, geaendert: 0 } }
}
function quoteZaehlen(feld: "gesendet" | "geaendert"): void {
try {
const q = quoteLesen()
q[feld] += 1
localStorage.setItem(QUOTE_KEY, JSON.stringify(q))
} catch { /* privater Modus o. ä. — die Zählung ist Beiwerk, nie ein Grund zu scheitern */ }
}
function IdeenSection() {
const { data, isLoading } = useIdeen()
const qc = useQueryClient()
@@ -79,8 +100,13 @@ function IdeenSection() {
// still in die IDE-Welt (submit setzte welt:"ide"), ohne das irgendwo zu sagen.
const [modus, setModus] = useState<"denken" | "repo" | "box">("denken")
const [tiefe, setTiefe] = useState<"schlank" | "gruendlich">("schlank")
// 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.
// Zugeklappt statt Assistent (26.07.2026): der Entscheidungsbaum EXISTIERT — Wer baut das?,
// und nur im Zed-Ast: wie startest du? — aber du läufst ihn nur, wenn die Einordnung
// danebenlag. Im Normalfall steht da eine Zeile und du drückst Enter.
const [offen, setOffen] = useState(false)
// Hat die Box den Text wirklich gelesen? Steuert nur das Etikett „gelesen" in der
// zugeklappten Zeile. Ohne Einordnung (Text zu kurz, Aufruf gescheitert) steht dort die
// Voreinstellung — und die darf sich nicht als Verständnis ausgeben.
const [vorgeschlagen, setVorgeschlagen] = useState(false)
const isIde = modus !== "box" // denken UND repo laufen über das projektstart-Profil
const isProjekt = modus === "repo" // nur DANN sind Tiefe und Ziel jetzt schon nötig
@@ -140,8 +166,10 @@ function IdeenSection() {
ziel: isProjekt ? ziel : "",
art: modus === "denken" ? "idee" : "projekt",
}) })
quoteZaehlen("gesendet")
setText("")
setVorgeschlagen(false)
setOffen(false)
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
@@ -471,90 +499,115 @@ function IdeenSection() {
</button>
</div>
{/* ZWEI BEREICHE, drei Karten — ausgewaehlt ist immer genau EINE Karte, nie ein
ganzer Bereich. Der Bereich gruppiert nur, was denselben Weg geht: „durchdenken"
und „Repo" sind zwei Stufen desselben Wegs (weiterfuehren macht aus der einen die
andere), waehrend die Box-Welt etwas voellig anderes tut. */}
<div>
<p className="mb-2 text-[11px] text-muted-foreground/70">Was soll damit passieren?</p>
<div className="rounded-xl border border-border/40 bg-background/20 p-3">
<div className="mb-2 flex items-center gap-2">
<Code2 className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Du baust selbst, in Zed
</span>
</div>
<div className="grid gap-2 sm:grid-cols-2">
{([
{ id: "denken" as const, icon: Lightbulb, titel: "Erst durchdenken",
text: "Du bekommst ein Konzept. Gefällt es dir, wird auf Klick ein Repo daraus." },
{ id: "repo" as const, icon: GitFork, titel: "Direkt ein Repo",
text: "Konzept und Gitea-Repo in einem Zug — wenn du es ohnehin bauen willst." },
]).map((k) => (
<Wahl key={k.id} k={k} aktiv={modus === k.id} vorgeschlagen={vorgeschlagen}
onWahl={() => { setModus(k.id); setVorgeschlagen(false) }} />
))}
</div>
{/* Tiefe und Ziel nur beim Repo-Weg: beim Durchdenken fragt die Weiter-Leiste
sie spaeter — dann weisst du, was das Ding ueberhaupt wird. */}
{modus === "repo" && (
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-border/30 pt-3">
<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>
))}
{/* ZUGEKLAPPT STATT ASSISTENT. Der Entscheidungsbaum ist derselbe, den ein Wizard
abfragen wuerde — Wer baut das?, und NUR im Zed-Ast: wie startest du? Nur laeufst
du ihn erst, wenn die Einordnung danebenlag. Ein Assistent wuerde dich fragen, was
die Box in ~500 ms schon weiss; das hier fragt nur im Zweifelsfall. */}
{text.trim().length >= 3 && (
offen ? (
<div className="space-y-3 rounded-xl border border-border/40 bg-background/20 p-3">
<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">
<Wahl aktiv={modus !== "box"}
onWahl={() => setModus(modus === "box" ? "denken" : modus)}
k={{ id: "zed", icon: Code2, titel: "Ich, in Zed",
text: "Du bekommst Konzept und Repo — den Code schreibst du selbst." }} />
<Wahl aktiv={modus === "box"}
onWahl={() => setModus("box")}
k={{ id: "box", icon: Hammer, titel: "Die Box, allein",
text: "Für dein Heimlabor. Das Ergebnis kommt als Patch-Vorschlag ins Auftragsbuch." }} />
</div>
<span className="text-[10px] text-muted-foreground/60">
{tiefe === "gruendlich"
? "mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Härtetest"
: "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>
)}
{modus === "denken" && (
<p className="mt-2.5 flex items-start gap-1.5 border-t border-border/30 pt-2.5 text-[10px] leading-relaxed text-muted-foreground/60">
<HelpCircle className="mt-px h-3 w-3 shrink-0" />
Wie gründlich und wo es läuft, fragt die Box erst beim gefällt mir" — dann weißt du mehr.
</p>
)}
</div>
<div className="mt-2 rounded-xl border border-border/40 bg-background/20 p-3">
<div className="mb-2 flex items-center gap-2">
<Hammer className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Oder die Box macht es
</span>
{/* Die zweite Frage gibt es NUR im Zed-Ast — eingerückt, damit die Verschachtelung
sichtbar ist. Bei „die Box" folgt keine Rückfrage: dann geht es direkt los. */}
{modus !== "box" && (
<div className="border-l-2 border-primary/40 pl-3">
<p className="mb-1.5 text-[11px] text-muted-foreground/70">Und wie startest du?</p>
<div className="grid gap-2 sm:grid-cols-2">
<Wahl aktiv={modus === "denken"} onWahl={() => setModus("denken")}
k={{ id: "denken", icon: Lightbulb, titel: "Erst durchdenken",
text: "Konzept zuerst — auf dein Ja wird ein Repo daraus." }} />
<Wahl aktiv={modus === "repo"} onWahl={() => setModus("repo")}
k={{ id: "repo", icon: GitFork, titel: "Direkt ein Repo",
text: "Konzept und Gitea-Repo in einem Zug." }} />
</div>
{modus === "repo" ? (
<div className="mt-2.5 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)}
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="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 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="mt-2 flex items-start gap-1.5 text-[10px] leading-relaxed text-muted-foreground/60">
<HelpCircle className="mt-px h-3 w-3 shrink-0" />
Wie gründlich und wo es läuft, fragt die Box erst beim gefällt mir" — dann weißt du mehr.
</p>
)}
</div>
)}
{/* Beweisbar statt behauptet: die Quote, an der dieser ganze Entwurf haengt. */}
{(() => {
const q = quoteLesen()
if (q.gesendet < 3) return null
return (
<p className="border-t border-border/30 pt-2 text-[10px] text-muted-foreground/50">
Du hast {q.geaendert} von {q.gesendet} Vorschlägen geändert. Wird das die Regel,
bauen wir hier einen richtigen Schritt-für-Schritt-Assistenten.
</p>
)
})()}
</div>
<Wahl
k={{ id: "box", icon: Hammer, titel: "Die Box baut allein",
text: "Für dein Heimlabor Mission Control, Lucy, Rippy, Wartung, Doku. Das Ergebnis kommt als Patch-Vorschlag ins Auftragsbuch; nichts geht ohne deinen Klick live." }}
aktiv={modus === "box"} vorgeschlagen={vorgeschlagen}
onWahl={() => { setModus("box"); setVorgeschlagen(false) }} />
</div>
</div>
) : (
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-border/40 bg-background/20 px-3 py-2.5 text-xs">
{modus === "box" ? <Hammer className="h-4 w-4 shrink-0 text-primary" />
: modus === "repo" ? <GitFork className="h-4 w-4 shrink-0 text-primary" />
: <Lightbulb className="h-4 w-4 shrink-0 text-primary" />}
<span className="text-muted-foreground">
{modus === "box" ? "Ich baue das selbst und lege dir das Ergebnis als Vorschlag hin."
: modus === "repo" ? "Du baust selbst ich lege dir Konzept und Repo an."
: "Du baust selbst ich arbeite erst ein Konzept aus."}
</span>
{/* Nur wenn die Box wirklich gelesen hat. Ohne Einordnung (Text noch zu kurz
oder Aufruf gescheitert) steht hier die Voreinstellung — und die darf sich
nicht als Verständnis ausgeben. */}
{vorgeschlagen && (
<span className="shrink-0 rounded border border-primary/40 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold text-primary">
gelesen
</span>
)}
<button
onClick={() => { setOffen(true); quoteZaehlen("geaendert") }}
className="ml-auto shrink-0 text-[11px] text-muted-foreground/60 underline underline-offset-2 transition-colors hover:text-foreground cursor-pointer">
anders
</button>
</div>
)
)}
{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>
)}
@@ -634,10 +687,9 @@ function IdeenSection() {
// Eine der drei Auswahl-Karten. Der Text steht AN der Option, nicht in einer Legende
// darunter — man liest nur, was zu dem gehört, was man gerade anschaut.
function Wahl({ k, aktiv, vorgeschlagen, onWahl }: {
function Wahl({ k, aktiv, onWahl }: {
k: { id: string; icon: typeof Hammer; titel: string; text: string }
aktiv: boolean
vorgeschlagen: boolean
onWahl: () => void
}) {
return (
@@ -648,13 +700,6 @@ function Wahl({ k, aktiv, vorgeschlagen, onWahl }: {
<div className="flex items-center gap-2">
<k.icon className={cn("h-4 w-4 shrink-0", aktiv ? "text-primary" : "text-muted-foreground")} />
<span className="text-xs font-semibold text-foreground">{k.titel}</span>
{/* Etikett nur an der AUSGEWÄHLTEN Karte und nur solange der Vorschlag unberührt
ist — sonst wäre nie klar, wessen Entscheidung gerade dasteht. */}
{aktiv && vorgeschlagen && (
<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>