create-interview-cockpit 0.23.2 → 0.25.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.23.2",
3
+ "version": "0.25.0",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 overflow-auto">
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
- <SyntaxHighlighter
614
- language={lang}
615
- style={oneDark}
616
- showLineNumbers
617
- wrapLines
618
- wrapLongLines={false}
619
- lineProps={(lineNumber) => {
620
- const hasAnnotation = codeAnnotations.some(
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
 
@@ -1,6 +1,12 @@
1
1
  import { useState } from "react";
2
2
  import { useStore } from "../store";
3
- import { DOCKER_DEEP_DIVE_LAB, parseInfraLabWorkspace } from "../infraLab";
3
+ import {
4
+ AZURE_NETWORK_ACL_BLANK_TEMPLATE,
5
+ AZURE_NETWORK_ACL_DOCKER_LAB,
6
+ DOCKER_DEEP_DIVE_LAB,
7
+ POSTGRES_DOCKER_PROVIDER_LAB,
8
+ parseInfraLabWorkspace,
9
+ } from "../infraLab";
4
10
  import {
5
11
  DEFAULT_GHA_LAB,
6
12
  parseGhaLabWorkspace,
@@ -641,6 +647,24 @@ export default function LabsPanel() {
641
647
  "Dockerfile + Compose + Node API + Redis, with command-line practice",
642
648
  onClick: () => openInfraLab(DOCKER_DEEP_DIVE_LAB),
643
649
  },
650
+ {
651
+ label: "Postgres DB in Docker",
652
+ description:
653
+ "Terraform Docker provider deploys a local PostgreSQL database container",
654
+ onClick: () => openInfraLab(POSTGRES_DOCKER_PROVIDER_LAB),
655
+ },
656
+ {
657
+ label: "Azure ACL Failure (Docker)",
658
+ description:
659
+ "Reproduce stale Azure subnet allowlists locally with Terraform + Docker mocks",
660
+ onClick: () => openInfraLab(AZURE_NETWORK_ACL_DOCKER_LAB),
661
+ },
662
+ {
663
+ label: "Azure ACL Blank Template",
664
+ description:
665
+ "Same Azure-style Terraform/Docker file layout, but every file starts empty",
666
+ onClick: () => openInfraLab(AZURE_NETWORK_ACL_BLANK_TEMPLATE),
667
+ },
644
668
  {
645
669
  label: "Enterprise BFF Docker Stack",
646
670
  description: