@cryptiklemur/lattice 1.1.0 → 1.2.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.
- package/client/src/components/project-settings/ProjectMemory.tsx +471 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/sidebar/AddProjectModal.tsx +432 -0
- package/client/src/components/sidebar/ProjectRail.tsx +8 -4
- package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
- package/client/src/hooks/useSidebar.ts +16 -0
- package/client/src/router.tsx +62 -0
- package/client/src/stores/sidebar.ts +28 -0
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/fs.ts +159 -0
- package/server/src/handlers/memory.ts +179 -0
- package/server/src/handlers/settings.ts +6 -1
- package/shared/src/messages.ts +97 -2
- package/shared/src/project-settings.ts +1 -1
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Plus, Trash2, Pencil, X, Loader2, Brain } from "lucide-react";
|
|
3
|
+
import Markdown from "react-markdown";
|
|
4
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
5
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
interface MemoryEntry {
|
|
8
|
+
filename: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
type: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProjectMemoryProps {
|
|
15
|
+
projectSlug?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var MEMORY_TYPES = ["user", "feedback", "project", "reference"];
|
|
19
|
+
|
|
20
|
+
function parseFrontmatter(content: string): { meta: Record<string, string>; body: string } {
|
|
21
|
+
var match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\s*\r?\n?([\s\S]*)$/);
|
|
22
|
+
if (!match) return { meta: {}, body: content };
|
|
23
|
+
var meta: Record<string, string> = {};
|
|
24
|
+
var lines = match[1].split(/\r?\n/);
|
|
25
|
+
for (var i = 0; i < lines.length; i++) {
|
|
26
|
+
var colonIdx = lines[i].indexOf(":");
|
|
27
|
+
if (colonIdx > 0) {
|
|
28
|
+
var key = lines[i].slice(0, colonIdx).trim();
|
|
29
|
+
var value = lines[i].slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
30
|
+
meta[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { meta, body: match[2] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function slugify(name: string): string {
|
|
37
|
+
return name
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
40
|
+
.replace(/^_+|_+$/g, "")
|
|
41
|
+
|| "memory";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildContent(name: string, description: string, type: string, body: string): string {
|
|
45
|
+
return "---\nname: " + name + "\ndescription: " + description + "\ntype: " + type + "\n---\n" + body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function MemoryCard({
|
|
49
|
+
memory,
|
|
50
|
+
onClick,
|
|
51
|
+
onDelete,
|
|
52
|
+
}: {
|
|
53
|
+
memory: MemoryEntry;
|
|
54
|
+
onClick: () => void;
|
|
55
|
+
onDelete: () => void;
|
|
56
|
+
}) {
|
|
57
|
+
var [confirmDelete, setConfirmDelete] = useState(false);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
onClick={onClick}
|
|
62
|
+
className="flex items-start gap-3 px-3 py-2.5 bg-base-300 border border-base-content/15 rounded-xl transition-colors duration-[120ms] cursor-pointer hover:border-base-content/30 hover:bg-base-300/80"
|
|
63
|
+
role="button"
|
|
64
|
+
tabIndex={0}
|
|
65
|
+
onKeyDown={function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }}
|
|
66
|
+
>
|
|
67
|
+
<Brain size={14} className="text-base-content/25 mt-0.5 flex-shrink-0" />
|
|
68
|
+
<div className="flex-1 min-w-0">
|
|
69
|
+
<div className="flex items-center gap-2">
|
|
70
|
+
<span className="text-[13px] font-bold text-base-content truncate">{memory.name || memory.filename}</span>
|
|
71
|
+
<span className="shrink-0 text-[10px] font-mono px-1.5 py-0.5 rounded-md bg-base-content/8 text-base-content/40">
|
|
72
|
+
{memory.type}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
{memory.description && (
|
|
76
|
+
<div className="text-[12px] text-base-content/40 mt-0.5 line-clamp-2">{memory.description}</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex gap-1 flex-shrink-0 mt-0.5" onClick={function (e) { e.stopPropagation(); }}>
|
|
80
|
+
{confirmDelete ? (
|
|
81
|
+
<div className="flex gap-1">
|
|
82
|
+
<button
|
|
83
|
+
onClick={function () { onDelete(); setConfirmDelete(false); }}
|
|
84
|
+
className="btn btn-error btn-xs"
|
|
85
|
+
>
|
|
86
|
+
Delete
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
onClick={function () { setConfirmDelete(false); }}
|
|
90
|
+
className="btn btn-ghost btn-xs"
|
|
91
|
+
>
|
|
92
|
+
Cancel
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<button
|
|
97
|
+
onClick={function () { setConfirmDelete(true); }}
|
|
98
|
+
aria-label={"Delete " + memory.name}
|
|
99
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error focus-visible:ring-2 focus-visible:ring-primary"
|
|
100
|
+
>
|
|
101
|
+
<Trash2 size={12} />
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function MemoryViewModal({
|
|
110
|
+
memory,
|
|
111
|
+
content,
|
|
112
|
+
onClose,
|
|
113
|
+
onEdit,
|
|
114
|
+
}: {
|
|
115
|
+
memory: MemoryEntry;
|
|
116
|
+
content: string;
|
|
117
|
+
onClose: () => void;
|
|
118
|
+
onEdit: () => void;
|
|
119
|
+
}) {
|
|
120
|
+
var parsed = parseFrontmatter(content);
|
|
121
|
+
var hasMeta = Object.keys(parsed.meta).length > 0;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
|
125
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
126
|
+
<div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col overflow-hidden">
|
|
127
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-base-content/15 flex-shrink-0">
|
|
128
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
129
|
+
<Brain size={16} className="text-primary flex-shrink-0" />
|
|
130
|
+
<h2 className="text-[15px] font-mono font-bold text-base-content truncate">
|
|
131
|
+
{parsed.meta.name || memory.filename}
|
|
132
|
+
</h2>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex items-center gap-1">
|
|
135
|
+
<button
|
|
136
|
+
onClick={onEdit}
|
|
137
|
+
className="btn btn-ghost btn-xs gap-1.5 text-base-content/50 hover:text-base-content"
|
|
138
|
+
>
|
|
139
|
+
<Pencil size={12} />
|
|
140
|
+
Edit
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
onClick={onClose}
|
|
144
|
+
aria-label="Close"
|
|
145
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
146
|
+
>
|
|
147
|
+
<X size={16} />
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="overflow-y-auto flex-1">
|
|
153
|
+
{hasMeta && (
|
|
154
|
+
<div className="px-5 py-3 border-b border-base-content/10 bg-base-300/30">
|
|
155
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1.5">
|
|
156
|
+
{Object.entries(parsed.meta).map(function ([key, value]) {
|
|
157
|
+
return (
|
|
158
|
+
<div key={key} className="flex items-baseline gap-1.5">
|
|
159
|
+
<span className="text-[11px] font-mono text-base-content/35 uppercase tracking-wider">{key}</span>
|
|
160
|
+
<span className="text-[12px] text-base-content/70">{value}</span>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<div className="px-5 py-4">
|
|
169
|
+
<div className="prose prose-sm max-w-none prose-headings:text-base-content prose-headings:font-mono prose-p:text-base-content/70 prose-strong:text-base-content prose-code:text-base-content/60 prose-code:bg-base-100/50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-[11px] prose-pre:bg-base-100 prose-pre:text-base-content/70 prose-pre:text-[11px] prose-a:text-primary prose-li:text-base-content/70 prose-li:marker:text-base-content/30 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
|
170
|
+
<Markdown>{parsed.body}</Markdown>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div className="px-5 py-3 border-t border-base-content/15 flex-shrink-0">
|
|
176
|
+
<div className="text-[11px] font-mono text-base-content/30 truncate">{memory.filename}</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function MemoryEditModal({
|
|
184
|
+
memory,
|
|
185
|
+
initialContent,
|
|
186
|
+
onClose,
|
|
187
|
+
onSave,
|
|
188
|
+
isSaving,
|
|
189
|
+
}: {
|
|
190
|
+
memory: MemoryEntry | null;
|
|
191
|
+
initialContent: string;
|
|
192
|
+
onClose: () => void;
|
|
193
|
+
onSave: (filename: string, content: string) => void;
|
|
194
|
+
isSaving: boolean;
|
|
195
|
+
}) {
|
|
196
|
+
var isNew = memory === null;
|
|
197
|
+
var parsed = parseFrontmatter(initialContent);
|
|
198
|
+
|
|
199
|
+
var [name, setName] = useState(parsed.meta.name || (memory ? memory.name : ""));
|
|
200
|
+
var [description, setDescription] = useState(parsed.meta.description || (memory ? memory.description : ""));
|
|
201
|
+
var [type, setType] = useState(parsed.meta.type || (memory ? memory.type : "project"));
|
|
202
|
+
var [body, setBody] = useState(parsed.body);
|
|
203
|
+
|
|
204
|
+
function handleSave() {
|
|
205
|
+
var content = buildContent(name, description, type, body);
|
|
206
|
+
var filename = isNew ? slugify(name) + ".md" : memory!.filename;
|
|
207
|
+
onSave(filename, content);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
|
212
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
213
|
+
<div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col overflow-hidden">
|
|
214
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-base-content/15 flex-shrink-0">
|
|
215
|
+
<div className="flex items-center gap-2">
|
|
216
|
+
<Brain size={16} className="text-primary flex-shrink-0" />
|
|
217
|
+
<h2 className="text-[15px] font-mono font-bold text-base-content">
|
|
218
|
+
{isNew ? "New Memory" : "Edit Memory"}
|
|
219
|
+
</h2>
|
|
220
|
+
</div>
|
|
221
|
+
<button
|
|
222
|
+
onClick={onClose}
|
|
223
|
+
aria-label="Close"
|
|
224
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
225
|
+
>
|
|
226
|
+
<X size={16} />
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="overflow-y-auto flex-1 px-5 py-4 space-y-4">
|
|
231
|
+
<div className="grid grid-cols-2 gap-3">
|
|
232
|
+
<div className="col-span-2">
|
|
233
|
+
<label className="text-[11px] font-mono text-base-content/40 uppercase tracking-wider mb-1.5 block">
|
|
234
|
+
Name
|
|
235
|
+
</label>
|
|
236
|
+
<input
|
|
237
|
+
type="text"
|
|
238
|
+
value={name}
|
|
239
|
+
onChange={function (e) { setName(e.target.value); }}
|
|
240
|
+
className="input input-sm w-full bg-base-300 border-base-content/15 text-[13px]"
|
|
241
|
+
placeholder="Memory name"
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="col-span-2">
|
|
246
|
+
<label className="text-[11px] font-mono text-base-content/40 uppercase tracking-wider mb-1.5 block">
|
|
247
|
+
Description
|
|
248
|
+
</label>
|
|
249
|
+
<input
|
|
250
|
+
type="text"
|
|
251
|
+
value={description}
|
|
252
|
+
onChange={function (e) { setDescription(e.target.value); }}
|
|
253
|
+
className="input input-sm w-full bg-base-300 border-base-content/15 text-[13px]"
|
|
254
|
+
placeholder="Brief description"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div>
|
|
259
|
+
<label className="text-[11px] font-mono text-base-content/40 uppercase tracking-wider mb-1.5 block">
|
|
260
|
+
Type
|
|
261
|
+
</label>
|
|
262
|
+
<select
|
|
263
|
+
value={type}
|
|
264
|
+
onChange={function (e) { setType(e.target.value); }}
|
|
265
|
+
className="select select-sm w-full bg-base-300 border-base-content/15 text-[13px]"
|
|
266
|
+
>
|
|
267
|
+
{MEMORY_TYPES.map(function (t) {
|
|
268
|
+
return <option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}</option>;
|
|
269
|
+
})}
|
|
270
|
+
</select>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{!isNew && (
|
|
274
|
+
<div>
|
|
275
|
+
<label className="text-[11px] font-mono text-base-content/40 uppercase tracking-wider mb-1.5 block">
|
|
276
|
+
Filename
|
|
277
|
+
</label>
|
|
278
|
+
<input
|
|
279
|
+
type="text"
|
|
280
|
+
value={memory!.filename}
|
|
281
|
+
readOnly
|
|
282
|
+
className="input input-sm w-full bg-base-100/50 border-base-content/10 text-[13px] text-base-content/40 cursor-not-allowed"
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div>
|
|
289
|
+
<label className="text-[11px] font-mono text-base-content/40 uppercase tracking-wider mb-1.5 block">
|
|
290
|
+
Content
|
|
291
|
+
</label>
|
|
292
|
+
<textarea
|
|
293
|
+
value={body}
|
|
294
|
+
onChange={function (e) { setBody(e.target.value); }}
|
|
295
|
+
className="textarea w-full bg-base-300 border-base-content/15 text-[13px] font-mono resize-none min-h-[200px]"
|
|
296
|
+
placeholder="Memory content (markdown supported)"
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div className="px-5 py-3 border-t border-base-content/15 flex items-center justify-end gap-2 flex-shrink-0">
|
|
302
|
+
<button onClick={onClose} className="btn btn-ghost btn-sm">Cancel</button>
|
|
303
|
+
<button
|
|
304
|
+
onClick={handleSave}
|
|
305
|
+
disabled={isSaving || !name.trim()}
|
|
306
|
+
className="btn btn-primary btn-sm gap-1.5"
|
|
307
|
+
>
|
|
308
|
+
{isSaving && <Loader2 size={12} className="animate-spin" />}
|
|
309
|
+
{isNew ? "Create" : "Save"}
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function ProjectMemory({ projectSlug }: ProjectMemoryProps) {
|
|
318
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
319
|
+
var [memories, setMemories] = useState<MemoryEntry[]>([]);
|
|
320
|
+
var [loading, setLoading] = useState(false);
|
|
321
|
+
var [viewState, setViewState] = useState<{ memory: MemoryEntry; content: string } | null>(null);
|
|
322
|
+
var [editState, setEditState] = useState<{ memory: MemoryEntry | null; content: string } | null>(null);
|
|
323
|
+
var [isSaving, setIsSaving] = useState(false);
|
|
324
|
+
var [pendingView, setPendingView] = useState<MemoryEntry | null>(null);
|
|
325
|
+
|
|
326
|
+
useEffect(function () {
|
|
327
|
+
function handleListResult(msg: ServerMessage) {
|
|
328
|
+
if (msg.type !== "memory:list_result") return;
|
|
329
|
+
var data = msg as { type: "memory:list_result"; memories: MemoryEntry[] };
|
|
330
|
+
setMemories(data.memories);
|
|
331
|
+
setLoading(false);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function handleViewResult(msg: ServerMessage) {
|
|
335
|
+
if (msg.type !== "memory:view_result") return;
|
|
336
|
+
var data = msg as { type: "memory:view_result"; content: string };
|
|
337
|
+
setPendingView(function (prev) {
|
|
338
|
+
if (prev) {
|
|
339
|
+
setViewState({ memory: prev, content: data.content });
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function handleSaveResult(msg: ServerMessage) {
|
|
346
|
+
if (msg.type !== "memory:save_result") return;
|
|
347
|
+
setIsSaving(false);
|
|
348
|
+
setEditState(null);
|
|
349
|
+
setViewState(null);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handleDeleteResult(msg: ServerMessage) {
|
|
353
|
+
if (msg.type !== "memory:delete_result") return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
subscribe("memory:list_result", handleListResult);
|
|
357
|
+
subscribe("memory:view_result", handleViewResult);
|
|
358
|
+
subscribe("memory:save_result", handleSaveResult);
|
|
359
|
+
subscribe("memory:delete_result", handleDeleteResult);
|
|
360
|
+
|
|
361
|
+
return function () {
|
|
362
|
+
unsubscribe("memory:list_result", handleListResult);
|
|
363
|
+
unsubscribe("memory:view_result", handleViewResult);
|
|
364
|
+
unsubscribe("memory:save_result", handleSaveResult);
|
|
365
|
+
unsubscribe("memory:delete_result", handleDeleteResult);
|
|
366
|
+
};
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
useEffect(function () {
|
|
370
|
+
if (!projectSlug) return;
|
|
371
|
+
setLoading(true);
|
|
372
|
+
send({ type: "memory:list", projectSlug } as any);
|
|
373
|
+
}, [projectSlug]);
|
|
374
|
+
|
|
375
|
+
function handleView(memory: MemoryEntry) {
|
|
376
|
+
setPendingView(memory);
|
|
377
|
+
send({ type: "memory:view", projectSlug, filename: memory.filename } as any);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function handleDelete(memory: MemoryEntry) {
|
|
381
|
+
send({ type: "memory:delete", projectSlug, filename: memory.filename } as any);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function handleSave(filename: string, content: string) {
|
|
385
|
+
setIsSaving(true);
|
|
386
|
+
send({ type: "memory:save", projectSlug, filename, content } as any);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function handleEditFromView() {
|
|
390
|
+
if (!viewState) return;
|
|
391
|
+
setEditState({ memory: viewState.memory, content: viewState.content });
|
|
392
|
+
setViewState(null);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
var grouped: Record<string, MemoryEntry[]> = {};
|
|
396
|
+
for (var i = 0; i < memories.length; i++) {
|
|
397
|
+
var m = memories[i];
|
|
398
|
+
var t = m.type || "other";
|
|
399
|
+
if (!grouped[t]) grouped[t] = [];
|
|
400
|
+
grouped[t].push(m);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
var groupKeys = Object.keys(grouped);
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div className="py-2 space-y-6">
|
|
407
|
+
<div className="flex items-center justify-end">
|
|
408
|
+
<button
|
|
409
|
+
onClick={function () { setEditState({ memory: null, content: "" }); }}
|
|
410
|
+
className="btn btn-primary btn-sm gap-1.5"
|
|
411
|
+
>
|
|
412
|
+
<Plus size={13} />
|
|
413
|
+
New Memory
|
|
414
|
+
</button>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
{loading && (
|
|
418
|
+
<div className="flex items-center justify-center py-12">
|
|
419
|
+
<Loader2 size={18} className="animate-spin text-base-content/30" />
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{!loading && memories.length === 0 && (
|
|
424
|
+
<div className="py-12 text-center text-[13px] text-base-content/40">
|
|
425
|
+
No memories found.
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{!loading && groupKeys.map(function (groupKey) {
|
|
430
|
+
return (
|
|
431
|
+
<div key={groupKey}>
|
|
432
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">
|
|
433
|
+
{groupKey.charAt(0).toUpperCase() + groupKey.slice(1)}
|
|
434
|
+
</div>
|
|
435
|
+
<div className="space-y-2">
|
|
436
|
+
{grouped[groupKey].map(function (memory) {
|
|
437
|
+
return (
|
|
438
|
+
<MemoryCard
|
|
439
|
+
key={memory.filename}
|
|
440
|
+
memory={memory}
|
|
441
|
+
onClick={function () { handleView(memory); }}
|
|
442
|
+
onDelete={function () { handleDelete(memory); }}
|
|
443
|
+
/>
|
|
444
|
+
);
|
|
445
|
+
})}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
450
|
+
|
|
451
|
+
{viewState && (
|
|
452
|
+
<MemoryViewModal
|
|
453
|
+
memory={viewState.memory}
|
|
454
|
+
content={viewState.content}
|
|
455
|
+
onClose={function () { setViewState(null); }}
|
|
456
|
+
onEdit={handleEditFromView}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
459
|
+
|
|
460
|
+
{editState !== null && (
|
|
461
|
+
<MemoryEditModal
|
|
462
|
+
memory={editState.memory}
|
|
463
|
+
initialContent={editState.content}
|
|
464
|
+
onClose={function () { setEditState(null); }}
|
|
465
|
+
onSave={handleSave}
|
|
466
|
+
isSaving={isSaving}
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
@@ -10,6 +10,7 @@ import { ProjectPermissions } from "./ProjectPermissions";
|
|
|
10
10
|
import { ProjectSkills } from "./ProjectSkills";
|
|
11
11
|
import { ProjectRules } from "./ProjectRules";
|
|
12
12
|
import { ProjectMcp } from "./ProjectMcp";
|
|
13
|
+
import { ProjectMemory } from "./ProjectMemory";
|
|
13
14
|
|
|
14
15
|
var SECTION_CONFIG: Record<string, { title: string }> = {
|
|
15
16
|
general: { title: "General" },
|
|
@@ -19,6 +20,7 @@ var SECTION_CONFIG: Record<string, { title: string }> = {
|
|
|
19
20
|
skills: { title: "Skills" },
|
|
20
21
|
rules: { title: "Rules" },
|
|
21
22
|
permissions: { title: "Permissions" },
|
|
23
|
+
memory: { title: "Memory" },
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
function renderSection(
|
|
@@ -55,6 +57,10 @@ function renderSection(
|
|
|
55
57
|
return <ProjectMcp settings={settings} updateSection={updateSection} />;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
if (section === "memory") {
|
|
61
|
+
return <ProjectMemory projectSlug={projectSlug} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
return (
|
|
59
65
|
<div className="py-2 text-[13px] text-base-content/40">
|
|
60
66
|
{section} section coming soon.
|