dsh-context-memory 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,48 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * Pure rendering half of the contextMemory page.
4
- *
5
- * Separate from `index.tsx` so a static render can assert in Node what the page draws —
6
- * the shipped bundle is a loader factory only a browser can run. Every user-visible
7
- * string comes from the `t` seat the renderer binds from this plugin's namespace, so the
8
- * page follows the UI language; no copy is hardcoded here.
9
- *
3
+ * Pure rendering half of the contextMemory page. Uses shared UI kit.
10
4
  * @module client/view
11
5
  */
12
- import { useCallback, useEffect, useState } from 'react';
6
+ import { useCallback, useState } from 'react';
7
+ import { Badge, Button, Card, ConfirmDialog, EmptyState, Field, Input, Modal, SectionTitle, Select, Spinner, StatCard, ToastProvider, tableStyles, usePanel, useToast, } from './ui.js';
13
8
  const PANEL_PATH = '/api/context-memory.panel';
14
9
  const RECALL_PATH = '/api/context-memory.recall';
15
10
  const REMEMBER_PATH = '/api/context-memory.remember';
16
11
  const FORGET_PATH = '/api/context-memory.forget';
17
- const wrap = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 820, fontFamily: 'inherit' };
18
- const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
19
- const muted = { fontSize: 12, opacity: 0.75 };
20
- const table = { borderCollapse: 'collapse', width: '100%' };
21
- const th = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' };
22
- const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
23
- const list = { margin: 0, paddingLeft: 18, fontSize: 13 };
24
- const card = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', fontSize: 13 };
25
- const btn = { fontSize: 12, cursor: 'pointer', padding: '3px 10px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)' };
26
- const dangerBtn = { ...btn, color: '#e55', borderColor: '#e55' };
27
- const inputStyle = { fontSize: 13, padding: '4px 8px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)', flex: 1 };
28
- export function usePanel() {
29
- const [state, setState] = useState({ payload: null, error: null });
30
- const [tick, setTick] = useState(0);
31
- const reload = useCallback(() => setTick((v) => v + 1), []);
32
- useEffect(() => {
33
- const c = new AbortController();
34
- setState((p) => ({ ...p, error: null }));
35
- fetch(PANEL_PATH, { signal: c.signal })
36
- .then(async (r) => { if (!r.ok)
37
- throw new Error(String(r.status)); return r.json(); })
38
- .then((payload) => { if (!c.signal.aborted)
39
- setState({ payload, error: null }); })
40
- .catch((e) => { if (!c.signal.aborted)
41
- setState({ payload: null, error: e instanceof Error ? e.message : String(e) }); });
42
- return () => c.abort();
43
- }, [tick]);
44
- return { ...state, reload };
45
- }
46
12
  export function checkLabel(memory, t) {
47
13
  if (!memory.guard)
48
14
  return t('checkNone');
@@ -54,38 +20,10 @@ export function checkLabel(memory, t) {
54
20
  }
55
21
  }
56
22
  export function provenance(memory) {
57
- return [
58
- memory.source,
59
- memory.sessionId !== undefined ? `session ${memory.sessionId}` : null,
60
- memory.step !== undefined ? `step ${String(memory.step)}` : null,
61
- ].filter((part) => part !== null).join(' · ');
62
- }
63
- // --- Search bar ---
64
- function SearchBar({ t, onResult }) {
65
- const [query, setQuery] = useState('');
66
- const [loading, setLoading] = useState(false);
67
- const handleSearch = useCallback(async () => {
68
- if (!query.trim()) {
69
- onResult(null);
70
- return;
71
- }
72
- setLoading(true);
73
- try {
74
- const r = await fetch(`${RECALL_PATH}?q=${encodeURIComponent(query)}&limit=10`);
75
- if (r.ok) {
76
- const data = await r.json();
77
- onResult(data.hits);
78
- }
79
- }
80
- finally {
81
- setLoading(false);
82
- }
83
- }, [query, onResult]);
84
- return (_jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsx("input", { style: inputStyle, placeholder: t('searchPlaceholder'), value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter')
85
- handleSearch(); } }), _jsx("button", { type: "button", style: btn, disabled: loading, onClick: handleSearch, children: loading ? '…' : t('search') })] }));
23
+ return [memory.source, memory.sessionId !== undefined ? `session ${memory.sessionId}` : null, memory.step !== undefined ? `step ${String(memory.step)}` : null].filter((part) => part !== null).join(' · ');
86
24
  }
