create-interview-cockpit 0.5.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 (29) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +734 -1
  3. package/template/client/package.json +1 -0
  4. package/template/client/src/App.tsx +3 -0
  5. package/template/client/src/api.ts +321 -4
  6. package/template/client/src/components/AiSettingsModal.tsx +818 -425
  7. package/template/client/src/components/ChatMessage.tsx +34 -12
  8. package/template/client/src/components/ChatView.tsx +298 -121
  9. package/template/client/src/components/CodeContextPanel.tsx +419 -2
  10. package/template/client/src/components/CodeRunnerModal.tsx +1601 -120
  11. package/template/client/src/components/DocRefModal.tsx +55 -6
  12. package/template/client/src/components/FileAttachments.tsx +20 -4
  13. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  14. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  15. package/template/client/src/components/MarkdownRenderer.tsx +22 -8
  16. package/template/client/src/components/NotesModal.tsx +977 -0
  17. package/template/client/src/components/PlotEmbed.tsx +173 -0
  18. package/template/client/src/components/Sidebar.tsx +184 -0
  19. package/template/client/src/components/VizCraftEmbed.tsx +257 -13
  20. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  21. package/template/client/src/infraLab.ts +124 -0
  22. package/template/client/src/reactLab.ts +477 -0
  23. package/template/client/src/store.ts +219 -6
  24. package/template/client/src/types.ts +35 -3
  25. package/template/client/tsconfig.tsbuildinfo +1 -1
  26. package/template/server/src/google-drive.ts +37 -3
  27. package/template/server/src/index.ts +693 -52
  28. package/template/server/src/infra-runner.ts +1104 -0
  29. package/template/server/src/storage.ts +13 -3
