dsh-context-memory 0.3.0 → 0.4.1
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.
- package/lib/client/index.js +1 -1
- package/lib/client/ui.d.ts +124 -0
- package/lib/client/ui.js +388 -0
- package/lib/client/view.d.ts +4 -17
- package/lib/client/view.js +56 -92
- package/lib/memory.web.js +687 -196
- package/lib/memory.web.js.map +4 -4
- package/package.json +1 -1
package/lib/client/view.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
}
|