create-interview-cockpit 0.4.0 → 0.5.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 +19 -0
- package/template/client/package.json +3 -0
- package/template/client/src/App.tsx +17 -0
- package/template/client/src/api.ts +135 -0
- package/template/client/src/components/AiSettingsModal.tsx +218 -4
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +110 -27
- package/template/client/src/components/ChatView.tsx +69 -4
- package/template/client/src/components/CodeContextPanel.tsx +297 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
- package/template/client/src/components/DocRefModal.tsx +502 -0
- package/template/client/src/components/FileAttachments.tsx +109 -9
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/MarkdownRenderer.tsx +205 -2
- package/template/client/src/components/Sidebar.tsx +213 -127
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +162 -19
- package/template/client/src/store.ts +201 -0
- package/template/client/src/types.ts +8 -0
- package/template/cockpit.json +1 -1
- package/template/package.json +1 -1
- package/template/server/src/google-drive.ts +109 -1
- package/template/server/src/index.ts +1107 -46
- package/template/server/src/storage.ts +263 -2
|
@@ -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
|
+
}
|