@@ -0,0 +1,128 @@
1
+ import { useEffect, useState } from "react";
2
+ import { X, MessageSquare, Check, Loader2 } from "lucide-react";
3
+ import type { Question } from "../types";
4
+ import { useStore } from "../store";
5
+
6
+ interface Props {
7
+ question: Question;
8
+ topicId: string;
9
+ onClose: () => void;
10
+ }
11
+
12
+ export default function LinkedConvosPicker({
13
+ question,
14
+ topicId,
15
+ onClose,
16
+ }: Props) {
17
+ const { linkConversation, unlinkConversation } = useStore();
18
+ const [siblings, setSiblings] = useState<Question[]>([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [pending, setPending] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ fetch(`/api/topics/${topicId}/questions`)
24
+ .then((r) => r.json())
25
+ .then((qs: Question[]) => {
26
+ // Exclude the current question itself
27
+ setSiblings(qs.filter((q2) => q2.id !== question.id));
28
+ })
29
+ .finally(() => setLoading(false));
30
+ }, [topicId, question.id]);
31
+
32
+ const linked = new Set(question.linkedConversationIds ?? []);
33
+
34
+ const toggle = async (targetId: string) => {
35
+ setPending(targetId);
36
+ try {
37
+ if (linked.has(targetId)) {
38
+ await unlinkConversation(question.id, targetId);
39
+ } else {
40
+ await linkConversation(question.id, targetId);
41
+ }
42
+ } finally {
43
+ setPending(null);
44
+ }
45
+ };
46
+
47
+ return (
48
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
49
+ <div className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[70vh]">
50
+ {/* Header */}
51
+ <div className="flex items-center gap-2 px-4 py-3 border-b border-slate-700 shrink-0">
52
+ <MessageSquare className="w-4 h-4 text-violet-400 shrink-0" />
53
+ <span className="text-sm font-semibold text-slate-200 flex-1">
54
+ Link conversations as context
55
+ </span>
56
+ <button
57
+ onClick={onClose}
58
+ className="p-1 rounded hover:bg-slate-700 text-slate-500 hover:text-slate-300 transition-colors"
59
+ >
60
+ <X className="w-4 h-4" />
61
+ </button>
62
+ </div>
63
+
64
+ {/* Body */}
65
+ <div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-1">
66
+ {loading && (
67
+ <div className="flex items-center justify-center py-8">
68
+ <Loader2 className="w-5 h-5 text-violet-400 animate-spin" />
69
+ </div>
70
+ )}
71
+ {!loading && siblings.length === 0 && (
72
+ <p className="text-xs text-slate-500 text-center py-8">
73
+ No other conversations in this topic yet.
74
+ </p>
75
+ )}
76
+ {!loading &&
77
+ siblings.map((sibling) => {
78
+ const isLinked = linked.has(sibling.id);
79
+ const isPending = pending === sibling.id;
80
+ const msgCount = sibling.messages?.length ?? 0;
81
+ return (
82
+ <button
83
+ key={sibling.id}
84
+ onClick={() => toggle(sibling.id)}
85
+ disabled={isPending}
86
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
87
+ isLinked
88
+ ? "bg-violet-500/15 border border-violet-500/30 hover:bg-violet-500/20"
89
+ : "bg-slate-800/50 border border-transparent hover:bg-slate-800"
90
+ }`}
91
+ >
92
+ <div
93
+ className={`w-4 h-4 rounded border shrink-0 flex items-center justify-center transition-colors ${
94
+ isLinked
95
+ ? "bg-violet-500 border-violet-500"
96
+ : "border-slate-600"
97
+ }`}
98
+ >
99
+ {isPending ? (
100
+ <Loader2 className="w-2.5 h-2.5 animate-spin text-white" />
101
+ ) : isLinked ? (
102
+ <Check className="w-2.5 h-2.5 text-white" />
103
+ ) : null}
104
+ </div>
105
+ <div className="flex-1 min-w-0">
106
+ <p className="text-xs font-medium text-slate-200 truncate">
107
+ {sibling.title}
108
+ </p>
109
+ <p className="text-[10px] text-slate-500 mt-0.5">
110
+ {msgCount} message{msgCount !== 1 ? "s" : ""}
111
+ </p>
112
+ </div>
113
+ </button>
114
+ );
115
+ })}
116
+ </div>
117
+
118
+ {/* Footer hint */}
119
+ <div className="px-4 py-2.5 border-t border-slate-800 shrink-0">
120
+ <p className="text-[10px] text-slate-500">
121
+ Linked conversations are included as read-only background context
122
+ for the AI.
123
+ </p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -4,6 +4,7 @@ import { useMemo, useRef, useEffect, useState } from "react";
4
4
  import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
5
5
  import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
6
6
  import MermaidDiagram from "./MermaidDiagram";
7
+ import PlotEmbed from "./PlotEmbed";
7
8
  import VizCraftEmbed from "./VizCraftEmbed";
8
9
  import { useStore } from "../store";
9
10
  import { Bookmark, ExternalLink, Play, Save, Check } from "lucide-react";
@@ -56,15 +57,24 @@ function InlineCodeBlock({
56
57
  const stableIdRef = useRef(`inline:${crypto.randomUUID()}`);
57
58
  const finalId = parsed.id ?? stableIdRef.current;
58
59
 
59
- // Register on mount when there is an explicit @ref so inlineref:// links resolve
60
+ // Register on mount when there is an explicit @ref so inlineref:// links resolve.
61
+ // Guard: skip the store update if the entry is already registered with the same
62
+ // content — this prevents an infinite render loop when the component remounts
63
+ // because a parent re-renders (each store update would otherwise remount this,
64
+ // triggering the effect again, updating the store again…).
60
65
  useEffect(() => {
61
- if (parsed.id) {
62
- registerInlineCode(parsed.id, {
63
- content: parsed.displayCode,
64
- language,
65
- label: parsed.label,
66
- });
67
- }
66
+ if (!parsed.id) return;
67
+ const existing = useStore.getState().inlineCodeSnippets[parsed.id];
68
+ if (
69
+ existing?.content === parsed.displayCode &&
70
+ existing?.language === language
71
+ )
72
+ return;
73
+ registerInlineCode(parsed.id, {
74
+ content: parsed.displayCode,
75
+ language,
76
+ label: parsed.label,
77
+ });
68
78
  // eslint-disable-next-line react-hooks/exhaustive-deps
69
79
  }, [parsed.id]);
70
80
 
@@ -169,6 +179,10 @@ const markdownComponents: Components = {
169
179
  return <MermaidDiagram chart={codeString} />;
170
180
  }
171
181
 
182
+ if (lang === "plot" || lang === "vega" || lang === "vega-lite") {
183
+ return <PlotEmbed spec={codeString} />;
184
+ }
185
+
172
186
  if (lang === "viz") {
173
187
  return <VizCraftEmbed spec={codeString} />;
174
188
  }