@swarmclawai/swarmclaw 1.9.34 → 1.9.37
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 +50 -0
- package/electron-dist/main.js +218 -0
- package/package.json +5 -3
- package/src/app/api/openclaw/history/route.ts +11 -6
- package/src/app/api/preview-server/route.ts +20 -12
- package/src/app/api/search/route.test.ts +63 -0
- package/src/app/api/search/route.ts +3 -2
- package/src/app/api/settings/route.ts +5 -1
- package/src/app/api/settings/settings-route.test.ts +38 -0
- package/src/app/api/usage/live/route.ts +2 -2
- package/src/app/globals.css +158 -0
- package/src/app/layout.tsx +12 -9
- package/src/app/protocols/builder/[templateId]/page.tsx +5 -5
- package/src/cli/binary.test.js +4 -1
- package/src/components/layout/dashboard-shell.tsx +9 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +106 -15
- package/src/components/providers/theme-provider.tsx +16 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +5 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +4 -4
- package/src/features/protocols/builder/utils/builder-template-access.test.ts +30 -0
- package/src/features/protocols/builder/utils/builder-template-access.ts +5 -0
- package/src/lib/server/messages/message-repository.test.ts +122 -0
- package/src/lib/server/messages/message-repository.ts +67 -11
- package/src/lib/server/runtime/devserver-launch.ts +7 -4
- package/src/lib/theme-mode.ts +5 -0
- package/src/types/app-settings.ts +2 -0
- package/src/views/settings/section-theme.tsx +41 -1
package/src/app/globals.css
CHANGED
|
@@ -144,6 +144,164 @@
|
|
|
144
144
|
--command-header: rgba(0,0,0,0.3);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
.light {
|
|
148
|
+
--background: color-mix(in srgb, var(--neutral-tint) 10%, #fff);
|
|
149
|
+
--foreground: #1f2937;
|
|
150
|
+
--card: color-mix(in srgb, var(--neutral-tint) 8%, #fff);
|
|
151
|
+
--card-foreground: #1f2937;
|
|
152
|
+
--popover: color-mix(in srgb, var(--neutral-tint) 8%, #fff);
|
|
153
|
+
--popover-foreground: #1f2937;
|
|
154
|
+
--primary: #4f46e5;
|
|
155
|
+
--primary-foreground: #ffffff;
|
|
156
|
+
--secondary: color-mix(in srgb, var(--neutral-tint) 12%, #fff);
|
|
157
|
+
--secondary-foreground: #253047;
|
|
158
|
+
--muted: color-mix(in srgb, var(--neutral-tint) 10%, #fff);
|
|
159
|
+
--muted-foreground: #5f6b85;
|
|
160
|
+
--accent: #4f46e5;
|
|
161
|
+
--accent-foreground: #ffffff;
|
|
162
|
+
--destructive: #dc2626;
|
|
163
|
+
--border: rgba(31,41,55,0.12);
|
|
164
|
+
--input: rgba(31,41,55,0.10);
|
|
165
|
+
--ring: rgba(79,70,229,0.38);
|
|
166
|
+
--sidebar: color-mix(in srgb, var(--neutral-tint) 9%, #fff);
|
|
167
|
+
--sidebar-foreground: #1f2937;
|
|
168
|
+
--sidebar-primary: #4f46e5;
|
|
169
|
+
--sidebar-primary-foreground: #ffffff;
|
|
170
|
+
--sidebar-accent: rgba(79,70,229,0.09);
|
|
171
|
+
--sidebar-accent-foreground: #253047;
|
|
172
|
+
--sidebar-border: rgba(31,41,55,0.11);
|
|
173
|
+
--sidebar-ring: rgba(79,70,229,0.35);
|
|
174
|
+
|
|
175
|
+
--color-bg: color-mix(in srgb, var(--neutral-tint) 10%, #fff);
|
|
176
|
+
--color-raised: color-mix(in srgb, var(--neutral-tint) 8%, #fff);
|
|
177
|
+
--color-surface: color-mix(in srgb, var(--neutral-tint) 14%, #fff);
|
|
178
|
+
--color-surface-2: color-mix(in srgb, var(--neutral-tint) 20%, #fff);
|
|
179
|
+
--color-surface-3: color-mix(in srgb, var(--neutral-tint) 24%, #fff);
|
|
180
|
+
--color-border-hi: rgba(31,41,55,0.12);
|
|
181
|
+
--color-border-focus: rgba(79,70,229,0.45);
|
|
182
|
+
--color-text: #1f2937;
|
|
183
|
+
--color-text-2: #4b5568;
|
|
184
|
+
--color-text-3: #6b7280;
|
|
185
|
+
--color-accent-soft: rgba(79,70,229,0.09);
|
|
186
|
+
--color-accent-glow: rgba(79,70,229,0.16);
|
|
187
|
+
--color-accent-bright: #4f46e5;
|
|
188
|
+
--color-user-text: #fff;
|
|
189
|
+
--color-success: #059669;
|
|
190
|
+
--color-success-soft: rgba(5,150,105,0.10);
|
|
191
|
+
--color-danger: #dc2626;
|
|
192
|
+
--color-danger-soft: rgba(220,38,38,0.10);
|
|
193
|
+
--color-shereen: #db2777;
|
|
194
|
+
--color-user-bubble: #4f46e5;
|
|
195
|
+
--color-user-bubble-2: #6366f1;
|
|
196
|
+
--color-ai-bubble: #f4f6fb;
|
|
197
|
+
--color-glass: rgba(255,255,255,0.82);
|
|
198
|
+
--color-glass-border: rgba(31,41,55,0.10);
|
|
199
|
+
|
|
200
|
+
--status-idle-bg: rgba(31,41,55,0.05);
|
|
201
|
+
--status-idle-border: rgba(31,41,55,0.10);
|
|
202
|
+
--status-idle-fg: #5f6b85;
|
|
203
|
+
--status-running-bg: rgba(5,150,105,0.10);
|
|
204
|
+
--status-running-border: rgba(5,150,105,0.18);
|
|
205
|
+
--status-running-fg: #047857;
|
|
206
|
+
--status-error-bg: rgba(220,38,38,0.10);
|
|
207
|
+
--status-error-border: rgba(220,38,38,0.18);
|
|
208
|
+
--status-error-fg: #dc2626;
|
|
209
|
+
--status-connecting-bg: rgba(217,119,6,0.10);
|
|
210
|
+
--status-connecting-border: rgba(217,119,6,0.18);
|
|
211
|
+
--status-connecting-fg: #b45309;
|
|
212
|
+
--status-connected-bg: rgba(2,132,199,0.10);
|
|
213
|
+
--status-connected-border: rgba(2,132,199,0.18);
|
|
214
|
+
--status-connected-fg: #0369a1;
|
|
215
|
+
--status-approval-bg: rgba(234,88,12,0.10);
|
|
216
|
+
--status-approval-border: rgba(234,88,12,0.18);
|
|
217
|
+
--status-approval-fg: #c2410c;
|
|
218
|
+
|
|
219
|
+
--command-bg: #ffffff;
|
|
220
|
+
--command-border: rgba(31,41,55,0.12);
|
|
221
|
+
--command-header: rgba(31,41,55,0.04);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.light .bg-white\/\[0\.01\] { background-color: rgba(31,41,55,0.01) !important; }
|
|
225
|
+
.light .bg-white\/\[0\.02\] { background-color: rgba(31,41,55,0.025) !important; }
|
|
226
|
+
.light .bg-white\/\[0\.025\] { background-color: rgba(31,41,55,0.03) !important; }
|
|
227
|
+
.light .bg-white\/\[0\.03\] { background-color: rgba(31,41,55,0.035) !important; }
|
|
228
|
+
.light .bg-white\/\[0\.035\] { background-color: rgba(31,41,55,0.04) !important; }
|
|
229
|
+
.light .bg-white\/\[0\.04\] { background-color: rgba(31,41,55,0.045) !important; }
|
|
230
|
+
.light .bg-white\/\[0\.05\] { background-color: rgba(31,41,55,0.055) !important; }
|
|
231
|
+
.light .bg-white\/\[0\.06\] { background-color: rgba(31,41,55,0.065) !important; }
|
|
232
|
+
.light .bg-white\/\[0\.07\] { background-color: rgba(31,41,55,0.075) !important; }
|
|
233
|
+
.light .bg-white\/\[0\.08\] { background-color: rgba(31,41,55,0.085) !important; }
|
|
234
|
+
.light .bg-white\/\[0\.10\],
|
|
235
|
+
.light .bg-white\/\[0\.1\] { background-color: rgba(31,41,55,0.10) !important; }
|
|
236
|
+
.light .bg-white\/\[0\.12\] { background-color: rgba(31,41,55,0.12) !important; }
|
|
237
|
+
.light .bg-white\/\[0\.15\] { background-color: rgba(31,41,55,0.15) !important; }
|
|
238
|
+
.light .bg-white\/\[0\.16\] { background-color: rgba(31,41,55,0.16) !important; }
|
|
239
|
+
.light .bg-white\/\[0\.18\] { background-color: rgba(31,41,55,0.18) !important; }
|
|
240
|
+
.light .bg-white\/\[0\.2\],
|
|
241
|
+
.light .bg-white\/\[0\.20\] { background-color: rgba(31,41,55,0.20) !important; }
|
|
242
|
+
.light .bg-white\/\[0\.22\] { background-color: rgba(31,41,55,0.22) !important; }
|
|
243
|
+
.light .bg-white\/\[0\.45\] { background-color: rgba(31,41,55,0.45) !important; }
|
|
244
|
+
|
|
245
|
+
.light .border-white\/\[0\.03\] { border-color: rgba(31,41,55,0.07) !important; }
|
|
246
|
+
.light .border-white\/\[0\.04\] { border-color: rgba(31,41,55,0.08) !important; }
|
|
247
|
+
.light .border-white\/\[0\.05\] { border-color: rgba(31,41,55,0.09) !important; }
|
|
248
|
+
.light .border-white\/\[0\.06\] { border-color: rgba(31,41,55,0.10) !important; }
|
|
249
|
+
.light .border-white\/\[0\.07\] { border-color: rgba(31,41,55,0.11) !important; }
|
|
250
|
+
.light .border-white\/\[0\.08\] { border-color: rgba(31,41,55,0.12) !important; }
|
|
251
|
+
.light .border-white\/\[0\.10\],
|
|
252
|
+
.light .border-white\/\[0\.1\] { border-color: rgba(31,41,55,0.14) !important; }
|
|
253
|
+
.light .border-white\/\[0\.12\] { border-color: rgba(31,41,55,0.16) !important; }
|
|
254
|
+
.light .border-white\/\[0\.14\] { border-color: rgba(31,41,55,0.18) !important; }
|
|
255
|
+
.light .border-white\/\[0\.15\] { border-color: rgba(31,41,55,0.19) !important; }
|
|
256
|
+
.light .border-white\/\[0\.16\] { border-color: rgba(31,41,55,0.20) !important; }
|
|
257
|
+
.light .border-white\/\[0\.2\],
|
|
258
|
+
.light .border-white\/\[0\.20\] { border-color: rgba(31,41,55,0.24) !important; }
|
|
259
|
+
.light .border-white\/\[0\.25\] { border-color: rgba(31,41,55,0.28) !important; }
|
|
260
|
+
.light .border-white\/\[0\.4\],
|
|
261
|
+
.light .border-white\/\[0\.40\] { border-color: rgba(31,41,55,0.42) !important; }
|
|
262
|
+
.light .divide-white\/\[0\.04\] > :not(:last-child) { border-color: rgba(31,41,55,0.08) !important; }
|
|
263
|
+
.light .divide-white\/\[0\.05\] > :not(:last-child) { border-color: rgba(31,41,55,0.09) !important; }
|
|
264
|
+
.light .ring-white\/\[0\.08\] { --tw-ring-color: rgba(31,41,55,0.14) !important; }
|
|
265
|
+
|
|
266
|
+
html.light,
|
|
267
|
+
html.light body {
|
|
268
|
+
color-scheme: light;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
html.light body {
|
|
272
|
+
background: color-mix(in srgb, var(--neutral-tint) 8%, #fff);
|
|
273
|
+
color: #1f2937;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
html.light .bg-bg { background-color: color-mix(in srgb, var(--neutral-tint) 8%, #fff) !important; }
|
|
277
|
+
html.light .bg-raised { background-color: color-mix(in srgb, var(--neutral-tint) 10%, #fff) !important; }
|
|
278
|
+
html.light .bg-surface { background-color: color-mix(in srgb, var(--neutral-tint) 14%, #fff) !important; }
|
|
279
|
+
html.light .bg-surface-2 { background-color: color-mix(in srgb, var(--neutral-tint) 18%, #fff) !important; }
|
|
280
|
+
html.light .bg-glass { background-color: rgba(255,255,255,0.88) !important; }
|
|
281
|
+
html.light .text-text { color: #1f2937 !important; }
|
|
282
|
+
html.light .text-text-2 { color: #4b5568 !important; }
|
|
283
|
+
html.light .text-text-3 { color: #6b7280 !important; }
|
|
284
|
+
html.light .text-text-3\/40 { color: rgba(107,114,128,0.40) !important; }
|
|
285
|
+
html.light .text-text-3\/45 { color: rgba(107,114,128,0.45) !important; }
|
|
286
|
+
html.light .text-text-3\/50 { color: rgba(107,114,128,0.50) !important; }
|
|
287
|
+
html.light .text-text-3\/60 { color: rgba(107,114,128,0.60) !important; }
|
|
288
|
+
html.light .text-text-3\/70 { color: rgba(107,114,128,0.70) !important; }
|
|
289
|
+
html.light .text-text-3\/75 { color: rgba(107,114,128,0.75) !important; }
|
|
290
|
+
html.light .text-accent-bright { color: #4f46e5 !important; }
|
|
291
|
+
html.light .bg-accent-soft { background-color: rgba(79,70,229,0.09) !important; }
|
|
292
|
+
html.light .placeholder\:text-text-3\/40::placeholder { color: rgba(107,114,128,0.40) !important; }
|
|
293
|
+
|
|
294
|
+
html.light .bg-\[\#0a0a14\],
|
|
295
|
+
html.light .bg-\[\#0c0c13\],
|
|
296
|
+
html.light .bg-\[\#12121e\],
|
|
297
|
+
html.light .bg-\[\#13131e\],
|
|
298
|
+
html.light .bg-\[\#16162a\],
|
|
299
|
+
html.light .bg-\[\#171a2b\],
|
|
300
|
+
html.light .bg-\[\#1a1a2e\],
|
|
301
|
+
html.light .bg-\[\#1e1e38\] {
|
|
302
|
+
background-color: color-mix(in srgb, var(--neutral-tint) 14%, #fff) !important;
|
|
303
|
+
}
|
|
304
|
+
|
|
147
305
|
@layer base {
|
|
148
306
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
149
307
|
}
|
package/src/app/layout.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"
|
|
|
3
3
|
import { Toaster } from "@/components/ui/sonner"
|
|
4
4
|
import { DashboardShell } from "@/components/layout/dashboard-shell"
|
|
5
5
|
import { AppQueryProvider } from "@/components/providers/app-query-provider"
|
|
6
|
+
import { ThemeProvider } from "@/components/providers/theme-provider"
|
|
6
7
|
import "./globals.css"
|
|
7
8
|
|
|
8
9
|
export const metadata: Metadata = {
|
|
@@ -27,16 +28,18 @@ export default function RootLayout({
|
|
|
27
28
|
children: React.ReactNode
|
|
28
29
|
}>) {
|
|
29
30
|
return (
|
|
30
|
-
<html lang="en"
|
|
31
|
+
<html lang="en" suppressHydrationWarning>
|
|
31
32
|
<body className="antialiased" cz-shortcut-listen="true">
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
<ThemeProvider>
|
|
34
|
+
<AppQueryProvider>
|
|
35
|
+
<TooltipProvider>
|
|
36
|
+
<DashboardShell>
|
|
37
|
+
{children}
|
|
38
|
+
</DashboardShell>
|
|
39
|
+
<Toaster />
|
|
40
|
+
</TooltipProvider>
|
|
41
|
+
</AppQueryProvider>
|
|
42
|
+
</ThemeProvider>
|
|
40
43
|
</body>
|
|
41
44
|
</html>
|
|
42
45
|
)
|
|
@@ -43,7 +43,7 @@ export default function ProtocolBuilderPage() {
|
|
|
43
43
|
|
|
44
44
|
if (isLoading) {
|
|
45
45
|
return (
|
|
46
|
-
<div className="flex h-
|
|
46
|
+
<div className="flex h-full min-h-0 min-w-0 flex-1 items-center justify-center">
|
|
47
47
|
<div className="text-sm text-muted-foreground">Loading builder...</div>
|
|
48
48
|
</div>
|
|
49
49
|
)
|
|
@@ -52,7 +52,7 @@ export default function ProtocolBuilderPage() {
|
|
|
52
52
|
const template = templates?.find((t) => t.id === templateId)
|
|
53
53
|
if (!template) {
|
|
54
54
|
return (
|
|
55
|
-
<div className="flex h-
|
|
55
|
+
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-3">
|
|
56
56
|
<div className="text-sm text-muted-foreground">Template not found</div>
|
|
57
57
|
<button
|
|
58
58
|
onClick={() => router.push('/protocols')}
|
|
@@ -65,9 +65,9 @@ export default function ProtocolBuilderPage() {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
return (
|
|
68
|
-
<div className="flex h-full flex-col">
|
|
68
|
+
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
|
69
69
|
{/* Header */}
|
|
70
|
-
<div className="flex items-center justify-between border-b px-4 py-2">
|
|
70
|
+
<div className="flex shrink-0 items-center justify-between border-b px-4 py-2">
|
|
71
71
|
<div className="flex items-center gap-3">
|
|
72
72
|
<button
|
|
73
73
|
onClick={() => router.push('/protocols')}
|
|
@@ -85,7 +85,7 @@ export default function ProtocolBuilderPage() {
|
|
|
85
85
|
</div>
|
|
86
86
|
|
|
87
87
|
{/* Canvas */}
|
|
88
|
-
<div className="flex-1 p-3">
|
|
88
|
+
<div className="min-h-0 flex-1 p-3">
|
|
89
89
|
<ProtocolBuilderCanvas />
|
|
90
90
|
</div>
|
|
91
91
|
</div>
|
package/src/cli/binary.test.js
CHANGED
|
@@ -199,10 +199,13 @@ test('binary -v alias output matches package version', () => {
|
|
|
199
199
|
assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`)
|
|
200
200
|
})
|
|
201
201
|
|
|
202
|
-
test('package ships
|
|
202
|
+
test('package ships type declarations required by installed builds', () => {
|
|
203
203
|
assert.equal(PACKAGE_JSON.dependencies.dagre, '^0.8.5')
|
|
204
204
|
assert.equal(PACKAGE_JSON.dependencies['@types/dagre'], '^0.7.54')
|
|
205
205
|
assert.equal(PACKAGE_JSON.devDependencies?.['@types/dagre'], undefined)
|
|
206
|
+
assert.equal(PACKAGE_JSON.dependencies['mime-types'], '^3.0.2')
|
|
207
|
+
assert.equal(PACKAGE_JSON.dependencies['@types/mime-types'], '^2.1.4')
|
|
208
|
+
assert.equal(PACKAGE_JSON.devDependencies?.['@types/mime-types'], undefined)
|
|
206
209
|
})
|
|
207
210
|
|
|
208
211
|
test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation'
|
|
5
|
+
import { useTheme } from 'next-themes'
|
|
5
6
|
import { initAudioContext } from '@/lib/tts'
|
|
6
7
|
import { clearStoredAccessKey } from '@/lib/app/api-client'
|
|
7
8
|
import { safeStorageGet, safeStorageRemove, safeStorageSet } from '@/lib/app/safe-storage'
|
|
@@ -14,6 +15,7 @@ import { useWs } from '@/hooks/use-ws'
|
|
|
14
15
|
import { api } from '@/lib/app/api-client'
|
|
15
16
|
import { pathToView, useNavigate } from '@/lib/app/navigation'
|
|
16
17
|
import { shouldAutoOpenPanelSidebar } from '@/lib/app/view-constants'
|
|
18
|
+
import { normalizeThemeMode } from '@/lib/theme-mode'
|
|
17
19
|
|
|
18
20
|
import { FullScreenLoader } from '@/components/ui/full-screen-loader'
|
|
19
21
|
import { SidebarRail } from '@/components/layout/sidebar-rail'
|
|
@@ -32,6 +34,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
32
34
|
const router = useRouter()
|
|
33
35
|
const pathname = usePathname()
|
|
34
36
|
const navigateTo = useNavigate()
|
|
37
|
+
const { setTheme } = useTheme()
|
|
35
38
|
|
|
36
39
|
const {
|
|
37
40
|
hydrated,
|
|
@@ -152,6 +155,12 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
152
155
|
}
|
|
153
156
|
}, [appSettings.themeHue])
|
|
154
157
|
|
|
158
|
+
// Theme mode
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!appSettings.themeMode) return
|
|
161
|
+
setTheme(normalizeThemeMode(appSettings.themeMode))
|
|
162
|
+
}, [appSettings.themeMode, setTheme])
|
|
163
|
+
|
|
155
164
|
// View validity check
|
|
156
165
|
const isViewEnabled = useCallback((view: AppView) => {
|
|
157
166
|
if (view === 'webhooks') return extensions['http']?.enabled !== false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, type DragEvent } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, type DragEvent } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
ReactFlow,
|
|
6
6
|
Background,
|
|
@@ -12,16 +12,19 @@ import {
|
|
|
12
12
|
type NodeChange,
|
|
13
13
|
type EdgeChange,
|
|
14
14
|
type Node,
|
|
15
|
+
type Edge,
|
|
16
|
+
type ReactFlowInstance,
|
|
15
17
|
} from '@xyflow/react'
|
|
16
18
|
import '@xyflow/react/dist/style.css'
|
|
17
|
-
import { useProtocolBuilderStore, type BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
|
|
19
|
+
import { useProtocolBuilderStore, type BuilderEdgeData, type BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
|
|
18
20
|
import { getNodeTypeForKind } from '@/features/protocols/builder/utils/template-to-nodes'
|
|
21
|
+
import { isBuilderTemplateReadOnly } from '@/features/protocols/builder/utils/builder-template-access'
|
|
19
22
|
import { PhaseNode, BranchNode, LoopNode, ParallelNode, JoinNode, ForEachNode, SubflowNode, SwarmNode, CompleteNode } from './node-types'
|
|
20
23
|
import { DefaultEdge, BranchEdge, LoopEdge } from './edge-types'
|
|
21
24
|
import { NodePalette } from './node-palette'
|
|
22
25
|
import { NodeInspector } from './node-inspector'
|
|
23
26
|
import { ValidationPanel } from './validation-panel'
|
|
24
|
-
import type { ProtocolStepKind } from '@/types'
|
|
27
|
+
import type { ProtocolStepKind, ProtocolTemplate } from '@/types'
|
|
25
28
|
|
|
26
29
|
const nodeTypes = {
|
|
27
30
|
phase: PhaseNode,
|
|
@@ -41,6 +44,54 @@ const edgeTypes = {
|
|
|
41
44
|
loop: LoopEdge,
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
function BuiltInTemplatePanel({
|
|
48
|
+
template,
|
|
49
|
+
onSelectStep,
|
|
50
|
+
}: {
|
|
51
|
+
template: ProtocolTemplate | null
|
|
52
|
+
onSelectStep: (stepId: string) => void
|
|
53
|
+
}) {
|
|
54
|
+
const steps = template?.steps && template.steps.length > 0
|
|
55
|
+
? template.steps
|
|
56
|
+
: template?.defaultPhases ?? []
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex w-52 shrink-0 flex-col overflow-y-auto rounded-lg border bg-card p-3 shadow-sm">
|
|
60
|
+
<div className="mb-3">
|
|
61
|
+
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
62
|
+
Built-in template
|
|
63
|
+
</div>
|
|
64
|
+
<div className="mt-1 text-sm font-semibold text-foreground">{template?.name || 'Template'}</div>
|
|
65
|
+
{template?.description && (
|
|
66
|
+
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
67
|
+
{template.description}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="space-y-1">
|
|
73
|
+
{steps.map((step, index) => (
|
|
74
|
+
<button
|
|
75
|
+
key={step.id}
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => onSelectStep(step.id)}
|
|
78
|
+
className="w-full rounded-md border bg-background px-3 py-2 text-left"
|
|
79
|
+
title={step.kind.replace(/_/g, ' ')}
|
|
80
|
+
>
|
|
81
|
+
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
82
|
+
Step {index + 1}
|
|
83
|
+
</div>
|
|
84
|
+
<div className="mt-1 text-sm font-medium text-foreground">{step.label}</div>
|
|
85
|
+
<div className="mt-1 text-xs capitalize text-muted-foreground">
|
|
86
|
+
{step.kind.replace(/_/g, ' ')}
|
|
87
|
+
</div>
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
44
95
|
export function ProtocolBuilderCanvas() {
|
|
45
96
|
const nodes = useProtocolBuilderStore((s) => s.nodes)
|
|
46
97
|
const edges = useProtocolBuilderStore((s) => s.edges)
|
|
@@ -54,23 +105,44 @@ export function ProtocolBuilderCanvas() {
|
|
|
54
105
|
const isDirty = useProtocolBuilderStore((s) => s.isDirty)
|
|
55
106
|
const undo = useProtocolBuilderStore((s) => s.undo)
|
|
56
107
|
const redo = useProtocolBuilderStore((s) => s.redo)
|
|
108
|
+
const currentTemplate = useProtocolBuilderStore((s) => s.currentTemplate)
|
|
109
|
+
const flowRef = useRef<ReactFlowInstance<Node<BuilderNodeData>, Edge<BuilderEdgeData>> | null>(null)
|
|
110
|
+
|
|
111
|
+
const readOnly = isBuilderTemplateReadOnly(currentTemplate)
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (nodes.length === 0) return
|
|
115
|
+
const frame = window.requestAnimationFrame(() => {
|
|
116
|
+
void flowRef.current?.fitView({ padding: 0.24, duration: 160 })
|
|
117
|
+
})
|
|
118
|
+
return () => window.cancelAnimationFrame(frame)
|
|
119
|
+
}, [nodes.length, edges.length])
|
|
57
120
|
|
|
58
121
|
const onNodesChange = useCallback(
|
|
59
122
|
(changes: NodeChange<Node<BuilderNodeData>>[]) => {
|
|
60
|
-
|
|
123
|
+
const allowedChanges = readOnly
|
|
124
|
+
? changes.filter((change) => change.type === 'select')
|
|
125
|
+
: changes
|
|
126
|
+
if (allowedChanges.length === 0) return
|
|
127
|
+
setNodes(applyNodeChanges(allowedChanges, nodes), { markDirty: !readOnly })
|
|
61
128
|
},
|
|
62
|
-
[nodes, setNodes],
|
|
129
|
+
[nodes, readOnly, setNodes],
|
|
63
130
|
)
|
|
64
131
|
|
|
65
132
|
const onEdgesChange = useCallback(
|
|
66
133
|
(changes: EdgeChange[]) => {
|
|
67
|
-
|
|
134
|
+
const allowedChanges = readOnly
|
|
135
|
+
? changes.filter((change) => change.type === 'select')
|
|
136
|
+
: changes
|
|
137
|
+
if (allowedChanges.length === 0) return
|
|
138
|
+
setEdges(applyEdgeChanges(allowedChanges, edges) as typeof edges, { markDirty: !readOnly })
|
|
68
139
|
},
|
|
69
|
-
[edges, setEdges],
|
|
140
|
+
[edges, readOnly, setEdges],
|
|
70
141
|
)
|
|
71
142
|
|
|
72
143
|
const onConnect = useCallback(
|
|
73
144
|
(connection: Connection) => {
|
|
145
|
+
if (readOnly) return
|
|
74
146
|
pushUndo()
|
|
75
147
|
addEdge({
|
|
76
148
|
id: `${connection.source}--${connection.target}--${Date.now()}`,
|
|
@@ -82,7 +154,7 @@ export function ProtocolBuilderCanvas() {
|
|
|
82
154
|
data: { edgeType: 'default' },
|
|
83
155
|
})
|
|
84
156
|
},
|
|
85
|
-
[addEdge, pushUndo],
|
|
157
|
+
[addEdge, pushUndo, readOnly],
|
|
86
158
|
)
|
|
87
159
|
|
|
88
160
|
const onNodeClick = useCallback(
|
|
@@ -112,6 +184,7 @@ export function ProtocolBuilderCanvas() {
|
|
|
112
184
|
const onDrop = useCallback(
|
|
113
185
|
(e: DragEvent) => {
|
|
114
186
|
e.preventDefault()
|
|
187
|
+
if (readOnly) return
|
|
115
188
|
const kind = e.dataTransfer.getData('application/x-protocol-node-kind') as ProtocolStepKind
|
|
116
189
|
const label = e.dataTransfer.getData('application/x-protocol-node-label')
|
|
117
190
|
if (!kind) return
|
|
@@ -127,32 +200,43 @@ export function ProtocolBuilderCanvas() {
|
|
|
127
200
|
}
|
|
128
201
|
addNode(newNode)
|
|
129
202
|
},
|
|
130
|
-
[addNode, pushUndo],
|
|
203
|
+
[addNode, pushUndo, readOnly],
|
|
131
204
|
)
|
|
132
205
|
|
|
133
206
|
const onKeyDown = useCallback(
|
|
134
207
|
(e: React.KeyboardEvent) => {
|
|
208
|
+
if (readOnly) return
|
|
135
209
|
if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
|
|
136
210
|
e.preventDefault()
|
|
137
211
|
if (e.shiftKey) redo()
|
|
138
212
|
else undo()
|
|
139
213
|
}
|
|
140
214
|
},
|
|
141
|
-
[undo, redo],
|
|
215
|
+
[readOnly, undo, redo],
|
|
142
216
|
)
|
|
143
217
|
|
|
144
218
|
const memoizedNodeTypes = useMemo(() => nodeTypes, [])
|
|
145
219
|
const memoizedEdgeTypes = useMemo(() => edgeTypes, [])
|
|
146
220
|
|
|
147
221
|
return (
|
|
148
|
-
<div className="flex h-full w-full gap-3" onKeyDown={onKeyDown} tabIndex={0}>
|
|
149
|
-
<NodePalette />
|
|
150
|
-
<div className="relative flex-1 overflow-hidden rounded-lg border">
|
|
222
|
+
<div className="flex h-full min-h-0 w-full min-w-0 gap-3" onKeyDown={onKeyDown} tabIndex={0}>
|
|
223
|
+
{readOnly ? <BuiltInTemplatePanel template={currentTemplate} onSelectStep={selectNode} /> : <NodePalette />}
|
|
224
|
+
<div className="relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-lg border">
|
|
151
225
|
{isDirty && (
|
|
152
226
|
<div className="absolute left-1/2 top-2 z-10 -translate-x-1/2 rounded bg-amber-500/10 px-2 py-1 text-xs text-amber-500">
|
|
153
227
|
Unsaved changes
|
|
154
228
|
</div>
|
|
155
229
|
)}
|
|
230
|
+
{nodes.length === 0 && (
|
|
231
|
+
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-6">
|
|
232
|
+
<div className="pointer-events-auto max-w-md rounded-lg border bg-card/95 p-4 text-center shadow-sm">
|
|
233
|
+
<div className="text-sm font-semibold text-foreground">No visual steps</div>
|
|
234
|
+
<div className="mt-1 text-sm text-muted-foreground">
|
|
235
|
+
This template does not expose a protocol graph yet.
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
156
240
|
<ReactFlow
|
|
157
241
|
nodes={nodes}
|
|
158
242
|
edges={edges}
|
|
@@ -166,8 +250,15 @@ export function ProtocolBuilderCanvas() {
|
|
|
166
250
|
onPaneClick={onPaneClick}
|
|
167
251
|
onDragOver={onDragOver}
|
|
168
252
|
onDrop={onDrop}
|
|
253
|
+
onInit={(instance) => {
|
|
254
|
+
flowRef.current = instance
|
|
255
|
+
}}
|
|
169
256
|
fitView
|
|
170
|
-
|
|
257
|
+
fitViewOptions={{ padding: 0.24 }}
|
|
258
|
+
nodesDraggable={!readOnly}
|
|
259
|
+
nodesConnectable={!readOnly}
|
|
260
|
+
edgesReconnectable={!readOnly}
|
|
261
|
+
deleteKeyCode={readOnly ? null : 'Delete'}
|
|
171
262
|
defaultEdgeOptions={{ type: 'default', data: { edgeType: 'default' } }}
|
|
172
263
|
>
|
|
173
264
|
<Background />
|
|
@@ -175,7 +266,7 @@ export function ProtocolBuilderCanvas() {
|
|
|
175
266
|
<MiniMap />
|
|
176
267
|
</ReactFlow>
|
|
177
268
|
</div>
|
|
178
|
-
<div className="flex w-72 flex-col gap-3">
|
|
269
|
+
<div className="flex w-72 shrink-0 flex-col gap-3">
|
|
179
270
|
<NodeInspector />
|
|
180
271
|
<ValidationPanel />
|
|
181
272
|
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemeProvider } from 'next-themes'
|
|
4
|
+
|
|
5
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<NextThemeProvider
|
|
8
|
+
attribute="class"
|
|
9
|
+
defaultTheme="dark"
|
|
10
|
+
enableSystem
|
|
11
|
+
disableTransitionOnChange
|
|
12
|
+
>
|
|
13
|
+
{children}
|
|
14
|
+
</NextThemeProvider>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
import { useProtocolBuilderStore } from '../protocol-builder-store'
|
|
3
3
|
import { useUpsertProtocolTemplateMutation, type ProtocolTemplatePayload } from '@/features/protocols/queries'
|
|
4
4
|
import { nodesToTemplate } from '../utils/nodes-to-template'
|
|
5
|
+
import { isBuilderTemplateReadOnly } from '../utils/builder-template-access'
|
|
5
6
|
|
|
6
7
|
export function useTemplateSync(autoSaveDelayMs = 2000) {
|
|
7
8
|
const nodes = useProtocolBuilderStore((s) => s.nodes)
|
|
@@ -15,6 +16,10 @@ export function useTemplateSync(autoSaveDelayMs = 2000) {
|
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
17
18
|
if (!isDirty || !currentTemplate || validationErrors.length > 0) return
|
|
19
|
+
if (isBuilderTemplateReadOnly(currentTemplate)) {
|
|
20
|
+
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
20
25
|
|
|
@@ -67,8 +67,8 @@ export interface ProtocolBuilderState {
|
|
|
67
67
|
redoStack: UndoSnapshot[]
|
|
68
68
|
activeRunId: string | null
|
|
69
69
|
|
|
70
|
-
setNodes: (nodes: BuilderNode[]) => void
|
|
71
|
-
setEdges: (edges: BuilderEdge[]) => void
|
|
70
|
+
setNodes: (nodes: BuilderNode[], options?: { markDirty?: boolean }) => void
|
|
71
|
+
setEdges: (edges: BuilderEdge[], options?: { markDirty?: boolean }) => void
|
|
72
72
|
selectNode: (nodeId: string | null) => void
|
|
73
73
|
selectEdge: (edgeId: string | null) => void
|
|
74
74
|
updateNodeData: (nodeId: string, data: Partial<BuilderNodeData>) => void
|
|
@@ -110,8 +110,8 @@ export const useProtocolBuilderStore = create<ProtocolBuilderState>()(
|
|
|
110
110
|
(set, get) => ({
|
|
111
111
|
...initialState,
|
|
112
112
|
|
|
113
|
-
setNodes: (nodes) => set({ nodes, isDirty: true }),
|
|
114
|
-
setEdges: (edges) => set({ edges, isDirty: true }),
|
|
113
|
+
setNodes: (nodes, options) => set({ nodes, isDirty: options?.markDirty ?? true }),
|
|
114
|
+
setEdges: (edges, options) => set({ edges, isDirty: options?.markDirty ?? true }),
|
|
115
115
|
|
|
116
116
|
selectNode: (nodeId) =>
|
|
117
117
|
set({ selectedNodeId: nodeId, selectedEdgeId: null }),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { ProtocolTemplate } from '@/types'
|
|
4
|
+
import { isBuilderTemplateReadOnly } from './builder-template-access'
|
|
5
|
+
|
|
6
|
+
function makeTemplate(overrides: Partial<ProtocolTemplate> = {}): ProtocolTemplate {
|
|
7
|
+
return {
|
|
8
|
+
id: 'template-1',
|
|
9
|
+
name: 'Template One',
|
|
10
|
+
description: 'A template',
|
|
11
|
+
builtIn: false,
|
|
12
|
+
defaultPhases: [],
|
|
13
|
+
...overrides,
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('isBuilderTemplateReadOnly', () => {
|
|
18
|
+
it('marks built-in templates as read-only', () => {
|
|
19
|
+
assert.equal(isBuilderTemplateReadOnly(makeTemplate({ builtIn: true })), true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('keeps custom templates editable', () => {
|
|
23
|
+
assert.equal(isBuilderTemplateReadOnly(makeTemplate({ builtIn: false })), false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('treats an unloaded template as non-editable only after one is present', () => {
|
|
27
|
+
assert.equal(isBuilderTemplateReadOnly(null), false)
|
|
28
|
+
assert.equal(isBuilderTemplateReadOnly(undefined), false)
|
|
29
|
+
})
|
|
30
|
+
})
|