87
- // --- Add memory form ---
88
- function AddMemoryForm({ t, onAdded }) {
25
+ function AddMemoryModal({ t, onClose, onAdded }) {
26
+ const toast = useToast();
89
27
  const [text, setText] = useState('');
90
28
  const [subject, setSubject] = useState('');
91
29
  const [kind, setKind] = useState('fact');
@@ -95,35 +33,61 @@ function AddMemoryForm({ t, onAdded }) {
95
33
  return;
96
34
  setAdding(true);
97
35
  try {
98
- await fetch(REMEMBER_PATH, {
99
- method: 'POST',
100
- headers: { 'content-type': 'application/json' },
101
- body: JSON.stringify({ text, subject: subject || undefined, kind, source: 'user' }),
102
- });
103
- setText('');
104
- setSubject('');
105
- setKind('fact');
106
- onAdded();
36
+ const r = await fetch(REMEMBER_PATH, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text, subject: subject || undefined, kind, source: 'user' }) });
37
+ if (r.ok) {
38
+ toast('success', t('memoryAdded'));
39
+ onAdded();
40
+ onClose();
41
+ }
107
42
  }
108
43
  finally {
109
44
  setAdding(false);
110
45
  }
111
- }, [text, subject, kind, onAdded]);
112
- return (_jsxs("div", { style: { ...card, display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("strong", { style: { fontSize: 12 }, children: ["\u2795 ", t('addMemory')] }), _jsx("input", { style: inputStyle, placeholder: t('memoryText'), value: text, onChange: (e) => setText(e.target.value) }), _jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsx("input", { style: inputStyle, placeholder: t('subjectOptional'), value: subject, onChange: (e) => setSubject(e.target.value) }), _jsxs("select", { style: { ...btn, flex: '0 0 auto' }, value: kind, onChange: (e) => setKind(e.target.value), children: [_jsx("option", { value: "fact", children: t('kindFact') }), _jsx("option", { value: "decision", children: t('kindDecision') }), _jsx("option", { value: "preference", children: t('kindPreference') }), _jsx("option", { value: "pitfall", children: t('kindPitfall') })] }), _jsx("button", { type: "button", style: btn, disabled: adding, onClick: handleAdd, children: adding ? '…' : t('add') })] })] }));
46
+ }, [text, subject, kind, t, toast, onAdded, onClose]);
47
+ return (_jsx(Modal, { title: t('addMemory'), onClose: onClose, width: 500, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: onClose, children: t('cancel') }), _jsxs(Button, { variant: "primary", disabled: adding || !text.trim(), onClick: handleAdd, children: [adding ? _jsx(Spinner, { size: 14 }) : null, " ", t('add')] })] }), children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 14 }, children: [_jsx(Field, { label: t('memoryText'), children: _jsx(Input, { value: text, onChange: (e) => setText(e.target.value), placeholder: t('memoryTextPlaceholder') }) }), _jsx(Field, { label: t('subjectOptional'), children: _jsx(Input, { value: subject, onChange: (e) => setSubject(e.target.value), placeholder: t('subjectPlaceholder') }) }), _jsx(Field, { label: t('kind'), children: _jsxs(Select, { value: kind, onChange: (e) => setKind(e.target.value), style: { width: '100%' }, children: [_jsx("option", { value: "fact", children: t('kindFact') }), _jsx("option", { value: "decision", children: t('kindDecision') }), _jsx("option", { value: "preference", children: t('kindPreference') }), _jsx("option", { value: "pitfall", children: t('kindPitfall') })] }) })] }) }));
113
48
  }
114
- export function ContextMemoryPanel({ t }) {
115
- const { payload, error, reload } = usePanel();
49
+ function ContextMemoryPanelInner({ t }) {
50
+ const { payload, error, reload } = usePanel(PANEL_PATH);
51
+ const toast = useToast();
52
+ const [searchQuery, setSearchQuery] = useState('');
116
53
  const [searchHits, setSearchHits] = useState(null);
117
- const handleForget = useCallback(async (id, reason) => {
118
- await fetch(FORGET_PATH, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, reason }) });
119
- reload();
120
- }, [reload]);
121
- const header = (_jsxs("header", { style: head, children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83E\uDDE0 ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsx("button", { type: "button", style: btn, onClick: reload, children: t('refresh') })] }));
122
- if (error !== null) {
123
- return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: 'flex-start' }, children: t('retry') })] }));
124
- }
54
+ const [searching, setSearching] = useState(false);
55
+ const [addOpen, setAddOpen] = useState(false);
56
+ const [forgetTarget, setForgetTarget] = useState(null);
57
+ const handleSearch = useCallback(async () => {
58
+ if (!searchQuery.trim()) {
59
+ setSearchHits(null);
60
+ return;
61
+ }
62
+ setSearching(true);
63
+ try {
64
+ const r = await fetch(`${RECALL_PATH}?q=${encodeURIComponent(searchQuery)}&limit=10`);
65
+ if (r.ok) {
66
+ setSearchHits((await r.json()).hits);
67
+ }
68
+ }
69
+ finally {
70
+ setSearching(false);
71
+ }
72
+ }, [searchQuery]);
73
+ const handleForget = useCallback(async () => {
74
+ if (forgetTarget === null)
75
+ return;
76
+ const r = await fetch(FORGET_PATH, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(forgetTarget) });
77
+ if (r.ok) {
78
+ toast('success', t('forgotten'));
79
+ setForgetTarget(null);
80
+ reload();
81
+ }
82
+ }, [forgetTarget, t, toast, reload]);
83
+ const header = (_jsxs("header", { style: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }, children: [_jsxs("strong", { style: { fontSize: 15 }, children: ["\uD83E\uDDE0 ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsxs(Button, { variant: "primary", size: "sm", onClick: () => setAddOpen(true), children: ["\u2795 ", t('addMemory')] }), _jsx(Button, { variant: "secondary", size: "sm", onClick: reload, children: t('refresh') })] }));
84
+ if (error !== null)
85
+ return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx(Card, { children: _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13, color: 'var(--error, #e53935)' }, children: [t('failed'), ": ", error] }) })] });
125
86
  if (payload === null)
