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
|
@@ -32,6 +32,9 @@ function questionsDirPath(id = _activeWorkspaceId) {
|
|
|
32
32
|
function contextFilesDirPath(id = _activeWorkspaceId) {
|
|
33
33
|
return path.join(workspaceDir(id), "context-files");
|
|
34
34
|
}
|
|
35
|
+
function workspaceFilesFilePath(id = _activeWorkspaceId) {
|
|
36
|
+
return path.join(workspaceDir(id), "workspace-files.json");
|
|
37
|
+
}
|
|
35
38
|
async function ensureWorkspaceDirs(id = _activeWorkspaceId) {
|
|
36
39
|
await fs.mkdir(workspaceDir(id), { recursive: true });
|
|
37
40
|
await fs.mkdir(questionsDirPath(id), { recursive: true });
|
|
@@ -42,6 +45,8 @@ export interface Topic {
|
|
|
42
45
|
id: string;
|
|
43
46
|
name: string;
|
|
44
47
|
contextFiles: ContextFile[];
|
|
48
|
+
/** Topic-wide system prompt prepended to every question's context in this topic. */
|
|
49
|
+
systemContext?: string;
|
|
45
50
|
createdAt: string;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -51,6 +56,17 @@ export interface ContextFile {
|
|
|
51
56
|
originalName: string;
|
|
52
57
|
driveFileId?: string;
|
|
53
58
|
createdAt: string;
|
|
59
|
+
/** Distinguishes how this file was added. 'upload' = user-uploaded doc,
|
|
60
|
+
* 'user' = code saved from Code Runner, 'ai' = AI-generated code block,
|
|
61
|
+
* 'sandbox' = paired server+client sandbox saved as JSON,
|
|
62
|
+
* 'infra' = Terraform-style infra lab workspace saved as JSON,
|
|
63
|
+
* 'react' = React + TypeScript lab workspace,
|
|
64
|
+
* 'nextjs' = Next.js App Router lab workspace. */
|
|
65
|
+
origin?: "user" | "ai" | "upload" | "sandbox" | "infra" | "react" | "nextjs";
|
|
66
|
+
/** Language hint for code snippets (e.g. 'typescript', 'javascript'). */
|
|
67
|
+
language?: string;
|
|
68
|
+
/** Short display label for code snippets. */
|
|
69
|
+
label?: string;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
export interface Message {
|
|
@@ -83,10 +99,20 @@ export interface ReadingBookmark {
|
|
|
83
99
|
blockIndex: number;
|
|
84
100
|
}
|
|
85
101
|
|
|
102
|
+
export interface StoredCodeAnnotation {
|
|
103
|
+
id: string;
|
|
104
|
+
lineNumber: number;
|
|
105
|
+
lineContent: string;
|
|
106
|
+
prompt: string;
|
|
107
|
+
response: string;
|
|
108
|
+
filePath: string;
|
|
109
|
+
createdAt: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
export interface Question {
|
|
87
113
|
id: string;
|
|
88
114
|
topicId: string;
|
|
89
|
-
parentQuestionId?: string;
|
|
115
|
+
parentQuestionId?: string | null;
|
|
90
116
|
title: string;
|
|
91
117
|
systemContext: string;
|
|
92
118
|
codeContextFiles: string[];
|
|
@@ -94,6 +120,10 @@ export interface Question {
|
|
|
94
120
|
messages: Message[];
|
|
95
121
|
annotations?: Annotation[];
|
|
96
122
|
readingBookmark?: ReadingBookmark;
|
|
123
|
+
/** Code-line annotations keyed by file path. */
|
|
124
|
+
codeAnnotations?: { [filePath: string]: StoredCodeAnnotation[] };
|
|
125
|
+
/** IDs of sibling questions whose conversation history is injected as context. */
|
|
126
|
+
linkedConversationIds?: string[];
|
|
97
127
|
createdAt: string;
|
|
98
128
|
}
|
|
99
129
|
|
|
@@ -470,6 +500,11 @@ export async function deleteContextFile(
|
|
|
470
500
|
} catch {
|
|
471
501
|
/* already gone */
|
|
472
502
|
}
|
|
503
|
+
try {
|
|
504
|
+
await fs.unlink(path.join(contextFilesDirPath(), `${fileId}.orig`));
|
|
505
|
+
} catch {
|
|
506
|
+
/* already gone */
|
|
507
|
+
}
|
|
473
508
|
const topics = await getTopics();
|
|
474
509
|
const topic = topics.find((t) => t.id === topicId);
|
|
475
510
|
if (topic) {
|
|
@@ -484,7 +519,117 @@ export async function readContextFileContent(fileId: string): Promise<string> {
|
|
|
484
519
|
return fs.readFile(path.join(contextFilesDirPath(), fileId), "utf-8");
|
|
485
520
|
}
|
|
486
521
|
|
|
487
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Store the raw original file bytes at {fileId}.orig so downloads can serve the
|
|
524
|
+
* real file rather than the extracted-text version used by the LLM.
|
|
525
|
+
*/
|
|
526
|
+
export async function writeOriginalBlob(
|
|
527
|
+
fileId: string,
|
|
528
|
+
buffer: Buffer,
|
|
529
|
+
workspaceId = _activeWorkspaceId,
|
|
530
|
+
): Promise<void> {
|
|
531
|
+
await ensureWorkspaceDirs(workspaceId);
|
|
532
|
+
await fs.writeFile(
|
|
533
|
+
path.join(contextFilesDirPath(workspaceId), `${fileId}.orig`),
|
|
534
|
+
buffer,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read the original file bytes. Returns null if the file was uploaded before
|
|
540
|
+
* original storage was added (graceful fallback).
|
|
541
|
+
*/
|
|
542
|
+
export async function readOriginalBlob(
|
|
543
|
+
fileId: string,
|
|
544
|
+
workspaceId = _activeWorkspaceId,
|
|
545
|
+
): Promise<Buffer | null> {
|
|
546
|
+
try {
|
|
547
|
+
return await fs.readFile(
|
|
548
|
+
path.join(contextFilesDirPath(workspaceId), `${fileId}.orig`),
|
|
549
|
+
);
|
|
550
|
+
} catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// --- Workspace-level Context Files ---
|
|
555
|
+
// Stored in data/workspaces/{id}/workspace-files.json
|
|
556
|
+
// Blobs share the same context-files/ dir as topic/question files.
|
|
557
|
+
|
|
558
|
+
export async function getWorkspaceContextFiles(
|
|
559
|
+
workspaceId = _activeWorkspaceId,
|
|
560
|
+
): Promise<ContextFile[]> {
|
|
561
|
+
try {
|
|
562
|
+
const data = await fs.readFile(
|
|
563
|
+
workspaceFilesFilePath(workspaceId),
|
|
564
|
+
"utf-8",
|
|
565
|
+
);
|
|
566
|
+
return JSON.parse(data) as ContextFile[];
|
|
567
|
+
} catch {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export async function saveWorkspaceContextFile(
|
|
573
|
+
fileId: string,
|
|
574
|
+
originalName: string,
|
|
575
|
+
buffer: Buffer,
|
|
576
|
+
workspaceId = _activeWorkspaceId,
|
|
577
|
+
): Promise<ContextFile> {
|
|
578
|
+
await ensureWorkspaceDirs(workspaceId);
|
|
579
|
+
await fs.writeFile(
|
|
580
|
+
path.join(contextFilesDirPath(workspaceId), fileId),
|
|
581
|
+
buffer,
|
|
582
|
+
);
|
|
583
|
+
const cf: ContextFile = {
|
|
584
|
+
id: fileId,
|
|
585
|
+
name: originalName,
|
|
586
|
+
originalName,
|
|
587
|
+
createdAt: new Date().toISOString(),
|
|
588
|
+
};
|
|
589
|
+
const existing = await getWorkspaceContextFiles(workspaceId);
|
|
590
|
+
existing.push(cf);
|
|
591
|
+
await fs.writeFile(
|
|
592
|
+
workspaceFilesFilePath(workspaceId),
|
|
593
|
+
JSON.stringify(existing, null, 2),
|
|
594
|
+
);
|
|
595
|
+
return cf;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export async function deleteWorkspaceContextFile(
|
|
599
|
+
fileId: string,
|
|
600
|
+
workspaceId = _activeWorkspaceId,
|
|
601
|
+
): Promise<void> {
|
|
602
|
+
try {
|
|
603
|
+
await fs.unlink(path.join(contextFilesDirPath(workspaceId), fileId));
|
|
604
|
+
} catch {
|
|
605
|
+
/* already gone */
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
await fs.unlink(
|
|
609
|
+
path.join(contextFilesDirPath(workspaceId), `${fileId}.orig`),
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
/* already gone */
|
|
613
|
+
}
|
|
614
|
+
const existing = await getWorkspaceContextFiles(workspaceId);
|
|
615
|
+
const updated = existing.filter((f) => f.id !== fileId);
|
|
616
|
+
await fs.writeFile(
|
|
617
|
+
workspaceFilesFilePath(workspaceId),
|
|
618
|
+
JSON.stringify(updated, null, 2),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// --- Workspace-level Context Files ---
|
|
623
|
+
|
|
624
|
+
export async function readWorkspaceContextFileContent(
|
|
625
|
+
fileId: string,
|
|
626
|
+
workspaceId = _activeWorkspaceId,
|
|
627
|
+
): Promise<string> {
|
|
628
|
+
return fs.readFile(
|
|
629
|
+
path.join(contextFilesDirPath(workspaceId), fileId),
|
|
630
|
+
"utf-8",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
488
633
|
|
|
489
634
|
export async function getQuestion(id: string): Promise<Question | null> {
|
|
490
635
|
await ensureWorkspaceDirs();
|
|
@@ -526,6 +671,31 @@ export async function getQuestionsByTopic(
|
|
|
526
671
|
}
|
|
527
672
|
}
|
|
528
673
|
|
|
674
|
+
export async function getAllQuestions(): Promise<Question[]> {
|
|
675
|
+
await ensureWorkspaceDirs();
|
|
676
|
+
try {
|
|
677
|
+
const files = await fs.readdir(questionsDirPath());
|
|
678
|
+
const questions: Question[] = [];
|
|
679
|
+
for (const file of files) {
|
|
680
|
+
if (!file.endsWith(".json")) continue;
|
|
681
|
+
try {
|
|
682
|
+
const data = await fs.readFile(
|
|
683
|
+
path.join(questionsDirPath(), file),
|
|
684
|
+
"utf-8",
|
|
685
|
+
);
|
|
686
|
+
const q: Question = JSON.parse(data);
|
|
687
|
+
if (!q.contextFiles) q.contextFiles = [];
|
|
688
|
+
questions.push(q);
|
|
689
|
+
} catch {
|
|
690
|
+
/* skip malformed */
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return questions;
|
|
694
|
+
} catch {
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
529
699
|
export async function saveQuestion(question: Question): Promise<Question> {
|
|
530
700
|
await ensureWorkspaceDirs();
|
|
531
701
|
await fs.writeFile(
|
|
@@ -559,14 +729,18 @@ export async function saveQuestionContextFile(
|
|
|
559
729
|
fileId: string,
|
|
560
730
|
originalName: string,
|
|
561
731
|
buffer: Buffer,
|
|
732
|
+
extra?: { origin?: ContextFile["origin"]; language?: string; label?: string },
|
|
562
733
|
): Promise<ContextFile> {
|
|
563
734
|
await ensureWorkspaceDirs();
|
|
564
735
|
await fs.writeFile(path.join(contextFilesDirPath(), fileId), buffer);
|
|
565
736
|
const cf: ContextFile = {
|
|
566
737
|
id: fileId,
|
|
567
|
-
name: originalName,
|
|
738
|
+
name: extra?.label ?? originalName,
|
|
568
739
|
originalName,
|
|
569
740
|
createdAt: new Date().toISOString(),
|
|
741
|
+
...(extra?.origin ? { origin: extra.origin } : {}),
|
|
742
|
+
...(extra?.language ? { language: extra.language } : {}),
|
|
743
|
+
...(extra?.label ? { label: extra.label } : {}),
|
|
570
744
|
};
|
|
571
745
|
const q = await getQuestion(questionId);
|
|
572
746
|
if (q) {
|
|
@@ -577,6 +751,32 @@ export async function saveQuestionContextFile(
|
|
|
577
751
|
return cf;
|
|
578
752
|
}
|
|
579
753
|
|
|
754
|
+
export async function overwriteQuestionContextFileContent(
|
|
755
|
+
questionId: string,
|
|
756
|
+
fileId: string,
|
|
757
|
+
buffer: Buffer,
|
|
758
|
+
): Promise<void> {
|
|
759
|
+
await ensureWorkspaceDirs();
|
|
760
|
+
await fs.writeFile(path.join(contextFilesDirPath(), fileId), buffer);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export async function renameQuestionContextFile(
|
|
764
|
+
questionId: string,
|
|
765
|
+
fileId: string,
|
|
766
|
+
label: string,
|
|
767
|
+
): Promise<ContextFile> {
|
|
768
|
+
const q = await getQuestion(questionId);
|
|
769
|
+
if (!q) throw new Error("Question not found");
|
|
770
|
+
const cf = (q.contextFiles || []).find((f) => f.id === fileId);
|
|
771
|
+
if (!cf) throw new Error("Context file not found");
|
|
772
|
+
const safe =
|
|
773
|
+
label.replace(/[/\\:*?"<>|]/g, "-").trim() || cf.label || cf.originalName;
|
|
774
|
+
cf.label = safe;
|
|
775
|
+
cf.name = safe;
|
|
776
|
+
await saveQuestion(q);
|
|
777
|
+
return cf;
|
|
778
|
+
}
|
|
779
|
+
|
|
580
780
|
export async function deleteQuestionContextFile(
|
|
581
781
|
questionId: string,
|
|
582
782
|
fileId: string,
|
|
@@ -603,6 +803,70 @@ export async function updateQuestionMessages(
|
|
|
603
803
|
await saveQuestion(q);
|
|
604
804
|
}
|
|
605
805
|
|
|
806
|
+
export async function updateCodeAnnotationsForFile(
|
|
807
|
+
questionId: string,
|
|
808
|
+
filePath: string,
|
|
809
|
+
annotations: StoredCodeAnnotation[],
|
|
810
|
+
): Promise<void> {
|
|
811
|
+
const q = await getQuestion(questionId);
|
|
812
|
+
if (!q) throw new Error("Question not found");
|
|
813
|
+
if (!q.codeAnnotations) q.codeAnnotations = {};
|
|
814
|
+
q.codeAnnotations[filePath] = annotations;
|
|
815
|
+
await saveQuestion(q);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Register an already-uploaded file in a topic's contextFiles without copying
|
|
820
|
+
* any bytes — the file content on disk (identified by fileId) is shared.
|
|
821
|
+
*/
|
|
822
|
+
export async function linkContextFileToTopic(
|
|
823
|
+
topicId: string,
|
|
824
|
+
fileId: string,
|
|
825
|
+
originalName: string,
|
|
826
|
+
): Promise<ContextFile> {
|
|
827
|
+
const cf: ContextFile = {
|
|
828
|
+
id: fileId,
|
|
829
|
+
name: originalName,
|
|
830
|
+
originalName,
|
|
831
|
+
createdAt: new Date().toISOString(),
|
|
832
|
+
};
|
|
833
|
+
const topics = await getTopics();
|
|
834
|
+
const topic = topics.find((t) => t.id === topicId);
|
|
835
|
+
if (topic) {
|
|
836
|
+
if (!topic.contextFiles) topic.contextFiles = [];
|
|
837
|
+
if (!topic.contextFiles.find((f) => f.id === fileId)) {
|
|
838
|
+
topic.contextFiles.push(cf);
|
|
839
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(topics, null, 2));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return cf;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Register an already-uploaded file in a question's contextFiles without copying bytes.
|
|
847
|
+
*/
|
|
848
|
+
export async function linkContextFileToQuestion(
|
|
849
|
+
questionId: string,
|
|
850
|
+
fileId: string,
|
|
851
|
+
originalName: string,
|
|
852
|
+
): Promise<ContextFile> {
|
|
853
|
+
const cf: ContextFile = {
|
|
854
|
+
id: fileId,
|
|
855
|
+
name: originalName,
|
|
856
|
+
originalName,
|
|
857
|
+
createdAt: new Date().toISOString(),
|
|
858
|
+
};
|
|
859
|
+
const q = await getQuestion(questionId);
|
|
860
|
+
if (q) {
|
|
861
|
+
if (!q.contextFiles) q.contextFiles = [];
|
|
862
|
+
if (!q.contextFiles.find((f) => f.id === fileId)) {
|
|
863
|
+
q.contextFiles.push(cf);
|
|
864
|
+
await saveQuestion(q);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return cf;
|
|
868
|
+
}
|
|
869
|
+
|
|
606
870
|
// ── AI Settings ───────────────────────────────────────────────
|
|
607
871
|
|
|
608
872
|
const AI_SETTINGS_FILE = path.join(DATA_DIR, "ai-settings.json");
|
|
@@ -626,8 +890,14 @@ export interface AiSettings {
|
|
|
626
890
|
responseProfiles: Record<string, ResponseProfile>;
|
|
627
891
|
/** Full viz diagram spec reference — returned by the getVizGuide tool. */
|
|
628
892
|
vizGuide: string;
|
|
893
|
+
/** Full plotting spec reference — returned by the getPlotGuide tool. */
|
|
894
|
+
plotGuide: string;
|
|
629
895
|
/** All user-selectable prompt groups. Add new entries here to extend the UI. */
|
|
630
896
|
promptGroups: Record<string, PromptGroup>;
|
|
897
|
+
/** Gemini thinking budget in tokens. 0 = disabled. Only applies to Google models. */
|
|
898
|
+
thinkingBudget?: number;
|
|
899
|
+
/** When true, preference prompt texts are appended to every message (not just on change). */
|
|
900
|
+
alwaysSendPrefsDefault?: boolean;
|
|
631
901
|
}
|
|
632
902
|
|
|
633
903
|
const DEFAULT_AI_SETTINGS: AiSettings = {
|
|
@@ -639,6 +909,7 @@ const DEFAULT_AI_SETTINGS: AiSettings = {
|
|
|
639
909
|
normal: { maxOutputTokens: 3000, maxSteps: 5 },
|
|
640
910
|
},
|
|
641
911
|
vizGuide: "No viz guide configured.",
|
|
912
|
+
plotGuide: "No plot guide configured.",
|
|
642
913
|
promptGroups: {
|
|
643
914
|
length: {
|
|
644
915
|
label: "Response Length",
|