create-interview-cockpit 0.4.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 (36) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +753 -1
  3. package/template/client/package.json +4 -0
  4. package/template/client/src/App.tsx +20 -0
  5. package/template/client/src/api.ts +455 -3
  6. package/template/client/src/components/AiSettingsModal.tsx +855 -248
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +132 -27
  9. package/template/client/src/components/ChatView.tsx +365 -123
  10. package/template/client/src/components/CodeContextPanel.tsx +714 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
  13. package/template/client/src/components/DocRefModal.tsx +551 -0
  14. package/template/client/src/components/FileAttachments.tsx +128 -12
  15. package/template/client/src/components/FilePickerModal.tsx +181 -0
  16. package/template/client/src/components/FileViewerModal.tsx +406 -28
  17. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  18. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  19. package/template/client/src/components/MarkdownRenderer.tsx +219 -2
  20. package/template/client/src/components/NotesModal.tsx +977 -0
  21. package/template/client/src/components/PlotEmbed.tsx +173 -0
  22. package/template/client/src/components/Sidebar.tsx +397 -127
  23. package/template/client/src/components/TextAnnotator.tsx +8 -15
  24. package/template/client/src/components/VizCraftEmbed.tsx +412 -25
  25. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  26. package/template/client/src/infraLab.ts +124 -0
  27. package/template/client/src/reactLab.ts +477 -0
  28. package/template/client/src/store.ts +416 -2
  29. package/template/client/src/types.ts +41 -1
  30. package/template/client/tsconfig.tsbuildinfo +1 -1
  31. package/template/cockpit.json +1 -1
  32. package/template/package.json +1 -1
  33. package/template/server/src/google-drive.ts +144 -2
  34. package/template/server/src/index.ts +1890 -188
  35. package/template/server/src/infra-runner.ts +1104 -0
  36. package/template/server/src/storage.ts +274 -3
@@ -16,9 +16,7 @@ interface Props {
16
16
  onUpdate: (updated: Annotation) => void;
17
17
  messageContent: string;
18
18
  initialPos?: { x: number; y: number };
19
- responseLength?: string;
20
- responseStyle?: string;
21
- responseAudience?: string;
19
+ preferenceSuffix?: string;
22
20
  }
23
21
 
