documentation-hub 5.7.2

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 (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,1174 @@
1
+ import { Button } from "@/components/common/Button";
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "@/components/common/Card";
9
+ import { Toaster } from "@/components/common/Toast";
10
+ import { ErrorDetailsDialog } from "@/components/common/ErrorDetailsDialog";
11
+ import { SimpleTooltip } from "@/components/common/Tooltip";
12
+ import { ChangeViewer } from "@/components/sessions/ChangeViewer";
13
+ import {
14
+ ProcessingOptions,
15
+ defaultOptions,
16
+ type ProcessingOption,
17
+ } from "@/components/sessions/ProcessingOptions";
18
+ import { ReplacementsTab } from "@/components/sessions/ReplacementsTab";
19
+ import { StylesEditor } from "@/components/sessions/StylesEditor";
20
+ import { TabContainer } from "@/components/sessions/TabContainer";
21
+ import { useSession } from "@/contexts/SessionContext";
22
+ import { useUserSettings } from "@/contexts/UserSettingsContext";
23
+ import { useDocumentQueue } from "@/hooks/useDocumentQueue";
24
+ import { useToast } from "@/hooks/useToast";
25
+ import type { Document } from "@/types/session";
26
+ import { cn } from "@/utils/cn";
27
+ import logger from "@/utils/logger";
28
+ import { AnimatePresence, motion } from "framer-motion";
29
+ import {
30
+ AlertCircle,
31
+ Archive,
32
+ Check,
33
+ CheckCircle,
34
+ Clock,
35
+ Download,
36
+ Edit2,
37
+ FileCheck,
38
+ FileText,
39
+ FolderOpen,
40
+ GitCompare,
41
+ Link,
42
+ Loader2,
43
+ MessageSquare,
44
+ Play,
45
+ RotateCcw,
46
+ Save,
47
+ Timer,
48
+ Upload,
49
+ X,
50
+ } from "lucide-react";
51
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
52
+ import { useNavigate, useParams } from "react-router-dom";
53
+
54
+ export function CurrentSession() {
55
+ const { id } = useParams<{ id: string }>();
56
+ const navigate = useNavigate();
57
+ const { toasts, toast, dismiss } = useToast();
58
+ const {
59
+ sessions,
60
+ currentSession,
61
+ loadSession,
62
+ closeSession,
63
+ reopenSession,
64
+ addDocuments,
65
+ removeDocument,
66
+ processDocument,
67
+ updateSessionName,
68
+ updateSessionOptions,
69
+ updateSessionStyles,
70
+ updateSessionListBulletSettings,
71
+ updateSessionTableShadingSettings,
72
+ resetSessionToDefaults,
73
+ saveAsCustomDefaults,
74
+ } = useSession();
75
+ const { settings } = useUserSettings();
76
+
77
+ const [isDragging, setIsDragging] = useState(false);
78
+ const [isEditingTitle, setIsEditingTitle] = useState(false);
79
+ const [editedTitle, setEditedTitle] = useState("");
80
+ const [errorDialogDoc, setErrorDialogDoc] = useState<Document | null>(null);
81
+ const [expandedChangeDocId, setExpandedChangeDocId] = useState<string | null>(null);
82
+ const [activeTabId, setActiveTabId] = useState<string | undefined>(undefined);
83
+
84
+ // Refs for preventing race conditions
85
+ const isSelectingFiles = useRef(false);
86
+ const isMountedRef = useRef(true);
87
+
88
+ // STALE CLOSURE FIX: Track latest sessions for async operations
89
+ // This ref always holds the current sessions value, even inside async callbacks
90
+ const sessionsRef = useRef(sessions);
91
+ sessionsRef.current = sessions; // Update on every render
92
+
93
+ // Track component mount status for safe async operations
94
+ useEffect(() => {
95
+ isMountedRef.current = true;
96
+ return () => {
97
+ isMountedRef.current = false;
98
+ };
99
+ }, []);
100
+
101
+ useEffect(() => {
102
+ if (id && !currentSession) {
103
+ loadSession(id);
104
+ }
105
+ }, [id, currentSession, loadSession]);
106
+
107
+ const session = sessions.find((s) => s.id === id);
108
+
109
+ // REFACTORED: Convert session processing options to ProcessingOption[] format
110
+ // Using useMemo to ensure we always have latest session data
111
+ // This prevents stale closure issues that caused toggle auto-revert bug
112
+ // IMPORTANT: Must be called before any conditional returns (Rules of Hooks)
113
+ const processingOptions = useMemo((): ProcessingOption[] => {
114
+ const enabledOps = session?.processingOptions?.enabledOperations || [];
115
+ return defaultOptions.map((opt) => ({
116
+ ...opt,
117
+ enabled: enabledOps.includes(opt.id),
118
+ }));
119
+ }, [session?.processingOptions]);
120
+
121
+ // Document processing queue - ensures documents process one at a time
122
+ // to avoid API throttling errors from simultaneous requests
123
+ const {
124
+ queue: documentQueue,
125
+ currentDocumentId,
126
+ addToQueue,
127
+ addManyToQueue,
128
+ clearQueue,
129
+ getQueuePosition,
130
+ isInQueue,
131
+ estimatedTimeRemainingFormatted,
132
+ isProcessing: isQueueProcessing,
133
+ } = useDocumentQueue({
134
+ onDocumentComplete: (docId, success) => {
135
+ // Get fresh session data after async operation
136
+ const freshSession = sessionsRef.current.find((s) => s.id === session?.id);
137
+ const processedDoc = freshSession?.documents.find((d) => d.id === docId);
138
+
139
+ if (success && processedDoc?.status === 'completed') {
140
+ toast({
141
+ title: 'Done',
142
+ description: processedDoc.name,
143
+ variant: 'success',
144
+ });
145
+ } else if (processedDoc?.status === 'error') {
146
+ toast({
147
+ title: 'Processing failed',
148
+ description: processedDoc.errors?.[0] || 'Document error',
149
+ variant: 'destructive',
150
+ });
151
+ }
152
+ },
153
+ onQueueComplete: () => {
154
+ if (documentQueue.length > 1) {
155
+ toast({
156
+ title: 'All documents processed',
157
+ variant: 'success',
158
+ duration: 5000,
159
+ });
160
+ }
161
+ },
162
+ });
163
+
164
+ const handleFileSelect = useCallback(async () => {
165
+ // Prevent concurrent file selections
166
+ if (isSelectingFiles.current) {
167
+ logger.debug("[File Select] Already selecting files, ignoring request");
168
+ return;
169
+ }
170
+
171
+ // Store current session ID for stale reference check
172
+ const currentSessionId = session?.id;
173
+ if (!currentSessionId) {
174
+ toast({
175
+ title: "No active session",
176
+ variant: "destructive",
177
+ });
178
+ return;
179
+ }
180
+
181
+ // Safely check if electronAPI is available
182
+ const api = window.electronAPI;
183
+ if (!api?.selectDocuments || !api?.getFileStats) {
184
+ console.warn("CurrentSession: electronAPI methods not available");
185
+ toast({
186
+ title: "File selection unavailable",
187
+ description: "Please restart app",
188
+ variant: "destructive",
189
+ });
190
+ return;
191
+ }
192
+
193
+ isSelectingFiles.current = true;
194
+
195
+ try {
196
+ // Use Electron's native file dialog
197
+ const filePaths = await api.selectDocuments();
198
+
199
+ // Check if component is still mounted and session hasn't changed
200
+ if (!isMountedRef.current) {
201
+ logger.debug("[File Select] Component unmounted during file selection");
202
+ return;
203
+ }
204
+
205
+ if (!filePaths || filePaths.length === 0) {
206
+ return; // User cancelled
207
+ }
208
+
209
+ // Verify session is still valid
210
+ if (session?.id !== currentSessionId) {
211
+ logger.warn("[File Select] Session changed during file selection");
212
+ toast({
213
+ title: "Session changed",
214
+ description: "Please try again",
215
+ variant: "destructive",
216
+ });
217
+ return;
218
+ }
219
+
220
+ // Convert file paths to File-like objects with path property and actual size
221
+ const validFiles: (File & { path: string })[] = [];
222
+ const invalidFiles: string[] = [];
223
+
224
+ for (const filePath of filePaths) {
225
+ // Early exit if component unmounted
226
+ if (!isMountedRef.current) {
227
+ logger.debug("[File Select] Component unmounted during file processing");
228
+ return;
229
+ }
230
+
231
+ const name = filePath.split(/[\\/]/).pop() || "document.docx";
232
+
233
+ try {
234
+ // Get actual file size from filesystem
235
+ const stats = await api.getFileStats(filePath);
236
+
237
+ // Create a File-like object with the required properties
238
+ // Note: File methods are stubs since we use the path for actual file operations
239
+ const fileWithPath = {
240
+ path: filePath,
241
+ name: name,
242
+ size: stats.size,
243
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
244
+ lastModified: stats.mtimeMs || Date.now(),
245
+ webkitRelativePath: "",
246
+ // Stub methods - these are not used since we process via Electron IPC with file paths
247
+ arrayBuffer: async () => {
248
+ throw new Error("Use Electron IPC for file operations");
249
+ },
250
+ slice: () => new Blob(),
251
+ stream: () => {
252
+ throw new Error("Use Electron IPC for file operations");
253
+ },
254
+ text: async () => {
255
+ throw new Error("Use Electron IPC for file operations");
256
+ },
257
+ bytes: async () => {
258
+ throw new Error("Use Electron IPC for file operations");
259
+ },
260
+ } as unknown as File & { path: string };
261
+
262
+ validFiles.push(fileWithPath);
263
+ logger.debug(`[File Select] Valid file: ${name} (${stats.size} bytes) at ${filePath}`);
264
+ } catch (error) {
265
+ logger.error(`[File Select] Failed to access file at "${filePath}":`, error);
266
+ invalidFiles.push(name);
267
+ }
268
+ }
269
+
270
+ // Final mount check before updating state
271
+ if (!isMountedRef.current) {
272
+ return;
273
+ }
274
+
275
+ // Add valid files to the session
276
+ if (validFiles.length > 0) {
277
+ await addDocuments(currentSessionId, validFiles);
278
+
279
+ if (isMountedRef.current) {
280
+ toast({
281
+ title: `${validFiles.length} file${validFiles.length > 1 ? "s" : ""} added`,
282
+ variant: "success",
283
+ });
284
+ }
285
+ }
286
+
287
+ // Show error toast if any files were invalid
288
+ if (invalidFiles.length > 0 && isMountedRef.current) {
289
+ logger.warn(`[File Select] Rejected ${invalidFiles.length} file(s):`, invalidFiles);
290
+ toast({
291
+ title: "Access denied",
292
+ description: `${invalidFiles.length} file${invalidFiles.length > 1 ? "s" : ""} skipped`,
293
+ variant: "destructive",
294
+ });
295
+ }
296
+
297
+ // If no files were valid at all
298
+ if (validFiles.length === 0 && filePaths.length > 0 && isMountedRef.current) {
299
+ toast({
300
+ title: "Cannot access files",
301
+ description: "Check file permissions",
302
+ variant: "destructive",
303
+ });
304
+ }
305
+ } catch (error) {
306
+ if (isMountedRef.current) {
307
+ logger.error("[File Select] Unexpected error:", error);
308
+ toast({
309
+ title: "Selection failed",
310
+ description: error instanceof Error ? error.message : "Unexpected error",
311
+ variant: "destructive",
312
+ });
313
+ }
314
+ } finally {
315
+ isSelectingFiles.current = false;
316
+ }
317
+ }, [session?.id, addDocuments, toast]);
318
+
319
+ if (!session) {
320
+ return (
321
+ <div className="flex items-center justify-center h-full">
322
+ <Card className="max-w-md">
323
+ <CardContent className="p-8 text-center">
324
+ <AlertCircle className="w-12 h-12 mx-auto text-muted-foreground mb-4" />
325
+ <h2 className="text-xl font-semibold mb-2">Session Not Found</h2>
326
+ <p className="text-muted-foreground mb-4">
327
+ The session you are looking for does not exist or has been deleted.
328
+ </p>
329
+ <Button onClick={() => navigate("/")}>Return to Dashboard</Button>
330
+ </CardContent>
331
+ </Card>
332
+ </div>
333
+ );
334
+ }
335
+
336
+ const handleDragOver = (e: React.DragEvent) => {
337
+ e.preventDefault();
338
+ setIsDragging(true);
339
+ };
340
+
341
+ const handleDragLeave = () => {
342
+ setIsDragging(false);
343
+ };
344
+
345
+ // Handle drag-drop files using getPathsForFiles API
346
+ const handleDrop = async (e: React.DragEvent) => {
347
+ e.preventDefault();
348
+ setIsDragging(false);
349
+
350
+ if (!session || !e.dataTransfer?.files?.length) return;
351
+
352
+ const files = Array.from(e.dataTransfer.files);
353
+
354
+ // Get file paths using the preload API
355
+ const paths = window.electronAPI?.getPathsForFiles?.(files) || [];
356
+
357
+ // Filter for .docx files with valid paths
358
+ const validFiles = files
359
+ .map((file, i) => ({
360
+ file,
361
+ path: paths[i] || "",
362
+ }))
363
+ .filter(({ file, path }) => file.name.endsWith(".docx") && path)
364
+ .map(
365
+ ({ file, path }) =>
366
+ ({
367
+ name: file.name,
368
+ path: path,
369
+ size: file.size,
370
+ type:
371
+ file.type ||
372
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
373
+ lastModified: file.lastModified,
374
+ arrayBuffer: async () => new ArrayBuffer(0),
375
+ slice: () => new Blob(),
376
+ stream: () => new ReadableStream(),
377
+ text: async () => "",
378
+ webkitRelativePath: "",
379
+ }) as File & { path: string }
380
+ );
381
+
382
+ if (validFiles.length === 0) {
383
+ logger.warn("[Drag-Drop] No valid .docx files dropped");
384
+ return;
385
+ }
386
+
387
+ addDocuments(session.id, validFiles);
388
+
389
+ toast({
390
+ title: `${validFiles.length} file${validFiles.length > 1 ? "s" : ""} added`,
391
+ variant: "success",
392
+ });
393
+ };
394
+
395
+ const handleProcessDocument = (documentId: string) => {
396
+ if (!session || isInQueue(documentId)) return;
397
+
398
+ // Add to queue - the queue hook handles sequential processing
399
+ // and toast notifications via onDocumentComplete callback
400
+ addToQueue(session.id, documentId);
401
+ };
402
+
403
+ const handleSaveAndClose = () => {
404
+ closeSession(session.id);
405
+ navigate("/");
406
+ };
407
+
408
+ const handleExportProcessedFiles = async () => {
409
+ const completedDocs = session.documents.filter(
410
+ (d) => d.status === "completed" && d.path
411
+ );
412
+ if (completedDocs.length === 0) {
413
+ toast({
414
+ title: "No completed files to export",
415
+ description: "Process some documents first",
416
+ variant: "destructive",
417
+ });
418
+ return;
419
+ }
420
+
421
+ try {
422
+ const folderPath = await window.electronAPI.selectFolder();
423
+ if (!folderPath) return;
424
+
425
+ const result = await window.electronAPI.copyFilesToFolder(
426
+ completedDocs.map((d) => d.path!),
427
+ folderPath
428
+ );
429
+
430
+ toast({
431
+ title: `Exported ${result.copied} file${result.copied !== 1 ? "s" : ""}`,
432
+ description: result.skipped > 0 ? `${result.skipped} file(s) skipped` : undefined,
433
+ variant: "success",
434
+ });
435
+ } catch (error) {
436
+ toast({
437
+ title: "Export failed",
438
+ description: error instanceof Error ? error.message : "Unknown error",
439
+ variant: "destructive",
440
+ });
441
+ }
442
+ };
443
+
444
+ const handleEditTitle = () => {
445
+ setEditedTitle(session.name);
446
+ setIsEditingTitle(true);
447
+ };
448
+
449
+ const handleSaveTitle = () => {
450
+ if (editedTitle.trim() && editedTitle !== session.name) {
451
+ updateSessionName(session.id, editedTitle.trim());
452
+ }
453
+ setIsEditingTitle(false);
454
+ };
455
+
456
+ const handleCancelEdit = () => {
457
+ setIsEditingTitle(false);
458
+ setEditedTitle("");
459
+ };
460
+
461
+ const handleProcessingOptionsChange = (options: Array<{ id: string; enabled: boolean }>) => {
462
+ // Update session with selected processing options
463
+ const enabledOperations = options.filter((opt) => opt.enabled).map((opt) => opt.id);
464
+
465
+ // DEBUG: Log processing options changes
466
+ console.log("[CurrentSession] Processing options changed:");
467
+ console.log(" - Enabled operations:", enabledOperations);
468
+ console.log(" - TOC enabled:", enabledOperations.includes("update-toc-hyperlinks"));
469
+ console.log(
470
+ " - Validate styles enabled:",
471
+ enabledOperations.includes("validate-document-styles")
472
+ );
473
+ console.log(
474
+ " - Validate Header2 tables enabled:",
475
+ enabledOperations.includes("validate-header2-tables")
476
+ );
477
+
478
+ // Update session processing options using the context method
479
+ updateSessionOptions(session.id, {
480
+ validateUrls: true,
481
+ createBackup: true,
482
+ processInternalLinks: enabledOperations.includes("fix-internal-hyperlinks"),
483
+ processExternalLinks: true,
484
+ enabledOperations: enabledOperations,
485
+ });
486
+ };
487
+
488
+ const handleTableShadingChange = (
489
+ header2: string,
490
+ other: string,
491
+ imageBorderWidth?: number,
492
+ paddingSettings?: {
493
+ padding1x1Top: number;
494
+ padding1x1Bottom: number;
495
+ padding1x1Left: number;
496
+ padding1x1Right: number;
497
+ paddingOtherTop: number;
498
+ paddingOtherBottom: number;
499
+ paddingOtherLeft: number;
500
+ paddingOtherRight: number;
501
+ cellBorderThickness?: number;
502
+ }
503
+ ) => {
504
+ // Update session table shading settings including padding and border thickness
505
+ updateSessionTableShadingSettings(session.id, {
506
+ header2Shading: header2,
507
+ otherShading: other,
508
+ imageBorderWidth: imageBorderWidth,
509
+ // Include padding settings if provided
510
+ ...(paddingSettings && {
511
+ padding1x1Top: paddingSettings.padding1x1Top,
512
+ padding1x1Bottom: paddingSettings.padding1x1Bottom,
513
+ padding1x1Left: paddingSettings.padding1x1Left,
514
+ padding1x1Right: paddingSettings.padding1x1Right,
515
+ paddingOtherTop: paddingSettings.paddingOtherTop,
516
+ paddingOtherBottom: paddingSettings.paddingOtherBottom,
517
+ paddingOtherLeft: paddingSettings.paddingOtherLeft,
518
+ paddingOtherRight: paddingSettings.paddingOtherRight,
519
+ cellBorderThickness: paddingSettings.cellBorderThickness,
520
+ }),
521
+ });
522
+ };
523
+
524
+ const handleAutoAcceptRevisionsChange = (autoAccept: boolean) => {
525
+ // Update session auto-accept revisions setting
526
+ // Note: We need to provide all required fields since TypeScript expects them
527
+ updateSessionOptions(session.id, {
528
+ validateUrls: session.processingOptions?.validateUrls ?? true,
529
+ createBackup: session.processingOptions?.createBackup ?? true,
530
+ processInternalLinks: session.processingOptions?.processInternalLinks ?? true,
531
+ processExternalLinks: session.processingOptions?.processExternalLinks ?? true,
532
+ enabledOperations: session.processingOptions?.enabledOperations ?? [],
533
+ autoAcceptRevisions: autoAccept,
534
+ });
535
+ };
536
+
537
+ const getStatusIcon = (status: Document["status"]) => {
538
+ switch (status) {
539
+ case "pending":
540
+ return <Clock className="w-4 h-4 text-muted-foreground" />;
541
+ case "processing":
542
+ return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
543
+ case "completed":
544
+ return <CheckCircle className="w-4 h-4 text-green-500" />;
545
+ case "error":
546
+ return <AlertCircle className="w-4 h-4 text-red-500" />;
547
+ }
548
+ };
549
+
550
+ const formatFileSize = (bytes: number) => {
551
+ if (bytes < 1024) return bytes + " B";
552
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
553
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
554
+ };
555
+
556
+ // Get count of completed documents for export button
557
+ const completedDocsCount = session.documents.filter(
558
+ (d) => d.status === "completed" && d.path
559
+ ).length;
560
+
561
+ // Create session content for the Session tab
562
+ const sessionContent = (
563
+ <div className="space-y-6">
564
+ {/* Action Buttons - Context dependent */}
565
+ <div className="flex justify-between items-center">
566
+ <Button
567
+ onClick={handleExportProcessedFiles}
568
+ variant="outline"
569
+ disabled={completedDocsCount === 0}
570
+ className="gap-2"
571
+ icon={<Download className="w-4 h-4" />}
572
+ >
573
+ Export Processed Files
574
+ </Button>
575
+ <div>
576
+ {session.status === 'closed' ? (
577
+ <Button
578
+ onClick={() => reopenSession(session.id)}
579
+ variant="default"
580
+ className="bg-green-600 hover:bg-green-700 text-white font-medium"
581
+ icon={<FolderOpen className="w-4 h-4" />}
582
+ >
583
+ Re-Open Session
584
+ </Button>
585
+ ) : (
586
+ <Button
587
+ onClick={handleSaveAndClose}
588
+ variant="default"
589
+ className="bg-primary hover:bg-primary/90 text-primary-foreground font-medium"
590
+ icon={<Save className="w-4 h-4" />}
591
+ >
592
+ Save and Close Session
593
+ </Button>
594
+ )}
595
+ </div>
596
+ </div>
597
+
598
+ {/* Stats Cards */}
599
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
600
+ <Card>
601
+ <CardContent className="p-4">
602
+ <div className="flex items-center gap-3">
603
+ <FileCheck className="w-8 h-8 text-green-500" />
604
+ <div>
605
+ <p className="text-xs text-muted-foreground">Documents</p>
606
+ <p className="text-2xl font-bold">{session.stats.documentsProcessed}</p>
607
+ </div>
608
+ </div>
609
+ </CardContent>
610
+ </Card>
611
+
612
+ <Card>
613
+ <CardContent className="p-4">
614
+ <div className="flex items-center gap-3">
615
+ <Link className="w-8 h-8 text-blue-500" />
616
+ <div>
617
+ <p className="text-xs text-muted-foreground">Hyperlinks</p>
618
+ <p className="text-2xl font-bold">{session.stats.hyperlinksChecked}</p>
619
+ </div>
620
+ </div>
621
+ </CardContent>
622
+ </Card>
623
+
624
+ <Card>
625
+ <CardContent className="p-4">
626
+ <div className="flex items-center gap-3">
627
+ <MessageSquare className="w-8 h-8 text-purple-500" />
628
+ <div>
629
+ <p className="text-xs text-muted-foreground">Feedback</p>
630
+ <p className="text-2xl font-bold">{session.stats.feedbackImported}</p>
631
+ </div>
632
+ </div>
633
+ </CardContent>
634
+ </Card>
635
+
636
+ <Card>
637
+ <CardContent className="p-4">
638
+ <div className="flex items-center gap-3">
639
+ <Timer className="w-8 h-8 text-orange-500" />
640
+ <div>
641
+ <p className="text-xs text-muted-foreground">Time Saved</p>
642
+ <p className="text-2xl font-bold">
643
+ {Math.round((session.stats.hyperlinksChecked * 101) / 60)}m
644
+ </p>
645
+ <p className="text-xs text-muted-foreground">101 seconds per hyperlink</p>
646
+ </div>
647
+ </div>
648
+ </CardContent>
649
+ </Card>
650
+ </div>
651
+
652
+ {/* Document Upload Area */}
653
+ <Card>
654
+ <CardHeader>
655
+ <CardTitle>Documents</CardTitle>
656
+ <CardDescription>Upload and process Word documents (.docx)</CardDescription>
657
+ </CardHeader>
658
+ <CardContent>
659
+ {session.documents.length === 0 ? (
660
+ <div
661
+ className={cn(
662
+ "border-2 border-dashed rounded-lg p-8 text-center transition-colors",
663
+ isDragging ? "border-primary bg-primary/5" : "border-border"
664
+ )}
665
+ onDragOver={handleDragOver}
666
+ onDragLeave={handleDragLeave}
667
+ onDrop={handleDrop}
668
+ >
669
+ <Upload className="w-12 h-12 mx-auto text-muted-foreground mb-4" />
670
+ <h3 className="text-lg font-medium mb-2">
671
+ {isDragging ? "Drop files here" : "Upload Documents"}
672
+ </h3>
673
+ <p className="text-sm text-muted-foreground mb-4">
674
+ Drag and drop .docx files here, or click to browse
675
+ </p>
676
+ <Button onClick={handleFileSelect} icon={<Upload className="w-4 h-4" />}>
677
+ Load Files
678
+ </Button>
679
+ </div>
680
+ ) : (
681
+ <>
682
+ {/* Queue status banner - shows when documents are queued */}
683
+ {documentQueue.length > 0 && (
684
+ <div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg flex items-center justify-between">
685
+ <div className="flex items-center gap-3">
686
+ <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
687
+ <span className="text-sm">
688
+ Processing document {documentQueue.findIndex(q => q.status === 'processing') + 1} of {documentQueue.length}
689
+ </span>
690
+ {estimatedTimeRemainingFormatted && (
691
+ <span className="text-xs text-muted-foreground flex items-center gap-1">
692
+ <Timer className="w-3 h-3" />
693
+ ~{estimatedTimeRemainingFormatted} remaining
694
+ </span>
695
+ )}
696
+ </div>
697
+ <Button size="xs" variant="ghost" onClick={clearQueue}>
698
+ Cancel All
699
+ </Button>
700
+ </div>
701
+ )}
702
+
703
+ <div className="mb-4 flex justify-between">
704
+ <Button
705
+ onClick={() => {
706
+ // Add all pending documents to the queue for sequential processing
707
+ const pendingDocs = session.documents
708
+ .filter((doc) => doc.status === "pending")
709
+ .map((doc) => doc.id);
710
+ addManyToQueue(session.id, pendingDocs);
711
+ }}
712
+ size="sm"
713
+ variant="default"
714
+ className="bg-green-600 hover:bg-green-700 text-white"
715
+ icon={<Play className="w-4 h-4" />}
716
+ disabled={!session.documents.some((doc) => doc.status === "pending") || documentQueue.length > 0}
717
+ >
718
+ Process Documents
719
+ </Button>
720
+ <Button
721
+ onClick={handleFileSelect}
722
+ size="sm"
723
+ variant="default"
724
+ className="bg-primary hover:bg-primary/90 text-primary-foreground"
725
+ icon={<Upload className="w-4 h-4" />}
726
+ >
727
+ Add More Files
728
+ </Button>
729
+ </div>
730
+
731
+ <div className="space-y-2">
732
+ <AnimatePresence>
733
+ {session.documents.map((doc) => (
734
+ <motion.div
735
+ key={doc.id}
736
+ initial={{ opacity: 0, y: 20 }}
737
+ animate={{ opacity: 1, y: 0 }}
738
+ exit={{ opacity: 0, x: -20 }}
739
+ className={cn(
740
+ "flex items-center justify-between p-3 rounded-lg border border-border hover:bg-muted/50 transition-all group",
741
+ doc.status === "completed" && "cursor-pointer"
742
+ )}
743
+ onDoubleClick={() => {
744
+ if (doc.status === "completed") {
745
+ // Switch to Document Changes tab and expand this document
746
+ setActiveTabId("tracked-changes");
747
+ setExpandedChangeDocId(doc.id);
748
+ }
749
+ }}
750
+ >
751
+ <div className="flex items-center gap-3">
752
+ {getStatusIcon(doc.status)}
753
+ <div>
754
+ <p className="font-medium text-sm">{doc.name}</p>
755
+ <p className="text-xs text-muted-foreground">
756
+ {formatFileSize(doc.size)}
757
+ {doc.processedAt &&
758
+ ` • Processed ${new Date(doc.processedAt).toLocaleTimeString()}`}
759
+ </p>
760
+ </div>
761
+ </div>
762
+
763
+ <div className="flex items-center gap-2">
764
+ {doc.status === "pending" && !isInQueue(doc.id) && (
765
+ <Button
766
+ size="xs"
767
+ onClick={() => handleProcessDocument(doc.id)}
768
+ disabled={isInQueue(doc.id)}
769
+ >
770
+ Process
771
+ </Button>
772
+ )}
773
+
774
+ {/* Queue position indicator */}
775
+ {doc.status === "pending" && isInQueue(doc.id) && (
776
+ <span className="text-xs text-blue-500 font-medium flex items-center gap-1">
777
+ {currentDocumentId === doc.id ? (
778
+ <>
779
+ <Loader2 className="w-3 h-3 animate-spin" />
780
+ Processing...
781
+ </>
782
+ ) : (
783
+ `Queue #${getQueuePosition(doc.id) + 1}`
784
+ )}
785
+ </span>
786
+ )}
787
+
788
+ {doc.status === "processing" && (
789
+ <span className="text-xs text-blue-500 font-medium">Processing...</span>
790
+ )}
791
+
792
+ {doc.status === "completed" && (
793
+ <span className="text-xs text-green-500 font-medium">Completed</span>
794
+ )}
795
+
796
+ {doc.status === "error" && (
797
+ <>
798
+ <button
799
+ onClick={() => setErrorDialogDoc(doc)}
800
+ className="text-xs text-red-500 font-medium hover:text-red-600 hover:underline cursor-pointer"
801
+ title="Click to view error details"
802
+ >
803
+ {doc.errorType === 'file_locked'
804
+ ? "Close File Before Processing"
805
+ : doc.errorType === 'api_timeout'
806
+ ? "Power Automate Timeout"
807
+ : doc.errorType === 'word_compatibility'
808
+ ? "Convert File in Word"
809
+ : "Error"}
810
+ </button>
811
+ {(doc.errorType === 'file_locked' || doc.errorType === 'api_timeout' || doc.errorType === 'word_compatibility') && (
812
+ <Button
813
+ size="xs"
814
+ onClick={() => handleProcessDocument(doc.id)}
815
+ disabled={isInQueue(doc.id)}
816
+ >
817
+ Retry
818
+ </Button>
819
+ )}
820
+ </>
821
+ )}
822
+
823
+ {/* Open Document button - only show for completed documents */}
824
+ {doc.status === "completed" && doc.path && (
825
+ <button
826
+ onClick={async () => {
827
+ try {
828
+ await window.electronAPI.openDocument(doc.path!);
829
+ toast({
830
+ title: "Opening in Word",
831
+ variant: "default",
832
+ });
833
+ } catch (err) {
834
+ logger.error("Failed to open document:", err);
835
+ toast({
836
+ title: "Cannot open file",
837
+ description: err instanceof Error ? err.message : undefined,
838
+ variant: "destructive",
839
+ });
840
+ }
841
+ }}
842
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-green-50 dark:hover:bg-green-950"
843
+ title="Open document in Word"
844
+ >
845
+ <FileText className="w-4 h-4 text-green-600 dark:text-green-400" />
846
+ </button>
847
+ )}
848
+
849
+ {/* Open Backup button - only show for completed documents with backup */}
850
+ {doc.status === "completed" && doc.processingResult?.backupPath && (
851
+ <button
852
+ onClick={async () => {
853
+ try {
854
+ await window.electronAPI.openDocument(doc.processingResult!.backupPath!);
855
+ toast({
856
+ title: "Opening backup in Word",
857
+ variant: "default",
858
+ });
859
+ } catch (err) {
860
+ logger.error("Failed to open backup:", err);
861
+ toast({
862
+ title: "Cannot open backup file",
863
+ description: err instanceof Error ? err.message : undefined,
864
+ variant: "destructive",
865
+ });
866
+ }
867
+ }}
868
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-yellow-50 dark:hover:bg-yellow-950"
869
+ title="Open backup file"
870
+ >
871
+ <Archive className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
872
+ </button>
873
+ )}
874
+
875
+ {/* Compare button - show backup vs processed side by side */}
876
+ {doc.status === "completed" && doc.path && doc.processingResult?.backupPath && (
877
+ <button
878
+ onClick={async () => {
879
+ if (!window.electronAPI?.display?.openComparison || !window.electronAPI?.display?.getAllDisplays) {
880
+ toast({
881
+ title: "Compare unavailable",
882
+ description: "Feature not available in this version",
883
+ variant: "destructive",
884
+ });
885
+ return;
886
+ }
887
+ try {
888
+ // Get available displays to validate monitor index
889
+ const displays = await window.electronAPI.display.getAllDisplays();
890
+ const savedMonitorIndex = settings.displaySettings?.comparisonMonitorId ?? 0;
891
+ // Ensure monitor index is within bounds (fallback to primary if saved monitor no longer exists)
892
+ const monitorIndex = Math.min(Math.max(0, savedMonitorIndex), displays.length - 1);
893
+
894
+ const result = await window.electronAPI.display.openComparison(
895
+ doc.processingResult!.backupPath!,
896
+ doc.path!,
897
+ monitorIndex
898
+ );
899
+ if (result.success) {
900
+ toast({
901
+ title: "Opening comparison",
902
+ description: "Documents opening in Word side by side",
903
+ variant: "default",
904
+ });
905
+ } else {
906
+ toast({
907
+ title: "Compare failed",
908
+ description: result.error,
909
+ variant: "destructive",
910
+ });
911
+ }
912
+ } catch (err) {
913
+ logger.error("Failed to open comparison:", err);
914
+ toast({
915
+ title: "Compare failed",
916
+ description: err instanceof Error ? err.message : undefined,
917
+ variant: "destructive",
918
+ });
919
+ }
920
+ }}
921
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-blue-50 dark:hover:bg-blue-950"
922
+ title="Compare before/after in Word"
923
+ >
924
+ <GitCompare className="w-4 h-4 text-blue-600 dark:text-blue-400" />
925
+ </button>
926
+ )}
927
+
928
+ {/* Show in Folder button */}
929
+ {doc.path && (
930
+ <button
931
+ onClick={async () => {
932
+ try {
933
+ await window.electronAPI.showInFolder(doc.path!);
934
+ } catch (err) {
935
+ logger.error("Failed to open file location:", err);
936
+ }
937
+ }}
938
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-background"
939
+ title="Open file location"
940
+ >
941
+ <FolderOpen className="w-4 h-4 text-muted-foreground" />
942
+ </button>
943
+ )}
944
+
945
+ <button
946
+ onClick={() => removeDocument(session.id, doc.id)}
947
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-background"
948
+ >
949
+ <X className="w-4 h-4 text-muted-foreground" />
950
+ </button>
951
+ </div>
952
+ </motion.div>
953
+ ))}
954
+ </AnimatePresence>
955
+ </div>
956
+
957
+ <div
958
+ className={cn(
959
+ "mt-4 border-2 border-dashed rounded-lg p-4 text-center transition-colors",
960
+ isDragging ? "border-primary bg-primary/5" : "border-border"
961
+ )}
962
+ onDragOver={handleDragOver}
963
+ onDragLeave={handleDragLeave}
964
+ onDrop={handleDrop}
965
+ >
966
+ <p className="text-sm text-muted-foreground">
967
+ {isDragging ? "Drop files here to add" : "Drag and drop more files here"}
968
+ </p>
969
+ </div>
970
+ </>
971
+ )}
972
+ </CardContent>
973
+ </Card>
974
+ </div>
975
+ );
976
+
977
+ // Header actions for tab headers - Reset/Save Default buttons
978
+ const handleResetToDefaults = () => {
979
+ if (session) {
980
+ resetSessionToDefaults(session.id);
981
+ toast({
982
+ title: "Settings reset",
983
+ variant: "default",
984
+ });
985
+ }
986
+ };
987
+
988
+ const handleSaveAsDefaults = () => {
989
+ if (session) {
990
+ saveAsCustomDefaults(session.id);
991
+ toast({
992
+ title: "Saved as default",
993
+ variant: "success",
994
+ });
995
+ }
996
+ };
997
+
998
+ const settingsButtons = (
999
+ <div className="flex items-center gap-2">
1000
+ <Button
1001
+ size="xs"
1002
+ variant="ghost"
1003
+ onClick={handleResetToDefaults}
1004
+ className="text-xs"
1005
+ icon={<RotateCcw className="w-3 h-3" />}
1006
+ >
1007
+ Reset
1008
+ </Button>
1009
+ <Button
1010
+ size="xs"
1011
+ variant="outline"
1012
+ onClick={handleSaveAsDefaults}
1013
+ className="text-xs"
1014
+ icon={<Save className="w-3 h-3" />}
1015
+ >
1016
+ Save as Default
1017
+ </Button>
1018
+ </div>
1019
+ );
1020
+
1021
+ const headerActions: Record<string, React.ReactNode> = {
1022
+ processing: settingsButtons,
1023
+ styles: settingsButtons,
1024
+ };
1025
+
1026
+ // Create tabs configuration
1027
+ const tabs = [
1028
+ {
1029
+ id: "session",
1030
+ label: `Session: ${session.name}`,
1031
+ content: sessionContent,
1032
+ },
1033
+ {
1034
+ id: "processing",
1035
+ label: "Processing Options",
1036
+ content: (
1037
+ <ProcessingOptions
1038
+ sessionId={session.id}
1039
+ options={processingOptions}
1040
+ onOptionsChange={handleProcessingOptionsChange}
1041
+ autoAcceptRevisions={session.processingOptions?.autoAcceptRevisions ?? false}
1042
+ onAutoAcceptRevisionsChange={handleAutoAcceptRevisionsChange}
1043
+ />
1044
+ ),
1045
+ },
1046
+ {
1047
+ id: "styles",
1048
+ label: "Styles",
1049
+ content: (
1050
+ <StylesEditor
1051
+ initialStyles={session.styles}
1052
+ initialListBulletSettings={session.listBulletSettings}
1053
+ onStylesChange={(styles) => {
1054
+ // Auto-save: changes are persisted immediately to SessionContext
1055
+ updateSessionStyles(session.id, styles);
1056
+ }}
1057
+ onListBulletSettingsChange={(settings) => {
1058
+ // Auto-save: changes are persisted immediately to SessionContext
1059
+ updateSessionListBulletSettings(session.id, settings);
1060
+ }}
1061
+ tableHeader2Shading={session.tableShadingSettings?.header2Shading || "#BFBFBF"}
1062
+ tableOtherShading={session.tableShadingSettings?.otherShading || "#DFDFDF"}
1063
+ imageBorderWidth={session.tableShadingSettings?.imageBorderWidth ?? 1.0}
1064
+ padding1x1Top={session.tableShadingSettings?.padding1x1Top ?? 0}
1065
+ padding1x1Bottom={session.tableShadingSettings?.padding1x1Bottom ?? 0}
1066
+ padding1x1Left={session.tableShadingSettings?.padding1x1Left ?? 0.08}
1067
+ padding1x1Right={session.tableShadingSettings?.padding1x1Right ?? 0.08}
1068
+ paddingOtherTop={session.tableShadingSettings?.paddingOtherTop ?? 0}
1069
+ paddingOtherBottom={session.tableShadingSettings?.paddingOtherBottom ?? 0}
1070
+ paddingOtherLeft={session.tableShadingSettings?.paddingOtherLeft ?? 0.08}
1071
+ paddingOtherRight={session.tableShadingSettings?.paddingOtherRight ?? 0.08}
1072
+ cellBorderThickness={session.tableShadingSettings?.cellBorderThickness ?? 0.5}
1073
+ onTableShadingChange={handleTableShadingChange}
1074
+ />
1075
+ ),
1076
+ },
1077
+ {
1078
+ id: "replacements",
1079
+ label: "Replacements",
1080
+ content: <ReplacementsTab sessionId={session.id} />,
1081
+ },
1082
+ {
1083
+ id: "tracked-changes",
1084
+ label: "Document Changes",
1085
+ content: (
1086
+ <ChangeViewer
1087
+ sessionId={session.id}
1088
+ expandDocumentId={expandedChangeDocId}
1089
+ onExpandHandled={() => setExpandedChangeDocId(null)}
1090
+ />
1091
+ ),
1092
+ },
1093
+ ];
1094
+
1095
+ return (
1096
+ <div className="h-full flex flex-col">
1097
+ {/* Toast notifications */}
1098
+ <Toaster toasts={toasts} onDismiss={dismiss} />
1099
+
1100
+ {/* Sticky Header Section */}
1101
+ <div className="sticky top-0 z-30 bg-background p-6 pb-0 max-w-6xl mx-auto w-full">
1102
+ {/* Title Edit */}
1103
+ <div className="flex items-center gap-2 mb-6">
1104
+ {isEditingTitle ? (
1105
+ <div className="flex items-center gap-2">
1106
+ <input
1107
+ type="text"
1108
+ value={editedTitle}
1109
+ onChange={(e) => setEditedTitle(e.target.value)}
1110
+ onKeyDown={(e) => {
1111
+ if (e.key === "Enter") handleSaveTitle();
1112
+ if (e.key === "Escape") handleCancelEdit();
1113
+ }}
1114
+ className="text-3xl font-bold bg-transparent border-b-2 border-primary outline-none px-1"
1115
+ autoFocus
1116
+ />
1117
+ <button
1118
+ onClick={handleSaveTitle}
1119
+ className="p-1.5 rounded-lg hover:bg-muted transition-colors"
1120
+ >
1121
+ <Check className="w-5 h-5 text-green-500" />
1122
+ </button>
1123
+ <button
1124
+ onClick={handleCancelEdit}
1125
+ className="p-1.5 rounded-lg hover:bg-muted transition-colors"
1126
+ >
1127
+ <X className="w-5 h-5 text-red-500" />
1128
+ </button>
1129
+ </div>
1130
+ ) : (
1131
+ <>
1132
+ <h1 className="text-3xl font-bold">{session.name}</h1>
1133
+ {session.status === 'closed' && (
1134
+ <span className="text-sm px-2 py-1 rounded bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400">
1135
+ Closed
1136
+ </span>
1137
+ )}
1138
+ <button
1139
+ onClick={handleEditTitle}
1140
+ className="p-1.5 rounded-lg hover:bg-muted transition-colors"
1141
+ title="Edit session name"
1142
+ >
1143
+ <Edit2 className="w-4 h-4 text-muted-foreground" />
1144
+ </button>
1145
+ </>
1146
+ )}
1147
+ </div>
1148
+ </div>
1149
+
1150
+ {/* Scrollable Content Area */}
1151
+ <div className="flex-1 overflow-auto px-6 max-w-6xl mx-auto w-full">
1152
+ <Card>
1153
+ <TabContainer
1154
+ tabs={tabs}
1155
+ defaultTab="session"
1156
+ headerActions={headerActions}
1157
+ activeTabId={activeTabId}
1158
+ onTabChange={(tabId) => setActiveTabId(tabId)}
1159
+ />
1160
+ </Card>
1161
+ </div>
1162
+
1163
+ {/* Error Details Dialog */}
1164
+ <ErrorDetailsDialog
1165
+ open={errorDialogDoc !== null}
1166
+ onOpenChange={(open) => !open && setErrorDialogDoc(null)}
1167
+ documentName={errorDialogDoc?.name ?? ""}
1168
+ errors={errorDialogDoc?.errors ?? []}
1169
+ errorType={errorDialogDoc?.errorType}
1170
+ processedAt={errorDialogDoc?.processedAt}
1171
+ />
1172
+ </div>
1173
+ );
1174
+ }