create-interview-cockpit 0.5.0 → 0.7.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 (30) 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 +384 -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 +530 -2
  10. package/template/client/src/components/CodeRunnerModal.tsx +1895 -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 +960 -0
  23. package/template/client/src/store.ts +250 -6
  24. package/template/client/src/types.ts +36 -3
  25. package/template/client/tsconfig.tsbuildinfo +1 -1
  26. package/template/cockpit.json +1 -1
  27. package/template/server/src/google-drive.ts +39 -3
  28. package/template/server/src/index.ts +954 -52
  29. package/template/server/src/infra-runner.ts +1104 -0
  30. package/template/server/src/storage.ts +22 -3
@@ -1,6 +1,9 @@
1
1
  import { useEffect, useState, useCallback } from "react";
2
2
  import { useStore } from "../store";
3
+ import { parseInfraLabWorkspace } from "../infraLab";
4
+ import { parseFrontendLabWorkspace } from "../reactLab";
3
5
  import FileViewerModal from "./FileViewerModal";
6
+ import NotesModal, { notesKey } from "./NotesModal";
4
7
  import {
5
8
  File,
6
9
  Search,
@@ -22,6 +25,10 @@ import {
22
25
  Trash2,
23
26
  Server,
24
27
  Pencil,
28
+ NotebookPen,
29
+ Globe,
30
+ Atom,
31
+ Layout,
25
32
  } from "lucide-react";
26
33
 
27
34
  // ─── Tree data structure ─────────────────────────────────
@@ -228,6 +235,10 @@ export default function CodeContextPanel() {
228
235
  clearSnippets,
229
236
  openCodeRunner,
230
237
  openSandbox,
238
+ openInfraLab,
239
+ openReactLab,
240
+ openNextLab,
241
+ openModuleFederationLab,
231
242
  removeQuestionFile,
232
243
  renameContextFile,
233
244
  } = useStore();
@@ -243,6 +254,19 @@ export default function CodeContextPanel() {
243
254
  new Set(),
244
255
  );
245
256
  const [viewingFile, setViewingFile] = useState<string | null>(null);
257
+ const [notesOpen, setNotesOpen] = useState(false);
258
+
259
+ // Show a dot in the Notes button when there are saved notes for this context
260
+ const hasNotes = (() => {
261
+ try {
262
+ const raw = localStorage.getItem(notesKey(currentQuestion?.id));
263
+ if (!raw) return false;
264
+ const parsed = JSON.parse(raw);
265
+ return Array.isArray(parsed) && parsed.length > 0;
266
+ } catch {
267
+ return false;
268
+ }
269
+ })();
246
270
 
247
271
  useEffect(() => {
248
272
  fetchAvailableFiles();
@@ -518,7 +542,11 @@ export default function CodeContextPanel() {
518
542
  )
519
543
  .then((r) => r.json())
520
544
  .then((d) => d.content);
521
- openCodeRunner(content, cf.language ?? "typescript");
545
+ openCodeRunner(
546
+ content,
547
+ cf.language ?? "typescript",
548
+ cf.id,
549
+ );
522
550
  }}
523
551
  className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
524
552
  title="Open in Code Runner"
@@ -583,7 +611,11 @@ export default function CodeContextPanel() {
583
611
  )
584
612
  .then((r) => r.json())
585
613
  .then((d) => d.content);
586
- openCodeRunner(content, cf.language ?? "typescript");
614
+ openCodeRunner(
615
+ content,
616
+ cf.language ?? "typescript",
617
+ cf.id,
618
+ );
587
619
  }}
588
620
  className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
589
621
  title="Open in Code Runner"
@@ -715,6 +747,9 @@ export default function CodeContextPanel() {
715
747
  serverLang: string;
716
748
  clientCode: string;
717
749
  clientLang: string;
750
+ clientType?: "script" | "react" | "nextjs";
751
+ reactFiles?: Record<string, string>;
752
+ reactActiveFile?: string;
718
753
  };
719
754
  openSandbox(
720
755
  parsed.serverCode,
@@ -722,6 +757,13 @@ export default function CodeContextPanel() {
722
757
  parsed.clientCode,
723
758
  parsed.clientLang,
724
759
  cf.id,
760
+ parsed.clientType
761
+ ? {
762
+ clientType: parsed.clientType,
763
+ reactFiles: parsed.reactFiles,
764
+ reactActiveFile: parsed.reactActiveFile,
765
+ }
766
+ : undefined,
725
767
  );
726
768
  } catch {
727
769
  /* malformed — ignore */
@@ -756,12 +798,498 @@ export default function CodeContextPanel() {
756
798
  </div>
757
799
  )}
