create-interview-cockpit 0.24.0 → 0.26.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/package.json +1 -1
- package/template/client/src/api.ts +26 -4
- package/template/client/src/components/ChatView.tsx +24 -3
- package/template/client/src/components/CodeContextPanel.tsx +242 -60
- package/template/client/src/components/FileViewerModal.tsx +209 -49
- package/template/client/src/components/GitDiffPanel.tsx +403 -73
- package/template/client/src/components/GitDiffViewerModal.tsx +2 -1
- package/template/client/src/components/MarkdownRenderer.tsx +8 -1
- package/template/client/src/store.ts +17 -2
- package/template/client/src/types.ts +8 -0
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +1739 -185
- package/template/server/src/storage.ts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useCallback } from "react";
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState, useCallback } from "react";
|
|
2
2
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
3
3
|
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
4
4
|
import {
|
|
@@ -72,9 +72,208 @@ const MIN_W = 380;
|
|
|
72
72
|
const MIN_H = 260;
|
|
73
73
|
const DEFAULT_W = 720;
|
|
74
74
|
const DEFAULT_H = 520;
|
|
75
|
+
const LARGE_FILE_LINE_THRESHOLD = 2_500;
|
|
76
|
+
const LARGE_FILE_CHAR_THRESHOLD = 220_000;
|
|
77
|
+
const VIRTUAL_ROW_HEIGHT = 20;
|
|
78
|
+
const VIRTUAL_OVERSCAN = 24;
|
|
75
79
|
|
|
76
80
|
type ResizeDir = "e" | "s" | "se" | "sw" | "w" | "ne" | "nw" | "n" | null;
|
|
77
81
|
|
|
82
|
+
interface CodeViewerContentProps {
|
|
83
|
+
content: string;
|
|
84
|
+
lang: string;
|
|
85
|
+
selectedLines: Set<number>;
|
|
86
|
+
chatContextLines: Set<number>;
|
|
87
|
+
codeAnnotations: CodeAnnotation[];
|
|
88
|
+
onLineClick: (e: React.MouseEvent, lineNumber: number) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const CodeViewerContent = memo(function CodeViewerContent({
|
|
92
|
+
content,
|
|
93
|
+
lang,
|
|
94
|
+
selectedLines,
|
|
95
|
+
chatContextLines,
|
|
96
|
+
codeAnnotations,
|
|
97
|
+
onLineClick,
|
|
98
|
+
}: CodeViewerContentProps) {
|
|
99
|
+
const annotationLineSet = useMemo(
|
|
100
|
+
() => new Set(codeAnnotations.map((annotation) => annotation.lineNumber)),
|
|
101
|
+
[codeAnnotations],
|
|
102
|
+
);
|
|
103
|
+
const lineCount = useMemo(() => content.split(/\r?\n/).length, [content]);
|
|
104
|
+
const isLargeFile =
|
|
105
|
+
lineCount > LARGE_FILE_LINE_THRESHOLD ||
|
|
106
|
+
content.length > LARGE_FILE_CHAR_THRESHOLD;
|
|
107
|
+
|
|
108
|
+
if (isLargeFile) {
|
|
109
|
+
return (
|
|
110
|
+
<VirtualizedPlainCodeViewer
|
|
111
|
+
content={content}
|
|
112
|
+
selectedLines={selectedLines}
|
|
113
|
+
chatContextLines={chatContextLines}
|
|
114
|
+
annotationLineSet={annotationLineSet}
|
|
115
|
+
onLineClick={onLineClick}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="h-full overflow-auto">
|
|
122
|
+
<SyntaxHighlighter
|
|
123
|
+
language={lang}
|
|
124
|
+
style={oneDark}
|
|
125
|
+
showLineNumbers
|
|
126
|
+
wrapLines
|
|
127
|
+
wrapLongLines={false}
|
|
128
|
+
lineProps={(lineNumber) => {
|
|
129
|
+
const hasAnnotation = annotationLineSet.has(lineNumber);
|
|
130
|
+
const isChatCtx = chatContextLines.has(lineNumber);
|
|
131
|
+
const isSelected = selectedLines.has(lineNumber);
|
|
132
|
+
let bg: string | undefined;
|
|
133
|
+
let outline: string | undefined;
|
|
134
|
+
if (hasAnnotation) {
|
|
135
|
+
bg = "rgba(139, 92, 246, 0.2)";
|
|
136
|
+
outline = "1px solid rgba(139, 92, 246, 0.35)";
|
|
137
|
+
} else if (isChatCtx) {
|
|
138
|
+
bg = "rgba(245, 158, 11, 0.15)";
|
|
139
|
+
outline = "1px solid rgba(245, 158, 11, 0.3)";
|
|
140
|
+
} else if (isSelected) {
|
|
141
|
+
bg = "rgba(6, 182, 212, 0.15)";
|
|
142
|
+
outline = "1px solid rgba(6, 182, 212, 0.3)";
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
onClick: (e: React.MouseEvent) =>
|
|
146
|
+
lineNumber !== undefined && onLineClick(e, lineNumber),
|
|
147
|
+
style: {
|
|
148
|
+
display: "block",
|
|
149
|
+
cursor: "pointer",
|
|
150
|
+
backgroundColor: bg,
|
|
151
|
+
outline,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}}
|
|
155
|
+
customStyle={{
|
|
156
|
+
margin: 0,
|
|
157
|
+
borderRadius: 0,
|
|
158
|
+
background: "#0f172a",
|
|
159
|
+
fontSize: "0.75rem",
|
|
160
|
+
lineHeight: "1.6",
|
|
161
|
+
minHeight: "100%",
|
|
162
|
+
}}
|
|
163
|
+
lineNumberStyle={{ color: "#334155", minWidth: "2.8em" }}
|
|
164
|
+
>
|
|
165
|
+
{content}
|
|
166
|
+
</SyntaxHighlighter>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
interface VirtualizedPlainCodeViewerProps {
|
|
172
|
+
content: string;
|
|
173
|
+
selectedLines: Set<number>;
|
|
174
|
+
chatContextLines: Set<number>;
|
|
175
|
+
annotationLineSet: Set<number>;
|
|
176
|
+
onLineClick: (e: React.MouseEvent, lineNumber: number) => void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function VirtualizedPlainCodeViewer({
|
|
180
|
+
content,
|
|
181
|
+
selectedLines,
|
|
182
|
+
chatContextLines,
|
|
183
|
+
annotationLineSet,
|
|
184
|
+
onLineClick,
|
|
185
|
+
}: VirtualizedPlainCodeViewerProps) {
|
|
186
|
+
const lines = useMemo(() => content.split(/\r?\n/), [content]);
|
|
187
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
188
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
189
|
+
const [viewportHeight, setViewportHeight] = useState(0);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const el = containerRef.current;
|
|
193
|
+
if (!el) return;
|
|
194
|
+
const update = () => setViewportHeight(el.clientHeight);
|
|
195
|
+
update();
|
|
196
|
+
const observer = new ResizeObserver(update);
|
|
197
|
+
observer.observe(el);
|
|
198
|
+
return () => observer.disconnect();
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
const visibleStart = Math.max(
|
|
202
|
+
0,
|
|
203
|
+
Math.floor(scrollTop / VIRTUAL_ROW_HEIGHT) - VIRTUAL_OVERSCAN,
|
|
204
|
+
);
|
|
205
|
+
const visibleEnd = Math.min(
|
|
206
|
+
lines.length,
|
|
207
|
+
Math.ceil((scrollTop + viewportHeight) / VIRTUAL_ROW_HEIGHT) +
|
|
208
|
+
VIRTUAL_OVERSCAN,
|
|
209
|
+
);
|
|
210
|
+
const visibleLines = lines.slice(visibleStart, visibleEnd);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
ref={containerRef}
|
|
215
|
+
className="h-full overflow-auto bg-slate-950 font-mono text-xs text-slate-300"
|
|
216
|
+
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
|
|
217
|
+
>
|
|
218
|
+
<div
|
|
219
|
+
style={{
|
|
220
|
+
height: lines.length * VIRTUAL_ROW_HEIGHT,
|
|
221
|
+
position: "relative",
|
|
222
|
+
minWidth: "max-content",
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<div
|
|
226
|
+
style={{
|
|
227
|
+
position: "absolute",
|
|
228
|
+
top: visibleStart * VIRTUAL_ROW_HEIGHT,
|
|
229
|
+
left: 0,
|
|
230
|
+
right: 0,
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{visibleLines.map((line, offset) => {
|
|
234
|
+
const lineNumber = visibleStart + offset + 1;
|
|
235
|
+
const hasAnnotation = annotationLineSet.has(lineNumber);
|
|
236
|
+
const isChatCtx = chatContextLines.has(lineNumber);
|
|
237
|
+
const isSelected = selectedLines.has(lineNumber);
|
|
238
|
+
const backgroundColor = hasAnnotation
|
|
239
|
+
? "rgba(139, 92, 246, 0.2)"
|
|
240
|
+
: isChatCtx
|
|
241
|
+
? "rgba(245, 158, 11, 0.15)"
|
|
242
|
+
: isSelected
|
|
243
|
+
? "rgba(6, 182, 212, 0.15)"
|
|
244
|
+
: undefined;
|
|
245
|
+
const outline = hasAnnotation
|
|
246
|
+
? "1px solid rgba(139, 92, 246, 0.35)"
|
|
247
|
+
: isChatCtx
|
|
248
|
+
? "1px solid rgba(245, 158, 11, 0.3)"
|
|
249
|
+
: isSelected
|
|
250
|
+
? "1px solid rgba(6, 182, 212, 0.3)"
|
|
251
|
+
: undefined;
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
key={lineNumber}
|
|
256
|
+
onClick={(e) => onLineClick(e, lineNumber)}
|
|
257
|
+
className="flex cursor-pointer whitespace-pre leading-none hover:bg-slate-800/60"
|
|
258
|
+
style={{
|
|
259
|
+
height: VIRTUAL_ROW_HEIGHT,
|
|
260
|
+
backgroundColor,
|
|
261
|
+
outline,
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<span className="sticky left-0 z-10 w-12 shrink-0 select-none bg-slate-950 pr-3 text-right text-slate-600">
|
|
265
|
+
{lineNumber}
|
|
266
|
+
</span>
|
|
267
|
+
<span className="pr-6">{line || " "}</span>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
78
277
|
export default function FileViewerModal({ filePath, onClose }: Props) {
|
|
79
278
|
const {
|
|
80
279
|
addSnippet,
|
|
@@ -598,7 +797,7 @@ export default function FileViewerModal({ filePath, onClose }: Props) {
|
|
|
598
797
|
</div>
|
|
599
798
|
|
|
600
799
|
{/* ── Content ── */}
|
|
601
|
-
<div className="flex-1
|
|
800
|
+
<div className="flex-1 min-h-0">
|
|
602
801
|
{loading && (
|
|
603
802
|
<div className="flex items-center justify-center h-full">
|
|
604
803
|
<Loader2 className="w-5 h-5 text-cyan-400 animate-spin" />
|
|
@@ -610,53 +809,14 @@ export default function FileViewerModal({ filePath, onClose }: Props) {
|
|
|
610
809
|
</div>
|
|
611
810
|
)}
|
|
612
811
|
{!loading && !error && content !== null && (
|
|
613
|
-
<
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
(a) => a.lineNumber === lineNumber,
|
|
622
|
-
);
|
|
623
|
-
const isChatCtx = chatContextLines.has(lineNumber);
|
|
624
|
-
const isSelected = selectedLines.has(lineNumber);
|
|
625
|
-
let bg: string | undefined;
|
|
626
|
-
let outline: string | undefined;
|
|
627
|
-
if (hasAnnotation) {
|
|
628
|
-
bg = "rgba(139, 92, 246, 0.2)";
|
|
629
|
-
outline = "1px solid rgba(139, 92, 246, 0.35)";
|
|
630
|
-
} else if (isChatCtx) {
|
|
631
|
-
bg = "rgba(245, 158, 11, 0.15)";
|
|
632
|
-
outline = "1px solid rgba(245, 158, 11, 0.3)";
|
|
633
|
-
} else if (isSelected) {
|
|
634
|
-
bg = "rgba(6, 182, 212, 0.15)";
|
|
635
|
-
outline = "1px solid rgba(6, 182, 212, 0.3)";
|
|
636
|
-
}
|
|
637
|
-
return {
|
|
638
|
-
onClick: (e: React.MouseEvent) =>
|
|
639
|
-
lineNumber !== undefined && handleLineClick(e, lineNumber),
|
|
640
|
-
style: {
|
|
641
|
-
display: "block",
|
|
642
|
-
cursor: "pointer",
|
|
643
|
-
backgroundColor: bg,
|
|
644
|
-
outline,
|
|
645
|
-
},
|
|
646
|
-
};
|
|
647
|
-
}}
|
|
648
|
-
customStyle={{
|
|
649
|
-
margin: 0,
|
|
650
|
-
borderRadius: 0,
|
|
651
|
-
background: "#0f172a",
|
|
652
|
-
fontSize: "0.75rem",
|
|
653
|
-
lineHeight: "1.6",
|
|
654
|
-
minHeight: "100%",
|
|
655
|
-
}}
|
|
656
|
-
lineNumberStyle={{ color: "#334155", minWidth: "2.8em" }}
|
|
657
|
-
>
|
|
658
|
-
{content}
|
|
659
|
-
</SyntaxHighlighter>
|
|
812
|
+
<CodeViewerContent
|
|
813
|
+
content={content}
|
|
814
|
+
lang={lang}
|
|
815
|
+
selectedLines={selectedLines}
|
|
816
|
+
chatContextLines={chatContextLines}
|
|
817
|
+
codeAnnotations={codeAnnotations}
|
|
818
|
+
onLineClick={handleLineClick}
|
|
819
|
+
/>
|
|
660
820
|
)}
|
|
661
821
|
</div>
|
|
662
822
|
|