This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+11
-6
File diff suppressed because one or more lines are too long
Vendored
+10
-5
File diff suppressed because one or more lines are too long
-44
File diff suppressed because one or more lines are too long
+12
-7
File diff suppressed because one or more lines are too long
+13
-8
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{W as b,u as f,Z as h,r as d,j as e,e as c,Y as g,T as p,a1 as j,a2 as N,g as v,a3 as w,q as x}from"./index-NrNmEOGE.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||
@@ -0,0 +1 @@
|
||||
import{K as b,u as f,N as h,r as d,j as e,e as c,M as g,T as p,R as j,U as N,g as v,V as w,q as x}from"./index-QPm6yWLB.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||
-52
File diff suppressed because one or more lines are too long
-73
File diff suppressed because one or more lines are too long
+83
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as s,j as e}from"./index-NrNmEOGE.js";/**
|
||||
import{c as s,j as e}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
import{am as y,r as x,g as L,j as e,L as v,S,af as W,R as C,e as w,an as E}from"./index-NrNmEOGE.js";import{F as M}from"./folder-open-Bw6KyBDQ.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function R(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
||||
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
||||
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{R as WissenView};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import{af as y,r as x,g as L,j as e,L as v,S,a9 as W,a1 as C,e as w,ag as E}from"./index-QPm6yWLB.js";import{F as M}from"./folder-open-c9H3RsKD.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
||||
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
||||
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import{c as a}from"./index-NrNmEOGE.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const e=a("BookOpen",[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]]);export{e as B};
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as o}from"./index-NrNmEOGE.js";/**
|
||||
import{c as o}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-NrNmEOGE.js";/**
|
||||
import{c as e}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import{c}from"./index-NrNmEOGE.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const t=c("Copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);export{t as C};
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-NrNmEOGE.js";/**
|
||||
import{c as a}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-1
File diff suppressed because one or more lines are too long
-450
File diff suppressed because one or more lines are too long
+430
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-NrNmEOGE.js";/**
|
||||
import{c as a}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-NrNmEOGE.js";/**
|
||||
import{c as t}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import{c as a}from"./index-NrNmEOGE.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const c=a("Send",[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",key:"1ffxy3"}],["path",{d:"m21.854 2.147-10.94 10.939",key:"12cjpa"}]]);export{c as S};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-NrNmEOGE.js";/**
|
||||
import{c as a}from"./index-QPm6yWLB.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-NrNmEOGE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ESDQHZDA.css">
|
||||
<script type="module" crossorigin src="/assets/index-QPm6yWLB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-vRtPLyEM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { CockpitView } from "@/views/cockpit/CockpitView"
|
||||
// kommt als eigenes Chunk erst beim ersten Aufruf — kleineres Start-Bundle.
|
||||
const ModelsView = lazy(() => import("@/views/ModelsView").then((m) => ({ default: m.ModelsView })))
|
||||
const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ default: m.ConnectView })))
|
||||
const MemoryView = lazy(() => import("@/views/MemoryView").then((m) => ({ default: m.MemoryView })))
|
||||
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
|
||||
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
|
||||
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
||||
@@ -206,7 +205,6 @@ export default function App() {
|
||||
{view === "auftraege" && <AuftragsbuchView />}
|
||||
{view === "models" && <ModelsView />}
|
||||
{view === "connect" && <ConnectView />}
|
||||
{view === "memory" && <MemoryView />}
|
||||
{view === "wissen" && <WissenView />}
|
||||
{view === "chronik" && <ChronikView />}
|
||||
|
||||
|
||||
+1
-3
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Boxes,
|
||||
Brain,
|
||||
Inbox,
|
||||
History,
|
||||
Library,
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
export type ViewId = "dashboard" | "ideen" | "auftraege" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||
export type ViewId = "dashboard" | "ideen" | "auftraege" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||
|
||||
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
||||
|
||||
@@ -36,7 +35,6 @@ export const NAV: NavItem[] = [
|
||||
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
||||
|
||||
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
||||
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain, group: "Wissen" },
|
||||
{ id: "wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
|
||||
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug, group: "Werkzeuge" },
|
||||
{ id: "konsole", label: "Konsole", hint: "Direkte Box-Shell (SSH-artig)", icon: SquareTerminal, group: "Werkzeuge" },
|
||||
|
||||
@@ -1,477 +0,0 @@
|
||||
import { useState, useMemo, lazy, Suspense, useEffect, useDeferredValue } from "react"
|
||||
import {
|
||||
Trash2, Sparkles, User, Scroll, Shield, Clock, Search, Plus, BookOpen,
|
||||
Share2, List, MessagesSquare, X, Copy, Check, Terminal as TerminalIcon, Send,
|
||||
Maximize2, Minimize2
|
||||
} from "lucide-react"
|
||||
import { api, type DedupeResult } from "@/lib/api"
|
||||
import { useMemory, useMemoryGraph, useQueryClient } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { GraphErrorBoundary } from "./GraphErrorBoundary"
|
||||
|
||||
// Sigma/graphology nur laden, wenn der Graph-Tab geöffnet wird (Code-Splitting).
|
||||
const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView })))
|
||||
|
||||
const CATEGORIES = ["identity", "knowledge", "rules", "events"]
|
||||
|
||||
// Herkunft: automatisch gelernt (Mem0-Extraktion / Agent / Hermes) vs. manuell/Vorlage.
|
||||
const AUTO_SOURCES = new Set(["auto", "agent", "hermes"])
|
||||
|
||||
const CAT_CONFIG: Record<string, { label: string; icon: any; bg: string; text: string }> = {
|
||||
identity: { label: "Identität", icon: User, bg: "bg-cyan-500/10", text: "text-cyan-400" },
|
||||
knowledge: { label: "Wissen", icon: Shield, bg: "bg-indigo-500/10", text: "text-indigo-400" },
|
||||
rules: { label: "Regeln", icon: Scroll, bg: "bg-violet-500/10", text: "text-violet-400" },
|
||||
events: { label: "Ereignisse", icon: Clock, bg: "bg-amber-500/10", text: "text-amber-400" },
|
||||
}
|
||||
const DEFAULT_CAT = { label: "Gedächtnis", icon: BookOpen, bg: "bg-muted/10", text: "text-muted-foreground" }
|
||||
|
||||
const BORDER_CLASSES: Record<string, string> = {
|
||||
identity: "border-l-cyan-500/80", knowledge: "border-l-indigo-500/80",
|
||||
rules: "border-l-violet-500/80", events: "border-l-amber-500/80",
|
||||
}
|
||||
|
||||
// Hex-Farben für Inline-Styles (Graph-Punkte, Detail-Panel) — Tailwind-Klassen aus
|
||||
// CAT_CONFIG greifen dort nicht.
|
||||
const CAT_COLOR_MAP: Record<string, string> = {
|
||||
identity: "#00f5ff", knowledge: "#3b82f6", rules: "#d946ef", events: "#fbbf24",
|
||||
}
|
||||
|
||||
// Onboarding-Prompt: der Nutzer startet damit ein Gespräch mit Hermes (Terminal/Telegram).
|
||||
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:
|
||||
1) wer ich bin und woran ich gerade arbeite,
|
||||
2) wie ich angesprochen werden möchte,
|
||||
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
||||
4) wichtige Regeln/Konventionen, die du beachten sollst,
|
||||
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() {
|
||||
const [filter, setFilter] = useState("")
|
||||
const [q, setQ] = useState("")
|
||||
const [content, setContent] = useState("")
|
||||
const [category, setCategory] = useState("knowledge")
|
||||
const [deduping, setDeduping] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [view, setView] = useState<"liste" | "graph">("liste")
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
// Nur EINE Graph-Instanz mounten: die CSS-Klassen (hidden/lg:hidden) verstecken zwar,
|
||||
// aber eine per display:none versteckte ForceGraph2D-Instanz simuliert und rendert
|
||||
// trotzdem weiter — das hat die CPU-Last der Ansicht glatt verdoppelt.
|
||||
const [isDesktop, setIsDesktop] = useState(
|
||||
() => typeof window !== "undefined" && window.matchMedia("(min-width: 1024px)").matches
|
||||
)
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia("(min-width: 1024px)")
|
||||
const onChange = (e: MediaQueryListEvent) => setIsDesktop(e.matches)
|
||||
mq.addEventListener("change", onChange)
|
||||
return () => mq.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const { data: allItems = [] } = useMemory({})
|
||||
const { data: items = [], error: itemsErr } = useMemory({ q, category: filter })
|
||||
const { data: graph } = useMemoryGraph(true)
|
||||
const error = itemsErr ? String(itemsErr) : ""
|
||||
|
||||
// Escape-Taste schließt den Vollbildmodus
|
||||
useEffect(() => {
|
||||
if (!isExpanded) return
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setIsExpanded(false)
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [isExpanded])
|
||||
|
||||
const reloadMemory = () => {
|
||||
qc.invalidateQueries({ queryKey: ["memory"] })
|
||||
qc.invalidateQueries({ queryKey: ["memory-graph"] })
|
||||
}
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const total = allItems.length
|
||||
const auto = allItems.filter((m) => AUTO_SOURCES.has(m.source)).length
|
||||
return { total, auto, manual: total - auto, cats: new Set(allItems.map((m) => m.category)).size }
|
||||
}, [allItems])
|
||||
const empty = allItems.length === 0
|
||||
|
||||
// Graph clientseitig nach Suchtext filtern (Knoten + deren Kanten).
|
||||
// useDeferredValue: der Graph-Umbau (Re-Layout) hinkt der Tipp-Eingabe nach,
|
||||
// statt bei jedem Tastendruck die D3-Simulation neu anzuwerfen.
|
||||
const deferredQ = useDeferredValue(q)
|
||||
const graphData = useMemo(() => {
|
||||
const g = graph ?? { nodes: [], edges: [] }
|
||||
if (!deferredQ.trim()) return g
|
||||
const ql = deferredQ.toLowerCase()
|
||||
const ns = g.nodes.filter((n) => n.content.toLowerCase().includes(ql))
|
||||
const ids = new Set(ns.map((n) => n.id))
|
||||
return { nodes: ns, edges: g.edges.filter((e) => ids.has(e.source) && ids.has(e.target)) }
|
||||
}, [graph, deferredQ])
|
||||
|
||||
// Liste nach Kategorie gruppieren (skaliert: klare Sektionen statt einer flachen Wand).
|
||||
const grouped = useMemo(() => {
|
||||
const g: Record<string, (typeof items)[number][]> = {}
|
||||
items.forEach((m) => { (g[m.category] ??= []).push(m) })
|
||||
return g
|
||||
}, [items])
|
||||
const otherItems = useMemo(() => items.filter((m) => !CATEGORIES.includes(m.category)), [items])
|
||||
|
||||
async function add() {
|
||||
if (!content.trim()) return
|
||||
await api("/api/memory", { method: "POST", body: JSON.stringify({ content, category, source: "ui" }) })
|
||||
setContent(""); setShowAdd(false); reloadMemory()
|
||||
}
|
||||
|
||||
function del(id: string) {
|
||||
showConfirm("Eintrag löschen?", "Diesen Gedächtnis-Eintrag dauerhaft entfernen?", async () => {
|
||||
try { await api(`/api/memory/${id}`, { method: "DELETE" }); reloadMemory() }
|
||||
catch (e: any) { showAlert("Fehler", `Löschen fehlgeschlagen: ${e.message || e}`) }
|
||||
})
|
||||
}
|
||||
|
||||
async function copyPrompt() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(ONBOARDING_PROMPT)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1800)
|
||||
} catch {
|
||||
showAlert("Kopieren fehlgeschlagen", "Markiere den Prompt und kopiere ihn manuell (Strg+C).")
|
||||
}
|
||||
}
|
||||
|
||||
function goTerminal() {
|
||||
copyPrompt()
|
||||
// "konsole" ist die echte View-Id (nav.ts) — "terminal" existierte nie, der Knopf lief ins Leere.
|
||||
window.dispatchEvent(new CustomEvent("mc-navigate", { detail: { view: "konsole" } }))
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
setDeduping(true)
|
||||
try {
|
||||
const dry = await api<DedupeResult>("/api/memory/dedupe", { method: "POST", body: JSON.stringify({ apply: false }) })
|
||||
if (dry.duplicate_count === 0) { showAlert("Ergebnis", "Keine Dubletten gefunden — alles sauber."); return }
|
||||
// Konkrete Vorschau statt bloßer Zahl: was bleibt, was fliegt (erste 5 Gruppen).
|
||||
const shorten = (s: string) => (s.length > 90 ? s.slice(0, 90) + "…" : s)
|
||||
const preview = dry.groups.slice(0, 5).map((g) =>
|
||||
`✓ bleibt: „${shorten(g.keep.content)}"\n✗ weg: ${g.remove.map((r) => `„${shorten(r.content)}"`).join(", ")}`
|
||||
).join("\n\n") + (dry.groups.length > 5 ? `\n\n… und ${dry.groups.length - 5} weitere Gruppe(n).` : "")
|
||||
showConfirm("Deduplizierung bestätigen",
|
||||
`${dry.duplicate_count} Dublette(n) in ${dry.groups.length} Gruppe(n) gefunden:\n\n${preview}\n\nZusammenführen?`,
|
||||
async () => {
|
||||
try { await api("/api/memory/dedupe", { method: "POST", body: JSON.stringify({ apply: true }) }); reloadMemory() }
|
||||
catch (e: any) { showAlert("Fehler", `Fehler beim Löschen: ${e.message}`) }
|
||||
})
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler bei der Deduplizierung: ${e.message}`)
|
||||
} finally {
|
||||
setDeduping(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Suche in ALLEN Elementen, damit Klicks auf dem Graphen nicht durch lokale Filter der linken Liste blockiert werden.
|
||||
const sel = useMemo(() => allItems.find((m) => m.id === selected), [allItems, selected])
|
||||
const neighbors = useMemo(() => {
|
||||
if (!sel || !graph) return []
|
||||
const connectedIds = new Set([
|
||||
...graph.edges.filter((e) => e.source === sel.id).map((e) => e.target),
|
||||
...graph.edges.filter((e) => e.target === sel.id).map((e) => e.source)
|
||||
])
|
||||
return allItems.filter((m) => connectedIds.has(m.id))
|
||||
}, [sel, graph, allItems])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{empty ? (
|
||||
<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">
|
||||
<MessagesSquare className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
<div className="space-y-1.5 max-w-lg">
|
||||
<h3 className="text-base font-semibold text-foreground">Lass Hermes dich kennenlernen</h3>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Statt Fakten einzutippen: führ ein kurzes Onboarding-Gespräch mit Hermes. Was ihr besprecht,
|
||||
merkt sich das Gedächtnis automatisch — du musst nichts manuell pflegen.
|
||||
</p>
|
||||
</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">
|
||||
<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">
|
||||
<TerminalIcon className="h-4 w-4" /> Im Terminal starten
|
||||
</button>
|
||||
<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">
|
||||
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />} Prompt kopieren
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground/70 flex items-center gap-1.5">
|
||||
<Send className="h-3 w-3" /> Funktioniert genauso über Telegram —{" "}
|
||||
<button onClick={() => setShowAdd(true)} className="underline hover:text-foreground">oder lieber manuell anlegen</button>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* Der voll integrierte, schwebende Wissens-Hub */
|
||||
<div className={cn(
|
||||
"bg-[#030712] overflow-hidden transition-all duration-300 ease-in-out",
|
||||
isExpanded
|
||||
? "fixed inset-0 z-50 w-screen h-screen"
|
||||
: "relative w-full h-[calc(100vh-8.5rem)] min-h-[620px] rounded-2xl border border-border/60 flex flex-col lg:block"
|
||||
)}>
|
||||
|
||||
{/* 1. GraphView nimmt auf Desktop 100% des Hintergrunds ein */}
|
||||
{isDesktop && (
|
||||
<div className="absolute inset-0 z-0 hidden lg:block">
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="absolute inset-0 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen…</div>}>
|
||||
<GraphView data={graphData} selectedNodeId={selected} onNodeSelect={setSelected} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2. Schwebendes linkes Panel (Wissens-Liste) */}
|
||||
<div className={cn(
|
||||
"z-10 flex flex-col transition-all duration-300",
|
||||
isExpanded
|
||||
? "lg:absolute lg:left-6 lg:top-6 lg:bottom-6 lg:w-96 lg:rounded-xl lg:border lg:border-border/40 lg:bg-background/65 lg:backdrop-blur-md lg:p-4 lg:shadow-2xl lg:shadow-black/50 lg:h-[calc(100%-3rem)]"
|
||||
: "lg:absolute lg:left-4 lg:top-4 lg:bottom-4 lg:w-96 lg:rounded-xl lg:border lg:border-border/40 lg:bg-background/65 lg:backdrop-blur-md lg:p-4 lg:shadow-2xl lg:shadow-black/50 lg:h-[calc(100%-2rem)]",
|
||||
"w-full h-full p-4 flex-1 lg:flex-none",
|
||||
view === "graph" && "hidden lg:flex"
|
||||
)}>
|
||||
{/* Header & Steuerung direkt im Panel */}
|
||||
<div className="space-y-3 mb-3 shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
|
||||
Gedächtnis-Pool
|
||||
</h1>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed mt-0.5">
|
||||
IDEs & Gateway lesen und lernen hier per MCP.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Maximize/Minimize Button für Desktop */}
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="p-1.5 rounded-lg border border-border/40 bg-card/45 hover:border-primary/50 text-muted-foreground hover:text-foreground transition-all cursor-pointer hidden lg:block"
|
||||
title={isExpanded ? "Vollbild verlassen (Esc)" : "In Vollbild maximieren"}
|
||||
>
|
||||
{isExpanded ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Statistiken (Chips) */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-[9px] font-semibold bg-primary/10 text-primary border border-primary/20 px-2 py-0.5 rounded-md">{stats.total} Fakten</span>
|
||||
<span className="text-[9px] font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-md">{stats.auto} auto</span>
|
||||
<span className="text-[9px] font-semibold bg-card/60 text-muted-foreground border border-border/40 px-2 py-0.5 rounded-md">{stats.cats} Kat.</span>
|
||||
</div>
|
||||
|
||||
{/* Steuerungsleiste: Suchen, Eintrag, Deduplizieren */}
|
||||
<div className="flex items-center gap-1.5 w-full">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
aria-label="Gedächtnis durchsuchen"
|
||||
type="search"
|
||||
placeholder="Semantisch suchen…"
|
||||
className="w-full h-8 pl-8 pr-3 rounded-lg border border-border/50 bg-background/40 text-[11px] outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
|
||||
/>
|
||||
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<button onClick={() => setShowAdd((s) => !s)} title="Eintrag hinzufügen"
|
||||
className="h-8 w-8 shrink-0 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center justify-center cursor-pointer shadow-md shadow-primary/10">
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
<button onClick={cleanup} disabled={deduping} title="Deduplizieren"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-card/45 hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50">
|
||||
<Sparkles className="h-3.5 w-3.5 text-primary" />
|
||||
</button>
|
||||
|
||||
{/* Mobile View Switcher */}
|
||||
<div className="flex items-center gap-1 p-0.5 bg-card/30 border border-border/40 rounded-lg lg:hidden">
|
||||
{([["liste", List], ["graph", Share2]] as const).map(([v, Icon]) => (
|
||||
<button key={v} onClick={() => setView(v)}
|
||||
className={cn("flex h-7 w-7 items-center justify-center rounded-md transition-all cursor-pointer",
|
||||
view === v ? "bg-primary text-primary-foreground" : "text-muted-foreground")}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kategorie-Filter */}
|
||||
<div className="flex flex-wrap items-center gap-1 p-0.5 bg-card/30 border border-border/40 rounded-lg w-full overflow-x-auto shrink-0 scrollbar-none">
|
||||
<button onClick={() => setFilter("")}
|
||||
className={cn("h-6 px-2 rounded-md text-[9px] font-semibold uppercase tracking-wider transition-all cursor-pointer shrink-0",
|
||||
!filter ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>Alle</button>
|
||||
{CATEGORIES.map((c) => {
|
||||
const conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||
return (
|
||||
<button key={c} onClick={() => setFilter(c)}
|
||||
className={cn("h-6 px-2 rounded-md text-[9px] font-semibold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1 shrink-0",
|
||||
filter === c ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
|
||||
{conf.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listenelemente (Scrollbar im Glaspanel) */}
|
||||
<div className="flex-1 overflow-y-auto pr-1 scrollbar-thin space-y-4">
|
||||
{showAdd && (
|
||||
<div className="rounded-xl border border-border/50 bg-background/50 p-3 space-y-2 shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Neuer Eintrag</div>
|
||||
<button onClick={() => setShowAdd(false)} className="text-muted-foreground hover:text-foreground"><X className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
<textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2}
|
||||
placeholder="Vorliebe, Regel oder Fakt…"
|
||||
className="w-full resize-none rounded-lg border border-border/50 bg-background/30 p-2 text-[11px] outline-none focus:ring-1 focus:ring-primary/50 text-foreground leading-relaxed" />
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<select value={category} onChange={(e) => setCategory(e.target.value)}
|
||||
className="h-7 rounded-md border border-border/50 bg-background/50 px-1 text-[10px] outline-none text-foreground cursor-pointer">
|
||||
{CATEGORIES.map((c) => <option key={c} value={c} className="bg-popover text-foreground">{CAT_CONFIG[c]?.label || c}</option>)}
|
||||
</select>
|
||||
<button onClick={add} className="h-7 px-2.5 rounded-md bg-primary text-primary-foreground text-[10px] font-semibold hover:opacity-90 transition-all cursor-pointer">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="rounded-lg border border-red-500/20 bg-red-500/5 p-3 text-[10px] text-red-400">Fehler: {error}</div>}
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-[11px] text-muted-foreground border border-dashed border-border/60 rounded-xl p-8 text-center bg-card/10">
|
||||
Keine Einträge gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{[...CATEGORIES, "__other"].map((c) => {
|
||||
const list = c === "__other" ? otherItems : (grouped[c] || [])
|
||||
if (!list.length) return null
|
||||
const conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||
const Icon = conf.icon
|
||||
return (
|
||||
<div key={c} className="space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 px-0.5">
|
||||
<Icon className={cn("h-3 w-3", conf.text)} />
|
||||
<span className={cn("text-[9px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
|
||||
<span className="text-[9px] font-mono text-muted-foreground/60 bg-card/50 rounded px-1">{list.length}</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{list.map((m) => {
|
||||
const isAuto = AUTO_SOURCES.has(m.source)
|
||||
const isSelected = selected === m.id
|
||||
return (
|
||||
<div key={m.id} onClick={() => setSelected(isSelected ? null : m.id)}
|
||||
className={cn(
|
||||
"flex items-start justify-between gap-3 p-2.5 rounded-lg border border-l-4 transition-all group cursor-pointer text-left",
|
||||
isSelected ? "bg-primary/10 border-primary shadow-md" : "bg-card/45 border-border/40 hover:bg-card/75",
|
||||
BORDER_CLASSES[m.category] || "border-l-muted"
|
||||
)}>
|
||||
<span className="text-[11px] text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0 self-center">
|
||||
{isAuto && <span title="Auto gelernt"><Sparkles className="h-2.5 w-2.5 text-emerald-400" /></span>}
|
||||
<button onClick={(e) => { e.stopPropagation(); del(m.id); if (selected === m.id) setSelected(null) }}
|
||||
className="h-5 w-5 rounded-md flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Mobiler Fallback für den Graph (unter lg) — nur mounten, wenn wirklich sichtbar */}
|
||||
{!isDesktop && view === "graph" && (
|
||||
<div className="w-full h-[500px] relative lg:hidden">
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="h-full rounded-2xl border border-border/60 bg-card/30 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen…</div>}>
|
||||
<GraphView data={graphData} selectedNodeId={selected} onNodeSelect={setSelected} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. Schwebendes rechtes Panel (Details zum ausgewählten Knoten) */}
|
||||
{sel && (
|
||||
<div className={cn(
|
||||
"z-10 flex flex-col transition-all duration-300",
|
||||
isExpanded
|
||||
? "lg:absolute lg:right-6 lg:top-6 lg:bottom-6 lg:w-80 lg:rounded-xl lg:border lg:border-border/40 lg:bg-background/65 lg:backdrop-blur-md lg:p-4 lg:shadow-2xl lg:shadow-black/50 lg:h-[calc(100%-3rem)]"
|
||||
: "lg:absolute lg:right-4 lg:top-4 lg:bottom-4 lg:w-80 lg:rounded-xl lg:border lg:border-border/40 lg:bg-background/65 lg:backdrop-blur-md lg:p-4 lg:shadow-2xl lg:shadow-black/50 lg:h-[calc(100%-2rem)]",
|
||||
"w-full p-4 border-t border-border/60 bg-card/45 shrink-0 flex flex-col"
|
||||
)}>
|
||||
<div className="flex items-center gap-2 mb-3 shrink-0">
|
||||
<span className="w-2.5 h-2.5 rounded-full" style={{ background: CAT_COLOR_MAP[sel.category] }} />
|
||||
<span className="text-[9px] font-bold uppercase tracking-wider" style={{ color: CAT_COLOR_MAP[sel.category] }}>
|
||||
{CAT_CONFIG[sel.category]?.label ?? sel.category}
|
||||
</span>
|
||||
<span className={`ml-auto text-[9px] font-mono flex items-center gap-1 ${AUTO_SOURCES.has(sel.source) ? "text-emerald-400" : "text-muted-foreground/70"}`}>
|
||||
{AUTO_SOURCES.has(sel.source) && <Sparkles className="h-2.5 w-2.5" />}
|
||||
{sel.source}
|
||||
</span>
|
||||
<button onClick={() => setSelected(null)} className="text-muted-foreground hover:text-foreground ml-1.5 cursor-pointer"><X className="h-4 w-4" /></button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-1 scrollbar-thin text-xs text-foreground leading-relaxed mb-4 break-words whitespace-pre-wrap text-left">
|
||||
{sel.content}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto shrink-0 space-y-3 pt-3 border-t border-border/30">
|
||||
<div className="text-[9px] font-bold uppercase tracking-wider text-muted-foreground/70 flex items-center gap-1">
|
||||
<Share2 className="h-2.5 w-2.5" /> verwandte Fakten
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 max-h-40 overflow-y-auto scrollbar-none text-left">
|
||||
{neighbors.length ? neighbors.map((n) => (
|
||||
<button key={n.id} onClick={() => setSelected(n.id)}
|
||||
className="flex items-center gap-2 text-[10px] text-muted-foreground hover:text-foreground text-left transition-colors cursor-pointer py-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: CAT_COLOR_MAP[n.category] }} />
|
||||
<span className="truncate">{n.content}</span>
|
||||
</button>
|
||||
)) : <span className="text-[10px] text-muted-foreground/60">—</span>}
|
||||
</div>
|
||||
|
||||
<button onClick={() => { del(sel.id); setSelected(null) }}
|
||||
className="flex w-full items-center justify-center gap-1.5 text-[10px] font-bold text-red-400 border border-red-500/20 rounded-lg py-2 hover:bg-red-500/5 transition-all cursor-pointer">
|
||||
<Trash2 className="h-3.5 w-3.5" /> Eintrag vergessen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Boxes, Brain, Server, ShieldAlert, Plug,
|
||||
Boxes, Server, ShieldAlert, Plug,
|
||||
HeartPulse, AlertTriangle, Loader2, Wrench, Check, HardDrive, ChevronRight,
|
||||
Inbox, HelpCircle,
|
||||
} from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import {
|
||||
useModels, useServices, useUpdates, useMemory, useAgentStatus, useConnectHealth,
|
||||
useModels, useServices, useUpdates, useAgentStatus, useConnectHealth,
|
||||
useAuftragsbuch, useIdeen, useQueryClient, qk,
|
||||
} from "@/lib/queries"
|
||||
import { useLucyHealth, type Repair } from "@/lib/useLucyHealth"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CockpitTile } from "./CockpitTile"
|
||||
import { ErinnerungenCard } from "./ErinnerungenCard"
|
||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||
import { LatencyCard } from "@/components/dashboard/LatencyCard"
|
||||
|
||||
@@ -28,7 +27,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
const { data: models } = useModels()
|
||||
const { data: svc } = useServices()
|
||||
const { data: updates } = useUpdates()
|
||||
const { data: memory } = useMemory()
|
||||
const { data: agent } = useAgentStatus()
|
||||
const { data: connect } = useConnectHealth()
|
||||
const { data: auftraege } = useAuftragsbuch()
|
||||
@@ -66,7 +64,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
const svcOk = svc?.services.filter((s) => s.ok).length ?? 0
|
||||
const svcTotal = svc?.services.length ?? 0
|
||||
const svcErrors = svcTotal - svcOk
|
||||
const notes = memory?.length ?? 0
|
||||
const pendingCount =
|
||||
(updates ? (updates.os > 0 ? 1 : 0) + (updates.engine > 0 ? 1 : 0) + (updates.swap > 0 ? 1 : 0) + (updates.models > 0 ? 1 : 0) : 0) +
|
||||
(updates?.components?.filter((c) => c.update === true).length ?? 0)
|
||||
@@ -85,7 +82,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
// Zusätzliche reaktive Livedaten für die Kacheln
|
||||
const activeBrain = models?.models?.find((m: any) => m.role === "hermes")
|
||||
const brainName = activeBrain?.name ? activeBrain.name.split("/").pop()?.replace(/\.gguf$/i, "") : ""
|
||||
const autoNotes = memory ? memory.filter((m: any) => ["auto", "agent", "hermes"].includes(m.source)).length : 0
|
||||
const failedSvc = svc?.services.filter((s: any) => !s.ok).map((s: any) => s.name).join(", ")
|
||||
|
||||
const upParts = updates ? [
|
||||
@@ -143,12 +139,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
hint={brainName ? `Hirn: ${brainName}` : "Kein Hermes-Modell geladen"}
|
||||
onClick={() => onNavigate("models")}
|
||||
/>
|
||||
<CockpitTile
|
||||
icon={Brain} title="Gedächtnis" value={memory ? notes : "…"} unit="Notizen"
|
||||
tone={memory ? "ok" : "loading"}
|
||||
hint={memory ? `${notes} Fakten (${autoNotes} auto gelernt)` : "Gedächtnis-Pool laden..."}
|
||||
onClick={() => onNavigate("memory")}
|
||||
/>
|
||||
<CockpitTile
|
||||
icon={Server} title="Dienste" value={svc ? `${svcOk}/${svcTotal}` : "…"} unit="laufen"
|
||||
tone={!svc ? "loading" : svcErrors > 0 ? "warn" : "ok"}
|
||||
@@ -186,12 +176,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
|
||||
{/* Anstehendes — Erinnerungen & Routinen */}
|
||||
<section>
|
||||
<ZoneLabel>Anstehendes</ZoneLabel>
|
||||
<ErinnerungenCard />
|
||||
</section>
|
||||
|
||||
{/* Werkzeuge & Shortcuts */}
|
||||
<section>
|
||||
<ZoneLabel>Werkzeuge & Diagnose</ZoneLabel>
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { useState } from "react"
|
||||
import { AlarmClock, Plus, Trash2, Loader2, X, Repeat } from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import { useReminders, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// „Anstehendes": Erinnerungen & Routinen sichtbar und klickbar machen. Die API kann
|
||||
// längst alles (Lucy legt per Stimme an) — hier kommt nur die GUI-Hälfte dazu.
|
||||
|
||||
const REPEAT_LABEL: Record<string, string> = {
|
||||
"": "einmalig", daily: "täglich", weekdays: "werktags", weekly: "wöchentlich",
|
||||
}
|
||||
|
||||
export function ErinnerungenCard() {
|
||||
const { data: items = [], isLoading } = useReminders()
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
const [text, setText] = useState("")
|
||||
const [when, setWhen] = useState("")
|
||||
const [repeat, setRepeat] = useState("")
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const reload = () => qc.invalidateQueries({ queryKey: qk.reminders })
|
||||
|
||||
async function add() {
|
||||
if (!text.trim() || !when) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await api("/api/reminders", { method: "POST", body: JSON.stringify({ text, when, repeat }) })
|
||||
setText(""); setWhen(""); setRepeat(""); setShowAdd(false)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Anlegen fehlgeschlagen", String(e?.message || e))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function del(id: number, t: string) {
|
||||
showConfirm("Erinnerung löschen?", `„${t}" wird entfernt.`, async () => {
|
||||
try { await api(`/api/reminders/${id}`, { method: "DELETE" }); reload() }
|
||||
catch (e: any) { showAlert("Fehler", String(e?.message || e)) }
|
||||
})
|
||||
}
|
||||
|
||||
const sorted = [...items].sort((a, b) => a.next_ts - b.next_ts)
|
||||
|
||||
return (
|
||||
<div className="mc-card p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-sm font-bold text-foreground">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg border border-border/40 bg-background/30 text-amber-300">
|
||||
<AlarmClock className="h-4 w-4" />
|
||||
</span>
|
||||
Anstehendes
|
||||
</span>
|
||||
<button onClick={() => setShowAdd((s) => !s)}
|
||||
className="flex h-8 items-center gap-1 rounded-lg border border-border/60 bg-background/30 px-2.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground hover:border-primary/40 transition-all cursor-pointer">
|
||||
{showAdd ? <X className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />} {showAdd ? "Abbrechen" : "Neu"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAdd && (
|
||||
<div className="mb-3 space-y-2 rounded-xl border border-border/50 bg-background/30 p-3">
|
||||
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Woran soll Lucy erinnern?"
|
||||
aria-label="Erinnerungstext"
|
||||
className="h-9 w-full rounded-lg border border-border/60 bg-card/45 px-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input type="datetime-local" value={when} onChange={(e) => setWhen(e.target.value)}
|
||||
aria-label="Zeitpunkt"
|
||||
className="h-9 rounded-lg border border-border/60 bg-card/45 px-2 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50 [color-scheme:dark]" />
|
||||
<select value={repeat} onChange={(e) => setRepeat(e.target.value)} aria-label="Wiederholung"
|
||||
className="h-9 cursor-pointer rounded-lg border border-border/60 bg-card/45 px-2 text-xs font-semibold text-foreground outline-none">
|
||||
{Object.entries(REPEAT_LABEL).map(([v, l]) => <option key={v} value={v} className="bg-popover">{l}</option>)}
|
||||
</select>
|
||||
<button onClick={add} disabled={saving || !text.trim() || !when}
|
||||
className="ml-auto flex h-9 items-center gap-1.5 rounded-lg bg-primary px-3 text-xs font-semibold text-primary-foreground hover:opacity-90 transition-all cursor-pointer disabled:opacity-50">
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />} Anlegen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-muted-foreground">Wird geladen …</p>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">Keine Erinnerungen. Lucy sagt dir hier (und per Stimme) Bescheid, wenn etwas ansteht.</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{sorted.slice(0, 6).map((r) => {
|
||||
const overdue = r.next_ts * 1000 < Date.now() && !r.repeat
|
||||
return (
|
||||
<div key={r.id} className="group flex items-center justify-between gap-2 rounded-lg border border-border/40 bg-background/20 px-2.5 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-xs text-foreground">{r.text}</p>
|
||||
<p className={cn("flex items-center gap-1.5 text-[10px]", overdue ? "text-amber-300" : "text-muted-foreground/70")}>
|
||||
{new Date(r.next_ts * 1000).toLocaleString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })}
|
||||
{r.repeat && <span className="flex items-center gap-0.5"><Repeat className="h-2.5 w-2.5" />{REPEAT_LABEL[r.repeat] ?? r.repeat}</span>}
|
||||
{overdue && "· überfällig"}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => del(r.id, r.text)} aria-label="Erinnerung löschen"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border border-border/40 text-muted-foreground opacity-0 transition-all hover:bg-red-500/5 hover:text-red-400 group-hover:opacity-100 cursor-pointer">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{sorted.length > 6 && <p className="text-[10px] text-muted-foreground/60">+{sorted.length - 6} weitere</p>}
|
||||
</div>
|
||||
)}
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user