claudeship 0.2.12 → 0.2.15
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 +18 -0
- package/apps/server/dist/app.module.js +10 -0
- package/apps/server/dist/app.module.js.map +1 -1
- package/apps/server/dist/chat/prompts/fullstack-express-prompt.d.ts +1 -1
- package/apps/server/dist/chat/prompts/fullstack-express-prompt.js +109 -1
- package/apps/server/dist/chat/prompts/fullstack-express-prompt.js.map +1 -1
- package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.d.ts +1 -1
- package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js +109 -1
- package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js.map +1 -1
- package/apps/server/dist/chat/prompts/web-system-prompt.d.ts +1 -1
- package/apps/server/dist/chat/prompts/web-system-prompt.js +156 -0
- package/apps/server/dist/chat/prompts/web-system-prompt.js.map +1 -1
- package/apps/server/dist/checkpoint/checkpoint.controller.d.ts +19 -0
- package/apps/server/dist/checkpoint/checkpoint.controller.js +93 -0
- package/apps/server/dist/checkpoint/checkpoint.controller.js.map +1 -0
- package/apps/server/dist/checkpoint/checkpoint.module.d.ts +2 -0
- package/apps/server/dist/checkpoint/checkpoint.module.js +25 -0
- package/apps/server/dist/checkpoint/checkpoint.module.js.map +1 -0
- package/apps/server/dist/checkpoint/checkpoint.service.d.ts +41 -0
- package/apps/server/dist/checkpoint/checkpoint.service.js +261 -0
- package/apps/server/dist/checkpoint/checkpoint.service.js.map +1 -0
- package/apps/server/dist/database/database.controller.d.ts +23 -0
- package/apps/server/dist/database/database.controller.js +109 -0
- package/apps/server/dist/database/database.controller.js.map +1 -0
- package/apps/server/dist/database/database.module.d.ts +2 -0
- package/apps/server/dist/database/database.module.js +25 -0
- package/apps/server/dist/database/database.module.js.map +1 -0
- package/apps/server/dist/database/database.service.d.ts +32 -0
- package/apps/server/dist/database/database.service.js +238 -0
- package/apps/server/dist/database/database.service.js.map +1 -0
- package/apps/server/dist/env/env.controller.d.ts +14 -0
- package/apps/server/dist/env/env.controller.js +84 -0
- package/apps/server/dist/env/env.controller.js.map +1 -0
- package/apps/server/dist/env/env.module.d.ts +2 -0
- package/apps/server/dist/env/env.module.js +25 -0
- package/apps/server/dist/env/env.module.js.map +1 -0
- package/apps/server/dist/env/env.service.d.ts +21 -0
- package/apps/server/dist/env/env.service.js +194 -0
- package/apps/server/dist/env/env.service.js.map +1 -0
- package/apps/server/dist/preview/preview.controller.d.ts +5 -0
- package/apps/server/dist/preview/preview.controller.js +41 -0
- package/apps/server/dist/preview/preview.controller.js.map +1 -1
- package/apps/server/dist/preview/preview.service.d.ts +20 -0
- package/apps/server/dist/preview/preview.service.js +51 -2
- package/apps/server/dist/preview/preview.service.js.map +1 -1
- package/apps/server/dist/project/project.controller.d.ts +10 -1
- package/apps/server/dist/project/project.controller.js +57 -0
- package/apps/server/dist/project/project.controller.js.map +1 -1
- package/apps/server/dist/project/project.service.d.ts +15 -0
- package/apps/server/dist/project/project.service.js +111 -0
- package/apps/server/dist/project/project.service.js.map +1 -1
- package/apps/server/dist/project-context/project-context.controller.d.ts +42 -0
- package/apps/server/dist/project-context/project-context.controller.js +127 -0
- package/apps/server/dist/project-context/project-context.controller.js.map +1 -0
- package/apps/server/dist/project-context/project-context.module.d.ts +2 -0
- package/apps/server/dist/project-context/project-context.module.js +25 -0
- package/apps/server/dist/project-context/project-context.module.js.map +1 -0
- package/apps/server/dist/project-context/project-context.service.d.ts +36 -0
- package/apps/server/dist/project-context/project-context.service.js +260 -0
- package/apps/server/dist/project-context/project-context.service.js.map +1 -0
- package/apps/server/dist/testing/testing.controller.d.ts +24 -0
- package/apps/server/dist/testing/testing.controller.js +126 -0
- package/apps/server/dist/testing/testing.controller.js.map +1 -0
- package/apps/server/dist/testing/testing.module.d.ts +2 -0
- package/apps/server/dist/testing/testing.module.js +26 -0
- package/apps/server/dist/testing/testing.module.js.map +1 -0
- package/apps/server/dist/testing/testing.service.d.ts +62 -0
- package/apps/server/dist/testing/testing.service.js +269 -0
- package/apps/server/dist/testing/testing.service.js.map +1 -0
- package/apps/server/dist/tsconfig.tsbuildinfo +1 -1
- package/apps/server/package.json +1 -1
- package/apps/web/.next/BUILD_ID +1 -1
- package/apps/web/.next/app-build-manifest.json +5 -5
- package/apps/web/.next/build-manifest.json +2 -2
- package/apps/web/.next/cache/.previewinfo +1 -1
- package/apps/web/.next/cache/.rscinfo +1 -1
- package/apps/web/.next/cache/.tsbuildinfo +1 -1
- package/apps/web/.next/cache/config.json +3 -3
- package/apps/web/.next/cache/eslint/.cache_j3uhuz +1 -1
- package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
- package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
- package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
- package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
- package/apps/web/.next/prerender-manifest.json +10 -10
- package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/_not-found.html +1 -1
- package/apps/web/.next/server/app/_not-found.rsc +2 -2
- package/apps/web/.next/server/app/index.html +1 -1
- package/apps/web/.next/server/app/index.rsc +3 -3
- package/apps/web/.next/server/app/page.js +2 -2
- package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/project/[id]/page.js +2 -2
- package/apps/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/settings.html +1 -1
- package/apps/web/.next/server/app/settings.rsc +3 -3
- package/apps/web/.next/server/chunks/392.js +1 -1
- package/apps/web/.next/server/pages/404.html +1 -1
- package/apps/web/.next/server/pages/500.html +1 -1
- package/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/apps/web/.next/static/chunks/574-1fe2bcd6cfb41646.js +1 -0
- package/apps/web/.next/static/chunks/app/page-f19cfa58541ca83d.js +1 -0
- package/apps/web/.next/static/chunks/app/project/[id]/page-dffaa1d02f012216.js +1 -0
- package/apps/web/.next/static/chunks/app/settings/page-d1318c2fd58729a5.js +1 -0
- package/apps/web/.next/static/css/0a24552d9794f8c8.css +3 -0
- package/apps/web/.next/trace +18 -17
- package/apps/web/node_modules/.bin/eslint +2 -2
- package/apps/web/package.json +2 -1
- package/apps/web/src/components/checkpoint/CheckpointPanel.tsx +384 -0
- package/apps/web/src/components/database/DatabasePanel.tsx +405 -0
- package/apps/web/src/components/env/EnvPanel.tsx +356 -0
- package/apps/web/src/components/preview/ConsoleViewer.tsx +270 -0
- package/apps/web/src/components/preview/ErrorOverlay.tsx +189 -0
- package/apps/web/src/components/preview/PreviewPanel.tsx +148 -6
- package/apps/web/src/components/testing/TestRunner.tsx +481 -0
- package/apps/web/src/components/ui/tabs.tsx +55 -0
- package/apps/web/src/components/visual-editor/VisualEditor.tsx +382 -0
- package/apps/web/src/components/workspace/WorkspaceLayout.tsx +66 -4
- package/apps/web/src/lib/api.ts +5 -2
- package/package.json +1 -1
- package/apps/web/.next/static/chunks/298-6f3d6b321c288cd3.js +0 -1
- package/apps/web/.next/static/chunks/app/page-3d093f7f480a8599.js +0 -1
- package/apps/web/.next/static/chunks/app/project/[id]/page-e5cda6f9050b0a52.js +0 -1
- package/apps/web/.next/static/chunks/app/settings/page-92d28565c3d8c755.js +0 -1
- package/apps/web/.next/static/css/8f946046a2047594.css +0 -3
- /package/apps/web/.next/static/{aXT20mSdxaem1-z8VH2F1 → mkY_TTl_ho_ehDKiX10AN}/_buildManifest.js +0 -0
- /package/apps/web/.next/static/{aXT20mSdxaem1-z8VH2F1 → mkY_TTl_ho_ehDKiX10AN}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Eye,
|
|
6
|
+
EyeOff,
|
|
7
|
+
Plus,
|
|
8
|
+
Trash2,
|
|
9
|
+
Save,
|
|
10
|
+
FileText,
|
|
11
|
+
RefreshCw,
|
|
12
|
+
AlertCircle,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
import { api } from "@/lib/api";
|
|
17
|
+
|
|
18
|
+
interface EnvVariable {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EnvFile {
|
|
24
|
+
path: string;
|
|
25
|
+
variables: EnvVariable[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EnvPanelProps {
|
|
29
|
+
projectId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Keys that should be masked by default
|
|
33
|
+
const SENSITIVE_KEYS = [
|
|
34
|
+
"SECRET",
|
|
35
|
+
"PASSWORD",
|
|
36
|
+
"KEY",
|
|
37
|
+
"TOKEN",
|
|
38
|
+
"API_KEY",
|
|
39
|
+
"PRIVATE",
|
|
40
|
+
"CREDENTIAL",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function isSensitiveKey(key: string): boolean {
|
|
44
|
+
const upperKey = key.toUpperCase();
|
|
45
|
+
return SENSITIVE_KEYS.some((sensitive) => upperKey.includes(sensitive));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function EnvPanel({ projectId }: EnvPanelProps) {
|
|
49
|
+
const [envFiles, setEnvFiles] = useState<EnvFile[]>([]);
|
|
50
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
51
|
+
const [variables, setVariables] = useState<EnvVariable[]>([]);
|
|
52
|
+
const [visibleValues, setVisibleValues] = useState<Set<string>>(new Set());
|
|
53
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
54
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
55
|
+
const [error, setError] = useState<string | null>(null);
|
|
56
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
57
|
+
|
|
58
|
+
// Fetch env files list
|
|
59
|
+
const fetchEnvFiles = useCallback(async () => {
|
|
60
|
+
setIsLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
try {
|
|
63
|
+
const files = await api.get<EnvFile[]>(`/projects/${projectId}/env`);
|
|
64
|
+
setEnvFiles(files);
|
|
65
|
+
if (files.length > 0 && !selectedFile) {
|
|
66
|
+
setSelectedFile(files[0].path);
|
|
67
|
+
setVariables(files[0].variables);
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
setError("Failed to load environment files");
|
|
71
|
+
} finally {
|
|
72
|
+
setIsLoading(false);
|
|
73
|
+
}
|
|
74
|
+
}, [projectId, selectedFile]);
|
|
75
|
+
|
|
76
|
+
// Fetch specific env file
|
|
77
|
+
const fetchEnvFile = useCallback(async (filePath: string) => {
|
|
78
|
+
setIsLoading(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
try {
|
|
81
|
+
const file = await api.get<EnvFile>(
|
|
82
|
+
`/projects/${projectId}/env/file?path=${encodeURIComponent(filePath)}`
|
|
83
|
+
);
|
|
84
|
+
setVariables(file.variables);
|
|
85
|
+
setHasChanges(false);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
setError(`Failed to load ${filePath}`);
|
|
88
|
+
} finally {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
}, [projectId]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
fetchEnvFiles();
|
|
95
|
+
}, [fetchEnvFiles]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (selectedFile) {
|
|
99
|
+
fetchEnvFile(selectedFile);
|
|
100
|
+
}
|
|
101
|
+
}, [selectedFile, fetchEnvFile]);
|
|
102
|
+
|
|
103
|
+
const handleSelectFile = (filePath: string) => {
|
|
104
|
+
if (hasChanges) {
|
|
105
|
+
if (!confirm("You have unsaved changes. Discard them?")) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
setSelectedFile(filePath);
|
|
110
|
+
setVisibleValues(new Set());
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleAddVariable = () => {
|
|
114
|
+
setVariables([...variables, { key: "", value: "" }]);
|
|
115
|
+
setHasChanges(true);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleUpdateVariable = (index: number, field: "key" | "value", value: string) => {
|
|
119
|
+
const newVariables = [...variables];
|
|
120
|
+
newVariables[index] = { ...newVariables[index], [field]: value };
|
|
121
|
+
setVariables(newVariables);
|
|
122
|
+
setHasChanges(true);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleDeleteVariable = (index: number) => {
|
|
126
|
+
const newVariables = variables.filter((_, i) => i !== index);
|
|
127
|
+
setVariables(newVariables);
|
|
128
|
+
setHasChanges(true);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleToggleVisibility = (key: string) => {
|
|
132
|
+
const newVisible = new Set(visibleValues);
|
|
133
|
+
if (newVisible.has(key)) {
|
|
134
|
+
newVisible.delete(key);
|
|
135
|
+
} else {
|
|
136
|
+
newVisible.add(key);
|
|
137
|
+
}
|
|
138
|
+
setVisibleValues(newVisible);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleSave = async () => {
|
|
142
|
+
if (!selectedFile) return;
|
|
143
|
+
|
|
144
|
+
// Validate: no empty keys
|
|
145
|
+
const emptyKeys = variables.filter((v) => !v.key.trim());
|
|
146
|
+
if (emptyKeys.length > 0) {
|
|
147
|
+
setError("Variable names cannot be empty");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate: no duplicate keys
|
|
152
|
+
const keys = variables.map((v) => v.key.trim());
|
|
153
|
+
const duplicates = keys.filter((key, i) => keys.indexOf(key) !== i);
|
|
154
|
+
if (duplicates.length > 0) {
|
|
155
|
+
setError(`Duplicate variable name: ${duplicates[0]}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setIsSaving(true);
|
|
160
|
+
setError(null);
|
|
161
|
+
try {
|
|
162
|
+
await api.put(
|
|
163
|
+
`/projects/${projectId}/env/file?path=${encodeURIComponent(selectedFile)}`,
|
|
164
|
+
{ variables }
|
|
165
|
+
);
|
|
166
|
+
setHasChanges(false);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
setError("Failed to save environment variables");
|
|
169
|
+
} finally {
|
|
170
|
+
setIsSaving(false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleCreateEnvFile = async () => {
|
|
175
|
+
const fileName = prompt("Enter .env file name (e.g., .env.local):");
|
|
176
|
+
if (!fileName) return;
|
|
177
|
+
|
|
178
|
+
if (!fileName.startsWith(".env")) {
|
|
179
|
+
setError("File name must start with .env");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await api.post(
|
|
185
|
+
`/projects/${projectId}/env/file?path=${encodeURIComponent(fileName)}`,
|
|
186
|
+
{ variables: [] }
|
|
187
|
+
);
|
|
188
|
+
await fetchEnvFiles();
|
|
189
|
+
setSelectedFile(fileName);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
setError("Failed to create environment file");
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (isLoading && envFiles.length === 0) {
|
|
196
|
+
return (
|
|
197
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
198
|
+
<RefreshCw className="h-5 w-5 animate-spin mr-2" />
|
|
199
|
+
Loading...
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="flex h-full flex-col">
|
|
206
|
+
{/* Header */}
|
|
207
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
208
|
+
<h3 className="font-medium">Environment Variables</h3>
|
|
209
|
+
<div className="flex items-center gap-2">
|
|
210
|
+
<Button
|
|
211
|
+
variant="outline"
|
|
212
|
+
size="sm"
|
|
213
|
+
onClick={handleCreateEnvFile}
|
|
214
|
+
className="h-8"
|
|
215
|
+
>
|
|
216
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
217
|
+
New File
|
|
218
|
+
</Button>
|
|
219
|
+
<Button
|
|
220
|
+
variant="default"
|
|
221
|
+
size="sm"
|
|
222
|
+
onClick={handleSave}
|
|
223
|
+
disabled={!hasChanges || isSaving}
|
|
224
|
+
className="h-8"
|
|
225
|
+
>
|
|
226
|
+
{isSaving ? (
|
|
227
|
+
<RefreshCw className="h-4 w-4 animate-spin mr-1" />
|
|
228
|
+
) : (
|
|
229
|
+
<Save className="h-4 w-4 mr-1" />
|
|
230
|
+
)}
|
|
231
|
+
Save
|
|
232
|
+
</Button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Error */}
|
|
237
|
+
{error && (
|
|
238
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-destructive/10 text-destructive text-sm">
|
|
239
|
+
<AlertCircle className="h-4 w-4" />
|
|
240
|
+
{error}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
<div className="flex flex-1 overflow-hidden">
|
|
245
|
+
{/* File List Sidebar */}
|
|
246
|
+
<div className="w-48 border-r overflow-auto">
|
|
247
|
+
<div className="p-2 space-y-1">
|
|
248
|
+
{envFiles.map((file) => (
|
|
249
|
+
<button
|
|
250
|
+
key={file.path}
|
|
251
|
+
onClick={() => handleSelectFile(file.path)}
|
|
252
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors ${
|
|
253
|
+
selectedFile === file.path
|
|
254
|
+
? "bg-primary/10 text-primary"
|
|
255
|
+
: "hover:bg-muted"
|
|
256
|
+
}`}
|
|
257
|
+
>
|
|
258
|
+
<FileText className="h-4 w-4 shrink-0" />
|
|
259
|
+
<span className="truncate">{file.path}</span>
|
|
260
|
+
</button>
|
|
261
|
+
))}
|
|
262
|
+
{envFiles.length === 0 && (
|
|
263
|
+
<p className="text-sm text-muted-foreground px-3 py-2">
|
|
264
|
+
No .env files found
|
|
265
|
+
</p>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Variables Editor */}
|
|
271
|
+
<div className="flex-1 overflow-auto">
|
|
272
|
+
{selectedFile ? (
|
|
273
|
+
<div className="p-4 space-y-3">
|
|
274
|
+
{/* Table Header */}
|
|
275
|
+
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 px-2 text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
276
|
+
<div>Key</div>
|
|
277
|
+
<div>Value</div>
|
|
278
|
+
<div className="w-16"></div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Variables */}
|
|
282
|
+
{variables.map((variable, index) => (
|
|
283
|
+
<div
|
|
284
|
+
key={index}
|
|
285
|
+
className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center"
|
|
286
|
+
>
|
|
287
|
+
<Input
|
|
288
|
+
value={variable.key}
|
|
289
|
+
onChange={(e) => handleUpdateVariable(index, "key", e.target.value)}
|
|
290
|
+
placeholder="VARIABLE_NAME"
|
|
291
|
+
className="font-mono text-sm"
|
|
292
|
+
/>
|
|
293
|
+
<div className="relative">
|
|
294
|
+
<Input
|
|
295
|
+
type={
|
|
296
|
+
isSensitiveKey(variable.key) && !visibleValues.has(variable.key)
|
|
297
|
+
? "password"
|
|
298
|
+
: "text"
|
|
299
|
+
}
|
|
300
|
+
value={variable.value}
|
|
301
|
+
onChange={(e) => handleUpdateVariable(index, "value", e.target.value)}
|
|
302
|
+
placeholder="value"
|
|
303
|
+
className="font-mono text-sm pr-10"
|
|
304
|
+
/>
|
|
305
|
+
{isSensitiveKey(variable.key) && (
|
|
306
|
+
<button
|
|
307
|
+
onClick={() => handleToggleVisibility(variable.key)}
|
|
308
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
309
|
+
>
|
|
310
|
+
{visibleValues.has(variable.key) ? (
|
|
311
|
+
<EyeOff className="h-4 w-4" />
|
|
312
|
+
) : (
|
|
313
|
+
<Eye className="h-4 w-4" />
|
|
314
|
+
)}
|
|
315
|
+
</button>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
<Button
|
|
319
|
+
variant="ghost"
|
|
320
|
+
size="sm"
|
|
321
|
+
onClick={() => handleDeleteVariable(index)}
|
|
322
|
+
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
|
|
323
|
+
>
|
|
324
|
+
<Trash2 className="h-4 w-4" />
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
327
|
+
))}
|
|
328
|
+
|
|
329
|
+
{/* Add Variable Button */}
|
|
330
|
+
<Button
|
|
331
|
+
variant="outline"
|
|
332
|
+
size="sm"
|
|
333
|
+
onClick={handleAddVariable}
|
|
334
|
+
className="w-full mt-4"
|
|
335
|
+
>
|
|
336
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
337
|
+
Add Variable
|
|
338
|
+
</Button>
|
|
339
|
+
|
|
340
|
+
{/* Tip */}
|
|
341
|
+
{variables.some((v) => v.key.startsWith("NEXT_PUBLIC_")) && (
|
|
342
|
+
<p className="text-xs text-muted-foreground mt-4">
|
|
343
|
+
Variables starting with NEXT_PUBLIC_ will be exposed to the browser.
|
|
344
|
+
</p>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
) : (
|
|
348
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
349
|
+
<p>Select a file or create a new one</p>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
4
|
+
import { Trash2, Download, Filter, AlertCircle, Terminal } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:14000";
|
|
8
|
+
|
|
9
|
+
interface LogEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
level: "stdout" | "stderr";
|
|
13
|
+
source: "frontend" | "backend";
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ConsoleViewerProps {
|
|
18
|
+
projectId: string;
|
|
19
|
+
isRunning: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type FilterType = "all" | "frontend" | "backend" | "error";
|
|
23
|
+
|
|
24
|
+
export function ConsoleViewer({ projectId, isRunning }: ConsoleViewerProps) {
|
|
25
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
26
|
+
const [filter, setFilter] = useState<FilterType>("all");
|
|
27
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
28
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
30
|
+
|
|
31
|
+
// Fetch initial logs
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isRunning) {
|
|
34
|
+
setLogs([]);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fetchLogs = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${API_BASE}/projects/${projectId}/preview/logs`);
|
|
41
|
+
if (response.ok) {
|
|
42
|
+
const initialLogs = await response.json();
|
|
43
|
+
setLogs(initialLogs);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("Failed to fetch logs:", error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
fetchLogs();
|
|
51
|
+
}, [projectId, isRunning]);
|
|
52
|
+
|
|
53
|
+
// Subscribe to log stream
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!isRunning) {
|
|
56
|
+
if (eventSourceRef.current) {
|
|
57
|
+
eventSourceRef.current.close();
|
|
58
|
+
eventSourceRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const eventSource = new EventSource(
|
|
64
|
+
`${API_BASE}/projects/${projectId}/preview/logs/stream`
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
eventSource.onmessage = (event) => {
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.parse(event.data);
|
|
70
|
+
if (data.type === "log" && data.entry) {
|
|
71
|
+
setLogs((prev) => {
|
|
72
|
+
const newLogs = [...prev, data.entry];
|
|
73
|
+
// Keep max 1000 logs
|
|
74
|
+
if (newLogs.length > 1000) {
|
|
75
|
+
return newLogs.slice(-1000);
|
|
76
|
+
}
|
|
77
|
+
return newLogs;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore parse errors
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
eventSource.onerror = () => {
|
|
86
|
+
eventSource.close();
|
|
87
|
+
eventSourceRef.current = null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
eventSourceRef.current = eventSource;
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
eventSource.close();
|
|
94
|
+
eventSourceRef.current = null;
|
|
95
|
+
};
|
|
96
|
+
}, [projectId, isRunning]);
|
|
97
|
+
|
|
98
|
+
// Auto-scroll to bottom
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (autoScroll && containerRef.current) {
|
|
101
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
102
|
+
}
|
|
103
|
+
}, [logs, autoScroll]);
|
|
104
|
+
|
|
105
|
+
const handleScroll = useCallback(() => {
|
|
106
|
+
if (!containerRef.current) return;
|
|
107
|
+
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
|
108
|
+
// Enable auto-scroll if scrolled near bottom
|
|
109
|
+
setAutoScroll(scrollHeight - scrollTop - clientHeight < 50);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const handleClear = async () => {
|
|
113
|
+
try {
|
|
114
|
+
await fetch(`${API_BASE}/projects/${projectId}/preview/logs`, {
|
|
115
|
+
method: "DELETE",
|
|
116
|
+
});
|
|
117
|
+
setLogs([]);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("Failed to clear logs:", error);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleDownload = () => {
|
|
124
|
+
const content = filteredLogs
|
|
125
|
+
.map((log) => {
|
|
126
|
+
const time = new Date(log.timestamp).toISOString();
|
|
127
|
+
return `[${time}] [${log.source}] [${log.level}] ${log.message}`;
|
|
128
|
+
})
|
|
129
|
+
.join("\n");
|
|
130
|
+
|
|
131
|
+
const blob = new Blob([content], { type: "text/plain" });
|
|
132
|
+
const url = URL.createObjectURL(blob);
|
|
133
|
+
const a = document.createElement("a");
|
|
134
|
+
a.href = url;
|
|
135
|
+
a.download = `console-${projectId}-${Date.now()}.log`;
|
|
136
|
+
a.click();
|
|
137
|
+
URL.revokeObjectURL(url);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const filteredLogs = logs.filter((log) => {
|
|
141
|
+
switch (filter) {
|
|
142
|
+
case "frontend":
|
|
143
|
+
return log.source === "frontend";
|
|
144
|
+
case "backend":
|
|
145
|
+
return log.source === "backend";
|
|
146
|
+
case "error":
|
|
147
|
+
return log.level === "stderr";
|
|
148
|
+
default:
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const formatTime = (timestamp: number) => {
|
|
154
|
+
return new Date(timestamp).toLocaleTimeString();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (!isRunning) {
|
|
158
|
+
return (
|
|
159
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
160
|
+
<div className="text-center">
|
|
161
|
+
<Terminal className="mx-auto h-8 w-8 mb-2 opacity-50" />
|
|
162
|
+
<p className="text-sm">Start the preview to see console output</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="flex h-full flex-col">
|
|
170
|
+
{/* Toolbar */}
|
|
171
|
+
<div className="flex h-10 items-center justify-between border-b px-2">
|
|
172
|
+
<div className="flex items-center gap-1">
|
|
173
|
+
<Button
|
|
174
|
+
variant={filter === "all" ? "secondary" : "ghost"}
|
|
175
|
+
size="sm"
|
|
176
|
+
onClick={() => setFilter("all")}
|
|
177
|
+
className="h-7 px-2 text-xs"
|
|
178
|
+
>
|
|
179
|
+
All
|
|
180
|
+
</Button>
|
|
181
|
+
<Button
|
|
182
|
+
variant={filter === "frontend" ? "secondary" : "ghost"}
|
|
183
|
+
size="sm"
|
|
184
|
+
onClick={() => setFilter("frontend")}
|
|
185
|
+
className="h-7 px-2 text-xs"
|
|
186
|
+
>
|
|
187
|
+
Frontend
|
|
188
|
+
</Button>
|
|
189
|
+
<Button
|
|
190
|
+
variant={filter === "backend" ? "secondary" : "ghost"}
|
|
191
|
+
size="sm"
|
|
192
|
+
onClick={() => setFilter("backend")}
|
|
193
|
+
className="h-7 px-2 text-xs"
|
|
194
|
+
>
|
|
195
|
+
Backend
|
|
196
|
+
</Button>
|
|
197
|
+
<Button
|
|
198
|
+
variant={filter === "error" ? "secondary" : "ghost"}
|
|
199
|
+
size="sm"
|
|
200
|
+
onClick={() => setFilter("error")}
|
|
201
|
+
className="h-7 px-2 text-xs text-red-500"
|
|
202
|
+
>
|
|
203
|
+
<AlertCircle className="h-3 w-3 mr-1" />
|
|
204
|
+
Errors
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="flex items-center gap-1">
|
|
208
|
+
<Button
|
|
209
|
+
variant="ghost"
|
|
210
|
+
size="sm"
|
|
211
|
+
onClick={handleDownload}
|
|
212
|
+
className="h-7 w-7 p-0"
|
|
213
|
+
title="Download logs"
|
|
214
|
+
>
|
|
215
|
+
<Download className="h-4 w-4" />
|
|
216
|
+
</Button>
|
|
217
|
+
<Button
|
|
218
|
+
variant="ghost"
|
|
219
|
+
size="sm"
|
|
220
|
+
onClick={handleClear}
|
|
221
|
+
className="h-7 w-7 p-0"
|
|
222
|
+
title="Clear logs"
|
|
223
|
+
>
|
|
224
|
+
<Trash2 className="h-4 w-4" />
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Log Output */}
|
|
230
|
+
<div
|
|
231
|
+
ref={containerRef}
|
|
232
|
+
onScroll={handleScroll}
|
|
233
|
+
className="flex-1 overflow-auto bg-zinc-950 p-2 font-mono text-xs"
|
|
234
|
+
>
|
|
235
|
+
{filteredLogs.length === 0 ? (
|
|
236
|
+
<div className="flex h-full items-center justify-center text-zinc-500">
|
|
237
|
+
<span>No logs yet...</span>
|
|
238
|
+
</div>
|
|
239
|
+
) : (
|
|
240
|
+
<div className="space-y-0.5">
|
|
241
|
+
{filteredLogs.map((log) => (
|
|
242
|
+
<div
|
|
243
|
+
key={log.id}
|
|
244
|
+
className={`flex gap-2 ${
|
|
245
|
+
log.level === "stderr" ? "text-red-400" : "text-zinc-300"
|
|
246
|
+
}`}
|
|
247
|
+
>
|
|
248
|
+
<span className="text-zinc-600 shrink-0">
|
|
249
|
+
{formatTime(log.timestamp)}
|
|
250
|
+
</span>
|
|
251
|
+
<span
|
|
252
|
+
className={`shrink-0 px-1 rounded text-[10px] ${
|
|
253
|
+
log.source === "frontend"
|
|
254
|
+
? "bg-blue-500/20 text-blue-400"
|
|
255
|
+
: "bg-green-500/20 text-green-400"
|
|
256
|
+
}`}
|
|
257
|
+
>
|
|
258
|
+
{log.source === "frontend" ? "FE" : "BE"}
|
|
259
|
+
</span>
|
|
260
|
+
<span className="whitespace-pre-wrap break-all">
|
|
261
|
+
{log.message}
|
|
262
|
+
</span>
|
|
263
|
+
</div>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|