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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +753 -1
  3. package/template/client/package.json +4 -0
  4. package/template/client/src/App.tsx +20 -0
  5. package/template/client/src/api.ts +455 -3
  6. package/template/client/src/components/AiSettingsModal.tsx +855 -248
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +132 -27
  9. package/template/client/src/components/ChatView.tsx +365 -123
  10. package/template/client/src/components/CodeContextPanel.tsx +714 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
  13. package/template/client/src/components/DocRefModal.tsx +551 -0
  14. package/template/client/src/components/FileAttachments.tsx +128 -12
  15. package/template/client/src/components/FilePickerModal.tsx +181 -0
  16. package/template/client/src/components/FileViewerModal.tsx +406 -28
  17. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  18. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  19. package/template/client/src/components/MarkdownRenderer.tsx +219 -2
  20. package/template/client/src/components/NotesModal.tsx +977 -0
  21. package/template/client/src/components/PlotEmbed.tsx +173 -0
  22. package/template/client/src/components/Sidebar.tsx +397 -127
  23. package/template/client/src/components/TextAnnotator.tsx +8 -15
  24. package/template/client/src/components/VizCraftEmbed.tsx +412 -25
  25. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  26. package/template/client/src/infraLab.ts +124 -0
  27. package/template/client/src/reactLab.ts +477 -0
  28. package/template/client/src/store.ts +416 -2
  29. package/template/client/src/types.ts +41 -1
  30. package/template/client/tsconfig.tsbuildinfo +1 -1
  31. package/template/cockpit.json +1 -1
  32. package/template/package.json +1 -1
  33. package/template/server/src/google-drive.ts +144 -2
  34. package/template/server/src/index.ts +1890 -188
  35. package/template/server/src/infra-runner.ts +1104 -0
  36. package/template/server/src/storage.ts +274 -3
@@ -1,27 +1,49 @@
1
- import { useRef } from "react";
1
+ import { useRef, useState } from "react";
2
2
  import type { ContextFile } from "../types";
3
- import { Paperclip, X, FileText } from "lucide-react";
3
+ import { Paperclip, X, FileText, Download, Link, Eye } from "lucide-react";
4
+ import FilePickerModal from "./FilePickerModal";
5
+ import { useStore } from "../store";
4
6
 
5
7
  interface Props {
6
8
  files: ContextFile[];
7
9
  onUpload: (files: FileList) => Promise<void>;
8
10
  onRemove: (fileId: string) => Promise<void>;
11
+ onLink?: (fileId: string, originalName: string) => Promise<void>;
12
+ /** URL prefix for downloads, e.g. "/api/topics/abc/context-files" */
13
+ downloadBase?: string;
9
14
  label: string;
10
15
  compact?: boolean;
11
16
  }
12
17
 
18
+ function downloadFile(url: string, filename: string) {
19
+ const a = document.createElement("a");
20
+ a.href = url;
21
+ a.download = filename;
22
+ a.click();
23
+ }
24
+
13
25
  export default function FileAttachments({
14
26
  files,
15
27
  onUpload,
16
28
  onRemove,
29
+ onLink,
30
+ downloadBase,
17
31
  label,
18
32
  compact,
19
33
  }: Props) {
20
34
  const inputRef = useRef<HTMLInputElement>(null);
35
+ const [showPicker, setShowPicker] = useState(false);
36
+ const [uploadError, setUploadError] = useState<string | null>(null);
37
+ const openDocViewer = useStore((s) => s.openDocViewer);
21
38
 
22
39
  const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
23
40
  if (e.target.files?.length) {
24
- await onUpload(e.target.files);
41
+ setUploadError(null);
42
+ try {
43
+ await onUpload(e.target.files);
44
+ } catch (err: any) {
45
+ setUploadError(err?.message ?? "Upload failed");
46
+ }
25
47
  e.target.value = "";
26
48
  }
27
49
  };
