- 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
141 lines
4.9 KiB
TypeScript
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 ## (timestamp) 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>
|
|
);
|
|
}
|