Feat: Memory-Onboarding via Hermes statt fester Starter-Vorlage

Empty State der Gedaechtnis-Seite fragt nicht mehr nach kanned Facts (war zu sehr
auf eine Person zugeschnitten), sondern leitet ein Onboarding-GESPRAECH mit Hermes
an: das UI zeigt einen fertigen, generischen Onboarding-Prompt (Hermes interviewt
den Nutzer in 5 Schritten). Buttons: "Im Terminal starten" (kopiert Prompt + springt
zum Terminal-Tab via mc-navigate-Event) und "Prompt kopieren"; Hinweis auf Telegram +
manuelles Anlegen. Das Gespraech fuellt das Gedaechtnis dann ueber den Auto-Lern-Hook.

App.tsx: mc-navigate CustomEvent -> setView (Tab-Wechsel aus Views heraus).

Live verifiziert (Onboarding-State, Prompt, Terminal-Navigation, kein Fehler).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 20:49:04 +02:00
parent 00209dedff
commit 689ea48d72
7 changed files with 194 additions and 177 deletions
@@ -1,4 +1,4 @@
import{r as K,a as Hv,g as Hr,R as Um,c as q2,j as Ne,l as Z2,S as K2,b as Vx,T as $2}from"./index-D7EBqqTV.js";/** import{r as K,a as Hv,g as Hr,R as Um,c as q2,j as Ne,l as Z2,S as K2,b as Vx,T as $2}from"./index-BVXzL-Al.js";/**
* @license * @license
* Copyright 2010-2023 Three.js Authors * Copyright 2010-2023 Three.js Authors
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-D7EBqqTV.js"></script> <script type="module" crossorigin src="/assets/index-BVXzL-Al.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DNH8F21o.css"> <link rel="stylesheet" crossorigin href="/assets/index-DGdkjhdp.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+9
View File
@@ -39,6 +39,15 @@ export default function App() {
return () => window.removeEventListener("open-system-drawer", handleOpen) return () => window.removeEventListener("open-system-drawer", handleOpen)
}, []) }, [])
useEffect(() => {
const handleNav = (e: Event) => {
const v = (e as CustomEvent).detail?.view as ViewId | undefined
if (v) setView(v)
}
window.addEventListener("mc-navigate", handleNav)
return () => window.removeEventListener("mc-navigate", handleNav)
}, [])
const active = NAV.find((n) => n.id === view)! const active = NAV.find((n) => n.id === view)!
return ( return (
+49 -52
View File
@@ -1,7 +1,7 @@
import { useState, useMemo, lazy, Suspense } from "react" import { useState, useMemo, lazy, Suspense } from "react"
import { import {
Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen, Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen,
Share2, List, Brain, X, Wand2, Share2, List, MessagesSquare, X, Copy, Check, Terminal as TerminalIcon, Send,
} from "lucide-react" } from "lucide-react"
import { api, type DedupeResult } from "@/lib/api" import { api, type DedupeResult } from "@/lib/api"
import { useMemory, useMemoryGraph, useQueryClient } from "@/lib/queries" import { useMemory, useMemoryGraph, useQueryClient } from "@/lib/queries"
@@ -31,16 +31,16 @@ const BORDER_CLASSES: Record<string, string> = {
versioned: "border-l-amber-500/80", ephemeral: "border-l-pink-500/80", versioned: "border-l-amber-500/80", ephemeral: "border-l-pink-500/80",
} }
// Starter-Vorlage („Beginner Template"): wird NUR auf Knopfdruck eingefügt (source=template), // Onboarding-Prompt: der Nutzer startet damit ein Gespräch mit Hermes (Terminal/Telegram).
// jeder Eintrag einzeln editier-/löschbar. Bewusst knapp & atomar gehalten. // Hermes interviewt ihn Schritt für Schritt — der Auto-Lern-Hook füllt dabei das Gedächtnis.
const STARTER_TEMPLATE: { content: string; category: string }[] = [ // Bewusst GENERISCH (nicht auf eine Person zugeschnitten).
{ content: "Der Nutzer heißt Tobi — Homelab-Entwickler, bevorzugt pragmatische Lösungen.", category: "user" }, const ONBOARDING_PROMPT = `Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
{ content: "Bevorzugt knappe, deutsche Antworten.", category: "user" }, 1) wer ich bin und woran ich gerade arbeite,
{ content: "Deploy nur via PowerShell-Push (Git Credential Manager).", category: "instruction" }, 2) wie ich angesprochen werden möchte,
{ content: "Tests bei Deployments nie überspringen.", category: "instruction" }, 3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
{ content: "AI-Box: 192.168.178.151 (Ryzen AI MAX+ 395, 122 GB Unified-RAM).", category: "stable" }, 4) wichtige Regeln/Konventionen, die du beachten sollst,
{ content: "Agent-Hirn = fast (Qwen3.6-35B-A3B), immer warm.", category: "stable" }, 5) meine Infrastruktur (Server, Dienste ohne Geheimnisse).
] Frag immer nur eine Sache auf einmal und fass am Ende zusammen, was du dir gemerkt hast.`
export function MemoryView() { export function MemoryView() {
const [filter, setFilter] = useState("") const [filter, setFilter] = useState("")
@@ -48,7 +48,7 @@ export function MemoryView() {
const [content, setContent] = useState("") const [content, setContent] = useState("")
const [category, setCategory] = useState("stable") const [category, setCategory] = useState("stable")
const [deduping, setDeduping] = useState(false) const [deduping, setDeduping] = useState(false)
const [seeding, setSeeding] = useState(false) const [copied, setCopied] = useState(false)
const [view, setView] = useState<"liste" | "graph">("graph") const [view, setView] = useState<"liste" | "graph">("graph")
const [showAdd, setShowAdd] = useState(false) const [showAdd, setShowAdd] = useState(false)
@@ -92,24 +92,19 @@ export function MemoryView() {
reloadMemory() reloadMemory()
} }
function applyTemplate() { async function copyPrompt() {
showConfirm(
"Starter-Vorlage einfügen",
`${STARTER_TEMPLATE.length} Beispiel-Fakten über dein Setup einfügen? Du kannst jeden einzeln bearbeiten oder löschen.`,
async () => {
setSeeding(true)
try { try {
for (const t of STARTER_TEMPLATE) { await navigator.clipboard.writeText(ONBOARDING_PROMPT)
await api("/api/memory", { method: "POST", body: JSON.stringify({ content: t.content, category: t.category, source: "template" }) }) setCopied(true)
setTimeout(() => setCopied(false), 1800)
} catch {
showAlert("Kopieren fehlgeschlagen", "Markiere den Prompt und kopiere ihn manuell (Strg+C).")
} }
reloadMemory()
} catch (e: any) {
showAlert("Fehler", `Vorlage konnte nicht eingefügt werden: ${e.message}`)
} finally {
setSeeding(false)
} }
},
) function goTerminal() {
copyPrompt()
window.dispatchEvent(new CustomEvent("mc-navigate", { detail: { view: "terminal" } }))
} }
async function cleanup() { async function cleanup() {
@@ -219,41 +214,43 @@ export function MemoryView() {
{/* Inhalt: Empty State · Graph · Liste */} {/* Inhalt: Empty State · Graph · Liste */}
{empty ? ( {empty ? (
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-10 flex flex-col items-center text-center gap-5"> <div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 sm:p-10 flex flex-col items-center text-center gap-5">
<div className="h-14 w-14 rounded-2xl bg-primary/10 flex items-center justify-center"> <div className="h-14 w-14 rounded-2xl bg-primary/10 flex items-center justify-center">
<Brain className="h-7 w-7 text-primary" /> <MessagesSquare className="h-7 w-7 text-primary" />
</div> </div>
<div className="space-y-1.5 max-w-md"> <div className="space-y-1.5 max-w-lg">
<h3 className="text-base font-semibold text-foreground">Dein Gedächtnis ist noch leer</h3> <h3 className="text-base font-semibold text-foreground">Lass Hermes dich kennenlernen</h3>
<p className="text-xs text-muted-foreground leading-relaxed"> <p className="text-xs text-muted-foreground leading-relaxed">
Es füllt sich automatisch, sobald Hermes mit dir spricht im Terminal, über Telegram oder aus der IDE. Statt Fakten einzutippen: führ ein kurzes Onboarding-Gespräch mit Hermes. Was ihr besprecht,
Oder leg jetzt mit einer Vorlage los: merkt sich das Gedächtnis automatisch du musst nichts manuell pflegen.
</p> </p>
</div> </div>
{/* Vorgeschlagener Prompt */}
<div className="w-full max-w-lg text-left">
<div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">Sende das an Hermes</span>
<button onClick={copyPrompt} className="flex items-center gap-1 text-[10px] font-semibold text-muted-foreground hover:text-foreground transition-colors">
{copied ? <><Check className="h-3 w-3 text-emerald-400" /> Kopiert</> : <><Copy className="h-3 w-3" /> Kopieren</>}
</button>
</div>
<pre className="w-full rounded-xl border border-border/60 bg-background/40 p-3.5 text-[11px] leading-relaxed text-foreground/90 whitespace-pre-wrap font-mono">{ONBOARDING_PROMPT}</pre>
</div>
<div className="flex flex-wrap items-center justify-center gap-2"> <div className="flex flex-wrap items-center justify-center gap-2">
<button onClick={applyTemplate} disabled={seeding} <button onClick={goTerminal}
className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer disabled:opacity-50"> className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer">
<Wand2 className="h-4 w-4" /> {seeding ? "Füge ein…" : "Starter-Vorlage einfügen"} <TerminalIcon className="h-4 w-4" /> Im Terminal starten
</button> </button>
<button onClick={() => setShowAdd(true)} <button onClick={copyPrompt}
className="h-9 px-4 rounded-lg border border-border/60 bg-card text-xs font-semibold hover:border-primary/50 transition-all flex items-center gap-1.5 cursor-pointer"> className="h-9 px-4 rounded-lg border border-border/60 bg-card text-xs font-semibold hover:border-primary/50 transition-all flex items-center gap-1.5 cursor-pointer">
<Plus className="h-4 w-4" /> Eigener Eintrag {copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />} Prompt kopieren
</button> </button>
</div> </div>
<div className="w-full max-w-md text-left"> <p className="text-[11px] text-muted-foreground/70 flex items-center gap-1.5">
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60 mb-2">Die Vorlage enthält</div> <Send className="h-3 w-3" /> Funktioniert genauso über Telegram {" "}
<div className="flex flex-col gap-1"> <button onClick={() => setShowAdd(true)} className="underline hover:text-foreground">oder lieber manuell anlegen</button>.
{STARTER_TEMPLATE.map((t, i) => { </p>
const conf = CAT_CONFIG[t.category] || DEFAULT_CAT
return (
<div key={i} className="flex items-center gap-2 text-[11px] text-muted-foreground">
<span className={cn("text-[8px] font-bold uppercase px-1.5 py-0.5 rounded shrink-0", conf.bg, conf.text)}>{conf.label}</span>
<span className="truncate">{t.content}</span>
</div>
)
})}
</div>
</div>
</div> </div>
) : view === "graph" ? ( ) : view === "graph" ? (
<GraphErrorBoundary> <GraphErrorBoundary>