create-interview-cockpit 0.1.0 → 0.2.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/index.js +13 -3
- package/package.json +1 -1
- package/template/client/src/App.tsx +7 -2
- package/template/client/src/api.ts +190 -1
- package/template/client/src/components/ChatMessage.tsx +27 -1
- package/template/client/src/components/ChatView.tsx +110 -6
- package/template/client/src/components/Sidebar.tsx +342 -182
- package/template/client/src/components/WorkspaceSwitcher.tsx +891 -0
- package/template/client/src/store.ts +188 -1
- package/template/client/src/types.ts +20 -0
- package/template/cockpit.json +1 -1
- package/template/server/package-lock.json +286 -0
- package/template/server/package.json +1 -0
- package/template/server/src/google-drive.ts +714 -0
- package/template/server/src/index.ts +193 -4
- package/template/server/src/storage.ts +332 -32
|
@@ -25,10 +25,11 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
25
25
|
import { randomUUID } from "crypto";
|
|
26
26
|
import fs from "fs/promises";
|
|
27
27
|
import * as storage from "./storage.js";
|
|
28
|
+
import * as googleDrive from "./google-drive.js";
|
|
28
29
|
|
|
29
30
|
const app = express();
|
|
30
31
|
app.use(cors());
|
|
31
|
-
app.use(express.json({ limit: "
|
|
32
|
+
app.use(express.json({ limit: "25mb" }));
|
|
32
33
|
|
|
33
34
|
const upload = multer({
|
|
34
35
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
|
|
@@ -108,6 +109,191 @@ function getTextFromUIMessage(message: any): string {
|
|
|
108
109
|
return "";
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// ─── Workspaces ──────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
app.get("/api/workspaces", async (_req, res) => {
|
|
115
|
+
const registry = await storage.getWorkspaces();
|
|
116
|
+
res.json(registry);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.post("/api/workspaces", async (req, res) => {
|
|
120
|
+
const { name, type } = req.body;
|
|
121
|
+
if (!name || !type)
|
|
122
|
+
return res.status(400).json({ error: "name and type required" });
|
|
123
|
+
const ws: storage.WorkspaceMeta = {
|
|
124
|
+
id: randomUUID(),
|
|
125
|
+
name,
|
|
126
|
+
type: type === "google_drive" ? "google_drive" : "local",
|
|
127
|
+
createdAt: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
await storage.saveWorkspace(ws);
|
|
130
|
+
const registry = await storage.getWorkspaces();
|
|
131
|
+
res.json(registry);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
app.patch("/api/workspaces/:id", async (req, res) => {
|
|
135
|
+
const ws = await storage.updateWorkspace(req.params.id, req.body);
|
|
136
|
+
if (!ws) return res.status(404).json({ error: "Not found" });
|
|
137
|
+
const registry = await storage.getWorkspaces();
|
|
138
|
+
res.json(registry);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
app.delete("/api/workspaces/:id", async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
await storage.deleteWorkspace(req.params.id);
|
|
144
|
+
const registry = await storage.getWorkspaces();
|
|
145
|
+
res.json(registry);
|
|
146
|
+
} catch (err: any) {
|
|
147
|
+
res.status(400).json({ error: err?.message || "Failed to delete" });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.post("/api/workspaces/:id/activate", async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const registry = await storage.activateWorkspace(req.params.id);
|
|
154
|
+
res.json(registry);
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
res.status(400).json({ error: err?.message || "Failed to activate" });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
app.post("/api/workspaces/:id/sync", async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
// Ensure all storage calls target this workspace (guards against server restarts
|
|
163
|
+
// that reset the in-memory _activeWorkspaceId).
|
|
164
|
+
storage.setActiveWorkspaceId(req.params.id);
|
|
165
|
+
const result = await googleDrive.syncWorkspace(req.params.id, extractText);
|
|
166
|
+
res.json(result);
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
console.error("[sync]", err);
|
|
169
|
+
res.status(500).json({ error: err?.message || "Sync failed" });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
app.get("/api/workspaces/:id/drive-subfolders", async (req, res) => {
|
|
174
|
+
try {
|
|
175
|
+
const folders = await googleDrive.listDriveSubfolders(req.params.id);
|
|
176
|
+
res.json(folders);
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
res
|
|
179
|
+
.status(500)
|
|
180
|
+
.json({ error: err?.message || "Failed to list subfolders" });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.post("/api/workspaces/:id/drive-subfolders", async (req, res) => {
|
|
185
|
+
const { name } = req.body as { name?: string };
|
|
186
|
+
if (!name?.trim()) return res.status(400).json({ error: "name required" });
|
|
187
|
+
try {
|
|
188
|
+
const folder = await googleDrive.createDriveSubfolder(
|
|
189
|
+
req.params.id,
|
|
190
|
+
name.trim(),
|
|
191
|
+
);
|
|
192
|
+
res.json(folder);
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
res.status(500).json({ error: err?.message || "Failed to create folder" });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.get("/api/drive/export-auth", (_req, res) => {
|
|
199
|
+
res.redirect(googleDrive.getExportAuthUrl());
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
app.get("/api/drive/export-callback", async (req, res) => {
|
|
203
|
+
const code = req.query.code as string | undefined;
|
|
204
|
+
if (!code) return res.status(400).send("Missing code parameter");
|
|
205
|
+
try {
|
|
206
|
+
await googleDrive.handleExportCallback(code);
|
|
207
|
+
const clientUrl = process.env.CLIENT_URL ?? "http://localhost:5173";
|
|
208
|
+
res.redirect(`${clientUrl}/?export_authed=1`);
|
|
209
|
+
} catch (err: any) {
|
|
210
|
+
res
|
|
211
|
+
.status(500)
|
|
212
|
+
.send(`OAuth callback failed: ${err?.message || "unknown error"}`);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
app.post("/api/workspaces/:id/export-drive", async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
if (!(await googleDrive.isExportAuthed())) {
|
|
219
|
+
return res.json({
|
|
220
|
+
needsAuth: true,
|
|
221
|
+
authUrl: googleDrive.getExportAuthUrl(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const { targetFolderId } = req.body as { targetFolderId?: string };
|
|
225
|
+
const result = await googleDrive.exportWorkspace(
|
|
226
|
+
req.params.id,
|
|
227
|
+
targetFolderId,
|
|
228
|
+
);
|
|
229
|
+
res.json(result);
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
console.error("[export-drive]", err);
|
|
232
|
+
res.status(500).json({ error: err?.message || "Export failed" });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
app.post("/api/drive/repair-permissions", async (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
const { folderId } = req.body as { folderId: string };
|
|
239
|
+
if (!folderId) return res.status(400).json({ error: "folderId required" });
|
|
240
|
+
const fixed = await googleDrive.repairDriveFolderPermissions(folderId);
|
|
241
|
+
res.json({ fixed });
|
|
242
|
+
} catch (err: any) {
|
|
243
|
+
console.error("[repair-permissions]", err);
|
|
244
|
+
res.status(500).json({ error: err?.message || "Repair failed" });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
app.post("/api/workspaces/:id/link-drive", async (req, res) => {
|
|
249
|
+
const { url } = req.body as { url?: string };
|
|
250
|
+
if (!url) return res.status(400).json({ error: "url required" });
|
|
251
|
+
const folderId = googleDrive.extractFolderIdFromUrl(url);
|
|
252
|
+
if (!folderId)
|
|
253
|
+
return res.status(400).json({
|
|
254
|
+
error:
|
|
255
|
+
"Could not extract folder ID from URL. Make sure it is a Google Drive folder share link.",
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
const meta = await googleDrive.getFolderMeta(folderId);
|
|
259
|
+
await storage.updateWorkspace(req.params.id, {
|
|
260
|
+
driveConfig: { folderId: meta.id, folderName: meta.name },
|
|
261
|
+
});
|
|
262
|
+
// Return available folders so the client can let the user pick which to sync
|
|
263
|
+
const folders = await googleDrive.listDriveSubfolders(req.params.id);
|
|
264
|
+
const registry = await storage.getWorkspaces();
|
|
265
|
+
res.json({ registry, folders });
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
res
|
|
268
|
+
.status(500)
|
|
269
|
+
.json({ error: err?.message || "Failed to link Drive folder" });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/** Attach a Drive folder to any workspace type WITHOUT syncing (for local workspaces). */
|
|
274
|
+
app.post("/api/workspaces/:id/attach-drive", async (req, res) => {
|
|
275
|
+
const { url } = req.body as { url?: string };
|
|
276
|
+
if (!url) return res.status(400).json({ error: "url required" });
|
|
277
|
+
const folderId = googleDrive.extractFolderIdFromUrl(url);
|
|
278
|
+
if (!folderId)
|
|
279
|
+
return res.status(400).json({
|
|
280
|
+
error:
|
|
281
|
+
"Could not extract folder ID from URL. Make sure it is a Google Drive folder share link.",
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
const meta = await googleDrive.getFolderMeta(folderId);
|
|
285
|
+
await storage.updateWorkspace(req.params.id, {
|
|
286
|
+
driveConfig: { folderId: meta.id, folderName: meta.name },
|
|
287
|
+
});
|
|
288
|
+
const registry = await storage.getWorkspaces();
|
|
289
|
+
res.json({ registry });
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
res
|
|
292
|
+
.status(500)
|
|
293
|
+
.json({ error: err?.message || "Failed to attach Drive folder" });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
111
297
|
// ─── Topics ──────────────────────────────────────────────
|
|
112
298
|
|
|
113
299
|
app.get("/api/topics", async (_req, res) => {
|
|
@@ -753,6 +939,9 @@ app.post("/api/fix-diagram", async (req, res) => {
|
|
|
753
939
|
|
|
754
940
|
// ─── Start ───────────────────────────────────────────────
|
|
755
941
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
942
|
+
(async () => {
|
|
943
|
+
await storage.migrateToWorkspaces();
|
|
944
|
+
app.listen(PORT, () => {
|
|
945
|
+
console.log(`\n ✈ Interview Cockpit server → http://localhost:${PORT}\n`);
|
|
946
|
+
});
|
|
947
|
+
})();
|