create-interview-cockpit 0.4.0 → 0.6.0
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/package.json +1 -1
- package/template/client/package-lock.json +753 -1
- package/template/client/package.json +4 -0
- package/template/client/src/App.tsx +20 -0
- package/template/client/src/api.ts +455 -3
- package/template/client/src/components/AiSettingsModal.tsx +855 -248
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +132 -27
- package/template/client/src/components/ChatView.tsx +365 -123
- package/template/client/src/components/CodeContextPanel.tsx +714 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
- package/template/client/src/components/DocRefModal.tsx +551 -0
- package/template/client/src/components/FileAttachments.tsx +128 -12
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/InfraLabModal.tsx +1706 -0
- package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
- package/template/client/src/components/MarkdownRenderer.tsx +219 -2
- package/template/client/src/components/NotesModal.tsx +977 -0
- package/template/client/src/components/PlotEmbed.tsx +173 -0
- package/template/client/src/components/Sidebar.tsx +397 -127
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +412 -25
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/infraLab.ts +124 -0
- package/template/client/src/reactLab.ts +477 -0
- package/template/client/src/store.ts +416 -2
- package/template/client/src/types.ts +41 -1
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/package.json +1 -1
- package/template/server/src/google-drive.ts +144 -2
- package/template/server/src/index.ts +1890 -188
- package/template/server/src/infra-runner.ts +1104 -0
- package/template/server/src/storage.ts +274 -3
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@ai-sdk/react": "^3.0.170",
|
|
12
|
+
"@types/prismjs": "^1.26.6",
|
|
12
13
|
"ai": "^6.0.168",
|
|
13
14
|
"lucide-react": "^0.460.0",
|
|
14
15
|
"mermaid": "^11.4.0",
|
|
16
|
+
"prismjs": "^1.30.0",
|
|
15
17
|
"react": "^19.0.0",
|
|
16
18
|
"react-dom": "^19.0.0",
|
|
17
19
|
"react-markdown": "^9.0.0",
|
|
20
|
+
"react-simple-code-editor": "^0.14.1",
|
|
18
21
|
"react-syntax-highlighter": "^15.6.1",
|
|
19
22
|
"remark-gfm": "^4.0.0",
|
|
23
|
+
"vega-embed": "^7.1.0",
|
|
20
24
|
"vizcraft": "^1.17.0",
|
|
21
25
|
"yaml": "^2.8.3",
|
|
22
26
|
"zustand": "^5.0.0"
|
|
@@ -4,7 +4,10 @@ import Sidebar from "./components/Sidebar";
|
|
|
4
4
|
import ChatView from "./components/ChatView";
|
|
5
5
|
import CodeContextPanel from "./components/CodeContextPanel";
|
|
6
6
|
import FileViewerModal from "./components/FileViewerModal";
|
|
7
|
+
import DocRefModal from "./components/DocRefModal";
|
|
7
8
|
import AiSettingsModal from "./components/AiSettingsModal";
|
|
9
|
+
import CodeRunnerModal from "./components/CodeRunnerModal";
|
|
10
|
+
import InfraLabModal from "./components/InfraLabModal";
|
|
8
11
|
import { Code, Plane, PanelLeftClose, PanelLeft, Settings } from "lucide-react";
|
|
9
12
|
|
|
10
13
|
export default function App() {
|
|
@@ -12,6 +15,7 @@ export default function App() {
|
|
|
12
15
|
fetchTopics,
|
|
13
16
|
fetchWorkspaces,
|
|
14
17
|
fetchAiSettings,
|
|
18
|
+
fetchWorkspaceFiles,
|
|
15
19
|
fetchQuestions,
|
|
16
20
|
selectQuestion,
|
|
17
21
|
currentQuestion,
|
|
@@ -21,9 +25,14 @@ export default function App() {
|
|
|
21
25
|
toggleSidebar,
|
|
22
26
|
viewingFile,
|
|
23
27
|
closeFileViewer,
|
|
28
|
+
viewingDoc,
|
|
29
|
+
closeDocViewer,
|
|
24
30
|
showSettings,
|
|
25
31
|
openSettings,
|
|
26
32
|
closeSettings,
|
|
33
|
+
showCodeRunner,
|
|
34
|
+
showInfraLab,
|
|
35
|
+
closeCodeRunner,
|
|
27
36
|
} = useStore();
|
|
28
37
|
|
|
29
38
|
useEffect(() => {
|
|
@@ -31,6 +40,7 @@ export default function App() {
|
|
|
31
40
|
await fetchWorkspaces();
|
|
32
41
|
await fetchTopics();
|
|
33
42
|
fetchAiSettings();
|
|
43
|
+
fetchWorkspaceFiles();
|
|
34
44
|
// Restore last-viewed question after page refresh
|
|
35
45
|
const topicId = sessionStorage.getItem("lastTopicId");
|
|
36
46
|
const questionId = sessionStorage.getItem("lastQuestionId");
|
|
@@ -139,7 +149,17 @@ export default function App() {
|
|
|
139
149
|
{viewingFile && (
|
|
140
150
|
<FileViewerModal filePath={viewingFile} onClose={closeFileViewer} />
|
|
141
151
|
)}
|
|
152
|
+
{viewingDoc && (
|
|
153
|
+
<DocRefModal
|
|
154
|
+
fileId={viewingDoc.fileId}
|
|
155
|
+
quote={viewingDoc.quote}
|
|
156
|
+
fileName={viewingDoc.fileName}
|
|
157
|
+
onClose={closeDocViewer}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
142
160
|
{showSettings && <AiSettingsModal />}
|
|
161
|
+
{showCodeRunner && <CodeRunnerModal />}
|
|
162
|
+
{showInfraLab && <InfraLabModal />}
|
|
143
163
|
</div>
|
|
144
164
|
);
|
|
145
165
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Topic,
|
|
3
|
+
Question,
|
|
4
|
+
ContextFile,
|
|
5
|
+
WorkspacesRegistry,
|
|
6
|
+
InfraLabWorkspace,
|
|
7
|
+
} from "./types";
|
|
2
8
|
|
|
3
9
|
const BASE = "/api";
|
|
4
10
|
|
|
@@ -20,10 +26,78 @@ export interface AiSettings {
|
|
|
20
26
|
{ maxOutputTokens: number; maxSteps: number }
|
|
21
27
|
>;
|
|
22
28
|
vizGuide: string;
|
|
29
|
+
plotGuide: string;
|
|
23
30
|
/** All user-selectable prompt groups. Add new entries here to extend the UI. */
|
|
24
31
|
promptGroups: Record<string, PromptGroup>;
|
|
32
|
+
/** When true, preference prompt texts are appended to every message (not just on change). */
|
|
33
|
+
alwaysSendPrefsDefault?: boolean;
|
|
34
|
+
/** Gemini thinking budget in tokens. 0 = disabled. Only applies when provider is google/gemini. */
|
|
35
|
+
thinkingBudget?: number;
|
|
36
|
+
/** Read-only: current AI provider from .env (openai | google | anthropic). */
|
|
37
|
+
provider?: string;
|
|
38
|
+
/** Read-only: current model name from .env. */
|
|
39
|
+
model?: string;
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
export type InfraRunAction = "validate" | "plan" | "command";
|
|
43
|
+
export type InfraRunStatus = "completed" | "failed";
|
|
44
|
+
|
|
45
|
+
export interface InfraRunArtifact {
|
|
46
|
+
key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
kind: "text" | "json";
|
|
49
|
+
fileName: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface InfraDiagnostic {
|
|
53
|
+
severity: string;
|
|
54
|
+
summary: string;
|
|
55
|
+
detail?: string;
|
|
56
|
+
address?: string;
|
|
57
|
+
filename?: string;
|
|
58
|
+
line?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface InfraPlanSummary {
|
|
62
|
+
add: number;
|
|
63
|
+
change: number;
|
|
64
|
+
destroy: number;
|
|
65
|
+
replace: number;
|
|
66
|
+
resourceCount: number;
|
|
67
|
+
outputCount: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface InfraRunListItem {
|
|
71
|
+
id: string;
|
|
72
|
+
fileId?: string;
|
|
73
|
+
questionId?: string;
|
|
74
|
+
label: string;
|
|
75
|
+
action: InfraRunAction;
|
|
76
|
+
command?: string;
|
|
77
|
+
status: InfraRunStatus;
|
|
78
|
+
startedAt: string;
|
|
79
|
+
completedAt: string;
|
|
80
|
+
durationMs: number;
|
|
81
|
+
provider: "aws";
|
|
82
|
+
executionMode: "plan-only" | "localstack";
|
|
83
|
+
diagnostics: InfraDiagnostic[];
|
|
84
|
+
planSummary?: InfraPlanSummary;
|
|
85
|
+
error?: string;
|
|
86
|
+
artifacts: InfraRunArtifact[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface InfraRunDetails extends InfraRunListItem {
|
|
90
|
+
logs: string;
|
|
91
|
+
validationJson?: unknown;
|
|
92
|
+
planJson?: unknown;
|
|
93
|
+
workspaceSnapshot?: InfraLabWorkspace;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type InfraCommandStreamMessage =
|
|
97
|
+
| { type: "output"; kind: "stdout" | "stderr" | "info"; text: string }
|
|
98
|
+
| { type: "complete"; run: InfraRunDetails }
|
|
99
|
+
| { type: "error"; error: string };
|
|
100
|
+
|
|
27
101
|
export async function fetchAiSettings(): Promise<AiSettings> {
|
|
28
102
|
const res = await fetch(`${BASE}/settings`);
|
|
29
103
|
return res.json();
|
|
@@ -60,7 +134,7 @@ export async function deleteTopic(id: string): Promise<void> {
|
|
|
60
134
|
|
|
61
135
|
export async function updateTopic(
|
|
62
136
|
id: string,
|
|
63
|
-
data: { name?: string },
|
|
137
|
+
data: { name?: string; systemContext?: string },
|
|
64
138
|
): Promise<Topic> {
|
|
65
139
|
const res = await fetch(`${BASE}/topics/${id}`, {
|
|
66
140
|
method: "PATCH",
|
|
@@ -82,6 +156,10 @@ export async function uploadTopicFiles(
|
|
|
82
156
|
method: "POST",
|
|
83
157
|
body: form,
|
|
84
158
|
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const err = await res.json().catch(() => ({ error: "Upload failed" }));
|
|
161
|
+
throw new Error(err.error ?? "Upload failed");
|
|
162
|
+
}
|
|
85
163
|
return res.json();
|
|
86
164
|
}
|
|
87
165
|
|
|
@@ -94,6 +172,35 @@ export async function deleteTopicFile(
|
|
|
94
172
|
});
|
|
95
173
|
}
|
|
96
174
|
|
|
175
|
+
// --- Workspace Context Files ---
|
|
176
|
+
|
|
177
|
+
export async function fetchWorkspaceFiles(): Promise<ContextFile[]> {
|
|
178
|
+
const res = await fetch(`${BASE}/workspace/context-files`);
|
|
179
|
+
return res.json();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function uploadWorkspaceFiles(
|
|
183
|
+
files: FileList | File[],
|
|
184
|
+
): Promise<ContextFile[]> {
|
|
185
|
+
const form = new FormData();
|
|
186
|
+
for (const file of files) form.append("files", file);
|
|
187
|
+
const res = await fetch(`${BASE}/workspace/context-files`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
body: form,
|
|
190
|
+
});
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
const err = await res.json().catch(() => ({ error: "Upload failed" }));
|
|
193
|
+
throw new Error(err.error ?? "Upload failed");
|
|
194
|
+
}
|
|
195
|
+
return res.json();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function deleteWorkspaceFile(fileId: string): Promise<void> {
|
|
199
|
+
await fetch(`${BASE}/workspace/context-files/${fileId}`, {
|
|
200
|
+
method: "DELETE",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
97
204
|
// --- Questions ---
|
|
98
205
|
|
|
99
206
|
export async function fetchQuestions(topicId: string): Promise<Question[]> {
|
|
@@ -151,6 +258,10 @@ export async function uploadQuestionFiles(
|
|
|
151
258
|
method: "POST",
|
|
152
259
|
body: form,
|
|
153
260
|
});
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
const err = await res.json().catch(() => ({ error: "Upload failed" }));
|
|
263
|
+
throw new Error(err.error ?? "Upload failed");
|
|
264
|
+
}
|
|
154
265
|
return res.json();
|
|
155
266
|
}
|
|
156
267
|
|
|
@@ -163,6 +274,307 @@ export async function deleteQuestionFile(
|
|
|
163
274
|
});
|
|
164
275
|
}
|
|
165
276
|
|
|
277
|
+
export interface PickableFile {
|
|
278
|
+
fileId: string;
|
|
279
|
+
originalName: string;
|
|
280
|
+
source: "workspace" | "topic" | "question";
|
|
281
|
+
sourceName: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function fetchAllContextFiles(): Promise<PickableFile[]> {
|
|
285
|
+
const res = await fetch(`${BASE}/context-files/all`);
|
|
286
|
+
return res.json();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function linkFileToTopic(
|
|
290
|
+
topicId: string,
|
|
291
|
+
fileId: string,
|
|
292
|
+
originalName: string,
|
|
293
|
+
): Promise<ContextFile> {
|
|
294
|
+
const res = await fetch(`${BASE}/topics/${topicId}/context-files/link`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: { "Content-Type": "application/json" },
|
|
297
|
+
body: JSON.stringify({ fileId, originalName }),
|
|
298
|
+
});
|
|
299
|
+
return res.json();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function linkFileToQuestion(
|
|
303
|
+
questionId: string,
|
|
304
|
+
fileId: string,
|
|
305
|
+
originalName: string,
|
|
306
|
+
): Promise<ContextFile> {
|
|
307
|
+
const res = await fetch(
|
|
308
|
+
`${BASE}/questions/${questionId}/context-files/link`,
|
|
309
|
+
{
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { "Content-Type": "application/json" },
|
|
312
|
+
body: JSON.stringify({ fileId, originalName }),
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
return res.json();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function saveCodeSnippet(
|
|
319
|
+
questionId: string,
|
|
320
|
+
code: string,
|
|
321
|
+
language: string,
|
|
322
|
+
label: string,
|
|
323
|
+
origin: "user" | "ai" | "sandbox" | "infra" | "react" | "nextjs",
|
|
324
|
+
): Promise<ContextFile> {
|
|
325
|
+
const res = await fetch(`${BASE}/questions/${questionId}/save-code-snippet`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "Content-Type": "application/json" },
|
|
328
|
+
body: JSON.stringify({ code, language, label, origin }),
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) throw new Error(await res.text());
|
|
331
|
+
return res.json();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function overwriteContextFileContent(
|
|
335
|
+
questionId: string,
|
|
336
|
+
fileId: string,
|
|
337
|
+
content: string,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const res = await fetch(
|
|
340
|
+
`${BASE}/questions/${questionId}/context-files/${fileId}/content`,
|
|
341
|
+
{
|
|
342
|
+
method: "PUT",
|
|
343
|
+
headers: { "Content-Type": "application/json" },
|
|
344
|
+
body: JSON.stringify({ code: content }),
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
if (!res.ok) throw new Error(await res.text());
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function renameContextFile(
|
|
351
|
+
questionId: string,
|
|
352
|
+
fileId: string,
|
|
353
|
+
label: string,
|
|
354
|
+
): Promise<ContextFile> {
|
|
355
|
+
const res = await fetch(
|
|
356
|
+
`${BASE}/questions/${questionId}/context-files/${fileId}`,
|
|
357
|
+
{
|
|
358
|
+
method: "PATCH",
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
body: JSON.stringify({ label }),
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
if (!res.ok) throw new Error(await res.text());
|
|
364
|
+
return res.json();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function runInfraAction(input: {
|
|
368
|
+
questionId?: string;
|
|
369
|
+
fileId?: string;
|
|
370
|
+
label?: string;
|
|
371
|
+
action: "validate" | "plan";
|
|
372
|
+
workspace: InfraLabWorkspace;
|
|
373
|
+
}): Promise<InfraRunDetails> {
|
|
374
|
+
const res = await fetch(`${BASE}/infra/run`, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: { "Content-Type": "application/json" },
|
|
377
|
+
body: JSON.stringify(input),
|
|
378
|
+
});
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
const err = await res.json().catch(() => ({ error: "Infra run failed" }));
|
|
381
|
+
throw new Error(err.error ?? "Infra run failed");
|
|
382
|
+
}
|
|
383
|
+
return res.json();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function fetchInfraRuns(
|
|
387
|
+
fileId: string,
|
|
388
|
+
): Promise<InfraRunListItem[]> {
|
|
389
|
+
const res = await fetch(
|
|
390
|
+
`${BASE}/infra/runs?fileId=${encodeURIComponent(fileId)}`,
|
|
391
|
+
);
|
|
392
|
+
if (!res.ok) throw new Error(await res.text());
|
|
393
|
+
return res.json();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function streamInfraCommand(
|
|
397
|
+
input: {
|
|
398
|
+
questionId?: string;
|
|
399
|
+
fileId?: string;
|
|
400
|
+
label?: string;
|
|
401
|
+
command: string;
|
|
402
|
+
workspace: InfraLabWorkspace;
|
|
403
|
+
},
|
|
404
|
+
onMessage: (message: InfraCommandStreamMessage) => void,
|
|
405
|
+
): Promise<void> {
|
|
406
|
+
const res = await fetch(`${BASE}/infra/command-stream`, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: { "Content-Type": "application/json" },
|
|
409
|
+
body: JSON.stringify(input),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (!res.ok || !res.body) {
|
|
413
|
+
const error = await res.text().catch(() => "Infra command failed");
|
|
414
|
+
throw new Error(error || "Infra command failed");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const reader = res.body.getReader();
|
|
418
|
+
const decoder = new TextDecoder();
|
|
419
|
+
let buffer = "";
|
|
420
|
+
|
|
421
|
+
const flushBuffer = () => {
|
|
422
|
+
const events = buffer.replace(/\r/g, "").split("\n\n");
|
|
423
|
+
buffer = events.pop() ?? "";
|
|
424
|
+
|
|
425
|
+
for (const event of events) {
|
|
426
|
+
if (!event.trim()) continue;
|
|
427
|
+
const payload = event
|
|
428
|
+
.split("\n")
|
|
429
|
+
.filter((line) => line.startsWith("data:"))
|
|
430
|
+
.map((line) => line.slice(5).trimStart())
|
|
431
|
+
.join("\n");
|
|
432
|
+
|
|
433
|
+
if (!payload) continue;
|
|
434
|
+
onMessage(JSON.parse(payload) as InfraCommandStreamMessage);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
while (true) {
|
|
439
|
+
const { value, done } = await reader.read();
|
|
440
|
+
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
|
|
441
|
+
flushBuffer();
|
|
442
|
+
if (done) break;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (buffer.trim()) {
|
|
446
|
+
flushBuffer();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export async function fetchInfraRun(runId: string): Promise<InfraRunDetails> {
|
|
451
|
+
const res = await fetch(`${BASE}/infra/runs/${encodeURIComponent(runId)}`);
|
|
452
|
+
if (!res.ok) throw new Error(await res.text());
|
|
453
|
+
return res.json();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function streamInfraAsk(
|
|
457
|
+
input: {
|
|
458
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
459
|
+
workspace: Record<string, string>;
|
|
460
|
+
questionId?: string;
|
|
461
|
+
},
|
|
462
|
+
onDelta: (chunk: string) => void,
|
|
463
|
+
): Promise<void> {
|
|
464
|
+
const res = await fetch(`${BASE}/infra/ask`, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: { "Content-Type": "application/json" },
|
|
467
|
+
body: JSON.stringify(input),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (!res.ok || !res.body) {
|
|
471
|
+
const err = await res.text().catch(() => "Infra ask failed");
|
|
472
|
+
throw new Error(err || "Infra ask failed");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const reader = res.body.getReader();
|
|
476
|
+
const decoder = new TextDecoder();
|
|
477
|
+
|
|
478
|
+
while (true) {
|
|
479
|
+
const { value, done } = await reader.read();
|
|
480
|
+
if (done) break;
|
|
481
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
482
|
+
// AI SDK UIMessageStream emits SSE lines like: data: {"type":"text-delta","id":"0","delta":"..."}
|
|
483
|
+
for (const line of chunk.split("\n")) {
|
|
484
|
+
if (!line.startsWith("data: ")) continue;
|
|
485
|
+
try {
|
|
486
|
+
const json = JSON.parse(line.slice(6));
|
|
487
|
+
if (json.type === "text-delta" && typeof json.delta === "string") {
|
|
488
|
+
onDelta(json.delta);
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// ignore malformed chunk
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function streamNotesAsk(
|
|
498
|
+
input: {
|
|
499
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
500
|
+
noteContent?: string;
|
|
501
|
+
noteName?: string;
|
|
502
|
+
},
|
|
503
|
+
onDelta: (chunk: string) => void,
|
|
504
|
+
): Promise<void> {
|
|
505
|
+
const res = await fetch(`${BASE}/notes/ask`, {
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: { "Content-Type": "application/json" },
|
|
508
|
+
body: JSON.stringify(input),
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (!res.ok || !res.body) {
|
|
512
|
+
const err = await res.text().catch(() => "Notes ask failed");
|
|
513
|
+
throw new Error(err || "Notes ask failed");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const reader = res.body.getReader();
|
|
517
|
+
const decoder = new TextDecoder();
|
|
518
|
+
|
|
519
|
+
while (true) {
|
|
520
|
+
const { value, done } = await reader.read();
|
|
521
|
+
if (done) break;
|
|
522
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
523
|
+
for (const line of chunk.split("\n")) {
|
|
524
|
+
if (!line.startsWith("data: ")) continue;
|
|
525
|
+
try {
|
|
526
|
+
const json = JSON.parse(line.slice(6));
|
|
527
|
+
if (json.type === "text-delta" && typeof json.delta === "string") {
|
|
528
|
+
onDelta(json.delta);
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
// ignore malformed chunk
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export async function streamFrontendLabAsk(
|
|
538
|
+
input: {
|
|
539
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
540
|
+
workspace: Record<string, string>;
|
|
541
|
+
labType: "react" | "nextjs";
|
|
542
|
+
questionId?: string;
|
|
543
|
+
},
|
|
544
|
+
onDelta: (chunk: string) => void,
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
const res = await fetch(`${BASE}/frontend-lab/ask`, {
|
|
547
|
+
method: "POST",
|
|
548
|
+
headers: { "Content-Type": "application/json" },
|
|
549
|
+
body: JSON.stringify(input),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (!res.ok || !res.body) {
|
|
553
|
+
const err = await res.text().catch(() => "Frontend lab ask failed");
|
|
554
|
+
throw new Error(err || "Frontend lab ask failed");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const reader = res.body.getReader();
|
|
558
|
+
const decoder = new TextDecoder();
|
|
559
|
+
|
|
560
|
+
while (true) {
|
|
561
|
+
const { value, done } = await reader.read();
|
|
562
|
+
if (done) break;
|
|
563
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
564
|
+
for (const line of chunk.split("\n")) {
|
|
565
|
+
if (!line.startsWith("data: ")) continue;
|
|
566
|
+
try {
|
|
567
|
+
const json = JSON.parse(line.slice(6));
|
|
568
|
+
if (json.type === "text-delta" && typeof json.delta === "string") {
|
|
569
|
+
onDelta(json.delta);
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
// ignore malformed chunk
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
166
578
|
// --- Code Context ---
|
|
167
579
|
|
|
168
580
|
export async function fetchCodeContextTree(): Promise<string[]> {
|
|
@@ -282,7 +694,7 @@ export async function fetchDriveSubfolders(
|
|
|
282
694
|
export async function createDriveSubfolder(
|
|
283
695
|
workspaceId: string,
|
|
284
696
|
name: string,
|
|
285
|
-
): Promise<DriveFolder> {
|
|
697
|
+
): Promise<DriveFolder | { needsAuth: true; authUrl: string }> {
|
|
286
698
|
const res = await fetch(
|
|
287
699
|
`${BASE}/workspaces/${workspaceId}/drive-subfolders`,
|
|
288
700
|
{
|
|
@@ -358,3 +770,43 @@ export async function attachDriveFolder(
|
|
|
358
770
|
const data = await res.json();
|
|
359
771
|
return data.registry;
|
|
360
772
|
}
|
|
773
|
+
|
|
774
|
+
// ── Next.js real-server sandbox ──────────────────────────────────────────────
|
|
775
|
+
|
|
776
|
+
export interface NextjsSandboxInfo {
|
|
777
|
+
id: string;
|
|
778
|
+
port: number;
|
|
779
|
+
url: string;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export async function startNextjsSandbox(
|
|
783
|
+
files: Record<string, string>,
|
|
784
|
+
): Promise<NextjsSandboxInfo> {
|
|
785
|
+
const res = await fetch(`${BASE}/nextjs/start`, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: { "Content-Type": "application/json" },
|
|
788
|
+
body: JSON.stringify({ files }),
|
|
789
|
+
});
|
|
790
|
+
if (!res.ok) {
|
|
791
|
+
const body = await res.json().catch(() => ({}));
|
|
792
|
+
throw new Error(
|
|
793
|
+
(body as any).error || `Failed to start Next.js (${res.status})`,
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
return res.json();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export async function updateNextjsFiles(
|
|
800
|
+
id: string,
|
|
801
|
+
files: Record<string, string>,
|
|
802
|
+
): Promise<void> {
|
|
803
|
+
await fetch(`${BASE}/nextjs/${id}/update-files`, {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: { "Content-Type": "application/json" },
|
|
806
|
+
body: JSON.stringify({ files }),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export async function stopNextjsSandbox(id: string): Promise<void> {
|
|
811
|
+
await fetch(`${BASE}/nextjs/${id}`, { method: "DELETE" });
|
|
812
|
+
}
|