claudeship 0.2.11 → 0.2.14

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.
Files changed (119) hide show
  1. package/apps/server/dist/app.module.js +8 -0
  2. package/apps/server/dist/app.module.js.map +1 -1
  3. package/apps/server/dist/chat/prompts/fullstack-express-prompt.d.ts +1 -1
  4. package/apps/server/dist/chat/prompts/fullstack-express-prompt.js +110 -1
  5. package/apps/server/dist/chat/prompts/fullstack-express-prompt.js.map +1 -1
  6. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.d.ts +1 -1
  7. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js +110 -1
  8. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js.map +1 -1
  9. package/apps/server/dist/chat/prompts/web-system-prompt.d.ts +1 -1
  10. package/apps/server/dist/chat/prompts/web-system-prompt.js +157 -0
  11. package/apps/server/dist/chat/prompts/web-system-prompt.js.map +1 -1
  12. package/apps/server/dist/checkpoint/checkpoint.controller.d.ts +19 -0
  13. package/apps/server/dist/checkpoint/checkpoint.controller.js +93 -0
  14. package/apps/server/dist/checkpoint/checkpoint.controller.js.map +1 -0
  15. package/apps/server/dist/checkpoint/checkpoint.module.d.ts +2 -0
  16. package/apps/server/dist/checkpoint/checkpoint.module.js +25 -0
  17. package/apps/server/dist/checkpoint/checkpoint.module.js.map +1 -0
  18. package/apps/server/dist/checkpoint/checkpoint.service.d.ts +41 -0
  19. package/apps/server/dist/checkpoint/checkpoint.service.js +261 -0
  20. package/apps/server/dist/checkpoint/checkpoint.service.js.map +1 -0
  21. package/apps/server/dist/database/database.controller.d.ts +23 -0
  22. package/apps/server/dist/database/database.controller.js +109 -0
  23. package/apps/server/dist/database/database.controller.js.map +1 -0
  24. package/apps/server/dist/database/database.module.d.ts +2 -0
  25. package/apps/server/dist/database/database.module.js +25 -0
  26. package/apps/server/dist/database/database.module.js.map +1 -0
  27. package/apps/server/dist/database/database.service.d.ts +32 -0
  28. package/apps/server/dist/database/database.service.js +238 -0
  29. package/apps/server/dist/database/database.service.js.map +1 -0
  30. package/apps/server/dist/preview/preview.controller.d.ts +5 -0
  31. package/apps/server/dist/preview/preview.controller.js +41 -0
  32. package/apps/server/dist/preview/preview.controller.js.map +1 -1
  33. package/apps/server/dist/preview/preview.service.d.ts +20 -0
  34. package/apps/server/dist/preview/preview.service.js +51 -2
  35. package/apps/server/dist/preview/preview.service.js.map +1 -1
  36. package/apps/server/dist/project/project.controller.d.ts +10 -1
  37. package/apps/server/dist/project/project.controller.js +57 -0
  38. package/apps/server/dist/project/project.controller.js.map +1 -1
  39. package/apps/server/dist/project/project.service.d.ts +15 -0
  40. package/apps/server/dist/project/project.service.js +111 -0
  41. package/apps/server/dist/project/project.service.js.map +1 -1
  42. package/apps/server/dist/project-context/project-context.controller.d.ts +42 -0
  43. package/apps/server/dist/project-context/project-context.controller.js +127 -0
  44. package/apps/server/dist/project-context/project-context.controller.js.map +1 -0
  45. package/apps/server/dist/project-context/project-context.module.d.ts +2 -0
  46. package/apps/server/dist/project-context/project-context.module.js +25 -0
  47. package/apps/server/dist/project-context/project-context.module.js.map +1 -0
  48. package/apps/server/dist/project-context/project-context.service.d.ts +36 -0
  49. package/apps/server/dist/project-context/project-context.service.js +260 -0
  50. package/apps/server/dist/project-context/project-context.service.js.map +1 -0
  51. package/apps/server/dist/testing/testing.controller.d.ts +24 -0
  52. package/apps/server/dist/testing/testing.controller.js +126 -0
  53. package/apps/server/dist/testing/testing.controller.js.map +1 -0
  54. package/apps/server/dist/testing/testing.module.d.ts +2 -0
  55. package/apps/server/dist/testing/testing.module.js +26 -0
  56. package/apps/server/dist/testing/testing.module.js.map +1 -0
  57. package/apps/server/dist/testing/testing.service.d.ts +62 -0
  58. package/apps/server/dist/testing/testing.service.js +269 -0
  59. package/apps/server/dist/testing/testing.service.js.map +1 -0
  60. package/apps/server/dist/tsconfig.tsbuildinfo +1 -1
  61. package/apps/server/package.json +1 -1
  62. package/apps/web/.next/BUILD_ID +1 -1
  63. package/apps/web/.next/app-build-manifest.json +7 -7
  64. package/apps/web/.next/app-path-routes-manifest.json +2 -2
  65. package/apps/web/.next/build-manifest.json +2 -2
  66. package/apps/web/.next/cache/.previewinfo +1 -1
  67. package/apps/web/.next/cache/.rscinfo +1 -1
  68. package/apps/web/.next/cache/.tsbuildinfo +1 -1
  69. package/apps/web/.next/cache/config.json +3 -3
  70. package/apps/web/.next/cache/eslint/.cache_j3uhuz +1 -1
  71. package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
  72. package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
  73. package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
  74. package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
  75. package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
  76. package/apps/web/.next/prerender-manifest.json +13 -13
  77. package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  78. package/apps/web/.next/server/app/_not-found.html +1 -1
  79. package/apps/web/.next/server/app/_not-found.rsc +2 -2
  80. package/apps/web/.next/server/app/index.html +1 -1
  81. package/apps/web/.next/server/app/index.rsc +3 -3
  82. package/apps/web/.next/server/app/page.js +2 -2
  83. package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/apps/web/.next/server/app/project/[id]/page.js +2 -2
  85. package/apps/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  86. package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  87. package/apps/web/.next/server/app/settings.html +1 -1
  88. package/apps/web/.next/server/app/settings.rsc +3 -3
  89. package/apps/web/.next/server/app-paths-manifest.json +2 -2
  90. package/apps/web/.next/server/chunks/392.js +1 -1
  91. package/apps/web/.next/server/pages/404.html +1 -1
  92. package/apps/web/.next/server/pages/500.html +1 -1
  93. package/apps/web/.next/server/server-reference-manifest.json +1 -1
  94. package/apps/web/.next/static/chunks/712-11fca77fb30a2a06.js +1 -0
  95. package/apps/web/.next/static/chunks/app/page-0db1c152fbd48359.js +1 -0
  96. package/apps/web/.next/static/chunks/app/project/[id]/page-7c44ae18c8984726.js +1 -0
  97. package/apps/web/.next/static/chunks/app/settings/page-d1318c2fd58729a5.js +1 -0
  98. package/apps/web/.next/static/css/d0f1b036f222bc16.css +3 -0
  99. package/apps/web/.next/trace +18 -17
  100. package/apps/web/node_modules/.bin/eslint +2 -2
  101. package/apps/web/package.json +2 -1
  102. package/apps/web/src/components/checkpoint/CheckpointPanel.tsx +384 -0
  103. package/apps/web/src/components/database/DatabasePanel.tsx +405 -0
  104. package/apps/web/src/components/env/EnvPanel.tsx +356 -0
  105. package/apps/web/src/components/preview/ConsoleViewer.tsx +270 -0
  106. package/apps/web/src/components/preview/ErrorOverlay.tsx +189 -0
  107. package/apps/web/src/components/preview/PreviewPanel.tsx +148 -6
  108. package/apps/web/src/components/testing/TestRunner.tsx +481 -0
  109. package/apps/web/src/components/ui/tabs.tsx +55 -0
  110. package/apps/web/src/components/visual-editor/VisualEditor.tsx +382 -0
  111. package/apps/web/src/lib/api.ts +5 -2
  112. package/package.json +1 -1
  113. package/apps/web/.next/static/chunks/298-6f3d6b321c288cd3.js +0 -1
  114. package/apps/web/.next/static/chunks/app/page-3d093f7f480a8599.js +0 -1
  115. package/apps/web/.next/static/chunks/app/project/[id]/page-e5cda6f9050b0a52.js +0 -1
  116. package/apps/web/.next/static/chunks/app/settings/page-92d28565c3d8c755.js +0 -1
  117. package/apps/web/.next/static/css/8f946046a2047594.css +0 -3
  118. /package/apps/web/.next/static/{zw4FcukMOho6_dzgpEdNW → tV_Qc76rupeogXvWEMw6p}/_buildManifest.js +0 -0
  119. /package/apps/web/.next/static/{zw4FcukMOho6_dzgpEdNW → tV_Qc76rupeogXvWEMw6p}/_ssgManifest.js +0 -0