126
- return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
127
- const { totals } = payload;
128
- return (_jsxs("div", { style: wrap, children: [header, _jsxs("span", { style: muted, children: [t('active'), " ", totals.active, " \u00B7 ", t('checkPassed'), " ", totals.verified, " \u00B7 ", t('unverified'), " ", totals.unverified, " \u00B7 ", t('superseded'), " ", totals.superseded] }), _jsx(SearchBar, { t: t, onResult: setSearchHits }), searchHits !== null && (_jsxs("div", { style: card, children: [_jsxs("strong", { style: { fontSize: 12 }, children: [t('searchResults'), " (", searchHits.length, ")"] }), searchHits.length === 0 ? (_jsx("p", { style: muted, children: t('noResults') })) : (_jsx("ul", { style: list, children: searchHits.map((hit) => (_jsxs("li", { style: { marginBottom: 4 }, children: [_jsx("code", { style: { fontSize: 11 }, children: hit.memory.id }), " (", hit.score.toFixed(2), ") ", hit.memory.text, _jsxs("span", { style: { ...muted, marginLeft: 8 }, children: ["\u2014 ", hit.why] })] }, hit.memory.id))) }))] })), _jsx(AddMemoryForm, { t: t, onAdded: reload }), _jsxs("strong", { style: { fontSize: 13 }, children: ["\u26A0\uFE0F ", t('conflicts')] }), payload.conflicts.length === 0 ? (_jsx("span", { style: muted, children: t('noConflicts') })) : (_jsx("ul", { style: list, children: payload.conflicts.map((conflict) => (_jsxs("li", { children: [_jsx("strong", { children: conflict.subject }), _jsx("ul", { style: { ...list, fontSize: 12, opacity: 0.85 }, children: conflict.memories.map((memory) => (_jsxs("li", { children: [_jsx("code", { style: { fontSize: 11 }, children: memory.id }), " ", memory.text, _jsx("button", { type: "button", style: { ...dangerBtn, marginLeft: 8 }, onClick: () => handleForget(memory.id, `conflict resolved: ${conflict.subject}`), children: t('resolve') })] }, memory.id))) })] }, conflict.subject))) })), _jsxs("strong", { style: { fontSize: 13 }, children: ["\u2705 ", t('active')] }), payload.active.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: "Id" }), _jsx("th", { style: th, children: t('memory') }), _jsx("th", { style: th, children: t('kind') }), _jsx("th", { style: th, children: t('status') }), _jsx("th", { style: th, children: t('from') }), _jsx("th", { style: th })] }) }), _jsx("tbody", { children: payload.active.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: td, children: memory.text }), _jsx("td", { style: td, children: memory.kind }), _jsx("td", { style: td, children: checkLabel(memory, t) }), _jsx("td", { style: td, children: provenance(memory) }), _jsx("td", { style: td, children: _jsxs("button", { type: "button", style: dangerBtn, onClick: () => handleForget(memory.id, 'superseded from panel'), children: ["\uD83D\uDDD1 ", t('forget')] }) })] }, memory.id))) })] })), payload.superseded.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDCE6 ", t('superseded'), " (", payload.superseded.length, ")"] }), _jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: "Id" }), _jsx("th", { style: th, children: t('memory') }), _jsx("th", { style: th, children: t('kind') }), _jsx("th", { style: th, children: t('status') }), _jsx("th", { style: th, children: t('from') })] }) }), _jsx("tbody", { children: payload.superseded.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: td, children: memory.text }), _jsx("td", { style: td, children: memory.kind }), _jsx("td", { style: td, children: checkLabel(memory, t) }), _jsx("td", { style: td, children: provenance(memory) })] }, memory.id))) })] })] }))] }));
87
+ return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx("div", { style: { display: 'flex', justifyContent: 'center', padding: 40 }, children: _jsx(Spinner, { size: 28 }) })] });
88
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 820 }, children: [header, _jsxs("div", { style: { display: 'flex', gap: 10, flexWrap: 'wrap' }, children: [_jsx(StatCard, { value: payload.totals.active, label: t('active') }), _jsx(StatCard, { value: payload.totals.verified, label: t('checkPassed') }), _jsx(StatCard, { value: payload.totals.unverified, label: t('unverified') }), _jsx(StatCard, { value: payload.totals.superseded, label: t('superseded') })] }), _jsxs(Card, { title: t('search'), icon: "\uD83D\uDD0D", children: [_jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsx(Input, { value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: t('searchPlaceholder'), onKeyDown: (e) => { if (e.key === 'Enter')
89
+ handleSearch(); } }), _jsxs(Button, { variant: "secondary", onClick: handleSearch, disabled: searching, children: [searching ? _jsx(Spinner, { size: 14 }) : null, " ", t('search')] })] }), searchHits !== null && (_jsx("div", { style: { marginTop: 10 }, children: searchHits.length === 0 ? _jsx(EmptyState, { message: t('noResults') }) : (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: searchHits.map((hit) => (_jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'baseline' }, children: [_jsx(Badge, { color: "info", children: hit.score.toFixed(2) }), _jsx("span", { style: { fontSize: 12 }, children: hit.memory.text }), _jsxs("span", { style: { fontSize: 10, opacity: 0.5 }, children: ["\u2014 ", hit.why] })] }, hit.memory.id))) })) }))] }), payload.conflicts.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\u26A0\uFE0F", children: t('conflicts') }), payload.conflicts.map((conflict) => (_jsx(Card, { title: conflict.subject, children: conflict.memories.map((memory) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }, children: [_jsx("code", { style: { fontSize: 11 }, children: memory.id }), _jsx("span", { style: { fontSize: 12, flex: 1 }, children: memory.text }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => setForgetTarget({ id: memory.id, reason: `conflict resolved: ${conflict.subject}` }), children: t('resolve') })] }, memory.id))) }, conflict.subject)))] })), _jsx(SectionTitle, { icon: "\u2705", children: t('active') }), payload.active.length === 0 ? _jsx(EmptyState, { icon: "\uD83D\uDCED", message: t('empty') }) : (_jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: "Id" }), _jsx("th", { style: tableStyles.th, children: t('memory') }), _jsx("th", { style: tableStyles.th, children: t('kind') }), _jsx("th", { style: tableStyles.th, children: t('status') }), _jsx("th", { style: tableStyles.th, children: t('from') }), _jsx("th", { style: tableStyles.th })] }) }), _jsx("tbody", { children: payload.active.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: tableStyles.td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: tableStyles.td, children: memory.text }), _jsx("td", { style: tableStyles.td, children: _jsx(Badge, { children: memory.kind }) }), _jsx("td", { style: tableStyles.td, children: checkLabel(memory, t) }), _jsx("td", { style: { ...tableStyles.td, fontSize: 11, opacity: 0.6 }, children: provenance(memory) }), _jsx("td", { style: tableStyles.td, children: _jsx(Button, { variant: "danger", size: "sm", onClick: () => setForgetTarget({ id: memory.id, reason: 'superseded from panel' }), children: "\uD83D\uDDD1" }) })] }, memory.id))) })] }) })), payload.superseded.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(SectionTitle, { icon: "\uD83D\uDCE6", children: [t('superseded'), " (", payload.superseded.length, ")"] }), _jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: "Id" }), _jsx("th", { style: tableStyles.th, children: t('memory') }), _jsx("th", { style: tableStyles.th, children: t('kind') }), _jsx("th", { style: tableStyles.th, children: t('status') })] }) }), _jsx("tbody", { children: payload.superseded.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: tableStyles.td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: tableStyles.td, children: memory.text }), _jsx("td", { style: tableStyles.td, children: _jsx(Badge, { children: memory.kind }) }), _jsx("td", { style: tableStyles.td, children: checkLabel(memory, t) })] }, memory.id))) })] }) })] })), addOpen && _jsx(AddMemoryModal, { t: t, onClose: () => setAddOpen(false), onAdded: reload }), forgetTarget !== null && _jsx(ConfirmDialog, { title: t('forget'), message: t('confirmForget'), confirmLabel: t('forget'), danger: true, onConfirm: handleForget, onClose: () => setForgetTarget(null) })] }));
90
+ }
91
+ export function ContextMemoryPanel({ t }) {
92
+ return _jsx(ToastProvider, { children: _jsx(ContextMemoryPanelInner, { t: t }) });
129
93
  }