@@ -29,13 +51,23 @@ export default function FileAttachments({
29
51
  if (compact) {
30
52
  return (
31
53
  <div className="flex items-center gap-1 flex-wrap">
54
+ {showPicker && onLink && (
55
+ <FilePickerModal
56
+ currentFiles={files}
57
+ onLink={async (fileId, originalName) => {
58
+ await onLink(fileId, originalName);
59
+ setShowPicker(false);
60
+ }}
61
+ onClose={() => setShowPicker(false)}
62
+ />
63
+ )}
32
64
  <input
33
65
  ref={inputRef}
34
66
  type="file"
35
67
  multiple
36
68
  onChange={handleChange}
37
69
  className="hidden"
38
- accept=".txt,.md,.ts,.tsx,.js,.jsx,.json,.css,.scss,.html,.xml,.yaml,.yml,.csv,.py,.java,.cs,.go,.rs,.sql,.sh,.toml,.ini,.log,.cfg,.conf,.env,.pdf,.docx"
70
+ accept="*"
39
71
  />
40
72
  {files.map((f) => (
41
73
  <span
@@ -44,6 +76,27 @@ export default function FileAttachments({
44
76
  >
45
77
  <FileText className="w-2.5 h-2.5" />
46
78
  {f.originalName}
79
+ <button
80
+ onClick={() => openDocViewer(f.id, "", f.originalName)}
81
+ className="hover:text-cyan-300 transition-colors"
82
+ title="View file"
83
+ >
84
+ <Eye className="w-2.5 h-2.5" />
85
+ </button>
86
+ {downloadBase && (
87
+ <button
88
+ onClick={() =>
89
+ downloadFile(
90
+ `${downloadBase}/${f.id}/download`,
91
+ f.originalName,
92
+ )
93
+ }
94
+ className="hover:text-cyan-300 transition-colors"
95
+ title="Download"
96
+ >
97
+ <Download className="w-2.5 h-2.5" />
98
+ </button>
99
+ )}
47
100
  <button
48
101
  onClick={() => onRemove(f.id)}
49
102
  className="hover:text-red-400 transition-colors"
@@ -60,19 +113,44 @@ export default function FileAttachments({
60
113
  <Paperclip className="w-2.5 h-2.5" />
61
114
  Attach
62
115
  </button>
116
+ {onLink && (
117
+ <button
118
+ onClick={() => setShowPicker(true)}
119
+ className="inline-flex items-center gap-0.5 text-[10px] text-slate-600 hover:text-cyan-400 transition-colors"
120
+ title="Link an already-uploaded file"
121
+ >
122
+ <Link className="w-2.5 h-2.5" />
123
+ Link
124
+ </button>
125
+ )}
126
+ {uploadError && (
127
+ <span className="text-[10px] text-red-400 ml-1" title={uploadError}>
128
+ ⚠ {uploadError}
129
+ </span>
130
+ )}
63
131
  </div>
64
132
  );
65
133
  }
66
134
 
67
135
  return (
68
136
  <div>
137
+ {showPicker && onLink && (
138
+ <FilePickerModal
139
+ currentFiles={files}
140
+ onLink={async (fileId, originalName) => {
141
+ await onLink(fileId, originalName);
142
+ setShowPicker(false);
143
+ }}
144
+ onClose={() => setShowPicker(false)}
145
+ />
146
+ )}
69
147
  <input
70
148
  ref={inputRef}
71
149
  type="file"
72
150
  multiple
73
151
  onChange={handleChange}
74
152
  className="hidden"
75
- accept=".txt,.md,.ts,.tsx,.js,.jsx,.json,.css,.scss,.html,.xml,.yaml,.yml,.csv,.py,.java,.cs,.go,.rs,.sql,.sh,.toml,.ini,.log,.cfg,.conf,.env,.pdf,.docx"
153
+ accept="*"
76
154
  />
77
155
 
78
156
  {files.length > 0 && (
@@ -84,6 +162,27 @@ export default function FileAttachments({
84
162
  >
85
163
  <FileText className="w-3 h-3 shrink-0" />
86
164
  <span className="truncate flex-1">{f.originalName}</span>
165
+ <button
166
+ onClick={() => openDocViewer(f.id, "", f.originalName)}
167
+ className="shrink-0 hover:text-cyan-300 transition-colors"
168
+ title="View file"
169
+ >
170
+ <Eye className="w-3 h-3" />
171
+ </button>
172
+ {downloadBase && (
173
+ <button
174
+ onClick={() =>
175
+ downloadFile(
176
+ `${downloadBase}/${f.id}/download`,
177
+ f.originalName,
178
+ )
179
+ }
180
+ className="shrink-0 hover:text-cyan-300 transition-colors"
181
+ title="Download"
182
+ >
183
+ <Download className="w-3 h-3" />
184
+ </button>
185
+ )}
87
186
  <button
88
187
  onClick={() => onRemove(f.id)}
89
188
  className="shrink-0 hover:text-red-400 transition-colors"
@@ -95,13 +194,30 @@ export default function FileAttachments({
95
194
  </div>
96
195
  )}
97
196
 
98
- <button
99
- onClick={() => inputRef.current?.click()}
100
- className="flex items-center gap-1 text-xs text-slate-600 hover:text-violet-400 transition-colors w-full"
101
- >
102
- <Paperclip className="w-3 h-3" />
103
- Attach files to {label}
104
- </button>
197
+ <div className="flex items-center gap-3 flex-wrap">
198
+ <button
199
+ onClick={() => inputRef.current?.click()}
200
+ className="flex items-center gap-1 text-xs text-slate-600 hover:text-violet-400 transition-colors"
201
+ >
202
+ <Paperclip className="w-3 h-3" />
203
+ Attach files to {label}
204
+ </button>
205
+ {onLink && (
206
+ <button
207
+ onClick={() => setShowPicker(true)}
208
+ className="flex items-center gap-1 text-xs text-slate-600 hover:text-cyan-400 transition-colors"
209
+ title="Link an already-uploaded file"
210
+ >
211
+ <Link className="w-3 h-3" />
212
+ Link existing
213
+ </button>
214
+ )}
215
+ {uploadError && (
216
+ <span className="text-xs text-red-400" title={uploadError}>
217
+ ⚠ {uploadError}
218
+ </span>
219
+ )}
220
+ </div>
105
221
  </div>
106
222
  );
107
223
  }
@@ -0,0 +1,181 @@
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ X,
4
+ FileText,
5
+ Search,
6
+ Globe,
7
+ BookOpen,
8
+ MessageSquare,
9
+ Link,
10
+ } from "lucide-react";
11
+ import * as api from "../api";
12
+ import type { PickableFile } from "../api";
13
+ import type { ContextFile } from "../types";
14
+
15
+ interface Props {
16
+ /** Files already attached to the target — shown as already linked */
17
+ currentFiles: ContextFile[];
18
+ onLink: (fileId: string, originalName: string) => Promise<void>;
19
+ onClose: () => void;
20
+ }
21
+
22
+ const SOURCE_ICON = {
23
+ workspace: <Globe className="w-3 h-3 text-violet-400 shrink-0" />,
24
+ topic: <BookOpen className="w-3 h-3 text-cyan-400 shrink-0" />,
25
+ question: <MessageSquare className="w-3 h-3 text-slate-400 shrink-0" />,
26
+ };
27
+
28
+ const SOURCE_LABEL = {
29
+ workspace: "Workspace",
30
+ topic: "Topic",
31
+ question: "Question",
32
+ };
33
+
34
+ export default function FilePickerModal({
35
+ currentFiles,
36
+ onLink,
37
+ onClose,
38
+ }: Props) {
39
+ const [files, setFiles] = useState<PickableFile[]>([]);
40
+ const [loading, setLoading] = useState(true);
41
+ const [query, setQuery] = useState("");
42
+ const [linking, setLinking] = useState<string | null>(null);
43
+
44
+ const linkedIds = new Set(currentFiles.map((f) => f.id));
45
+
46
+ useEffect(() => {
47
+ api.fetchAllContextFiles().then((f) => {
48
+ setFiles(f);
49
+ setLoading(false);
50
+ });
51
+ }, []);
52
+
53
+ const filtered = files.filter(
54
+ (f) =>
55
+ f.originalName.toLowerCase().includes(query.toLowerCase()) ||
56
+ f.sourceName.toLowerCase().includes(query.toLowerCase()),
57
+ );
58
+
59
+ // Group by source type → source name
60
+ const groups = new Map<string, PickableFile[]>();
61
+ for (const f of filtered) {
62
+ const key = `${f.source}:${f.sourceName}`;
63
+ if (!groups.has(key)) groups.set(key, []);
64
+ groups.get(key)!.push(f);
65
+ }
66
+
67
+ const handleLink = async (f: PickableFile) => {
68
+ if (linkedIds.has(f.fileId) || linking) return;
69
+ setLinking(f.fileId);
70
+ try {
71
+ await onLink(f.fileId, f.originalName);
72
+ } finally {
73
+ setLinking(null);
74
+ }
75
+ };
76
+
77
+ return (
78
+ <div
79
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
80
+ onClick={(e) => e.target === e.currentTarget && onClose()}
81
+ >
82
+ <div className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md max-h-[80vh] flex flex-col">
83
+ {/* Header */}
84
+ <div className="flex items-center justify-between px-4 py-3 border-b border-slate-800">
85
+ <div className="flex items-center gap-2">
86
+ <Link className="w-4 h-4 text-cyan-400" />
87
+ <span className="text-sm font-semibold text-slate-200">
88
+ Link Existing File
89
+ </span>
90
+ </div>
91
+ <button
92
+ onClick={onClose}
93
+ className="text-slate-500 hover:text-slate-300 transition-colors"
94
+ >
95
+ <X className="w-4 h-4" />
96
+ </button>
97
+ </div>
98
+
99
+ {/* Search */}
100
+ <div className="px-4 py-2 border-b border-slate-800">
101
+ <div className="flex items-center gap-2 bg-slate-800 rounded-lg px-2 py-1.5">
102
+ <Search className="w-3.5 h-3.5 text-slate-500 shrink-0" />
103
+ <input
104
+ autoFocus
105
+ value={query}
106
+ onChange={(e) => setQuery(e.target.value)}
107
+ placeholder="Search files…"
108
+ className="flex-1 bg-transparent text-sm text-slate-200 placeholder-slate-600 focus:outline-none"
109
+ />
110
+ </div>
111
+ </div>
112
+
113
+ {/* File list */}
114
+ <div className="overflow-y-auto flex-1 px-2 py-2">
115
+ {loading && (
116
+ <p className="text-xs text-slate-500 text-center py-6">Loading…</p>
117
+ )}
118
+ {!loading && groups.size === 0 && (
119
+ <p className="text-xs text-slate-500 text-center py-6">
120
+ No files found.
121
+ </p>
122
+ )}
123
+ {!loading &&
124
+ Array.from(groups.entries()).map(([key, items]) => {
125
+ const [sourceType, sourceName] = key.split(":") as [
126
+ PickableFile["source"],
127
+ string,
128
+ ];
129
+ return (
130
+ <div key={key} className="mb-3">
131
+ <div className="flex items-center gap-1.5 px-2 py-0.5 mb-1">
132
+ {SOURCE_ICON[sourceType]}
133
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-slate-500">
134
+ {SOURCE_LABEL[sourceType]}: {sourceName}
135
+ </span>
136
+ </div>
137
+ {items.map((f) => {
138
+ const alreadyLinked = linkedIds.has(f.fileId);
139
+ const isLinking = linking === f.fileId;
140
+ return (
141
+ <button
142
+ key={f.fileId}
143
+ onClick={() => handleLink(f)}
144
+ disabled={alreadyLinked || !!linking}
145
+ className={`w-full flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm transition-colors ${
146
+ alreadyLinked
147
+ ? "text-slate-600 cursor-default"
148
+ : "text-slate-300 hover:bg-slate-800 cursor-pointer"
149
+ }`}
150
+ >
151
+ <FileText className="w-3.5 h-3.5 shrink-0 text-violet-400" />
152
+ <span className="truncate flex-1 text-left">
153
+ {f.originalName}
154
+ </span>
155
+ {alreadyLinked && (
156
+ <span className="text-[10px] text-slate-600 shrink-0">
157
+ linked
158
+ </span>
159
+ )}
160
+ {isLinking && (
161
+ <span className="text-[10px] text-cyan-500 shrink-0">
162
+ linking…
163
+ </span>
164
+ )}
165
+ </button>
166
+ );
167
+ })}
168
+ </div>
169
+ );
170
+ })}
171
+ </div>
172
+
173
+ <div className="px-4 py-2 border-t border-slate-800">
174
+ <p className="text-[10px] text-slate-600">
175
+ Linked files share content with the original — no re-upload needed.
176
+ </p>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ }