claude-smart 0.2.31 → 0.2.32
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/README.md +2 -2
- package/bin/claude-smart.js +172 -18
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/api/config/route.ts +11 -2
- package/plugin/dashboard/app/api/health/route.ts +45 -6
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
- package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
- package/plugin/dashboard/app/configure/env/page.tsx +36 -31
- package/plugin/dashboard/app/configure/layout.tsx +1 -1
- package/plugin/dashboard/app/configure/server/page.tsx +8 -14
- package/plugin/dashboard/app/dashboard/page.tsx +311 -115
- package/plugin/dashboard/app/globals.css +80 -66
- package/plugin/dashboard/app/layout.tsx +13 -10
- package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
- package/plugin/dashboard/app/preferences/page.tsx +154 -54
- package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
- package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
- package/plugin/dashboard/app/sessions/page.tsx +14 -10
- package/plugin/dashboard/app/skills/page.tsx +175 -56
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
- package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
- package/plugin/dashboard/components/common/empty-state.tsx +4 -2
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +5 -3
- package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
- package/plugin/dashboard/components/common/stat-card.tsx +9 -5
- package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
- package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
- package/plugin/dashboard/components/ui/input.tsx +1 -0
- package/plugin/dashboard/hooks/use-settings.tsx +30 -61
- package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
- package/plugin/dashboard/lib/config-file.ts +2 -0
- package/plugin/dashboard/lib/reflexio-client.ts +23 -48
- package/plugin/dashboard/lib/session-reader.ts +222 -6
- package/plugin/dashboard/lib/types.ts +20 -1
- package/plugin/dashboard/package-lock.json +70 -95
- package/plugin/dashboard/package.json +5 -2
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +61 -0
- package/plugin/scripts/backend-service.sh +13 -2
- package/plugin/scripts/cli.sh +1 -0
- package/plugin/scripts/codex-hook.js +100 -3
- package/plugin/scripts/dashboard-service.sh +95 -19
- package/plugin/scripts/hook_entry.sh +13 -7
- package/plugin/scripts/smart-install.sh +18 -13
- package/plugin/src/claude_smart/cli.py +86 -20
- package/plugin/src/claude_smart/context_format.py +244 -6
- package/plugin/src/claude_smart/context_inject.py +8 -1
- package/plugin/src/claude_smart/cs_cite.py +186 -34
- package/plugin/src/claude_smart/env_config.py +102 -0
- package/plugin/src/claude_smart/events/stop.py +32 -6
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/publish.py +5 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
- package/plugin/uv.lock +1 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
@theme inline {
|
|
8
8
|
--color-background: var(--background);
|
|
9
9
|
--color-foreground: var(--foreground);
|
|
10
|
-
--font-sans: var(--font-sans);
|
|
11
|
-
--font-mono: var(--font-
|
|
10
|
+
--font-sans: var(--dashboard-font-sans);
|
|
11
|
+
--font-mono: var(--dashboard-font-mono);
|
|
12
12
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
13
13
|
--color-sidebar-border: var(--sidebar-border);
|
|
14
14
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
@@ -48,72 +48,76 @@
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
:root {
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
--
|
|
54
|
-
|
|
55
|
-
--
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
--
|
|
64
|
-
--
|
|
65
|
-
--
|
|
66
|
-
--
|
|
67
|
-
--
|
|
68
|
-
--
|
|
69
|
-
--
|
|
70
|
-
--
|
|
71
|
-
--
|
|
72
|
-
--
|
|
73
|
-
--chart-
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
--
|
|
77
|
-
--
|
|
78
|
-
--
|
|
79
|
-
--sidebar
|
|
80
|
-
--sidebar-
|
|
81
|
-
--sidebar-
|
|
82
|
-
--sidebar-
|
|
51
|
+
--dashboard-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
52
|
+
"Segoe UI", sans-serif;
|
|
53
|
+
--dashboard-font-mono: "SFMono-Regular", "SF Mono", Consolas, "Liberation Mono",
|
|
54
|
+
Menlo, monospace;
|
|
55
|
+
--background: oklch(0.982 0.006 197);
|
|
56
|
+
--foreground: oklch(0.19 0.018 236);
|
|
57
|
+
--card: oklch(0.997 0.003 197);
|
|
58
|
+
--card-foreground: oklch(0.19 0.018 236);
|
|
59
|
+
--popover: oklch(0.997 0.003 197);
|
|
60
|
+
--popover-foreground: oklch(0.19 0.018 236);
|
|
61
|
+
--primary: oklch(0.48 0.142 202);
|
|
62
|
+
--primary-foreground: oklch(0.99 0.006 197);
|
|
63
|
+
--secondary: oklch(0.939 0.025 202);
|
|
64
|
+
--secondary-foreground: oklch(0.245 0.038 218);
|
|
65
|
+
--muted: oklch(0.949 0.011 218);
|
|
66
|
+
--muted-foreground: oklch(0.486 0.024 230);
|
|
67
|
+
--accent: oklch(0.928 0.054 185);
|
|
68
|
+
--accent-foreground: oklch(0.235 0.052 205);
|
|
69
|
+
--destructive: oklch(0.58 0.216 27);
|
|
70
|
+
--border: oklch(0.882 0.018 218);
|
|
71
|
+
--input: oklch(0.892 0.018 218);
|
|
72
|
+
--ring: oklch(0.65 0.104 192);
|
|
73
|
+
--chart-1: oklch(0.68 0.13 188);
|
|
74
|
+
--chart-2: oklch(0.62 0.15 154);
|
|
75
|
+
--chart-3: oklch(0.72 0.16 82);
|
|
76
|
+
--chart-4: oklch(0.64 0.16 24);
|
|
77
|
+
--chart-5: oklch(0.54 0.13 258);
|
|
78
|
+
--radius: 0.5rem;
|
|
79
|
+
--sidebar: oklch(0.964 0.013 213);
|
|
80
|
+
--sidebar-foreground: oklch(0.22 0.026 236);
|
|
81
|
+
--sidebar-primary: oklch(0.48 0.142 202);
|
|
82
|
+
--sidebar-primary-foreground: oklch(0.99 0.006 197);
|
|
83
|
+
--sidebar-accent: oklch(0.918 0.043 190);
|
|
84
|
+
--sidebar-accent-foreground: oklch(0.24 0.054 206);
|
|
85
|
+
--sidebar-border: oklch(0.862 0.02 218);
|
|
86
|
+
--sidebar-ring: oklch(0.65 0.104 192);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
.dark {
|
|
86
|
-
--background: oklch(0.
|
|
87
|
-
--foreground: oklch(0.
|
|
88
|
-
--card: oklch(0.205 0
|
|
89
|
-
--card-foreground: oklch(0.
|
|
90
|
-
--popover: oklch(0.205 0
|
|
91
|
-
--popover-foreground: oklch(0.
|
|
92
|
-
--primary: oklch(0.
|
|
93
|
-
--primary-foreground: oklch(0.
|
|
94
|
-
--secondary: oklch(0.
|
|
95
|
-
--secondary-foreground: oklch(0.
|
|
96
|
-
--muted: oklch(0.
|
|
97
|
-
--muted-foreground: oklch(0.
|
|
98
|
-
--accent: oklch(0.
|
|
99
|
-
--accent-foreground: oklch(0.
|
|
100
|
-
--destructive: oklch(0.
|
|
101
|
-
--border: oklch(
|
|
102
|
-
--input: oklch(
|
|
103
|
-
--ring: oklch(0.
|
|
104
|
-
--chart-1: oklch(0.
|
|
105
|
-
--chart-2: oklch(0.
|
|
106
|
-
--chart-3: oklch(0.
|
|
107
|
-
--chart-4: oklch(0.
|
|
108
|
-
--chart-5: oklch(0.
|
|
109
|
-
--sidebar: oklch(0.
|
|
110
|
-
--sidebar-foreground: oklch(0.
|
|
111
|
-
--sidebar-primary: oklch(0.
|
|
112
|
-
--sidebar-primary-foreground: oklch(0.
|
|
113
|
-
--sidebar-accent: oklch(0.
|
|
114
|
-
--sidebar-accent-foreground: oklch(0.
|
|
115
|
-
--sidebar-border: oklch(
|
|
116
|
-
--sidebar-ring: oklch(0.
|
|
90
|
+
--background: oklch(0.154 0.022 236);
|
|
91
|
+
--foreground: oklch(0.938 0.012 202);
|
|
92
|
+
--card: oklch(0.205 0.027 236);
|
|
93
|
+
--card-foreground: oklch(0.938 0.012 202);
|
|
94
|
+
--popover: oklch(0.205 0.027 236);
|
|
95
|
+
--popover-foreground: oklch(0.938 0.012 202);
|
|
96
|
+
--primary: oklch(0.72 0.119 188);
|
|
97
|
+
--primary-foreground: oklch(0.154 0.022 236);
|
|
98
|
+
--secondary: oklch(0.265 0.029 235);
|
|
99
|
+
--secondary-foreground: oklch(0.914 0.014 202);
|
|
100
|
+
--muted: oklch(0.258 0.027 235);
|
|
101
|
+
--muted-foreground: oklch(0.69 0.026 219);
|
|
102
|
+
--accent: oklch(0.306 0.054 202);
|
|
103
|
+
--accent-foreground: oklch(0.9 0.05 183);
|
|
104
|
+
--destructive: oklch(0.66 0.21 24);
|
|
105
|
+
--border: oklch(0.332 0.031 236);
|
|
106
|
+
--input: oklch(0.354 0.034 236);
|
|
107
|
+
--ring: oklch(0.72 0.119 188);
|
|
108
|
+
--chart-1: oklch(0.72 0.119 188);
|
|
109
|
+
--chart-2: oklch(0.72 0.14 148);
|
|
110
|
+
--chart-3: oklch(0.78 0.15 82);
|
|
111
|
+
--chart-4: oklch(0.68 0.17 24);
|
|
112
|
+
--chart-5: oklch(0.69 0.12 258);
|
|
113
|
+
--sidebar: oklch(0.184 0.027 236);
|
|
114
|
+
--sidebar-foreground: oklch(0.916 0.014 202);
|
|
115
|
+
--sidebar-primary: oklch(0.72 0.119 188);
|
|
116
|
+
--sidebar-primary-foreground: oklch(0.154 0.022 236);
|
|
117
|
+
--sidebar-accent: oklch(0.29 0.051 202);
|
|
118
|
+
--sidebar-accent-foreground: oklch(0.9 0.05 183);
|
|
119
|
+
--sidebar-border: oklch(0.332 0.031 236);
|
|
120
|
+
--sidebar-ring: oklch(0.72 0.119 188);
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
@layer base {
|
|
@@ -122,8 +126,18 @@
|
|
|
122
126
|
}
|
|
123
127
|
body {
|
|
124
128
|
@apply bg-background text-foreground;
|
|
129
|
+
background-image:
|
|
130
|
+
linear-gradient(to right, oklch(0.88 0.018 218 / 0.45) 1px, transparent 1px),
|
|
131
|
+
linear-gradient(to bottom, oklch(0.88 0.018 218 / 0.45) 1px, transparent 1px);
|
|
132
|
+
background-size: 32px 32px;
|
|
133
|
+
background-position: -1px -1px;
|
|
125
134
|
}
|
|
126
135
|
html {
|
|
127
136
|
@apply font-sans;
|
|
128
137
|
}
|
|
129
|
-
|
|
138
|
+
.dark body {
|
|
139
|
+
background-image:
|
|
140
|
+
linear-gradient(to right, oklch(0.38 0.032 236 / 0.28) 1px, transparent 1px),
|
|
141
|
+
linear-gradient(to bottom, oklch(0.38 0.032 236 / 0.28) 1px, transparent 1px);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import { Inter } from "next/font/google";
|
|
3
2
|
import "./globals.css";
|
|
4
3
|
import { Providers } from "./providers";
|
|
5
4
|
import { Sidebar } from "@/components/layout/sidebar";
|
|
6
5
|
import { TopBar } from "@/components/layout/top-bar";
|
|
7
6
|
import { StallBanner } from "@/components/stall-banner";
|
|
8
7
|
|
|
9
|
-
const inter = Inter({
|
|
10
|
-
variable: "--font-sans",
|
|
11
|
-
subsets: ["latin"],
|
|
12
|
-
});
|
|
13
|
-
|
|
14
8
|
export const metadata: Metadata = {
|
|
15
9
|
title: "Claude-Smart Dashboard",
|
|
16
10
|
description: "Manage sessions, preferences, skills, and configuration",
|
|
@@ -22,16 +16,25 @@ export default function RootLayout({
|
|
|
22
16
|
children: React.ReactNode;
|
|
23
17
|
}>) {
|
|
24
18
|
return (
|
|
25
|
-
<html
|
|
26
|
-
|
|
19
|
+
<html
|
|
20
|
+
lang="en"
|
|
21
|
+
className="h-full"
|
|
22
|
+
suppressHydrationWarning
|
|
23
|
+
>
|
|
24
|
+
<body
|
|
25
|
+
className="h-full flex flex-col antialiased font-sans"
|
|
26
|
+
suppressHydrationWarning
|
|
27
|
+
>
|
|
27
28
|
<Providers>
|
|
28
29
|
<StallBanner />
|
|
29
30
|
<TopBar />
|
|
30
31
|
<div className="flex flex-1 min-h-0">
|
|
31
|
-
<aside className="hidden lg:block w-
|
|
32
|
+
<aside className="hidden lg:block w-64 border-r border-sidebar-border bg-sidebar/95 shrink-0">
|
|
32
33
|
<Sidebar />
|
|
33
34
|
</aside>
|
|
34
|
-
<main className="flex-1 min-w-0 flex flex-col">
|
|
35
|
+
<main className="flex-1 min-w-0 flex flex-col bg-background/88">
|
|
36
|
+
{children}
|
|
37
|
+
</main>
|
|
35
38
|
</div>
|
|
36
39
|
</Providers>
|
|
37
40
|
</body>
|
|
@@ -5,7 +5,6 @@ import Link from "next/link";
|
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
6
|
import {
|
|
7
7
|
ArrowLeft,
|
|
8
|
-
Trash2,
|
|
9
8
|
Save,
|
|
10
9
|
AlertTriangle,
|
|
11
10
|
Pencil,
|
|
@@ -23,12 +22,12 @@ import {
|
|
|
23
22
|
} from "lucide-react";
|
|
24
23
|
import { PageHeader } from "@/components/common/page-header";
|
|
25
24
|
import { EmptyState } from "@/components/common/empty-state";
|
|
25
|
+
import { DeleteLearningDangerZone } from "@/components/common/delete-learning-danger-zone";
|
|
26
26
|
import { Button } from "@/components/ui/button";
|
|
27
27
|
import { Label } from "@/components/ui/label";
|
|
28
28
|
import { Badge } from "@/components/ui/badge";
|
|
29
29
|
import { Separator } from "@/components/ui/separator";
|
|
30
30
|
import { reflexio } from "@/lib/reflexio-client";
|
|
31
|
-
import { useSettings } from "@/hooks/use-settings";
|
|
32
31
|
import { formatTimestamp, truncateId } from "@/lib/format";
|
|
33
32
|
import { cn } from "@/lib/utils";
|
|
34
33
|
import { statusLabel as status } from "@/lib/status";
|
|
@@ -42,7 +41,6 @@ export default function PreferenceDetailPage({
|
|
|
42
41
|
const { id: rawId } = use(params);
|
|
43
42
|
const id = decodeURIComponent(rawId);
|
|
44
43
|
const router = useRouter();
|
|
45
|
-
const { reflexioUrl } = useSettings();
|
|
46
44
|
|
|
47
45
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
48
46
|
const [notFound, setNotFound] = useState(false);
|
|
@@ -55,7 +53,7 @@ export default function PreferenceDetailPage({
|
|
|
55
53
|
useEffect(() => {
|
|
56
54
|
let cancelled = false;
|
|
57
55
|
reflexio
|
|
58
|
-
.getAllProfiles({
|
|
56
|
+
.getAllProfiles({ limit: 500 })
|
|
59
57
|
.then((res) => {
|
|
60
58
|
if (cancelled) return;
|
|
61
59
|
const found = (res.user_profiles ?? []).find(
|
|
@@ -74,7 +72,7 @@ export default function PreferenceDetailPage({
|
|
|
74
72
|
return () => {
|
|
75
73
|
cancelled = true;
|
|
76
74
|
};
|
|
77
|
-
}, [id
|
|
75
|
+
}, [id]);
|
|
78
76
|
|
|
79
77
|
const dirty = useMemo(
|
|
80
78
|
() => !!profile && profile.content !== content,
|
|
@@ -92,7 +90,6 @@ export default function PreferenceDetailPage({
|
|
|
92
90
|
profile_id: profile.profile_id,
|
|
93
91
|
content,
|
|
94
92
|
},
|
|
95
|
-
reflexioUrl,
|
|
96
93
|
);
|
|
97
94
|
setProfile({ ...profile, content });
|
|
98
95
|
setEditing(false);
|
|
@@ -105,12 +102,10 @@ export default function PreferenceDetailPage({
|
|
|
105
102
|
|
|
106
103
|
const remove = async () => {
|
|
107
104
|
if (!profile) return;
|
|
108
|
-
if (!confirm("Delete this preference? This cannot be undone.")) return;
|
|
109
105
|
setDeleting(true);
|
|
110
106
|
try {
|
|
111
107
|
await reflexio.deleteUserProfile(
|
|
112
108
|
{ user_id: profile.user_id, profile_id: profile.profile_id },
|
|
113
|
-
reflexioUrl,
|
|
114
109
|
);
|
|
115
110
|
router.push("/preferences");
|
|
116
111
|
} catch (e) {
|
|
@@ -200,7 +195,7 @@ export default function PreferenceDetailPage({
|
|
|
200
195
|
<div className="mx-auto max-w-5xl grid gap-6 lg:grid-cols-[1fr_280px]">
|
|
201
196
|
<div className="space-y-6 min-w-0">
|
|
202
197
|
{error && (
|
|
203
|
-
<div className="rounded-
|
|
198
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm flex items-start gap-2">
|
|
204
199
|
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
|
|
205
200
|
<span>{error}</span>
|
|
206
201
|
</div>
|
|
@@ -237,7 +232,7 @@ export default function PreferenceDetailPage({
|
|
|
237
232
|
onChange={(e) => setContent(e.target.value)}
|
|
238
233
|
rows={6}
|
|
239
234
|
placeholder="e.g. Project bans pytest-asyncio; uses anyio with trio backend for async tests."
|
|
240
|
-
className="w-full rounded-
|
|
235
|
+
className="w-full rounded-lg 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
236
|
/>
|
|
242
237
|
) : (
|
|
243
238
|
<Prose text={profile?.content ?? ""} />
|
|
@@ -270,7 +265,7 @@ export default function PreferenceDetailPage({
|
|
|
270
265
|
title="Custom features"
|
|
271
266
|
hint="Structured metadata attached to this preference."
|
|
272
267
|
>
|
|
273
|
-
<div className="rounded-
|
|
268
|
+
<div className="rounded-lg border border-border bg-card overflow-hidden">
|
|
274
269
|
<dl className="divide-y divide-border">
|
|
275
270
|
{customEntries.map(([k, v]) => (
|
|
276
271
|
<div
|
|
@@ -306,8 +301,8 @@ export default function PreferenceDetailPage({
|
|
|
306
301
|
|
|
307
302
|
{profile && (
|
|
308
303
|
<aside className="space-y-3 lg:sticky lg:top-6 lg:self-start">
|
|
309
|
-
<div className="rounded-
|
|
310
|
-
<h3 className="text-xs font-semibold uppercase
|
|
304
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
305
|
+
<h3 className="text-xs font-semibold uppercase text-muted-foreground mb-3">
|
|
311
306
|
Metadata
|
|
312
307
|
</h3>
|
|
313
308
|
<dl className="space-y-2.5 text-sm">
|
|
@@ -391,7 +386,7 @@ function Prose({ text }: { text: string }) {
|
|
|
391
386
|
return <p className="text-sm text-muted-foreground italic">—</p>;
|
|
392
387
|
}
|
|
393
388
|
return (
|
|
394
|
-
<div className="rounded-
|
|
389
|
+
<div className="rounded-lg border border-border bg-card px-4 py-3">
|
|
395
390
|
<p className="text-sm leading-relaxed whitespace-pre-wrap break-words">
|
|
396
391
|
{text}
|
|
397
392
|
</p>
|
|
@@ -509,23 +504,17 @@ function DangerZone({
|
|
|
509
504
|
disabled: boolean;
|
|
510
505
|
}) {
|
|
511
506
|
return (
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
disabled={deleting || disabled}
|
|
525
|
-
>
|
|
526
|
-
<Trash2 className="h-3.5 w-3.5" />
|
|
527
|
-
{deleting ? "Deleting…" : "Delete"}
|
|
528
|
-
</Button>
|
|
529
|
-
</section>
|
|
507
|
+
<DeleteLearningDangerZone
|
|
508
|
+
learningName="this preference"
|
|
509
|
+
description="Delete this claude-smart preference permanently."
|
|
510
|
+
consequences={[
|
|
511
|
+
"This preference will stop being applied in future claude-smart responses.",
|
|
512
|
+
"Historical session records may still show prior uses.",
|
|
513
|
+
"Future interactions may learn a similar preference again, but this record cannot be restored.",
|
|
514
|
+
]}
|
|
515
|
+
onDelete={onDelete}
|
|
516
|
+
deleting={deleting}
|
|
517
|
+
disabled={disabled}
|
|
518
|
+
/>
|
|
530
519
|
);
|
|
531
520
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { Users, ChevronRight } from "lucide-react";
|
|
6
6
|
import { PageHeader } from "@/components/common/page-header";
|
|
@@ -8,40 +8,92 @@ import { EmptyState } from "@/components/common/empty-state";
|
|
|
8
8
|
import { DeleteAllButton } from "@/components/common/delete-all-button";
|
|
9
9
|
import { Badge } from "@/components/ui/badge";
|
|
10
10
|
import { Input } from "@/components/ui/input";
|
|
11
|
+
import {
|
|
12
|
+
Select,
|
|
13
|
+
SelectContent,
|
|
14
|
+
SelectItem,
|
|
15
|
+
SelectTrigger,
|
|
16
|
+
SelectValue,
|
|
17
|
+
} from "@/components/ui/select";
|
|
11
18
|
import { reflexio } from "@/lib/reflexio-client";
|
|
12
|
-
import { useSettings } from "@/hooks/use-settings";
|
|
13
19
|
import { formatRelative, truncateId } from "@/lib/format";
|
|
14
|
-
import type { UserProfile } from "@/lib/types";
|
|
20
|
+
import type { PlaybookApplicationStat, UserProfile } from "@/lib/types";
|
|
21
|
+
|
|
22
|
+
type PreferenceSort = "newest" | "applied";
|
|
23
|
+
|
|
24
|
+
function profileStatKey(profile: UserProfile): string {
|
|
25
|
+
return `profile:profile:${profile.profile_id}`;
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
export default function PreferencesPage() {
|
|
17
|
-
const { reflexioUrl } = useSettings();
|
|
18
29
|
const [profiles, setProfiles] = useState<UserProfile[] | null>(null);
|
|
30
|
+
const [appStats, setAppStats] = useState<PlaybookApplicationStat[] | null>(
|
|
31
|
+
null,
|
|
32
|
+
);
|
|
19
33
|
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const [sortBy, setSortBy] = useState<PreferenceSort>("newest");
|
|
20
35
|
const [filter, setFilter] = useState("");
|
|
21
36
|
|
|
22
37
|
useEffect(() => {
|
|
23
38
|
let cancelled = false;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
async function load() {
|
|
40
|
+
try {
|
|
41
|
+
const [profileRes, statsRes] = await Promise.all([
|
|
42
|
+
reflexio.getAllProfiles(),
|
|
43
|
+
fetch("/api/rules/applied?daysBack=30&limit=200", {
|
|
44
|
+
cache: "no-store",
|
|
45
|
+
})
|
|
46
|
+
.then((r) => r.json())
|
|
47
|
+
.catch(() => ({
|
|
48
|
+
success: false,
|
|
49
|
+
stats: [] as PlaybookApplicationStat[],
|
|
50
|
+
})),
|
|
51
|
+
]);
|
|
52
|
+
if (cancelled) return;
|
|
53
|
+
setProfiles(profileRes.user_profiles ?? []);
|
|
54
|
+
setAppStats(statsRes.stats ?? []);
|
|
55
|
+
setError(null);
|
|
56
|
+
} catch (e) {
|
|
27
57
|
if (!cancelled) {
|
|
28
|
-
|
|
29
|
-
setError(null);
|
|
58
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
30
59
|
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
load();
|
|
35
63
|
return () => {
|
|
36
64
|
cancelled = true;
|
|
37
65
|
};
|
|
38
|
-
}, [
|
|
66
|
+
}, []);
|
|
39
67
|
|
|
40
|
-
const
|
|
41
|
-
(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
const statsByProfile = useMemo(() => {
|
|
69
|
+
const map = new Map<string, PlaybookApplicationStat>();
|
|
70
|
+
for (const s of appStats ?? []) {
|
|
71
|
+
map.set(`${s.kind}:${s.source_kind ?? "unknown"}:${s.real_id}`, s);
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
}, [appStats]);
|
|
75
|
+
|
|
76
|
+
const filtered = useMemo(() => {
|
|
77
|
+
const query = filter.toLowerCase();
|
|
78
|
+
const matches = (profiles ?? []).filter(
|
|
79
|
+
(p) =>
|
|
80
|
+
p.content.toLowerCase().includes(query) ||
|
|
81
|
+
p.user_id.toLowerCase().includes(query),
|
|
82
|
+
);
|
|
83
|
+
return matches.sort((a, b) => {
|
|
84
|
+
if (sortBy === "applied") {
|
|
85
|
+
const aStat = statsByProfile.get(profileStatKey(a));
|
|
86
|
+
const bStat = statsByProfile.get(profileStatKey(b));
|
|
87
|
+
const appliedDelta =
|
|
88
|
+
(bStat?.applied_count ?? 0) - (aStat?.applied_count ?? 0);
|
|
89
|
+
if (appliedDelta !== 0) return appliedDelta;
|
|
90
|
+
const recencyDelta =
|
|
91
|
+
(bStat?.last_applied_at ?? 0) - (aStat?.last_applied_at ?? 0);
|
|
92
|
+
if (recencyDelta !== 0) return recencyDelta;
|
|
93
|
+
}
|
|
94
|
+
return b.last_modified_timestamp - a.last_modified_timestamp;
|
|
95
|
+
});
|
|
96
|
+
}, [filter, profiles, sortBy, statsByProfile]);
|
|
45
97
|
|
|
46
98
|
return (
|
|
47
99
|
<div className="flex-1 overflow-auto">
|
|
@@ -49,19 +101,33 @@ export default function PreferencesPage() {
|
|
|
49
101
|
title="Preferences"
|
|
50
102
|
description="Project-scoped preferences extracted from interactions."
|
|
51
103
|
actions={
|
|
52
|
-
<div className="flex items-center gap-2">
|
|
104
|
+
<div className="flex max-w-full flex-wrap items-center justify-end gap-2">
|
|
105
|
+
<Select
|
|
106
|
+
value={sortBy}
|
|
107
|
+
onValueChange={(v) => setSortBy((v as PreferenceSort) ?? "newest")}
|
|
108
|
+
>
|
|
109
|
+
<SelectTrigger size="sm" className="w-36 text-xs bg-background/80">
|
|
110
|
+
<SelectValue placeholder="Sort">
|
|
111
|
+
{sortBy === "applied" ? "Most applied" : "Newest"}
|
|
112
|
+
</SelectValue>
|
|
113
|
+
</SelectTrigger>
|
|
114
|
+
<SelectContent>
|
|
115
|
+
<SelectItem value="newest">Newest</SelectItem>
|
|
116
|
+
<SelectItem value="applied">Most applied</SelectItem>
|
|
117
|
+
</SelectContent>
|
|
118
|
+
</Select>
|
|
53
119
|
<Input
|
|
54
120
|
value={filter}
|
|
55
121
|
onChange={(e) => setFilter(e.target.value)}
|
|
56
122
|
placeholder="Filter"
|
|
57
|
-
className="h-
|
|
123
|
+
className="h-9 w-56 text-xs bg-background/80"
|
|
58
124
|
/>
|
|
59
125
|
<DeleteAllButton
|
|
60
126
|
label={`Delete all${profiles && profiles.length > 0 ? ` (${profiles.length})` : ""}`}
|
|
61
127
|
confirmMessage={`Delete ALL ${profiles?.length ?? 0} preferences? Preferences regenerate from fresh interactions, but this cannot be undone.`}
|
|
62
128
|
disabled={!profiles || profiles.length === 0}
|
|
63
129
|
onConfirm={async () => {
|
|
64
|
-
await reflexio.deleteAllProfiles(
|
|
130
|
+
await reflexio.deleteAllProfiles();
|
|
65
131
|
setProfiles([]);
|
|
66
132
|
}}
|
|
67
133
|
/>
|
|
@@ -72,7 +138,7 @@ export default function PreferencesPage() {
|
|
|
72
138
|
<div className="p-6">
|
|
73
139
|
{error && (
|
|
74
140
|
<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
|
|
141
|
+
{error}. Is reflexio running on the configured backend URL?
|
|
76
142
|
</div>
|
|
77
143
|
)}
|
|
78
144
|
|
|
@@ -85,42 +151,76 @@ export default function PreferencesPage() {
|
|
|
85
151
|
description="Keep using Claude with claude-smart enabled — preferences will appear here automatically as the extractor learns patterns from your interactions."
|
|
86
152
|
/>
|
|
87
153
|
) : (
|
|
88
|
-
<div className="
|
|
89
|
-
{filtered.map((p) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
102
|
-
{p.status}
|
|
154
|
+
<div className="overflow-hidden rounded-lg border border-border bg-card/92 shadow-sm">
|
|
155
|
+
{filtered.map((p) => {
|
|
156
|
+
const stat = statsByProfile.get(profileStatKey(p));
|
|
157
|
+
return (
|
|
158
|
+
<Link
|
|
159
|
+
key={p.profile_id}
|
|
160
|
+
href={`/preferences/project/${encodeURIComponent(p.profile_id)}`}
|
|
161
|
+
className="group block border-b border-border px-4 py-3.5 transition-colors last:border-b-0 hover:bg-accent/35"
|
|
162
|
+
>
|
|
163
|
+
<header className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
164
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
165
|
+
<Badge variant="outline" className="h-5 font-mono text-[10px]">
|
|
166
|
+
{truncateId(p.user_id, 32, 8)}
|
|
103
167
|
</Badge>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
168
|
+
{p.status && (
|
|
169
|
+
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
170
|
+
{p.status}
|
|
171
|
+
</Badge>
|
|
172
|
+
)}
|
|
173
|
+
<PreferenceApplicationStatBadge stat={stat} />
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
176
|
+
<span className="text-[11px] text-muted-foreground">
|
|
177
|
+
{formatRelative(p.last_modified_timestamp)}
|
|
178
|
+
</span>
|
|
179
|
+
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground/60 group-hover:text-foreground transition-colors" />
|
|
180
|
+
</div>
|
|
181
|
+
</header>
|
|
182
|
+
<p className="max-w-5xl text-sm leading-relaxed line-clamp-3">
|
|
183
|
+
{p.content}
|
|
117
184
|
</p>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
185
|
+
{p.source && (
|
|
186
|
+
<p className="text-[11px] text-muted-foreground mt-2 font-mono">
|
|
187
|
+
source: {p.source}
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
</Link>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
121
193
|
</div>
|
|
122
194
|
)}
|
|
123
195
|
</div>
|
|
124
196
|
</div>
|
|
125
197
|
);
|
|
126
198
|
}
|
|
199
|
+
|
|
200
|
+
function PreferenceApplicationStatBadge({
|
|
201
|
+
stat,
|
|
202
|
+
}: {
|
|
203
|
+
stat: PlaybookApplicationStat | undefined;
|
|
204
|
+
}) {
|
|
205
|
+
if (!stat || stat.applied_count === 0) {
|
|
206
|
+
return (
|
|
207
|
+
<Badge
|
|
208
|
+
variant="outline"
|
|
209
|
+
className="h-5 text-[10px] text-muted-foreground"
|
|
210
|
+
title="No citations recorded yet for this preference. It will count once an assistant reply cites it."
|
|
211
|
+
>
|
|
212
|
+
Never applied
|
|
213
|
+
</Badge>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const last = formatRelative(stat.last_applied_at);
|
|
217
|
+
return (
|
|
218
|
+
<Badge
|
|
219
|
+
variant="secondary"
|
|
220
|
+
className="h-5 text-[10px]"
|
|
221
|
+
title={`Last applied ${last}`}
|
|
222
|
+
>
|
|
223
|
+
Applied {stat.applied_count}×{stat.last_applied_at ? ` · ${last}` : ""}
|
|
224
|
+
</Badge>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../[id]/page";
|