Files
tusk/src/components/memory/MemoryPanel.tsx
Aleksey Shakhmatov da0001e77e feat(ui): "Graphite & Honey" redesign — warm dark, monospace-first
- new design system in globals.css: warm graphite surfaces, ivory text, honey
  accent; semantic status/data-type/syntax tokens replacing hardcoded colors
- IBM Plex Mono as the universal UI font (sans + mono), tabular numerals
- custom CodeMirror SQL theme (src/lib/editor-theme.ts) matching the palette
- data grid: zebra striping + honey row hover, stronger sticky header
- route status dots, JSON syntax, EXPLAIN cost, schema-tree icons and the
  read/write toggle through the new tokens
- TUSK wordmark in the toolbar
2026-05-23 15:02:19 +03:00

141 lines
4.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { Brain, RefreshCw, Save } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { useAppStore } from "@/stores/app-store";
import { useConnections } from "@/hooks/use-connections";
import { useMemory, useSaveMemory } from "@/hooks/use-memory";
export function MemoryPanel() {
const activeConnectionId = useAppStore((s) => s.activeConnectionId);
const { data: connections } = useConnections();
const activeConn = connections?.find((c) => c.id === activeConnectionId);
const { data: serverContent, isFetching, refetch } = useMemory(activeConnectionId);
const saveMutation = useSaveMemory();
const [draft, setDraft] = useState<string>("");
const [dirty, setDirty] = useState(false);
// Sync local textarea when the active connection changes or server reloads.
// Only overwrite if the user hasn't edited.
useEffect(() => {
if (!dirty) {
setDraft(serverContent ?? "");
}
}, [serverContent, activeConnectionId, dirty]);
if (!activeConnectionId) {
return (
<div className="flex flex-col items-center gap-2 p-6 text-center">
<Brain className="h-8 w-8 text-muted-foreground/20" />
<p className="text-sm text-muted-foreground/60">
Connect to a database to view its memory.
</p>
</div>
);
}
const noteCount = (draft.match(/^## /gm) ?? []).length;
const isEmpty = draft.trim().length === 0;
const handleSave = () => {
saveMutation.mutate(
{ connectionId: activeConnectionId, content: draft },
{
onSuccess: () => {
setDirty(false);
toast.success("Memory saved");
},
onError: (err) => toast.error("Save failed", { description: String(err) }),
}
);
};
const handleReload = () => {
setDirty(false);
refetch();
};
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between gap-1 border-b border-border/40 px-3 py-2">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Brain className="h-3.5 w-3.5 text-primary/70" />
<span className="font-medium">Memory</span>
{activeConn && (
<span className="ml-1 truncate text-muted-foreground/60">· {activeConn.name}</span>
)}
{!isEmpty && (
<span className="ml-1 text-[10px] text-muted-foreground/50">
{noteCount} note{noteCount === 1 ? "" : "s"}
</span>
)}
</div>
<div className="flex items-center gap-1">
<Button
size="icon-xs"
variant="ghost"
onClick={handleReload}
disabled={isFetching}
title="Reload from disk"
className="text-muted-foreground hover:text-foreground"
>
<RefreshCw className={`h-3.5 w-3.5 ${isFetching ? "animate-spin" : ""}`} />
</Button>
<Button
size="xs"
variant={dirty ? "default" : "ghost"}
onClick={handleSave}
disabled={!dirty || saveMutation.isPending}
title="Save"
className="gap-1"
>
<Save className="h-3 w-3" />
<span className="text-xs">Save</span>
</Button>
</div>
</div>
{isEmpty ? (
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 py-8 text-center">
<Brain className="h-7 w-7 text-muted-foreground/30" />
<p className="text-xs text-muted-foreground/70">
No notes yet for this connection.
</p>
<p className="text-[11px] text-muted-foreground/50 max-w-[260px]">
The agent will populate this as it learns about your database. You can also
edit notes here directly anything you type is loaded into the agent's
context on its next turn.
</p>
<textarea
className="mt-2 h-48 w-full max-w-[360px] resize-none rounded-md border border-border/40 bg-background/50 p-2 font-mono text-[11px] outline-none focus:border-primary/40"
placeholder="# Memory&#10;&#10;## (timestamp)&#10;your note..."
value={draft}
onChange={(e) => {
setDraft(e.target.value);
setDirty(true);
}}
/>
</div>
) : (
<textarea
className="min-h-0 flex-1 resize-none overflow-y-auto bg-background/40 p-3 font-mono text-[11px] leading-relaxed outline-none"
spellCheck={false}
value={draft}
onChange={(e) => {
setDraft(e.target.value);
setDirty(true);
}}
/>
)}
{dirty && (
<div className="border-t border-border/40 px-3 py-1.5 text-[10px] text-warning/80">
Unsaved changes
</div>
)}
</div>
);
}