claude-smart 0.2.22 → 0.2.24
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +69 -27
- package/bin/claude-smart.js +296 -11
- package/package.json +11 -1
- package/plugin/.claude-plugin/plugin.json +17 -0
- package/plugin/.codex-plugin/plugin.json +35 -0
- package/plugin/LICENSE +202 -0
- package/plugin/README.md +37 -0
- package/plugin/bin/cs-cite +77 -0
- package/plugin/commands/clear-all.md +8 -0
- package/plugin/commands/dashboard.md +8 -0
- package/plugin/commands/learn.md +12 -0
- package/plugin/commands/restart.md +8 -0
- package/plugin/commands/show.md +8 -0
- package/plugin/dashboard/AGENTS.md +6 -0
- package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
- package/plugin/dashboard/app/api/config/route.ts +16 -0
- package/plugin/dashboard/app/api/health/route.ts +10 -0
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
- package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
- package/plugin/dashboard/app/api/sessions/route.ts +14 -0
- package/plugin/dashboard/app/configure/env/page.tsx +318 -0
- package/plugin/dashboard/app/configure/layout.tsx +47 -0
- package/plugin/dashboard/app/configure/page.tsx +5 -0
- package/plugin/dashboard/app/configure/server/page.tsx +258 -0
- package/plugin/dashboard/app/dashboard/page.tsx +227 -0
- package/plugin/dashboard/app/globals.css +129 -0
- package/plugin/dashboard/app/icon.png +0 -0
- package/plugin/dashboard/app/layout.tsx +40 -0
- package/plugin/dashboard/app/page.tsx +5 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
- package/plugin/dashboard/app/preferences/page.tsx +126 -0
- package/plugin/dashboard/app/providers.tsx +12 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
- package/plugin/dashboard/app/sessions/page.tsx +186 -0
- package/plugin/dashboard/app/skills/page.tsx +362 -0
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
- package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
- package/plugin/dashboard/components/common/empty-state.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
- package/plugin/dashboard/components/common/page-header.tsx +34 -0
- package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
- package/plugin/dashboard/components/common/stat-card.tsx +38 -0
- package/plugin/dashboard/components/layout/nav-items.ts +22 -0
- package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
- package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
- package/plugin/dashboard/components/stall-banner.tsx +53 -0
- package/plugin/dashboard/components/ui/badge.tsx +52 -0
- package/plugin/dashboard/components/ui/button.tsx +60 -0
- package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
- package/plugin/dashboard/components/ui/input.tsx +20 -0
- package/plugin/dashboard/components/ui/label.tsx +20 -0
- package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
- package/plugin/dashboard/components/ui/select.tsx +201 -0
- package/plugin/dashboard/components/ui/separator.tsx +25 -0
- package/plugin/dashboard/components/ui/sheet.tsx +135 -0
- package/plugin/dashboard/components/ui/switch.tsx +32 -0
- package/plugin/dashboard/components.json +25 -0
- package/plugin/dashboard/eslint.config.mjs +16 -0
- package/plugin/dashboard/hooks/use-settings.tsx +88 -0
- package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
- package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
- package/plugin/dashboard/lib/config-file.ts +131 -0
- package/plugin/dashboard/lib/format.ts +58 -0
- package/plugin/dashboard/lib/reflexio-client.ts +238 -0
- package/plugin/dashboard/lib/reflexio-url.ts +17 -0
- package/plugin/dashboard/lib/session-reader.ts +245 -0
- package/plugin/dashboard/lib/status.ts +24 -0
- package/plugin/dashboard/lib/types.ts +145 -0
- package/plugin/dashboard/lib/utils.ts +6 -0
- package/plugin/dashboard/next.config.ts +7 -0
- package/plugin/dashboard/package-lock.json +10275 -0
- package/plugin/dashboard/package.json +37 -0
- package/plugin/dashboard/postcss.config.mjs +7 -0
- package/plugin/dashboard/public/claude-smart-icon.png +0 -0
- package/plugin/dashboard/tsconfig.json +34 -0
- package/plugin/hooks/codex-hooks.json +67 -0
- package/plugin/hooks/hooks.json +111 -0
- package/plugin/pyproject.toml +49 -0
- package/plugin/scripts/_codex_env.sh +27 -0
- package/plugin/scripts/_lib.sh +325 -0
- package/plugin/scripts/backend-service.sh +208 -0
- package/plugin/scripts/cli.sh +40 -0
- package/plugin/scripts/dashboard-build.sh +139 -0
- package/plugin/scripts/dashboard-open.sh +107 -0
- package/plugin/scripts/dashboard-service.sh +195 -0
- package/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/scripts/hook_entry.sh +70 -0
- package/plugin/scripts/smart-install.sh +411 -0
- package/plugin/src/claude_smart/__init__.py +3 -0
- package/plugin/src/claude_smart/cli.py +1273 -0
- package/plugin/src/claude_smart/context_format.py +277 -0
- package/plugin/src/claude_smart/context_inject.py +92 -0
- package/plugin/src/claude_smart/cs_cite.py +236 -0
- package/plugin/src/claude_smart/events/__init__.py +1 -0
- package/plugin/src/claude_smart/events/post_tool.py +148 -0
- package/plugin/src/claude_smart/events/pre_tool.py +52 -0
- package/plugin/src/claude_smart/events/session_end.py +20 -0
- package/plugin/src/claude_smart/events/session_start.py +119 -0
- package/plugin/src/claude_smart/events/stop.py +393 -0
- package/plugin/src/claude_smart/events/user_prompt.py +73 -0
- package/plugin/src/claude_smart/hook.py +114 -0
- package/plugin/src/claude_smart/ids.py +56 -0
- package/plugin/src/claude_smart/internal_call.py +89 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
- package/plugin/src/claude_smart/publish.py +71 -0
- package/plugin/src/claude_smart/query_compose.py +51 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
- package/plugin/src/claude_smart/runtime.py +52 -0
- package/plugin/src/claude_smart/stall_banner.py +61 -0
- package/plugin/src/claude_smart/state.py +276 -0
- package/plugin/uv.lock +3720 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { use, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import {
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
Trash2,
|
|
9
|
+
Save,
|
|
10
|
+
AlertTriangle,
|
|
11
|
+
Pencil,
|
|
12
|
+
X,
|
|
13
|
+
Copy,
|
|
14
|
+
Check,
|
|
15
|
+
Hash,
|
|
16
|
+
Clock,
|
|
17
|
+
CalendarClock,
|
|
18
|
+
FileText,
|
|
19
|
+
Sparkles,
|
|
20
|
+
Tags,
|
|
21
|
+
Braces,
|
|
22
|
+
FolderGit2,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { PageHeader } from "@/components/common/page-header";
|
|
25
|
+
import { EmptyState } from "@/components/common/empty-state";
|
|
26
|
+
import { Button } from "@/components/ui/button";
|
|
27
|
+
import { Label } from "@/components/ui/label";
|
|
28
|
+
import { Badge } from "@/components/ui/badge";
|
|
29
|
+
import { Separator } from "@/components/ui/separator";
|
|
30
|
+
import { reflexio } from "@/lib/reflexio-client";
|
|
31
|
+
import { useSettings } from "@/hooks/use-settings";
|
|
32
|
+
import { formatTimestamp, truncateId } from "@/lib/format";
|
|
33
|
+
import { cn } from "@/lib/utils";
|
|
34
|
+
import { statusLabel as status } from "@/lib/status";
|
|
35
|
+
import type { UserProfile } from "@/lib/types";
|
|
36
|
+
|
|
37
|
+
export default function PreferenceDetailPage({
|
|
38
|
+
params,
|
|
39
|
+
}: {
|
|
40
|
+
params: Promise<{ id: string }>;
|
|
41
|
+
}) {
|
|
42
|
+
const { id: rawId } = use(params);
|
|
43
|
+
const id = decodeURIComponent(rawId);
|
|
44
|
+
const router = useRouter();
|
|
45
|
+
const { reflexioUrl } = useSettings();
|
|
46
|
+
|
|
47
|
+
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
48
|
+
const [notFound, setNotFound] = useState(false);
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
const [saving, setSaving] = useState(false);
|
|
51
|
+
const [deleting, setDeleting] = useState(false);
|
|
52
|
+
const [editing, setEditing] = useState(false);
|
|
53
|
+
const [content, setContent] = useState("");
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
reflexio
|
|
58
|
+
.getAllProfiles({ reflexioUrl, limit: 500 })
|
|
59
|
+
.then((res) => {
|
|
60
|
+
if (cancelled) return;
|
|
61
|
+
const found = (res.user_profiles ?? []).find(
|
|
62
|
+
(p) => p.profile_id === id,
|
|
63
|
+
);
|
|
64
|
+
if (!found) {
|
|
65
|
+
setNotFound(true);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setProfile(found);
|
|
69
|
+
setContent(found.content);
|
|
70
|
+
})
|
|
71
|
+
.catch((e) => {
|
|
72
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
73
|
+
});
|
|
74
|
+
return () => {
|
|
75
|
+
cancelled = true;
|
|
76
|
+
};
|
|
77
|
+
}, [id, reflexioUrl]);
|
|
78
|
+
|
|
79
|
+
const dirty = useMemo(
|
|
80
|
+
() => !!profile && profile.content !== content,
|
|
81
|
+
[profile, content],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const save = async () => {
|
|
85
|
+
if (!profile || !dirty) return;
|
|
86
|
+
setSaving(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
try {
|
|
89
|
+
await reflexio.updateUserProfile(
|
|
90
|
+
{
|
|
91
|
+
user_id: profile.user_id,
|
|
92
|
+
profile_id: profile.profile_id,
|
|
93
|
+
content,
|
|
94
|
+
},
|
|
95
|
+
reflexioUrl,
|
|
96
|
+
);
|
|
97
|
+
setProfile({ ...profile, content });
|
|
98
|
+
setEditing(false);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
101
|
+
} finally {
|
|
102
|
+
setSaving(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const remove = async () => {
|
|
107
|
+
if (!profile) return;
|
|
108
|
+
if (!confirm("Delete this preference? This cannot be undone.")) return;
|
|
109
|
+
setDeleting(true);
|
|
110
|
+
try {
|
|
111
|
+
await reflexio.deleteUserProfile(
|
|
112
|
+
{ user_id: profile.user_id, profile_id: profile.profile_id },
|
|
113
|
+
reflexioUrl,
|
|
114
|
+
);
|
|
115
|
+
router.push("/preferences");
|
|
116
|
+
} catch (e) {
|
|
117
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
118
|
+
setDeleting(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const cancelEdit = () => {
|
|
123
|
+
if (profile) setContent(profile.content);
|
|
124
|
+
setEditing(false);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (notFound) {
|
|
128
|
+
return (
|
|
129
|
+
<div className="flex-1 overflow-auto">
|
|
130
|
+
<PageHeader title="Preference not found" />
|
|
131
|
+
<div className="p-6 max-w-2xl mx-auto">
|
|
132
|
+
<EmptyState
|
|
133
|
+
icon={AlertTriangle}
|
|
134
|
+
title="Preference not found"
|
|
135
|
+
description="It may have been deleted, archived, or moved outside the retrieval window."
|
|
136
|
+
action={
|
|
137
|
+
<Link href="/preferences">
|
|
138
|
+
<Button variant="outline" size="sm">
|
|
139
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
140
|
+
Back to preferences
|
|
141
|
+
</Button>
|
|
142
|
+
</Link>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const customEntries = profile?.custom_features
|
|
151
|
+
? Object.entries(profile.custom_features).filter(
|
|
152
|
+
([, v]) => v !== null && v !== undefined && v !== "",
|
|
153
|
+
)
|
|
154
|
+
: [];
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="flex-1 overflow-auto">
|
|
158
|
+
<PageHeader
|
|
159
|
+
title="Preference"
|
|
160
|
+
description="Project-scoped preference extracted by claude-smart."
|
|
161
|
+
actions={
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
<Link href="/preferences">
|
|
164
|
+
<Button variant="outline" size="sm">
|
|
165
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
166
|
+
Back
|
|
167
|
+
</Button>
|
|
168
|
+
</Link>
|
|
169
|
+
{!editing ? (
|
|
170
|
+
<Button
|
|
171
|
+
size="sm"
|
|
172
|
+
onClick={() => setEditing(true)}
|
|
173
|
+
disabled={!profile}
|
|
174
|
+
>
|
|
175
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
176
|
+
Edit
|
|
177
|
+
</Button>
|
|
178
|
+
) : (
|
|
179
|
+
<>
|
|
180
|
+
<Button
|
|
181
|
+
variant="outline"
|
|
182
|
+
size="sm"
|
|
183
|
+
onClick={cancelEdit}
|
|
184
|
+
disabled={saving}
|
|
185
|
+
>
|
|
186
|
+
<X className="h-3.5 w-3.5" />
|
|
187
|
+
Cancel
|
|
188
|
+
</Button>
|
|
189
|
+
<Button size="sm" onClick={save} disabled={saving || !dirty}>
|
|
190
|
+
<Save className="h-3.5 w-3.5" />
|
|
191
|
+
{saving ? "Saving…" : "Save"}
|
|
192
|
+
</Button>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
<div className="p-6">
|
|
200
|
+
<div className="mx-auto max-w-5xl grid gap-6 lg:grid-cols-[1fr_280px]">
|
|
201
|
+
<div className="space-y-6 min-w-0">
|
|
202
|
+
{error && (
|
|
203
|
+
<div className="rounded-xl border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm flex items-start gap-2">
|
|
204
|
+
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
|
|
205
|
+
<span>{error}</span>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{profile && (
|
|
210
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
211
|
+
<StatusBadge status={status(profile)} />
|
|
212
|
+
<Badge variant="outline" className="font-mono gap-1.5">
|
|
213
|
+
<FolderGit2 className="h-3 w-3" />
|
|
214
|
+
{truncateId(profile.user_id, 32, 8)}
|
|
215
|
+
</Badge>
|
|
216
|
+
{profile.source && (
|
|
217
|
+
<Badge variant="secondary" className="font-mono text-[10px]">
|
|
218
|
+
{profile.source}
|
|
219
|
+
</Badge>
|
|
220
|
+
)}
|
|
221
|
+
{dirty && (
|
|
222
|
+
<Badge variant="destructive" className="gap-1.5">
|
|
223
|
+
unsaved changes
|
|
224
|
+
</Badge>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
<Section
|
|
230
|
+
icon={Sparkles}
|
|
231
|
+
title="Preference"
|
|
232
|
+
hint="Project-scoped preference. Reinjected into future sessions in this project until its configured TTL expires."
|
|
233
|
+
>
|
|
234
|
+
{editing ? (
|
|
235
|
+
<textarea
|
|
236
|
+
value={content}
|
|
237
|
+
onChange={(e) => setContent(e.target.value)}
|
|
238
|
+
rows={6}
|
|
239
|
+
placeholder="e.g. Project bans pytest-asyncio; uses anyio with trio backend for async tests."
|
|
240
|
+
className="w-full rounded-xl border border-input bg-transparent px-4 py-3 text-sm leading-relaxed font-sans resize-y outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 placeholder:text-muted-foreground"
|
|
241
|
+
/>
|
|
242
|
+
) : (
|
|
243
|
+
<Prose text={profile?.content ?? ""} />
|
|
244
|
+
)}
|
|
245
|
+
</Section>
|
|
246
|
+
|
|
247
|
+
{profile?.extractor_names && profile.extractor_names.length > 0 && (
|
|
248
|
+
<Section
|
|
249
|
+
icon={Tags}
|
|
250
|
+
title="Extractors"
|
|
251
|
+
hint="Which reflexio extractor generated this preference."
|
|
252
|
+
>
|
|
253
|
+
<div className="flex flex-wrap gap-1.5">
|
|
254
|
+
{profile.extractor_names.map((name) => (
|
|
255
|
+
<Badge
|
|
256
|
+
key={name}
|
|
257
|
+
variant="outline"
|
|
258
|
+
className="font-mono text-[10px]"
|
|
259
|
+
>
|
|
260
|
+
{name}
|
|
261
|
+
</Badge>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</Section>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{customEntries.length > 0 && (
|
|
268
|
+
<Section
|
|
269
|
+
icon={Braces}
|
|
270
|
+
title="Custom features"
|
|
271
|
+
hint="Structured metadata attached to this preference."
|
|
272
|
+
>
|
|
273
|
+
<div className="rounded-xl border border-border bg-card overflow-hidden">
|
|
274
|
+
<dl className="divide-y divide-border">
|
|
275
|
+
{customEntries.map(([k, v]) => (
|
|
276
|
+
<div
|
|
277
|
+
key={k}
|
|
278
|
+
className="flex items-start justify-between gap-4 px-4 py-2.5"
|
|
279
|
+
>
|
|
280
|
+
<dt className="text-xs font-medium text-muted-foreground font-mono shrink-0">
|
|
281
|
+
{k}
|
|
282
|
+
</dt>
|
|
283
|
+
<dd className="text-xs min-w-0 break-words text-right">
|
|
284
|
+
{typeof v === "string"
|
|
285
|
+
? v
|
|
286
|
+
: JSON.stringify(v, null, 0)}
|
|
287
|
+
</dd>
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
</dl>
|
|
291
|
+
</div>
|
|
292
|
+
</Section>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{!editing && profile && (
|
|
296
|
+
<>
|
|
297
|
+
<Separator />
|
|
298
|
+
<DangerZone
|
|
299
|
+
onDelete={remove}
|
|
300
|
+
deleting={deleting}
|
|
301
|
+
disabled={saving}
|
|
302
|
+
/>
|
|
303
|
+
</>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{profile && (
|
|
308
|
+
<aside className="space-y-3 lg:sticky lg:top-6 lg:self-start">
|
|
309
|
+
<div className="rounded-xl border border-border bg-card p-4">
|
|
310
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-3">
|
|
311
|
+
Metadata
|
|
312
|
+
</h3>
|
|
313
|
+
<dl className="space-y-2.5 text-sm">
|
|
314
|
+
<Meta
|
|
315
|
+
icon={Clock}
|
|
316
|
+
label="Modified"
|
|
317
|
+
value={formatTimestamp(profile.last_modified_timestamp)}
|
|
318
|
+
/>
|
|
319
|
+
{profile.expiration_timestamp &&
|
|
320
|
+
profile.expiration_timestamp > 0 && (
|
|
321
|
+
<Meta
|
|
322
|
+
icon={CalendarClock}
|
|
323
|
+
label="Expires"
|
|
324
|
+
value={formatTimestamp(profile.expiration_timestamp)}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
{profile.profile_time_to_live && (
|
|
328
|
+
<Meta
|
|
329
|
+
label="TTL"
|
|
330
|
+
value={profile.profile_time_to_live}
|
|
331
|
+
mono
|
|
332
|
+
/>
|
|
333
|
+
)}
|
|
334
|
+
<CopyMeta
|
|
335
|
+
icon={Hash}
|
|
336
|
+
label="ID"
|
|
337
|
+
value={profile.profile_id}
|
|
338
|
+
display={truncateId(profile.profile_id, 8, 4)}
|
|
339
|
+
/>
|
|
340
|
+
<CopyMeta
|
|
341
|
+
icon={FolderGit2}
|
|
342
|
+
label="Project"
|
|
343
|
+
value={profile.user_id}
|
|
344
|
+
display={truncateId(profile.user_id, 32, 8)}
|
|
345
|
+
/>
|
|
346
|
+
{profile.generated_from_request_id && (
|
|
347
|
+
<CopyMeta
|
|
348
|
+
icon={FileText}
|
|
349
|
+
label="Request"
|
|
350
|
+
value={profile.generated_from_request_id}
|
|
351
|
+
display={truncateId(profile.generated_from_request_id, 8, 4)}
|
|
352
|
+
/>
|
|
353
|
+
)}
|
|
354
|
+
</dl>
|
|
355
|
+
</div>
|
|
356
|
+
</aside>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function Section({
|
|
365
|
+
icon: Icon,
|
|
366
|
+
title,
|
|
367
|
+
hint,
|
|
368
|
+
children,
|
|
369
|
+
}: {
|
|
370
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
371
|
+
title: string;
|
|
372
|
+
hint?: string;
|
|
373
|
+
children: React.ReactNode;
|
|
374
|
+
}) {
|
|
375
|
+
return (
|
|
376
|
+
<section className="space-y-2">
|
|
377
|
+
<div className="flex items-baseline gap-2 flex-wrap">
|
|
378
|
+
<Label className="text-sm font-semibold flex items-center gap-1.5">
|
|
379
|
+
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
380
|
+
{title}
|
|
381
|
+
</Label>
|
|
382
|
+
{hint && <span className="text-xs text-muted-foreground">{hint}</span>}
|
|
383
|
+
</div>
|
|
384
|
+
{children}
|
|
385
|
+
</section>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function Prose({ text }: { text: string }) {
|
|
390
|
+
if (!text) {
|
|
391
|
+
return <p className="text-sm text-muted-foreground italic">—</p>;
|
|
392
|
+
}
|
|
393
|
+
return (
|
|
394
|
+
<div className="rounded-xl border border-border bg-card px-4 py-3">
|
|
395
|
+
<p className="text-sm leading-relaxed whitespace-pre-wrap break-words">
|
|
396
|
+
{text}
|
|
397
|
+
</p>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function StatusBadge({
|
|
403
|
+
status,
|
|
404
|
+
}: {
|
|
405
|
+
status: "CURRENT" | "ARCHIVED" | "PENDING";
|
|
406
|
+
}) {
|
|
407
|
+
const variant =
|
|
408
|
+
status === "CURRENT"
|
|
409
|
+
? "secondary"
|
|
410
|
+
: status === "ARCHIVED"
|
|
411
|
+
? "outline"
|
|
412
|
+
: "default";
|
|
413
|
+
return (
|
|
414
|
+
<Badge variant={variant} className="gap-1.5">
|
|
415
|
+
<span
|
|
416
|
+
className={cn(
|
|
417
|
+
"h-1.5 w-1.5 rounded-full",
|
|
418
|
+
status === "CURRENT" && "bg-emerald-500",
|
|
419
|
+
status === "PENDING" && "bg-amber-500",
|
|
420
|
+
status === "ARCHIVED" && "bg-muted-foreground",
|
|
421
|
+
)}
|
|
422
|
+
/>
|
|
423
|
+
{status}
|
|
424
|
+
</Badge>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function Meta({
|
|
429
|
+
icon: Icon,
|
|
430
|
+
label,
|
|
431
|
+
value,
|
|
432
|
+
mono,
|
|
433
|
+
}: {
|
|
434
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
435
|
+
label: string;
|
|
436
|
+
value: string;
|
|
437
|
+
mono?: boolean;
|
|
438
|
+
}) {
|
|
439
|
+
return (
|
|
440
|
+
<div className="flex items-start justify-between gap-3">
|
|
441
|
+
<dt className="text-xs text-muted-foreground shrink-0 flex items-center gap-1.5">
|
|
442
|
+
{Icon && <Icon className="h-3 w-3" />}
|
|
443
|
+
{label}
|
|
444
|
+
</dt>
|
|
445
|
+
<dd
|
|
446
|
+
className={cn(
|
|
447
|
+
"text-xs text-right min-w-0 break-words",
|
|
448
|
+
mono && "font-mono",
|
|
449
|
+
)}
|
|
450
|
+
>
|
|
451
|
+
{value}
|
|
452
|
+
</dd>
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function CopyMeta({
|
|
458
|
+
icon: Icon,
|
|
459
|
+
label,
|
|
460
|
+
value,
|
|
461
|
+
display,
|
|
462
|
+
}: {
|
|
463
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
464
|
+
label: string;
|
|
465
|
+
value: string;
|
|
466
|
+
display: string;
|
|
467
|
+
}) {
|
|
468
|
+
const [copied, setCopied] = useState(false);
|
|
469
|
+
const copy = async () => {
|
|
470
|
+
try {
|
|
471
|
+
await navigator.clipboard.writeText(value);
|
|
472
|
+
setCopied(true);
|
|
473
|
+
setTimeout(() => setCopied(false), 1200);
|
|
474
|
+
} catch {
|
|
475
|
+
// ignore
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
return (
|
|
479
|
+
<div className="flex items-start justify-between gap-3">
|
|
480
|
+
<dt className="text-xs text-muted-foreground shrink-0 flex items-center gap-1.5">
|
|
481
|
+
{Icon && <Icon className="h-3 w-3" />}
|
|
482
|
+
{label}
|
|
483
|
+
</dt>
|
|
484
|
+
<dd className="text-xs min-w-0 flex items-center gap-1.5">
|
|
485
|
+
<code className="font-mono">{display}</code>
|
|
486
|
+
<button
|
|
487
|
+
onClick={copy}
|
|
488
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
489
|
+
title="Copy full id"
|
|
490
|
+
>
|
|
491
|
+
{copied ? (
|
|
492
|
+
<Check className="h-3 w-3 text-emerald-500" />
|
|
493
|
+
) : (
|
|
494
|
+
<Copy className="h-3 w-3" />
|
|
495
|
+
)}
|
|
496
|
+
</button>
|
|
497
|
+
</dd>
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function DangerZone({
|
|
503
|
+
onDelete,
|
|
504
|
+
deleting,
|
|
505
|
+
disabled,
|
|
506
|
+
}: {
|
|
507
|
+
onDelete: () => void;
|
|
508
|
+
deleting: boolean;
|
|
509
|
+
disabled: boolean;
|
|
510
|
+
}) {
|
|
511
|
+
return (
|
|
512
|
+
<section className="rounded-xl border border-destructive/30 bg-destructive/5 p-4 flex items-start justify-between gap-4">
|
|
513
|
+
<div className="min-w-0">
|
|
514
|
+
<h3 className="text-sm font-semibold text-destructive">Danger zone</h3>
|
|
515
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
516
|
+
Deleting removes this preference permanently. Preferences regenerate from
|
|
517
|
+
fresh interactions, so this is safe but not reversible.
|
|
518
|
+
</p>
|
|
519
|
+
</div>
|
|
520
|
+
<Button
|
|
521
|
+
variant="destructive"
|
|
522
|
+
size="sm"
|
|
523
|
+
onClick={onDelete}
|
|
524
|
+
disabled={deleting || disabled}
|
|
525
|
+
>
|
|
526
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
527
|
+
{deleting ? "Deleting…" : "Delete"}
|
|
528
|
+
</Button>
|
|
529
|
+
</section>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Users, ChevronRight } from "lucide-react";
|
|
6
|
+
import { PageHeader } from "@/components/common/page-header";
|
|
7
|
+
import { EmptyState } from "@/components/common/empty-state";
|
|
8
|
+
import { DeleteAllButton } from "@/components/common/delete-all-button";
|
|
9
|
+
import { Badge } from "@/components/ui/badge";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { reflexio } from "@/lib/reflexio-client";
|
|
12
|
+
import { useSettings } from "@/hooks/use-settings";
|
|
13
|
+
import { formatRelative, truncateId } from "@/lib/format";
|
|
14
|
+
import type { UserProfile } from "@/lib/types";
|
|
15
|
+
|
|
16
|
+
export default function PreferencesPage() {
|
|
17
|
+
const { reflexioUrl } = useSettings();
|
|
18
|
+
const [profiles, setProfiles] = useState<UserProfile[] | null>(null);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const [filter, setFilter] = useState("");
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let cancelled = false;
|
|
24
|
+
reflexio
|
|
25
|
+
.getAllProfiles({ reflexioUrl })
|
|
26
|
+
.then((res) => {
|
|
27
|
+
if (!cancelled) {
|
|
28
|
+
setProfiles(res.user_profiles ?? []);
|
|
29
|
+
setError(null);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch((e) => {
|
|
33
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
};
|
|
38
|
+
}, [reflexioUrl]);
|
|
39
|
+
|
|
40
|
+
const filtered = (profiles ?? []).filter(
|
|
41
|
+
(p) =>
|
|
42
|
+
p.content.toLowerCase().includes(filter.toLowerCase()) ||
|
|
43
|
+
p.user_id.toLowerCase().includes(filter.toLowerCase()),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex-1 overflow-auto">
|
|
48
|
+
<PageHeader
|
|
49
|
+
title="Preferences"
|
|
50
|
+
description="Project-scoped preferences extracted from interactions."
|
|
51
|
+
actions={
|
|
52
|
+
<div className="flex items-center gap-2">
|
|
53
|
+
<Input
|
|
54
|
+
value={filter}
|
|
55
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
56
|
+
placeholder="Filter"
|
|
57
|
+
className="h-8 w-56 text-xs"
|
|
58
|
+
/>
|
|
59
|
+
<DeleteAllButton
|
|
60
|
+
label={`Delete all${profiles && profiles.length > 0 ? ` (${profiles.length})` : ""}`}
|
|
61
|
+
confirmMessage={`Delete ALL ${profiles?.length ?? 0} preferences? Preferences regenerate from fresh interactions, but this cannot be undone.`}
|
|
62
|
+
disabled={!profiles || profiles.length === 0}
|
|
63
|
+
onConfirm={async () => {
|
|
64
|
+
await reflexio.deleteAllProfiles(reflexioUrl);
|
|
65
|
+
setProfiles([]);
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<div className="p-6">
|
|
73
|
+
{error && (
|
|
74
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm mb-4">
|
|
75
|
+
{error}. Is reflexio running on the URL in the top bar?
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{profiles === null && !error ? (
|
|
80
|
+
<div className="text-sm text-muted-foreground">Loading…</div>
|
|
81
|
+
) : filtered.length === 0 ? (
|
|
82
|
+
<EmptyState
|
|
83
|
+
icon={Users}
|
|
84
|
+
title="No preferences yet"
|
|
85
|
+
description="Keep using Claude with claude-smart enabled — preferences will appear here automatically as the extractor learns patterns from your interactions."
|
|
86
|
+
/>
|
|
87
|
+
) : (
|
|
88
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
89
|
+
{filtered.map((p) => (
|
|
90
|
+
<Link
|
|
91
|
+
key={p.profile_id}
|
|
92
|
+
href={`/preferences/${encodeURIComponent(p.profile_id)}`}
|
|
93
|
+
className="group block rounded-xl border border-border bg-card p-4 hover:bg-accent/40 transition-colors"
|
|
94
|
+
>
|
|
95
|
+
<header className="flex items-center justify-between gap-2 mb-2">
|
|
96
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
97
|
+
<Badge variant="outline" className="h-5 font-mono text-[10px]">
|
|
98
|
+
{truncateId(p.user_id, 32, 8)}
|
|
99
|
+
</Badge>
|
|
100
|
+
{p.status && (
|
|
101
|
+
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
102
|
+
{p.status}
|
|
103
|
+
</Badge>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
107
|
+
<span className="text-[11px] text-muted-foreground">
|
|
108
|
+
{formatRelative(p.last_modified_timestamp)}
|
|
109
|
+
</span>
|
|
110
|
+
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground/60 group-hover:text-foreground transition-colors" />
|
|
111
|
+
</div>
|
|
112
|
+
</header>
|
|
113
|
+
<p className="text-sm leading-relaxed line-clamp-4">{p.content}</p>
|
|
114
|
+
{p.source && (
|
|
115
|
+
<p className="text-[11px] text-muted-foreground mt-2 font-mono">
|
|
116
|
+
source: {p.source}
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
</Link>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider } from "next-themes";
|
|
4
|
+
import { SettingsProvider } from "@/hooks/use-settings";
|
|
5
|
+
|
|
6
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
9
|
+
<SettingsProvider>{children}</SettingsProvider>
|
|
10
|
+
</ThemeProvider>
|
|
11
|
+
);
|
|
12
|
+
}
|