@@ -0,0 +1,481 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import {
5
+ Play,
6
+ Square,
7
+ Plus,
8
+ Trash2,
9
+ CheckCircle,
10
+ XCircle,
11
+ Clock,
12
+ RefreshCw,
13
+ AlertCircle,
14
+ FlaskConical,
15
+ } from "lucide-react";
16
+ import { Button } from "@/components/ui/button";
17
+ import { Input } from "@/components/ui/input";
18
+ import { api } from "@/lib/api";
19
+
20
+ interface TestStep {
21
+ action: "navigate" | "click" | "fill" | "waitFor" | "screenshot" | "assert";
22
+ selector?: string;
23
+ url?: string;
24
+ value?: string;
25
+ name?: string;
26
+ }
27
+
28
+ interface TestScenario {
29
+ id: string;
30
+ name: string;
31
+ steps: TestStep[];
32
+ createdAt: number;
33
+ }
34
+
35
+ interface StepResult {
36
+ step: TestStep;
37
+ status: "passed" | "failed" | "skipped";
38
+ duration: number;
39
+ error?: string;
40
+ }
41
+
42
+ interface TestResult {
43
+ scenarioId: string;
44
+ scenarioName: string;
45
+ status: "passed" | "failed" | "running";
46
+ startTime: number;
47
+ endTime?: number;
48
+ steps: StepResult[];
49
+ error?: string;
50
+ }
51
+
52
+ interface TestRunnerProps {
53
+ projectId: string;
54
+ }
55
+
56
+ const ACTION_LABELS: Record<TestStep["action"], string> = {
57
+ navigate: "Navigate to URL",
58
+ click: "Click element",
59
+ fill: "Fill input",
60
+ waitFor: "Wait for element",
61
+ screenshot: "Take screenshot",
62
+ assert: "Assert visible",
63
+ };
64
+
65
+ export function TestRunner({ projectId }: TestRunnerProps) {
66
+ const [scenarios, setScenarios] = useState<TestScenario[]>([]);
67
+ const [selectedScenario, setSelectedScenario] = useState<TestScenario | null>(null);
68
+ const [isLoading, setIsLoading] = useState(false);
69
+ const [isRunning, setIsRunning] = useState(false);
70
+ const [error, setError] = useState<string | null>(null);
71
+ const [lastResult, setLastResult] = useState<TestResult | null>(null);
72
+ const [isEditing, setIsEditing] = useState(false);
73
+ const [editName, setEditName] = useState("");
74
+ const [editSteps, setEditSteps] = useState<TestStep[]>([]);
75
+
76
+ // Fetch scenarios
77
+ const fetchScenarios = useCallback(async () => {
78
+ setIsLoading(true);
79
+ setError(null);
80
+ try {
81
+ const result = await api.get<TestScenario[]>(
82
+ `/projects/${projectId}/testing/scenarios`
83
+ );
84
+ setScenarios(result);
85
+ } catch (e) {
86
+ setError(e instanceof Error ? e.message : "Failed to load scenarios");
87
+ } finally {
88
+ setIsLoading(false);
89
+ }
90
+ }, [projectId]);
91
+
92
+ useEffect(() => {
93
+ fetchScenarios();
94
+ }, [fetchScenarios]);
95
+
96
+ const handleSelectScenario = (scenario: TestScenario) => {
97
+ setSelectedScenario(scenario);
98
+ setLastResult(null);
99
+ setIsEditing(false);
100
+ };
101
+
102
+ const handleNewScenario = () => {
103
+ setSelectedScenario(null);
104
+ setIsEditing(true);
105
+ setEditName("New Test");
106
+ setEditSteps([{ action: "navigate", url: "/" }]);
107
+ };
108
+
109
+ const handleEditScenario = () => {
110
+ if (!selectedScenario) return;
111
+ setIsEditing(true);
112
+ setEditName(selectedScenario.name);
113
+ setEditSteps([...selectedScenario.steps]);
114
+ };
115
+
116
+ const handleSaveScenario = async () => {
117
+ if (!editName.trim() || editSteps.length === 0) {
118
+ setError("Name and at least one step are required");
119
+ return;
120
+ }
121
+
122
+ setIsLoading(true);
123
+ setError(null);
124
+ try {
125
+ if (selectedScenario) {
126
+ await api.put(
127
+ `/projects/${projectId}/testing/scenarios/${selectedScenario.id}`,
128
+ { name: editName, steps: editSteps }
129
+ );
130
+ } else {
131
+ await api.post(`/projects/${projectId}/testing/scenarios`, {
132
+ name: editName,
133
+ steps: editSteps,
134
+ });
135
+ }
136
+ await fetchScenarios();
137
+ setIsEditing(false);
138
+ } catch (e) {
139
+ setError(e instanceof Error ? e.message : "Failed to save scenario");
140
+ } finally {
141
+ setIsLoading(false);
142
+ }
143
+ };
144
+
145
+ const handleDeleteScenario = async () => {
146
+ if (!selectedScenario) return;
147
+ if (!confirm("Delete this test scenario?")) return;
148
+
149
+ try {
150
+ await api.delete(
151
+ `/projects/${projectId}/testing/scenarios/${selectedScenario.id}`
152
+ );
153
+ setSelectedScenario(null);
154
+ await fetchScenarios();
155
+ } catch (e) {
156
+ setError(e instanceof Error ? e.message : "Failed to delete scenario");
157
+ }
158
+ };
159
+
160
+ const handleRunTest = async () => {
161
+ if (!selectedScenario) return;
162
+
163
+ setIsRunning(true);
164
+ setError(null);
165
+ setLastResult(null);
166
+ try {
167
+ const result = await api.post<TestResult>(
168
+ `/projects/${projectId}/testing/scenarios/${selectedScenario.id}/run`
169
+ );
170
+ setLastResult(result);
171
+ } catch (e) {
172
+ setError(e instanceof Error ? e.message : "Test execution failed");
173
+ } finally {
174
+ setIsRunning(false);
175
+ }
176
+ };
177
+
178
+ const handleAddStep = () => {
179
+ setEditSteps([...editSteps, { action: "click", selector: "" }]);
180
+ };
181
+
182
+ const handleUpdateStep = (index: number, updates: Partial<TestStep>) => {
183
+ const newSteps = [...editSteps];
184
+ newSteps[index] = { ...newSteps[index], ...updates };
185
+ setEditSteps(newSteps);
186
+ };
187
+
188
+ const handleRemoveStep = (index: number) => {
189
+ setEditSteps(editSteps.filter((_, i) => i !== index));
190
+ };
191
+
192
+ if (isLoading && scenarios.length === 0) {
193
+ return (
194
+ <div className="flex h-full items-center justify-center text-muted-foreground">
195
+ <RefreshCw className="h-5 w-5 animate-spin mr-2" />
196
+ Loading...
197
+ </div>
198
+ );
199
+ }
200
+
201
+ return (
202
+ <div className="flex h-full flex-col">
203
+ {/* Header */}
204
+ <div className="flex items-center justify-between border-b px-4 py-3">
205
+ <h3 className="font-medium flex items-center gap-2">
206
+ <FlaskConical className="h-4 w-4" />
207
+ Browser Testing
208
+ </h3>
209
+ <Button variant="outline" size="sm" onClick={handleNewScenario}>
210
+ <Plus className="h-4 w-4 mr-1" />
211
+ New Test
212
+ </Button>
213
+ </div>
214
+
215
+ {/* Error */}
216
+ {error && (
217
+ <div className="flex items-center gap-2 px-4 py-2 bg-destructive/10 text-destructive text-sm">
218
+ <AlertCircle className="h-4 w-4" />
219
+ {error}
220
+ </div>
221
+ )}
222
+
223
+ <div className="flex flex-1 overflow-hidden">
224
+ {/* Scenario List */}
225
+ <div className="w-56 border-r overflow-auto">
226
+ <div className="p-2 space-y-1">
227
+ {scenarios.map((scenario) => (
228
+ <button
229
+ key={scenario.id}
230
+ onClick={() => handleSelectScenario(scenario)}
231
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors ${
232
+ selectedScenario?.id === scenario.id
233
+ ? "bg-primary/10 text-primary"
234
+ : "hover:bg-muted"
235
+ }`}
236
+ >
237
+ <FlaskConical className="h-4 w-4 shrink-0" />
238
+ <span className="truncate">{scenario.name}</span>
239
+ </button>
240
+ ))}
241
+ {scenarios.length === 0 && (
242
+ <p className="text-sm text-muted-foreground px-3 py-2">
243
+ No test scenarios yet
244
+ </p>
245
+ )}
246
+ </div>
247
+ </div>
248
+
249
+ {/* Main Content */}
250
+ <div className="flex-1 overflow-auto">
251
+ {isEditing ? (
252
+ // Edit Mode
253
+ <div className="p-4 space-y-4">
254
+ <Input
255
+ value={editName}
256
+ onChange={(e) => setEditName(e.target.value)}
257
+ placeholder="Test name"
258
+ className="font-medium"
259
+ />
260
+
261
+ <div className="space-y-3">
262
+ <div className="text-sm font-medium text-muted-foreground">Steps</div>
263
+ {editSteps.map((step, i) => (
264
+ <div key={i} className="flex gap-2 items-start p-3 border rounded-lg">
265
+ <select
266
+ value={step.action}
267
+ onChange={(e) =>
268
+ handleUpdateStep(i, { action: e.target.value as TestStep["action"] })
269
+ }
270
+ className="h-9 px-2 border rounded-md bg-background text-sm"
271
+ >
272
+ {Object.entries(ACTION_LABELS).map(([action, label]) => (
273
+ <option key={action} value={action}>
274
+ {label}
275
+ </option>
276
+ ))}
277
+ </select>
278
+
279
+ {step.action === "navigate" && (
280
+ <Input
281
+ value={step.url || ""}
282
+ onChange={(e) => handleUpdateStep(i, { url: e.target.value })}
283
+ placeholder="/path or full URL"
284
+ className="flex-1"
285
+ />
286
+ )}
287
+
288
+ {(step.action === "click" ||
289
+ step.action === "waitFor" ||
290
+ step.action === "assert") && (
291
+ <Input
292
+ value={step.selector || ""}
293
+ onChange={(e) => handleUpdateStep(i, { selector: e.target.value })}
294
+ placeholder="CSS selector"
295
+ className="flex-1 font-mono text-sm"
296
+ />
297
+ )}
298
+
299
+ {step.action === "fill" && (
300
+ <>
301
+ <Input
302
+ value={step.selector || ""}
303
+ onChange={(e) => handleUpdateStep(i, { selector: e.target.value })}
304
+ placeholder="CSS selector"
305
+ className="flex-1 font-mono text-sm"
306
+ />
307
+ <Input
308
+ value={step.value || ""}
309
+ onChange={(e) => handleUpdateStep(i, { value: e.target.value })}
310
+ placeholder="Value"
311
+ className="flex-1"
312
+ />
313
+ </>
314
+ )}
315
+
316
+ {step.action === "screenshot" && (
317
+ <Input
318
+ value={step.name || ""}
319
+ onChange={(e) => handleUpdateStep(i, { name: e.target.value })}
320
+ placeholder="Screenshot name"
321
+ className="flex-1"
322
+ />
323
+ )}
324
+
325
+ <Button
326
+ variant="ghost"
327
+ size="sm"
328
+ onClick={() => handleRemoveStep(i)}
329
+ className="h-9 w-9 p-0 text-muted-foreground hover:text-destructive"
330
+ >
331
+ <Trash2 className="h-4 w-4" />
332
+ </Button>
333
+ </div>
334
+ ))}
335
+
336
+ <Button variant="outline" size="sm" onClick={handleAddStep}>
337
+ <Plus className="h-4 w-4 mr-1" />
338
+ Add Step
339
+ </Button>
340
+ </div>
341
+
342
+ <div className="flex gap-2 pt-4 border-t">
343
+ <Button onClick={handleSaveScenario} disabled={isLoading}>
344
+ Save
345
+ </Button>
346
+ <Button variant="outline" onClick={() => setIsEditing(false)}>
347
+ Cancel
348
+ </Button>
349
+ </div>
350
+ </div>
351
+ ) : selectedScenario ? (
352
+ // View Mode
353
+ <div className="p-4 space-y-4">
354
+ <div className="flex items-center justify-between">
355
+ <h4 className="text-lg font-medium">{selectedScenario.name}</h4>
356
+ <div className="flex gap-2">
357
+ <Button
358
+ variant="outline"
359
+ size="sm"
360
+ onClick={handleEditScenario}
361
+ >
362
+ Edit
363
+ </Button>
364
+ <Button
365
+ variant="outline"
366
+ size="sm"
367
+ onClick={handleDeleteScenario}
368
+ className="text-destructive"
369
+ >
370
+ <Trash2 className="h-4 w-4" />
371
+ </Button>
372
+ <Button
373
+ onClick={handleRunTest}
374
+ disabled={isRunning}
375
+ size="sm"
376
+ >
377
+ {isRunning ? (
378
+ <RefreshCw className="h-4 w-4 animate-spin mr-1" />
379
+ ) : (
380
+ <Play className="h-4 w-4 mr-1" />
381
+ )}
382
+ Run Test
383
+ </Button>
384
+ </div>
385
+ </div>
386
+
387
+ {/* Steps */}
388
+ <div className="space-y-2">
389
+ <div className="text-sm font-medium text-muted-foreground">
390
+ {selectedScenario.steps.length} steps
391
+ </div>
392
+ {selectedScenario.steps.map((step, i) => {
393
+ const stepResult = lastResult?.steps[i];
394
+ return (
395
+ <div
396
+ key={i}
397
+ className={`flex items-center gap-3 p-3 border rounded-lg ${
398
+ stepResult?.status === "passed"
399
+ ? "border-green-500/50 bg-green-500/5"
400
+ : stepResult?.status === "failed"
401
+ ? "border-red-500/50 bg-red-500/5"
402
+ : ""
403
+ }`}
404
+ >
405
+ {stepResult ? (
406
+ stepResult.status === "passed" ? (
407
+ <CheckCircle className="h-4 w-4 text-green-500" />
408
+ ) : stepResult.status === "failed" ? (
409
+ <XCircle className="h-4 w-4 text-red-500" />
410
+ ) : (
411
+ <Clock className="h-4 w-4 text-muted-foreground" />
412
+ )
413
+ ) : (
414
+ <div className="h-4 w-4 rounded-full border-2" />
415
+ )}
416
+ <div className="flex-1">
417
+ <span className="font-medium">{ACTION_LABELS[step.action]}</span>
418
+ {step.selector && (
419
+ <code className="ml-2 text-xs bg-muted px-1.5 py-0.5 rounded">
420
+ {step.selector}
421
+ </code>
422
+ )}
423
+ {step.url && (
424
+ <code className="ml-2 text-xs bg-muted px-1.5 py-0.5 rounded">
425
+ {step.url}
426
+ </code>
427
+ )}
428
+ {step.value && (
429
+ <span className="ml-2 text-sm text-muted-foreground">
430
+ = &quot;{step.value}&quot;
431
+ </span>
432
+ )}
433
+ </div>
434
+ </div>
435
+ );
436
+ })}
437
+ </div>
438
+
439
+ {/* Result */}
440
+ {lastResult && (
441
+ <div
442
+ className={`p-4 rounded-lg ${
443
+ lastResult.status === "passed"
444
+ ? "bg-green-500/10 text-green-700 dark:text-green-400"
445
+ : "bg-red-500/10 text-red-700 dark:text-red-400"
446
+ }`}
447
+ >
448
+ <div className="flex items-center gap-2">
449
+ {lastResult.status === "passed" ? (
450
+ <CheckCircle className="h-5 w-5" />
451
+ ) : (
452
+ <XCircle className="h-5 w-5" />
453
+ )}
454
+ <span className="font-medium">
455
+ Test {lastResult.status === "passed" ? "Passed" : "Failed"}
456
+ </span>
457
+ </div>
458
+ {lastResult.error && (
459
+ <p className="mt-2 text-sm">{lastResult.error}</p>
460
+ )}
461
+ {lastResult.endTime && lastResult.startTime && (
462
+ <p className="mt-1 text-sm opacity-70">
463
+ Duration: {lastResult.endTime - lastResult.startTime}ms
464
+ </p>
465
+ )}
466
+ </div>
467
+ )}
468
+ </div>
469
+ ) : (
470
+ <div className="flex h-full items-center justify-center text-muted-foreground">
471
+ <div className="text-center">
472
+ <FlaskConical className="h-8 w-8 mx-auto mb-2 opacity-50" />
473
+ <p>Select a test or create a new one</p>
474
+ </div>
475
+ </div>
476
+ )}
477
+ </div>
478
+ </div>
479
+ </div>
480
+ );
481
+ }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Tabs = TabsPrimitive.Root
9
+
10
+ const TabsList = React.forwardRef<
11
+ React.ElementRef<typeof TabsPrimitive.List>,
12
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
13
+ >(({ className, ...props }, ref) => (
14
+ <TabsPrimitive.List
15
+ ref={ref}
16
+ className={cn(
17
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ TabsList.displayName = TabsPrimitive.List.displayName
24
+
25
+ const TabsTrigger = React.forwardRef<
26
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
28
+ >(({ className, ...props }, ref) => (
29
+ <TabsPrimitive.Trigger
30
+ ref={ref}
31
+ className={cn(
32
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ ))
38
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39
+
40
+ const TabsContent = React.forwardRef<
41
+ React.ElementRef<typeof TabsPrimitive.Content>,
42
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
43
+ >(({ className, ...props }, ref) => (
44
+ <TabsPrimitive.Content
45
+ ref={ref}
46
+ className={cn(
47
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ TabsContent.displayName = TabsPrimitive.Content.displayName
54
+
55
+ export { Tabs, TabsList, TabsTrigger, TabsContent }