758
800
 
801
+ {currentQuestion && (
802
+ <div className="border-t border-slate-800 px-3 py-2">
803
+ <div className="flex items-center justify-between mb-1">
804
+ <div className="flex items-center gap-1">
805
+ <Globe className="w-3 h-3 text-cyan-400/70" />
806
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
807
+ Infra Labs (
808
+ {
809
+ (currentQuestion.contextFiles || []).filter(
810
+ (f) => f.origin === "infra",
811
+ ).length
812
+ }
813
+ )
814
+ </span>
815
+ </div>
816
+ <button
817
+ onClick={() => openInfraLab()}
818
+ className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
819
+ title="Open Infrastructure Lab"
820
+ >
821
+ <Plus className="w-3.5 h-3.5" />
822
+ </button>
823
+ </div>
824
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
825
+ {(currentQuestion.contextFiles || [])
826
+ .filter((f) => f.origin === "infra")
827
+ .map((cf) => (
828
+ <div
829
+ key={cf.id}
830
+ className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
831
+ >
832
+ {renamingId !== cf.id && (
833
+ <span
834
+ className="text-cyan-200 font-medium truncate flex-1"
835
+ title={cf.label || cf.originalName}
836
+ >
837
+ {cf.label || cf.originalName}
838
+ </span>
839
+ )}
840
+ {renamingId === cf.id ? (
841
+ <>
842
+ <input
843
+ autoFocus
844
+ value={renameValue}
845
+ onChange={(e) => setRenameValue(e.target.value)}
846
+ onKeyDown={async (e) => {
847
+ if (e.key === "Enter") {
848
+ e.preventDefault();
849
+ if (renameValue.trim()) {
850
+ await renameContextFile(
851
+ currentQuestion.id,
852
+ cf.id,
853
+ renameValue.trim(),
854
+ );
855
+ }
856
+ setRenamingId(null);
857
+ } else if (e.key === "Escape") {
858
+ setRenamingId(null);
859
+ }
860
+ }}
861
+ className="w-28 bg-slate-900 border border-cyan-600/50 rounded px-1.5 py-0.5 text-[11px] text-slate-200 outline-none focus:border-cyan-500 shrink-0"
862
+ />
863
+ <button
864
+ onClick={async () => {
865
+ if (renameValue.trim()) {
866
+ await renameContextFile(
867
+ currentQuestion.id,
868
+ cf.id,
869
+ renameValue.trim(),
870
+ );
871
+ }
872
+ setRenamingId(null);
873
+ }}
874
+ className="shrink-0 p-0.5 rounded text-cyan-400 hover:bg-cyan-600/20 transition-colors"
875
+ title="Confirm"
876
+ >
877
+ <Check className="w-3 h-3" />
878
+ </button>
879
+ <button
880
+ onClick={() => setRenamingId(null)}
881
+ className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
882
+ title="Cancel"
883
+ >
884
+ <X className="w-3 h-3" />
885
+ </button>
886
+ </>
887
+ ) : (
888
+ <>
889
+ <button
890
+ onClick={() => {
891
+ setRenamingId(cf.id);
892
+ setRenameValue(cf.label || cf.originalName);
893
+ }}
894
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-300 transition-all"
895
+ title="Rename"
896
+ >
897
+ <Pencil className="w-3 h-3" />
898
+ </button>
899
+ <button
900
+ onClick={async () => {
901
+ const raw = await fetch(
902
+ `/api/context-files/${cf.id}/content`,
903
+ )
904
+ .then((r) => r.json())
905
+ .then((d) => d.content as string);
906
+ const parsed = parseInfraLabWorkspace(raw);
907
+ if (parsed) openInfraLab(parsed, cf.id);
908
+ }}
909
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
910
+ title="Open in Infrastructure Lab"
911
+ >
912
+ <Play className="w-3 h-3" />
913
+ </button>
914
+ <button
915
+ onClick={() =>
916
+ removeQuestionFile(currentQuestion.id, cf.id)
917
+ }
918
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
919
+ title="Remove"
920
+ >
921
+ <Trash2 className="w-3 h-3" />
922
+ </button>
923
+ </>
924
+ )}
925
+ </div>
926
+ ))}
927
+ {(currentQuestion.contextFiles || []).filter(
928
+ (f) => f.origin === "infra",
929
+ ).length === 0 && (
930
+ <p className="text-[10px] text-slate-700 italic">
931
+ Save an infra lab to reopen it here
932
+ </p>
933
+ )}
934
+ </div>
935
+ </div>
936
+ )}
937
+
938
+ {/* ── React Labs section ───────────────────── */}
939
+ {currentQuestion && (
940
+ <div className="border-t border-slate-800 px-3 py-2">
941
+ <div className="flex items-center justify-between mb-1">
942
+ <div className="flex items-center gap-1">
943
+ <Atom className="w-3 h-3 text-cyan-400/70" />
944
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
945
+ React Labs (
946
+ {
947
+ (currentQuestion.contextFiles || []).filter(
948
+ (f) => f.origin === "react",
949
+ ).length
950
+ }
951
+ )
952
+ </span>
953
+ </div>
954
+ <button
955
+ onClick={() => openReactLab()}
956
+ className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
957
+ title="Open React Lab"
958
+ >
959
+ <Plus className="w-3.5 h-3.5" />
960
+ </button>
961
+ </div>
962
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
963
+ {(currentQuestion.contextFiles || [])
964
+ .filter((f) => f.origin === "react")
965
+ .map((cf) => (
966
+ <div
967
+ key={cf.id}
968
+ className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
969
+ >
970
+ <span
971
+ className="text-cyan-200 font-medium truncate flex-1"
972
+ title={cf.label || cf.originalName}
973
+ >
974
+ {cf.label || cf.originalName}
975
+ </span>
976
+ <button
977
+ onClick={async () => {
978
+ try {
979
+ const raw = await fetch(
980
+ `/api/context-files/${cf.id}/content`,
981
+ )
982
+ .then((r) => r.json())
983
+ .then((d) => d.content as string);
984
+ // Try new extended sandbox format first
985
+ const ext = JSON.parse(raw) as {
986
+ clientType?: string;
987
+ reactFiles?: Record<string, string>;
988
+ reactActiveFile?: string;
989
+ serverCode?: string;
990
+ serverLang?: string;
991
+ };
992
+ if (ext?.clientType === "react" && ext.reactFiles) {
993
+ openReactLab(
994
+ {
995
+ version: 1,
996
+ type: "react",
997
+ label: cf.label || "React Lab",
998
+ activeFile:
999
+ ext.reactActiveFile ??
1000
+ Object.keys(ext.reactFiles)[0] ??
1001
+ "App.tsx",
1002
+ files: ext.reactFiles,
1003
+ },
1004
+ cf.id,
1005
+ ext.serverCode,
1006
+ ext.serverLang,
1007
+ );
1008
+ } else {
1009
+ // Fall back to old FrontendLabWorkspace format
1010
+ const ws = parseFrontendLabWorkspace(raw);
1011
+ if (ws) openReactLab(ws, cf.id);
1012
+ }
1013
+ } catch {
1014
+ /* ignore */
1015
+ }
1016
+ }}
1017
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
1018
+ title="Open in React Lab"
1019
+ >
1020
+ <Play className="w-3 h-3" />
1021
+ </button>
1022
+ <button
1023
+ onClick={() =>
1024
+ removeQuestionFile(currentQuestion.id, cf.id)
1025
+ }
1026
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
1027
+ title="Remove"
1028
+ >
1029
+ <Trash2 className="w-3 h-3" />
1030
+ </button>
1031
+ </div>
1032
+ ))}
1033
+ {(currentQuestion.contextFiles || []).filter(
1034
+ (f) => f.origin === "react",
1035
+ ).length === 0 && (
1036
+ <p className="text-[10px] text-slate-700 italic">
1037
+ Save a React lab to reopen it here
1038
+ </p>
1039
+ )}
1040
+ </div>
1041
+ </div>
1042
+ )}
1043
+
1044
+ {/* ── Next.js Labs section ──────────────────── */}
1045
+ {currentQuestion && (
1046
+ <div className="border-t border-slate-800 px-3 py-2">
1047
+ <div className="flex items-center justify-between mb-1">
1048
+ <div className="flex items-center gap-1">
1049
+ <Layout className="w-3 h-3 text-violet-400/70" />
1050
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
1051
+ Next.js Labs (
1052
+ {
1053
+ (currentQuestion.contextFiles || []).filter(
1054
+ (f) => f.origin === "nextjs",
1055
+ ).length
1056
+ }
1057
+ )
1058
+ </span>
1059
+ </div>
1060
+ <button
1061
+ onClick={() => openNextLab()}
1062
+ className="p-0.5 rounded text-slate-600 hover:text-violet-400 hover:bg-slate-700 transition-colors"
1063
+ title="Open Next.js Lab"
1064
+ >
1065
+ <Plus className="w-3.5 h-3.5" />
1066
+ </button>
1067
+ </div>
1068
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
1069
+ {(currentQuestion.contextFiles || [])
1070
+ .filter((f) => f.origin === "nextjs")
1071
+ .map((cf) => (
1072
+ <div
1073
+ key={cf.id}
1074
+ className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
1075
+ >
1076
+ <span
1077
+ className="text-violet-200 font-medium truncate flex-1"
1078
+ title={cf.label || cf.originalName}
1079
+ >
1080
+ {cf.label || cf.originalName}
1081
+ </span>
1082
+ <button
1083
+ onClick={async () => {
1084
+ try {
1085
+ const raw = await fetch(
1086
+ `/api/context-files/${cf.id}/content`,
1087
+ )
1088
+ .then((r) => r.json())
1089
+ .then((d) => d.content as string);
1090
+ const ext = JSON.parse(raw) as {
1091
+ clientType?: string;
1092
+ reactFiles?: Record<string, string>;
1093
+ reactActiveFile?: string;
1094
+ serverCode?: string;
1095
+ serverLang?: string;
1096
+ };
1097
+ if (ext?.clientType === "nextjs" && ext.reactFiles) {
1098
+ openNextLab(
1099
+ {
1100
+ version: 1,
1101
+ type: "nextjs",
1102
+ label: cf.label || "Next.js Lab",
1103
+ activeFile:
1104
+ ext.reactActiveFile ??
1105
+ Object.keys(ext.reactFiles)[0] ??
1106
+ "app/page.tsx",
1107
+ files: ext.reactFiles,
1108
+ },
1109
+ cf.id,
1110
+ ext.serverCode,
1111
+ ext.serverLang,
1112
+ );
1113
+ } else {
1114
+ const ws = parseFrontendLabWorkspace(raw);
1115
+ if (ws) openNextLab(ws, cf.id);
1116
+ }
1117
+ } catch {
1118
+ /* ignore */
1119
+ }
1120
+ }}
1121
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
1122
+ title="Open in Next.js Lab"
1123
+ >
1124
+ <Play className="w-3 h-3" />
1125
+ </button>
1126
+ <button
1127
+ onClick={() =>
1128
+ removeQuestionFile(currentQuestion.id, cf.id)
1129
+ }
1130
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
1131
+ title="Remove"
1132
+ >
1133
+ <Trash2 className="w-3 h-3" />
1134
+ </button>
1135
+ </div>
1136
+ ))}
1137
+ {(currentQuestion.contextFiles || []).filter(
1138
+ (f) => f.origin === "nextjs",
1139
+ ).length === 0 && (
1140
+ <p className="text-[10px] text-slate-700 italic">
1141
+ Save a Next.js lab to reopen it here
1142
+ </p>
1143
+ )}
1144
+ </div>
1145
+ </div>
1146
+ )}
1147
+
1148
+ {/* ── Webpack Module Federation Labs section ───────────── */}
1149
+ {currentQuestion && (
1150
+ <div className="border-t border-slate-800 px-3 py-2">
1151
+ <div className="flex items-center justify-between mb-1">
1152
+ <div className="flex items-center gap-1">
1153
+ <Server className="w-3 h-3 text-emerald-400/70" />
1154
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
1155
+ Webpack MF Labs (
1156
+ {
1157
+ (currentQuestion.contextFiles || []).filter(
1158
+ (f) => f.origin === "module-federation",
1159
+ ).length
1160
+ }
1161
+ )
1162
+ </span>
1163
+ </div>
1164
+ <button
1165
+ onClick={() => openModuleFederationLab()}
1166
+ className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
1167
+ title="Open Webpack Module Federation Lab"
1168
+ >
1169
+ <Plus className="w-3.5 h-3.5" />
1170
+ </button>
1171
+ </div>
1172
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
1173
+ {(currentQuestion.contextFiles || [])
1174
+ .filter((f) => f.origin === "module-federation")
1175
+ .map((cf) => (
1176
+ <div
1177
+ key={cf.id}
1178
+ className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
1179
+ >
1180
+ <span
1181
+ className="text-emerald-200 font-medium truncate flex-1"
1182
+ title={cf.label || cf.originalName}
1183
+ >
1184
+ {cf.label || cf.originalName}
1185
+ </span>
1186
+ <button
1187
+ onClick={async () => {
1188
+ try {
1189
+ const raw = await fetch(
1190
+ `/api/context-files/${cf.id}/content`,
1191
+ )
1192
+ .then((r) => r.json())
1193
+ .then((d) => d.content as string);
1194
+ const ext = JSON.parse(raw) as {
1195
+ clientType?: string;
1196
+ reactFiles?: Record<string, string>;
1197
+ reactActiveFile?: string;
1198
+ serverCode?: string;
1199
+ serverLang?: string;
1200
+ };
1201
+ if (
1202
+ ext?.clientType === "module-federation" &&
1203
+ ext.reactFiles
1204
+ ) {
1205
+ openModuleFederationLab(
1206
+ {
1207
+ version: 1,
1208
+ type: "module-federation",
1209
+ label:
1210
+ cf.label || "Webpack Module Federation Lab",
1211
+ activeFile:
1212
+ ext.reactActiveFile ??
1213
+ Object.keys(ext.reactFiles)[0] ??
1214
+ "apps/host/webpack.config.js",
1215
+ files: ext.reactFiles,
1216
+ },
1217
+ cf.id,
1218
+ ext.serverCode,
1219
+ ext.serverLang,
1220
+ );
1221
+ } else {
1222
+ const ws = parseFrontendLabWorkspace(raw);
1223
+ if (ws?.type === "module-federation") {
1224
+ openModuleFederationLab(ws, cf.id);
1225
+ }
1226
+ }
1227
+ } catch {
1228
+ /* ignore */
1229
+ }
1230
+ }}
1231
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
1232
+ title="Open in Webpack Module Federation Lab"
1233
+ >
1234
+ <Play className="w-3 h-3" />
1235
+ </button>
1236
+ <button
1237
+ onClick={() =>
1238
+ removeQuestionFile(currentQuestion.id, cf.id)
1239
+ }
1240
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
1241
+ title="Remove"
1242
+ >
1243
+ <Trash2 className="w-3 h-3" />
1244
+ </button>
1245
+ </div>
1246
+ ))}
1247
+ {(currentQuestion.contextFiles || []).filter(
1248
+ (f) => f.origin === "module-federation",
1249
+ ).length === 0 && (
1250
+ <p className="text-[10px] text-slate-700 italic">
1251
+ Save a webpack module federation lab to reopen it here
1252
+ </p>
1253
+ )}
1254
+ </div>
1255
+ </div>
1256
+ )}
1257
+
1258
+ {/* ── Notes section ────────────────────────────────────── */}
1259
+ <div className="border-t border-slate-800 px-3 py-2">
1260
+ <button
1261
+ onClick={() => setNotesOpen((v) => !v)}
1262
+ className="w-full flex items-center gap-2 group"
1263
+ >
1264
+ <NotebookPen className="w-3 h-3 text-amber-400/70 shrink-0" />
1265
+ <span className="text-[10px] uppercase tracking-wider text-slate-600 flex-1 text-left">
1266
+ Notes
1267
+ </span>
1268
+ {hasNotes && (
1269
+ <span
1270
+ className="w-1.5 h-1.5 rounded-full bg-amber-400/70 shrink-0"
1271
+ title="Has notes"
1272
+ />
1273
+ )}
1274
+ <span className="text-[10px] text-slate-700 group-hover:text-slate-500 transition-colors">
1275
+ {notesOpen ? "close" : "open"}
1276
+ </span>
1277
+ </button>
1278
+ </div>
1279
+
759
1280
  {viewingFile && (
760
1281
  <FileViewerModal
761
1282
  filePath={viewingFile}
762
1283
  onClose={() => setViewingFile(null)}
763
1284
  />
764
1285
  )}
1286
+
1287
+ {notesOpen && (
1288
+ <NotesModal
1289
+ questionId={currentQuestion?.id}
1290
+ onClose={() => setNotesOpen(false)}
1291
+ />
1292
+ )}
765
1293
  </div>
766
1294
  );
767
1295
  }