create-interview-cockpit 0.3.0 → 0.5.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 (28) hide show
  1. package/README.md +23 -0
  2. package/package.json +1 -1
  3. package/template/client/package-lock.json +42 -0
  4. package/template/client/package.json +5 -0
  5. package/template/client/src/App.tsx +45 -12
  6. package/template/client/src/api.ts +174 -0
  7. package/template/client/src/components/AiSettingsModal.tsx +1041 -0
  8. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  9. package/template/client/src/components/ChatMessage.tsx +110 -27
  10. package/template/client/src/components/ChatView.tsx +239 -137
  11. package/template/client/src/components/CodeContextPanel.tsx +297 -0
  12. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  13. package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
  14. package/template/client/src/components/DocRefModal.tsx +502 -0
  15. package/template/client/src/components/FileAttachments.tsx +109 -9
  16. package/template/client/src/components/FilePickerModal.tsx +181 -0
  17. package/template/client/src/components/FileViewerModal.tsx +406 -28
  18. package/template/client/src/components/MarkdownRenderer.tsx +210 -2
  19. package/template/client/src/components/Sidebar.tsx +213 -125
  20. package/template/client/src/components/TextAnnotator.tsx +8 -15
  21. package/template/client/src/components/VizCraftEmbed.tsx +645 -0
  22. package/template/client/src/store.ts +275 -0
  23. package/template/client/src/types.ts +9 -0
  24. package/template/cockpit.json +1 -1
  25. package/template/data/ai-settings.json +49 -0
  26. package/template/server/src/google-drive.ts +109 -1
  27. package/template/server/src/index.ts +1187 -76
  28. package/template/server/src/storage.ts +359 -2
@@ -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,14 @@
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
+ } from "lucide-react";
4
12
  import TextAnnotator from "./TextAnnotator";
5
13
  import type { Annotation } from "../types";
6
14
 
@@ -11,9 +19,12 @@ interface Props {
11
19
  onAnnotationUpdate?: (annotation: Annotation) => void;
12
20
  bookmarkedBlockIndex?: number;
13
21
  onSetBookmark?: (messageId: string, blockIndex: number) => void;
14
- responseLength?: string;
15
- responseStyle?: string;
16
- responseAudience?: string;
22
+ preferenceSuffix?: string;
23
+ onSpecRefined?: (
24
+ messageId: string,
25
+ originalSpec: string,
26
+ newSpec: string,
27
+ ) => void;
17
28
  }
18
29
 
19
30
  function getTextContent(message: UIMessage): string {
@@ -26,6 +37,47 @@ function getTextContent(message: UIMessage): string {
26
37
  return "";
27
38
  }
28
39
 
40
+ function getReasoningContent(message: UIMessage): string {
41
+ if (!message.parts) return "";
42
+ return message.parts
43
+ .filter(
44
+ (p): p is { type: "reasoning"; reasoning: string } =>
45
+ p.type === "reasoning" && typeof (p as any).reasoning === "string",
46
+ )
47
+ .map((p) => p.reasoning)
48
+ .join("\n\n");
49
+ }
50
+
51
+ function ThinkingBlock({ reasoning }: { reasoning: string }) {
52
+ const [open, setOpen] = useState(false);
53
+ const wordCount = reasoning.trim().split(/\s+/).length;
54
+ return (
55
+ <div className="mb-2 rounded-lg border border-slate-700/60 bg-slate-800/40 text-xs overflow-hidden">
56
+ <button
57
+ type="button"
58
+ onClick={() => setOpen((v) => !v)}
59
+ className="w-full flex items-center gap-1.5 px-3 py-2 text-slate-500 hover:text-slate-300 transition-colors text-left"
60
+ >
61
+ <Brain className="w-3 h-3 shrink-0 text-violet-400" />
62
+ <span className="text-violet-400/80 font-medium">Thinking</span>
63
+ <span className="text-slate-600 ml-auto mr-1">
64
+ {wordCount.toLocaleString()} words
65
+ </span>
66
+ {open ? (
67
+ <ChevronDown className="w-3 h-3 shrink-0" />
68
+ ) : (
69
+ <ChevronRight className="w-3 h-3 shrink-0" />
70
+ )}
71
+ </button>
72
+ {open && (
73
+ <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">
74
+ {reasoning}
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ }
80
+
29
81
  const ChatMessage = memo(function ChatMessage({
30
82
  message,
31
83
  annotations = [],
@@ -33,15 +85,24 @@ const ChatMessage = memo(function ChatMessage({
33
85
  onAnnotationUpdate,
34
86
  bookmarkedBlockIndex,
35
87
  onSetBookmark,
36
- responseLength,
37
- responseStyle,
38
- responseAudience,
88
+ preferenceSuffix,
89
+ onSpecRefined,
39
90
  }: Props) {
40
91
  const isUser = message.role === "user";
41
92
  const content = getTextContent(message);
93
+ const reasoning = !isUser ? getReasoningContent(message) : "";
94
+ const [copied, setCopied] = useState(false);
95
+
96
+ const handleCopy = useCallback(() => {
97
+ if (!content) return;
98
+ navigator.clipboard.writeText(content).then(() => {
99
+ setCopied(true);
100
+ setTimeout(() => setCopied(false), 1500);
101
+ });
102
+ }, [content]);
42
103
 
43
104
  return (
44
- <div className="flex gap-3 animate-fadeIn">
105
+ <div className="group flex gap-3 animate-fadeIn">
45
106
  <div
46
107
  className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-0.5 ${
47
108
  isUser
@@ -56,8 +117,23 @@ const ChatMessage = memo(function ChatMessage({
56
117
  )}
57
118
  </div>
58
119
  <div className="min-w-0 flex-1">
59
- <div className="text-[10px] font-medium text-slate-600 mb-1">
60
- {isUser ? "You" : "Coach"}
120
+ <div className="flex items-center gap-2 mb-1">
121
+ <div className="text-[10px] font-medium text-slate-600">
122
+ {isUser ? "You" : "Coach"}
123
+ </div>
124
+ {content && (
125
+ <button
126
+ onClick={handleCopy}
127
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded text-slate-500 hover:text-slate-300 hover:bg-slate-700"
128
+ title="Copy message"
129
+ >
130
+ {copied ? (
131
+ <Check className="w-3 h-3 text-cyan-400" />
132
+ ) : (
133
+ <Copy className="w-3 h-3" />
134
+ )}
135
+ </button>
136
+ )}
61
137
  </div>
62
138
  <div className="text-sm leading-relaxed text-slate-200">
63
139
  {isUser ? (
@@ -89,22 +165,29 @@ const ChatMessage = memo(function ChatMessage({
89
165
  )}
90
166
  </div>
91
167
  ) : (
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
- />
168
+ <>
169
+ {reasoning && <ThinkingBlock reasoning={reasoning} />}
170
+ <TextAnnotator
171
+ content={content}
172
+ messageId={message.id}
173
+ annotations={annotations}
174
+ onAnnotationCreate={onAnnotationCreate ?? (() => {})}
175
+ onAnnotationUpdate={onAnnotationUpdate ?? (() => {})}
176
+ bookmarkedBlockIndex={bookmarkedBlockIndex}
177
+ onBookmarkBlock={
178
+ onSetBookmark
179
+ ? (idx) => onSetBookmark(message.id, idx)
180
+ : undefined
181
+ }
182
+ preferenceSuffix={preferenceSuffix}
183
+ onSpecRefined={
184
+ onSpecRefined
185
+ ? (orig, refined) =>
186
+ onSpecRefined(message.id, orig, refined)
187
+ : undefined
188
+ }
189
+ />
190
+ </>
108
191
  )}
109
192
  </div>
110
193
  </div>