24
22
  export default function AnnotationDialog({
@@ -27,9 +25,7 @@ export default function AnnotationDialog({
27
25
  onUpdate,
28
26
  messageContent,
29
27
  initialPos,
30
- responseLength,
31
- responseStyle,
32
- responseAudience,
28
+ preferenceSuffix,
33
29
  }: Props) {
34
30
  const [pos, setPos] = useState(() => ({
35
31
  x: initialPos?.x ?? Math.max(0, (window.innerWidth - DEFAULT_W) / 2),
@@ -169,9 +165,7 @@ export default function AnnotationDialog({
169
165
  messageContent,
170
166
  priorResponse: ann.response,
171
167
  followUps: ann.followUps ?? [],
172
- responseLength,
173
- responseStyle,
174
- responseAudience,
168
+ preferenceSuffix,
175
169
  }),
176
170
  });
177
171
  const data = await res.json();
@@ -1,6 +1,15 @@
1
- import { memo } from "react";
1
+ import { memo, useState, useCallback } from "react";
2
2
  import type { UIMessage } from "ai";
3
- import { User, Bot } from "lucide-react";
3
+ import {
4
+ User,
5
+ Bot,
6
+ Copy,
7
+ Check,
8
+ ChevronDown,
9
+ ChevronRight,
10
+ Brain,
11
+ Trash2,
12
+ } from "lucide-react";
4
13
  import TextAnnotator from "./TextAnnotator";
5
14
  import type { Annotation } from "../types";
6
15
 
@@ -11,9 +20,13 @@ interface Props {
11
20
  onAnnotationUpdate?: (annotation: Annotation) => void;
12
21
  bookmarkedBlockIndex?: number;
13
22
  onSetBookmark?: (messageId: string, blockIndex: number) => void;
14
- responseLength?: string;
15
- responseStyle?: string;
16
- responseAudience?: string;
23
+ preferenceSuffix?: string;
24
+ onSpecRefined?: (
25
+ messageId: string,
26
+ originalSpec: string,
27
+ newSpec: string,
28
+ ) => void;
29
+ onDeleteMessage?: (messageId: string) => void;
17
30
  }
18
31
 
19
32
  function getTextContent(message: UIMessage): string {
@@ -26,6 +39,47 @@ function getTextContent(message: UIMessage): string {
26
39
  return "";
27
40
  }
28
41
 
42
+ function getReasoningContent(message: UIMessage): string {
43
+ if (!message.parts) return "";
44
+ return message.parts
45
+ .filter(
46
+ (p): p is Extract<UIMessage["parts"][number], { type: "reasoning" }> =>
47
+ p.type === "reasoning" && typeof (p as any).text === "string",
48
+ )
49
+ .map((p) => p.text)
50
+ .join("\n\n");
51
+ }
52
+
53
+ function ThinkingBlock({ reasoning }: { reasoning: string }) {
54
+ const [open, setOpen] = useState(false);
55
+ const wordCount = reasoning.trim().split(/\s+/).length;
56
+ return (
57
+ <div className="mb-2 rounded-lg border border-slate-700/60 bg-slate-800/40 text-xs overflow-hidden">
58
+ <button
59
+ type="button"
60
+ onClick={() => setOpen((v) => !v)}
61
+ className="w-full flex items-center gap-1.5 px-3 py-2 text-slate-500 hover:text-slate-300 transition-colors text-left"
62
+ >
63
+ <Brain className="w-3 h-3 shrink-0 text-violet-400" />
64
+ <span className="text-violet-400/80 font-medium">Thinking</span>
65
+ <span className="text-slate-600 ml-auto mr-1">
66
+ {wordCount.toLocaleString()} words
67
+ </span>
68
+ {open ? (
69
+ <ChevronDown className="w-3 h-3 shrink-0" />
70
+ ) : (
71
+ <ChevronRight className="w-3 h-3 shrink-0" />
72
+ )}
73
+ </button>
74
+ {open && (
75
+ <div className="px-3 pb-3 pt-0 text-slate-500 whitespace-pre-wrap leading-relaxed border-t border-slate-700/40 max-h-80 overflow-y-auto">
76
+ {reasoning}
77
+ </div>
78
+ )}
79
+ </div>
80
+ );
81
+ }
82
+
29
83
  const ChatMessage = memo(function ChatMessage({
30
84
  message,
31
85
  annotations = [],
@@ -33,15 +87,42 @@ const ChatMessage = memo(function ChatMessage({
33
87
  onAnnotationUpdate,
34
88
  bookmarkedBlockIndex,
35
89
  onSetBookmark,
36
- responseLength,
37
- responseStyle,
38
- responseAudience,
90
+ preferenceSuffix,
91
+ onSpecRefined,
92
+ onDeleteMessage,
39
93
  }: Props) {
40
94
  const isUser = message.role === "user";
41
95
  const content = getTextContent(message);
96
+ const reasoning = !isUser ? getReasoningContent(message) : "";
97
+ const [copied, setCopied] = useState(false);
98
+
99
+ // Stable wrappers so MarkdownRenderer's `components` useMemo doesn't invalidate
100
+ // on every ChatMessage re-render (which would remount InlineCodeBlock and cause
101
+ // a Zustand update loop).
102
+ const stableBookmarkBlock = useCallback(
103
+ (idx: number) => onSetBookmark?.(message.id, idx),
104
+ [onSetBookmark, message.id],
105
+ );
106
+ const stableSpecRefined = useCallback(
107
+ (orig: string, refined: string) =>
108
+ onSpecRefined?.(message.id, orig, refined),
109
+ [onSpecRefined, message.id],
110
+ );
111
+
112
+ const handleCopy = useCallback(() => {
113
+ if (!content) return;
114
+ navigator.clipboard.writeText(content).then(() => {
115
+ setCopied(true);
116
+ setTimeout(() => setCopied(false), 1500);
117
+ });
118
+ }, [content]);
119
+
120
+ const handleDelete = useCallback(() => {
121
+ onDeleteMessage?.(message.id);
122
+ }, [onDeleteMessage, message.id]);
42
123
 
43
124
  return (
44
- <div className="flex gap-3 animate-fadeIn">
125
+ <div className="group flex gap-3 animate-fadeIn">
45
126
  <div
46
127
  className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-0.5 ${
47
128
  isUser
@@ -56,8 +137,32 @@ const ChatMessage = memo(function ChatMessage({
56
137
  )}
57
138
  </div>
58
139
  <div className="min-w-0 flex-1">
59
- <div className="text-[10px] font-medium text-slate-600 mb-1">
60
- {isUser ? "You" : "Coach"}
140
+ <div className="flex items-center gap-2 mb-1">
141
+ <div className="text-[10px] font-medium text-slate-600">
142
+ {isUser ? "You" : "Coach"}
143
+ </div>
144
+ {content && (
145
+ <button
146
+ onClick={handleCopy}
147
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded text-slate-500 hover:text-slate-300 hover:bg-slate-700"
148
+ title="Copy message"
149
+ >
150
+ {copied ? (
151
+ <Check className="w-3 h-3 text-cyan-400" />
152
+ ) : (
153
+ <Copy className="w-3 h-3" />
154
+ )}
155
+ </button>
156
+ )}
157
+ {onDeleteMessage && (
158
+ <button
159
+ onClick={handleDelete}
160
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded text-slate-500 hover:text-red-400 hover:bg-slate-700"
161
+ title="Delete message"
162
+ >
163
+ <Trash2 className="w-3 h-3" />
164
+ </button>
165
+ )}
61
166
  </div>
62
167
  <div className="text-sm leading-relaxed text-slate-200">
63
168
  {isUser ? (
@@ -89,22 +194,22 @@ const ChatMessage = memo(function ChatMessage({
89
194
  )}
90
195
  </div>
91
196
  ) : (
92
- <TextAnnotator
93
- content={content}
94
- messageId={message.id}
95
- annotations={annotations}
96
- onAnnotationCreate={onAnnotationCreate ?? (() => {})}
97
- onAnnotationUpdate={onAnnotationUpdate ?? (() => {})}
98
- bookmarkedBlockIndex={bookmarkedBlockIndex}
99
- onBookmarkBlock={
100
- onSetBookmark
101
- ? (idx) => onSetBookmark(message.id, idx)
102
- : undefined
103
- }
104
- responseLength={responseLength}
105
- responseStyle={responseStyle}
106
- responseAudience={responseAudience}
107
- />
197
+ <>
198
+ {reasoning && <ThinkingBlock reasoning={reasoning} />}
199
+ <TextAnnotator
200
+ content={content}
201
+ messageId={message.id}
202
+ annotations={annotations}
203
+ onAnnotationCreate={onAnnotationCreate ?? (() => {})}
204
+ onAnnotationUpdate={onAnnotationUpdate ?? (() => {})}
205
+ bookmarkedBlockIndex={bookmarkedBlockIndex}
206
+ onBookmarkBlock={
207
+ onSetBookmark ? stableBookmarkBlock : undefined
208
+ }
209
+ preferenceSuffix={preferenceSuffix}
210
+ onSpecRefined={onSpecRefined ? stableSpecRefined : undefined}
211
+ />
212
+ </>
108
213
  )}
109
214
  </div>
110
215
  </div>