create-interview-cockpit 0.2.0 → 0.4.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/README.md +23 -0
- package/package.json +1 -1
- package/template/client/package-lock.json +23 -0
- package/template/client/package.json +2 -0
- package/template/client/src/App.tsx +48 -12
- package/template/client/src/api.ts +39 -0
- package/template/client/src/components/AiSettingsModal.tsx +827 -0
- package/template/client/src/components/ChatView.tsx +173 -136
- package/template/client/src/components/MarkdownRenderer.tsx +5 -0
- package/template/client/src/components/Sidebar.tsx +3 -1
- package/template/client/src/components/VizCraftEmbed.tsx +502 -0
- package/template/client/src/store.ts +76 -0
- package/template/client/src/types.ts +1 -0
- package/template/cockpit.json +1 -1
- package/template/data/ai-settings.json +49 -0
- package/template/package.json +1 -1
- package/template/server/src/index.ts +84 -34
- package/template/server/src/storage.ts +96 -0
|
@@ -447,6 +447,37 @@ app.delete(
|
|
|
447
447
|
},
|
|
448
448
|
);
|
|
449
449
|
|
|
450
|
+
// ─── AI Settings ────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
app.get("/api/settings", async (_req, res) => {
|
|
453
|
+
const settings = await storage.getAiSettings();
|
|
454
|
+
res.json(settings);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
app.patch("/api/settings", async (req, res) => {
|
|
458
|
+
const current = await storage.getAiSettings();
|
|
459
|
+
const updated: storage.AiSettings = {
|
|
460
|
+
systemPrompt:
|
|
461
|
+
typeof req.body.systemPrompt === "string"
|
|
462
|
+
? req.body.systemPrompt
|
|
463
|
+
: current.systemPrompt,
|
|
464
|
+
responseProfiles:
|
|
465
|
+
req.body.responseProfiles != null
|
|
466
|
+
? req.body.responseProfiles
|
|
467
|
+
: current.responseProfiles,
|
|
468
|
+
vizGuide:
|
|
469
|
+
typeof req.body.vizGuide === "string"
|
|
470
|
+
? req.body.vizGuide
|
|
471
|
+
: current.vizGuide,
|
|
472
|
+
promptGroups:
|
|
473
|
+
req.body.promptGroups != null
|
|
474
|
+
? req.body.promptGroups
|
|
475
|
+
: current.promptGroups,
|
|
476
|
+
};
|
|
477
|
+
await storage.saveAiSettings(updated);
|
|
478
|
+
res.json(updated);
|
|
479
|
+
});
|
|
480
|
+
|
|
450
481
|
// ─── Chat ────────────────────────────────────────────────
|
|
451
482
|
|
|
452
483
|
app.post("/api/chat", async (req, res) => {
|
|
@@ -462,38 +493,12 @@ app.post("/api/chat", async (req, res) => {
|
|
|
462
493
|
responseLength,
|
|
463
494
|
} = req.body;
|
|
464
495
|
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
concise: {
|
|
470
|
-
maxOutputTokens: 1000,
|
|
471
|
-
maxSteps: 3,
|
|
472
|
-
},
|
|
473
|
-
moderate: {
|
|
474
|
-
maxOutputTokens: 1000,
|
|
475
|
-
maxSteps: 5,
|
|
476
|
-
},
|
|
477
|
-
normal: {
|
|
478
|
-
maxOutputTokens: 3000,
|
|
479
|
-
maxSteps: 5,
|
|
480
|
-
},
|
|
481
|
-
};
|
|
482
|
-
const selectedResponseProfile =
|
|
483
|
-
responseProfiles[responseLength] || responseProfiles.normal;
|
|
484
|
-
|
|
485
|
-
let system = `You are a senior engineering interview coach.
|
|
496
|
+
const aiSettings = await storage.getAiSettings();
|
|
497
|
+
const { responseProfiles, vizGuide } = aiSettings;
|
|
498
|
+
const selectedResponseProfile = responseProfiles[responseLength] ??
|
|
499
|
+
responseProfiles["normal"] ?? { maxOutputTokens: 3000, maxSteps: 5 };
|
|
486
500
|
|
|
487
|
-
|
|
488
|
-
Explain clearly, accurately, and practically.
|
|
489
|
-
Only include Mermaid diagrams, code blocks, or tables when the user explicitly asks for them or when they materially improve the answer.
|
|
490
|
-
If you show code, use a fenced code block with the correct language.
|
|
491
|
-
|
|
492
|
-
Mermaid syntax rules (follow strictly):
|
|
493
|
-
- Wrap node labels in quotes when they contain special characters: A["Microservice A (Producer)"]
|
|
494
|
-
- Edge labels use |text| syntax: A -->|sends message| B
|
|
495
|
-
- Never put parentheses or brackets inside [] without quoting the label
|
|
496
|
-
- Use simple node IDs (letters/numbers) and put descriptive text in the label`;
|
|
501
|
+
let system = aiSettings.systemPrompt;
|
|
497
502
|
|
|
498
503
|
if (topicTitle || questionTitle) {
|
|
499
504
|
system += `\n\n--- Current Context ---`;
|
|
@@ -625,8 +630,14 @@ Only use coderef:// for [code] files. Do not use it for uploaded documents.`;
|
|
|
625
630
|
}),
|
|
626
631
|
system,
|
|
627
632
|
messages: modelMessages,
|
|
628
|
-
tools:
|
|
629
|
-
|
|
633
|
+
tools: {
|
|
634
|
+
getVizGuide: tool({
|
|
635
|
+
description:
|
|
636
|
+
"Get the full viz diagram spec reference. Call this before writing a ```viz block so you have the correct schema, rules, and examples.",
|
|
637
|
+
inputSchema: z.object({}),
|
|
638
|
+
execute: async () => ({ guide: vizGuide }),
|
|
639
|
+
}),
|
|
640
|
+
...(fileRegistry.size > 0
|
|
630
641
|
? {
|
|
631
642
|
readFile: tool({
|
|
632
643
|
description:
|
|
@@ -650,7 +661,8 @@ Only use coderef:// for [code] files. Do not use it for uploaded documents.`;
|
|
|
650
661
|
},
|
|
651
662
|
}),
|
|
652
663
|
}
|
|
653
|
-
:
|
|
664
|
+
: {}),
|
|
665
|
+
},
|
|
654
666
|
stopWhen: stepCountIs(selectedResponseProfile.maxSteps),
|
|
655
667
|
});
|
|
656
668
|
|
|
@@ -666,6 +678,7 @@ Only use coderef:// for [code] files. Do not use it for uploaded documents.`;
|
|
|
666
678
|
id: message.id || randomUUID(),
|
|
667
679
|
role: message.role,
|
|
668
680
|
content: getTextFromUIMessage(message),
|
|
681
|
+
...(Array.isArray(message.parts) ? { parts: message.parts } : {}),
|
|
669
682
|
}));
|
|
670
683
|
|
|
671
684
|
await storage.updateQuestionMessages(questionId, normalized);
|
|
@@ -899,6 +912,43 @@ Their question: "${prompt}"`,
|
|
|
899
912
|
}
|
|
900
913
|
});
|
|
901
914
|
|
|
915
|
+
// ─── Fix Viz ────────────────────────────────────────────
|
|
916
|
+
|
|
917
|
+
app.post("/api/fix-viz", async (req, res) => {
|
|
918
|
+
const { spec, error: renderError } = req.body;
|
|
919
|
+
if (typeof spec !== "string" || !spec.trim()) {
|
|
920
|
+
return res.status(400).json({ error: "spec is required" });
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
try {
|
|
924
|
+
const isGoogle = ["google", "gemini"].includes(
|
|
925
|
+
(process.env.AI_PROVIDER || "openai").toLowerCase(),
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const { text } = await generateText({
|
|
929
|
+
model: getModel(),
|
|
930
|
+
maxOutputTokens: 1200,
|
|
931
|
+
...(isGoogle && {
|
|
932
|
+
providerOptions: {
|
|
933
|
+
google: { thinkingConfig: { thinkingBudget: 0 } },
|
|
934
|
+
},
|
|
935
|
+
}),
|
|
936
|
+
prompt: `The following viz diagram spec failed to render.${renderError ? `\n\nRender error:\n${renderError}` : ""}\n\nFix the spec so it renders correctly. Common issues to check:\n- x/y coordinates must be numbers, not strings\n- node ids must be kebab-case with no spaces\n- chain arrays in autoSignals/signals must reference valid node ids\n- YAML flow sequences ([a, b]) must be correctly indented and closed\n- Do NOT mix autoSignals and steps in the same spec\n- YAML string quoting: if a label or value contains special characters (hyphens like --, colons, brackets, slashes, or leading/trailing spaces), wrap the ENTIRE value in double quotes. Do NOT use single quotes — single-quoted YAML strings must be self-contained; trailing words after the closing quote cause parse errors. Example of wrong: label: 'update-index --skip-worktree' changes file metadata Example of correct: label: "update-index --skip-worktree changes file metadata"\n- Never split a label across multiple YAML lines unless using a literal block scalar (| or >)\n\nReturn ONLY the corrected YAML (or JSON) spec with no explanation and no markdown fences — just the raw spec.\n\nFailed spec:\n${spec}`,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Strip any fences the model might have added
|
|
940
|
+
const fixed = text
|
|
941
|
+
.replace(/^```(?:yaml|json|viz)?\s*/i, "")
|
|
942
|
+
.replace(/```\s*$/, "")
|
|
943
|
+
.trim();
|
|
944
|
+
|
|
945
|
+
res.json({ spec: fixed });
|
|
946
|
+
} catch (err: any) {
|
|
947
|
+
console.error("fix-viz error:", err?.message || err);
|
|
948
|
+
res.status(500).json({ error: err?.message || "Failed to fix viz" });
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
902
952
|
// ─── Fix Diagram ────────────────────────────────────────
|
|
903
953
|
|
|
904
954
|
app.post("/api/fix-diagram", async (req, res) => {
|
|
@@ -57,6 +57,7 @@ export interface Message {
|
|
|
57
57
|
id: string;
|
|
58
58
|
role: string;
|
|
59
59
|
content: string;
|
|
60
|
+
parts?: any[];
|
|
60
61
|
createdAt?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -601,3 +602,98 @@ export async function updateQuestionMessages(
|
|
|
601
602
|
q.messages = messages;
|
|
602
603
|
await saveQuestion(q);
|
|
603
604
|
}
|
|
605
|
+
|
|
606
|
+
// ── AI Settings ───────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
const AI_SETTINGS_FILE = path.join(DATA_DIR, "ai-settings.json");
|
|
609
|
+
|
|
610
|
+
export interface ResponseProfile {
|
|
611
|
+
maxOutputTokens: number;
|
|
612
|
+
maxSteps: number;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export interface PromptGroup {
|
|
616
|
+
label: string;
|
|
617
|
+
description?: string;
|
|
618
|
+
default: string;
|
|
619
|
+
options: Record<string, string>;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export interface AiSettings {
|
|
623
|
+
/** Base system prompt sent to the AI on every chat turn. */
|
|
624
|
+
systemPrompt: string;
|
|
625
|
+
/** Per-length-preference token and step limits. */
|
|
626
|
+
responseProfiles: Record<string, ResponseProfile>;
|
|
627
|
+
/** Full viz diagram spec reference — returned by the getVizGuide tool. */
|
|
628
|
+
vizGuide: string;
|
|
629
|
+
/** All user-selectable prompt groups. Add new entries here to extend the UI. */
|
|
630
|
+
promptGroups: Record<string, PromptGroup>;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const DEFAULT_AI_SETTINGS: AiSettings = {
|
|
634
|
+
systemPrompt:
|
|
635
|
+
"You are a senior engineering interview coach.\n\nExplain clearly, accurately, and practically.",
|
|
636
|
+
responseProfiles: {
|
|
637
|
+
concise: { maxOutputTokens: 1000, maxSteps: 3 },
|
|
638
|
+
moderate: { maxOutputTokens: 1000, maxSteps: 5 },
|
|
639
|
+
normal: { maxOutputTokens: 3000, maxSteps: 5 },
|
|
640
|
+
},
|
|
641
|
+
vizGuide: "No viz guide configured.",
|
|
642
|
+
promptGroups: {
|
|
643
|
+
length: {
|
|
644
|
+
label: "Response Length",
|
|
645
|
+
description:
|
|
646
|
+
"Appended to the user message when the selected length changes.",
|
|
647
|
+
default: "normal",
|
|
648
|
+
options: {
|
|
649
|
+
concise: "Keep the response concise.",
|
|
650
|
+
moderate: "Keep the response moderately detailed.",
|
|
651
|
+
normal:
|
|
652
|
+
"Use a fuller answer with enough context to explain the idea clearly.",
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
style: {
|
|
656
|
+
label: "Response Style",
|
|
657
|
+
description:
|
|
658
|
+
"Appended to the user message when the selected style changes.",
|
|
659
|
+
default: "prose",
|
|
660
|
+
options: {
|
|
661
|
+
prose: "Use natural prose with short paragraphs.",
|
|
662
|
+
bullets: "Use bullet points and short lists as the main format.",
|
|
663
|
+
structured:
|
|
664
|
+
"Use structured sections with headings and numbered steps when helpful.",
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
audience: {
|
|
668
|
+
label: "Response Audience",
|
|
669
|
+
description:
|
|
670
|
+
"Appended to the user message when the selected audience changes.",
|
|
671
|
+
default: "normal",
|
|
672
|
+
options: {
|
|
673
|
+
normal: "",
|
|
674
|
+
beginner:
|
|
675
|
+
"When using technical terms or abbreviations, immediately expand their meaning in square brackets.",
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
export async function getAiSettings(): Promise<AiSettings> {
|
|
682
|
+
try {
|
|
683
|
+
const raw = await fs.readFile(AI_SETTINGS_FILE, "utf-8");
|
|
684
|
+
return {
|
|
685
|
+
...DEFAULT_AI_SETTINGS,
|
|
686
|
+
...(JSON.parse(raw) as Partial<AiSettings>),
|
|
687
|
+
};
|
|
688
|
+
} catch {
|
|
689
|
+
return DEFAULT_AI_SETTINGS;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export async function saveAiSettings(settings: AiSettings): Promise<void> {
|
|
694
|
+
await fs.writeFile(
|
|
695
|
+
AI_SETTINGS_FILE,
|
|
696
|
+
JSON.stringify(settings, null, 2),
|
|
697
|
+
"utf-8",
|
|
698
|
+
);
|
|
699
|
+
}
|