diff --git a/canvas/src/components/MemoryEditorDialog.tsx b/canvas/src/components/MemoryEditorDialog.tsx deleted file mode 100644 index 6625412a..00000000 --- a/canvas/src/components/MemoryEditorDialog.tsx +++ /dev/null @@ -1,261 +0,0 @@ -'use client'; - -import { useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { api } from "@/lib/api"; -import type { MemoryEntry } from "@/components/MemoryInspectorPanel"; - -type Scope = "LOCAL" | "TEAM" | "GLOBAL"; -const SCOPES: Scope[] = ["LOCAL", "TEAM", "GLOBAL"]; - -interface AddProps { - open: boolean; - mode: "add"; - workspaceId: string; - defaultScope: Scope; - defaultNamespace?: string; - entry?: undefined; - onClose: () => void; - onSaved: () => void; -} - -interface EditProps { - open: boolean; - mode: "edit"; - workspaceId: string; - entry: MemoryEntry; - defaultScope?: undefined; - defaultNamespace?: undefined; - onClose: () => void; - onSaved: () => void; -} - -type Props = AddProps | EditProps; - -export function MemoryEditorDialog(props: Props) { - const { open, mode, workspaceId, onClose, onSaved } = props; - const dialogRef = useRef(null); - const [mounted, setMounted] = useState(false); - const [scope, setScope] = useState("LOCAL"); - const [namespace, setNamespace] = useState("general"); - const [content, setContent] = useState(""); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - setMounted(true); - }, []); - - // Reset form whenever the dialog opens. - useEffect(() => { - if (!open) return; - setError(null); - setSaving(false); - if (mode === "edit" && props.entry) { - setScope(props.entry.scope); - setNamespace(props.entry.namespace || "general"); - setContent(props.entry.content); - } else if (mode === "add") { - setScope(props.defaultScope); - setNamespace(props.defaultNamespace || "general"); - setContent(""); - } - // mode/props are stable per-open; intentional shallow deps. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open]); - - // Move focus into the dialog when it opens (WCAG SC 2.4.3). - useEffect(() => { - if (!open || !mounted) return; - const raf = requestAnimationFrame(() => { - dialogRef.current?.querySelector("textarea, input, select")?.focus(); - }); - return () => cancelAnimationFrame(raf); - }, [open, mounted]); - - // Escape closes; Cmd/Ctrl-Enter saves. - const onCloseRef = useRef(onClose); - onCloseRef.current = onClose; - const handleSaveRef = useRef<() => void>(() => {}); - useEffect(() => { - if (!open) return; - const handler = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - onCloseRef.current(); - } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - handleSaveRef.current(); - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [open]); - - const handleSave = async () => { - if (saving) return; - const trimmed = content.trim(); - if (!trimmed) { - setError("Content cannot be empty"); - return; - } - setError(null); - setSaving(true); - try { - if (mode === "add") { - await api.post(`/workspaces/${workspaceId}/memories`, { - content: trimmed, - scope, - namespace: namespace.trim() || "general", - }); - } else { - // PATCH only sends fields that changed. Content always changeable; - // namespace only sent if it differs from the original (saves a - // no-op write through redactSecrets + re-embed). - const original = props.entry; - const body: Record = {}; - if (trimmed !== original.content) body.content = trimmed; - const ns = namespace.trim() || "general"; - if (ns !== original.namespace) body.namespace = ns; - if (Object.keys(body).length === 0) { - // No-op edit β€” close without an HTTP round-trip. - onSaved(); - onClose(); - return; - } - await api.patch( - `/workspaces/${workspaceId}/memories/${encodeURIComponent(original.id)}`, - body, - ); - } - onSaved(); - onClose(); - } catch (e) { - setError(e instanceof Error ? e.message : "Save failed"); - } finally { - setSaving(false); - } - }; - handleSaveRef.current = handleSave; - - if (!open || !mounted) return null; - - const titleId = "memory-editor-title"; - const isEdit = mode === "edit"; - - return createPortal( -
-
- -
-
-

- {isEdit ? "Edit memory" : "Add memory"} -

- - {/* Scope */} -
- - {isEdit ? ( -
- {scope} -
- ) : ( -
- {SCOPES.map((s) => ( - - ))} -
- )} -
- - {/* Namespace */} -
- - setNamespace(e.target.value)} - placeholder="general" - className="w-full bg-surface border border-line/60 focus:border-accent/60 rounded px-2 py-1.5 text-[12px] text-ink placeholder-zinc-600 focus:outline-none transition-colors" - /> -
- - {/* Content */} -
- -