create-interview-cockpit 0.3.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.
@@ -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 responseProfiles: Record<
466
- string,
467
- { maxOutputTokens: number; maxSteps: number }
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
- Highest priority: follow the user's explicit response preferences and current conversation context. If they conflict with your default teaching behavior, the user's preference wins.
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
- fileRegistry.size > 0
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
- : undefined,
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
+ }