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.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,2129 @@
|
|
|
1
|
+
import type { HyperlinkProcessingOptions } from '@/types/hyperlink';
|
|
2
|
+
import {
|
|
3
|
+
CustomSessionDefaults,
|
|
4
|
+
Document,
|
|
5
|
+
ListBulletSettings,
|
|
6
|
+
ReplacementRule,
|
|
7
|
+
RevisionHandlingMode,
|
|
8
|
+
Session,
|
|
9
|
+
SessionContextType,
|
|
10
|
+
SessionStats,
|
|
11
|
+
SessionStyle,
|
|
12
|
+
TableOfContentsSettings,
|
|
13
|
+
TableShadingSettings,
|
|
14
|
+
TableUniformitySettings,
|
|
15
|
+
} from '@/types/session';
|
|
16
|
+
import { DocumentSnapshotService } from '@/services/document/DocumentSnapshotService';
|
|
17
|
+
import { requireElectronAPI } from '@/utils/electronGuard';
|
|
18
|
+
import {
|
|
19
|
+
deleteSession as deleteSessionFromDB,
|
|
20
|
+
ensureDBSizeLimit,
|
|
21
|
+
handleQuotaExceededError,
|
|
22
|
+
loadSessions,
|
|
23
|
+
migrateFromLocalStorage,
|
|
24
|
+
saveSession as saveSessionToDB,
|
|
25
|
+
truncateSessionChanges,
|
|
26
|
+
} from '@/utils/indexedDB';
|
|
27
|
+
import { logger, debugModes, isDebugEnabled, createDebugLogger } from '@/utils/logger';
|
|
28
|
+
import { isPathSafe } from '@/utils/pathSecurity';
|
|
29
|
+
import { safeJsonParse, safeJsonStringify } from '@/utils/safeJsonParse';
|
|
30
|
+
import {
|
|
31
|
+
createContext,
|
|
32
|
+
ReactNode,
|
|
33
|
+
useCallback,
|
|
34
|
+
useContext,
|
|
35
|
+
useEffect,
|
|
36
|
+
useRef,
|
|
37
|
+
useState,
|
|
38
|
+
} from 'react';
|
|
39
|
+
import { useGlobalStats } from './GlobalStatsContext';
|
|
40
|
+
|
|
41
|
+
const SessionContext = createContext<SessionContextType | undefined>(undefined);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a promise with a timeout to prevent hanging operations.
|
|
45
|
+
* @param promise The promise to wrap
|
|
46
|
+
* @param ms Timeout in milliseconds
|
|
47
|
+
* @param operation Name of the operation for error messages
|
|
48
|
+
*/
|
|
49
|
+
const withTimeout = <T,>(promise: Promise<T>, ms: number, operation: string): Promise<T> =>
|
|
50
|
+
Promise.race([
|
|
51
|
+
promise,
|
|
52
|
+
new Promise<never>((_, reject) =>
|
|
53
|
+
setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
|
|
54
|
+
),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// Constants for IPC operations
|
|
58
|
+
const IPC_TIMEOUT_MS = 300000; // 5 minutes for document processing (large docs can take time)
|
|
59
|
+
const TIME_SAVED_SECONDS_PER_HYPERLINK = 101;
|
|
60
|
+
const SECONDS_PER_MINUTE = 60;
|
|
61
|
+
|
|
62
|
+
type SerializedDocument = Omit<Document, 'processedAt'> & {
|
|
63
|
+
processedAt?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type SerializedSession = Omit<Session, 'createdAt' | 'lastModified' | 'closedAt' | 'documents'> & {
|
|
67
|
+
createdAt: string;
|
|
68
|
+
lastModified: string;
|
|
69
|
+
closedAt?: string;
|
|
70
|
+
documents: SerializedDocument[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Default list bullet settings factory
|
|
75
|
+
* Creates standard 5-level indentation configuration matching StylesEditor defaults
|
|
76
|
+
* Used for new sessions and backfilling existing sessions without list settings
|
|
77
|
+
*
|
|
78
|
+
* Cascading indentation: L0 symbol indent defaults to 0.25"
|
|
79
|
+
* Each level: text indent = symbol indent + 0.25", next symbol = prev text + 0.25"
|
|
80
|
+
*/
|
|
81
|
+
const createDefaultListBulletSettings = (): ListBulletSettings => ({
|
|
82
|
+
enabled: true,
|
|
83
|
+
indentationLevels: [
|
|
84
|
+
{ level: 0, symbolIndent: 0.25, textIndent: 0.50, bulletChar: '•', numberedFormat: '1.' },
|
|
85
|
+
{ level: 1, symbolIndent: 0.75, textIndent: 1.00, bulletChar: '○', numberedFormat: 'a.' },
|
|
86
|
+
{ level: 2, symbolIndent: 1.25, textIndent: 1.50, bulletChar: '•', numberedFormat: 'i.' },
|
|
87
|
+
{ level: 3, symbolIndent: 1.75, textIndent: 2.00, bulletChar: '○', numberedFormat: 'A.' },
|
|
88
|
+
{ level: 4, symbolIndent: 2.25, textIndent: 2.50, bulletChar: '•', numberedFormat: 'I.' },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default session styles - Shared between createSession and resetSessionToDefaults
|
|
94
|
+
* Matches StylesEditor defaults for consistency
|
|
95
|
+
*/
|
|
96
|
+
const DEFAULT_SESSION_STYLES: SessionStyle[] = [
|
|
97
|
+
{
|
|
98
|
+
id: 'header1',
|
|
99
|
+
name: 'Heading 1',
|
|
100
|
+
fontSize: 18,
|
|
101
|
+
fontFamily: 'Verdana',
|
|
102
|
+
bold: true,
|
|
103
|
+
italic: false,
|
|
104
|
+
underline: false,
|
|
105
|
+
alignment: 'left',
|
|
106
|
+
spaceBefore: 0,
|
|
107
|
+
spaceAfter: 12,
|
|
108
|
+
lineSpacing: 1.0,
|
|
109
|
+
color: '#000000',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'header2',
|
|
113
|
+
name: 'Heading 2',
|
|
114
|
+
fontSize: 14,
|
|
115
|
+
fontFamily: 'Verdana',
|
|
116
|
+
bold: true,
|
|
117
|
+
italic: false,
|
|
118
|
+
underline: false,
|
|
119
|
+
alignment: 'left',
|
|
120
|
+
spaceBefore: 6,
|
|
121
|
+
spaceAfter: 6,
|
|
122
|
+
lineSpacing: 1.0,
|
|
123
|
+
color: '#000000',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'header3',
|
|
127
|
+
name: 'Heading 3',
|
|
128
|
+
fontSize: 12,
|
|
129
|
+
fontFamily: 'Verdana',
|
|
130
|
+
bold: true,
|
|
131
|
+
italic: false,
|
|
132
|
+
underline: false,
|
|
133
|
+
alignment: 'left',
|
|
134
|
+
spaceBefore: 3,
|
|
135
|
+
spaceAfter: 3,
|
|
136
|
+
lineSpacing: 1.0,
|
|
137
|
+
color: '#000000',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'normal',
|
|
141
|
+
name: 'Normal',
|
|
142
|
+
fontSize: 12,
|
|
143
|
+
fontFamily: 'Verdana',
|
|
144
|
+
bold: false,
|
|
145
|
+
italic: false,
|
|
146
|
+
underline: false,
|
|
147
|
+
preserveBold: true,
|
|
148
|
+
preserveItalic: false,
|
|
149
|
+
preserveUnderline: false,
|
|
150
|
+
preserveCenterAlignment: true,
|
|
151
|
+
alignment: 'left',
|
|
152
|
+
spaceBefore: 3,
|
|
153
|
+
spaceAfter: 3,
|
|
154
|
+
lineSpacing: 1.0,
|
|
155
|
+
color: '#000000',
|
|
156
|
+
noSpaceBetweenSame: false,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'listParagraph',
|
|
160
|
+
name: 'List Paragraph',
|
|
161
|
+
fontSize: 12,
|
|
162
|
+
fontFamily: 'Verdana',
|
|
163
|
+
bold: false,
|
|
164
|
+
italic: false,
|
|
165
|
+
underline: false,
|
|
166
|
+
preserveBold: true,
|
|
167
|
+
preserveItalic: false,
|
|
168
|
+
preserveUnderline: false,
|
|
169
|
+
alignment: 'left',
|
|
170
|
+
spaceBefore: 0,
|
|
171
|
+
spaceAfter: 6,
|
|
172
|
+
lineSpacing: 1.0,
|
|
173
|
+
color: '#000000',
|
|
174
|
+
noSpaceBetweenSame: true,
|
|
175
|
+
// Indentation removed: numbering level definitions (symbolIndent/textIndent sliders)
|
|
176
|
+
// control list indentation via numbering.xml. Adding style-level indentation here
|
|
177
|
+
// creates conflicting w:ind in styles.xml that can produce negative bullet positions.
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Default processing options - Shared between createSession and resetSessionToDefaults
|
|
183
|
+
*/
|
|
184
|
+
const DEFAULT_PROCESSING_OPTIONS = {
|
|
185
|
+
validateUrls: true,
|
|
186
|
+
createBackup: true,
|
|
187
|
+
processInternalLinks: true,
|
|
188
|
+
processExternalLinks: true,
|
|
189
|
+
autoAcceptRevisions: false,
|
|
190
|
+
enabledOperations: [
|
|
191
|
+
'remove-italics',
|
|
192
|
+
'normalize-dashes',
|
|
193
|
+
'replace-outdated-titles',
|
|
194
|
+
'validate-document-styles',
|
|
195
|
+
'update-top-hyperlinks',
|
|
196
|
+
'update-toc-hyperlinks',
|
|
197
|
+
'force-remove-heading1-toc',
|
|
198
|
+
'fix-internal-hyperlinks',
|
|
199
|
+
'fix-content-ids',
|
|
200
|
+
'center-border-images',
|
|
201
|
+
'remove-whitespace',
|
|
202
|
+
'remove-paragraph-lines',
|
|
203
|
+
'remove-headers-footers',
|
|
204
|
+
'add-document-warning',
|
|
205
|
+
'validate-header2-tables',
|
|
206
|
+
'set-landscape-margins',
|
|
207
|
+
'list-indentation',
|
|
208
|
+
'bullet-uniformity',
|
|
209
|
+
'normalize-table-lists',
|
|
210
|
+
'smart-tables',
|
|
211
|
+
'adjust-table-padding',
|
|
212
|
+
'standardize-table-borders',
|
|
213
|
+
'correct-misapplied-styles',
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Default table shading settings
|
|
219
|
+
*/
|
|
220
|
+
const DEFAULT_TABLE_SHADING_SETTINGS: TableShadingSettings = {
|
|
221
|
+
header2Shading: '#BFBFBF',
|
|
222
|
+
otherShading: '#DFDFDF',
|
|
223
|
+
imageBorderWidth: 1.0,
|
|
224
|
+
// 1x1 Tables padding (in inches)
|
|
225
|
+
padding1x1Top: 0,
|
|
226
|
+
padding1x1Bottom: 0,
|
|
227
|
+
padding1x1Left: 0.08,
|
|
228
|
+
padding1x1Right: 0.08,
|
|
229
|
+
// Other Tables padding (in inches)
|
|
230
|
+
paddingOtherTop: 0,
|
|
231
|
+
paddingOtherBottom: 0,
|
|
232
|
+
paddingOtherLeft: 0.08,
|
|
233
|
+
paddingOtherRight: 0.08,
|
|
234
|
+
// Cell border thickness (in points)
|
|
235
|
+
cellBorderThickness: 0.5,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* localStorage key for custom session defaults
|
|
240
|
+
* Stores user's preferred defaults set via "Save as Default" button
|
|
241
|
+
*/
|
|
242
|
+
const CUSTOM_DEFAULTS_KEY = 'dochub_custom_defaults';
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Load custom defaults from localStorage if available
|
|
246
|
+
* Returns null if no custom defaults exist or if parsing fails
|
|
247
|
+
*/
|
|
248
|
+
const loadCustomDefaults = (): CustomSessionDefaults | null => {
|
|
249
|
+
try {
|
|
250
|
+
const stored = localStorage.getItem(CUSTOM_DEFAULTS_KEY);
|
|
251
|
+
if (!stored) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const parsed = safeJsonParse<CustomSessionDefaults | null>(stored, null, 'loadCustomDefaults');
|
|
255
|
+
if (!parsed) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
// Validate that we have at least some valid data
|
|
259
|
+
if (!parsed.styles && !parsed.listBulletSettings && !parsed.processingOptions && !parsed.tableShadingSettings) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return parsed;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
logger.warn('[SessionContext] Failed to load custom defaults:', error);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Ensures session has valid listBulletSettings with non-empty indentation levels
|
|
271
|
+
* Backfills with defaults if missing or invalid
|
|
272
|
+
*/
|
|
273
|
+
const ensureListBulletSettings = (session: Session): Session => {
|
|
274
|
+
const needsBackfill =
|
|
275
|
+
!session.listBulletSettings ||
|
|
276
|
+
!session.listBulletSettings.indentationLevels ||
|
|
277
|
+
session.listBulletSettings.indentationLevels.length === 0;
|
|
278
|
+
|
|
279
|
+
if (needsBackfill) {
|
|
280
|
+
return {
|
|
281
|
+
...session,
|
|
282
|
+
listBulletSettings: createDefaultListBulletSettings(),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return session;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Ensures session has valid styles array with all required styles
|
|
291
|
+
* Backfills with defaults if missing or invalid
|
|
292
|
+
*
|
|
293
|
+
* This is critical for:
|
|
294
|
+
* 1. Historical sessions created before styles were added
|
|
295
|
+
* 2. Sessions loaded from storage that may have corrupted/missing styles
|
|
296
|
+
* 3. Ensuring font color is properly applied (requires styles to be defined)
|
|
297
|
+
*/
|
|
298
|
+
const ensureSessionStyles = (session: Session): Session => {
|
|
299
|
+
const needsBackfill =
|
|
300
|
+
!session.styles ||
|
|
301
|
+
!Array.isArray(session.styles) ||
|
|
302
|
+
session.styles.length === 0;
|
|
303
|
+
|
|
304
|
+
if (needsBackfill) {
|
|
305
|
+
return {
|
|
306
|
+
...session,
|
|
307
|
+
styles: [...DEFAULT_SESSION_STYLES],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return session;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Ensures session has all default-enabled processing options
|
|
316
|
+
* Adds any new options that have enabled: true in defaultOptions but are missing from the session
|
|
317
|
+
* This is critical for existing sessions to get newly added processing options
|
|
318
|
+
*/
|
|
319
|
+
const ensureProcessingOptions = (session: Session): Session => {
|
|
320
|
+
// List of all options that should be enabled by default
|
|
321
|
+
// Must match defaultOptions in ProcessingOptions.tsx where enabled: true
|
|
322
|
+
// IMPORTANT: These IDs must exactly match the option IDs in ProcessingOptions.tsx
|
|
323
|
+
// This list is used to backfill existing sessions with newly added default-enabled options
|
|
324
|
+
const defaultEnabledOptionIds = [
|
|
325
|
+
'remove-italics',
|
|
326
|
+
'normalize-dashes',
|
|
327
|
+
'validate-document-styles',
|
|
328
|
+
'replace-outdated-titles',
|
|
329
|
+
'update-top-hyperlinks',
|
|
330
|
+
'update-toc-hyperlinks',
|
|
331
|
+
'force-remove-heading1-toc',
|
|
332
|
+
'fix-internal-hyperlinks',
|
|
333
|
+
'fix-content-ids',
|
|
334
|
+
'center-border-images',
|
|
335
|
+
'remove-whitespace',
|
|
336
|
+
'remove-paragraph-lines',
|
|
337
|
+
'remove-headers-footers',
|
|
338
|
+
'add-document-warning',
|
|
339
|
+
'validate-header2-tables',
|
|
340
|
+
'set-landscape-margins',
|
|
341
|
+
'list-indentation',
|
|
342
|
+
'bullet-uniformity',
|
|
343
|
+
'normalize-table-lists',
|
|
344
|
+
'smart-tables',
|
|
345
|
+
'adjust-table-padding',
|
|
346
|
+
'standardize-table-borders',
|
|
347
|
+
'correct-misapplied-styles',
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
const currentEnabled = session.processingOptions?.enabledOperations || [];
|
|
351
|
+
|
|
352
|
+
// Add any missing default-enabled options
|
|
353
|
+
const missingOptions = defaultEnabledOptionIds.filter(id => !currentEnabled.includes(id));
|
|
354
|
+
|
|
355
|
+
if (missingOptions.length === 0) {
|
|
356
|
+
return session;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
...session,
|
|
361
|
+
processingOptions: {
|
|
362
|
+
validateUrls: session.processingOptions?.validateUrls ?? true,
|
|
363
|
+
createBackup: session.processingOptions?.createBackup ?? true,
|
|
364
|
+
processInternalLinks: session.processingOptions?.processInternalLinks ?? true,
|
|
365
|
+
processExternalLinks: session.processingOptions?.processExternalLinks ?? true,
|
|
366
|
+
...session.processingOptions,
|
|
367
|
+
enabledOperations: [...currentEnabled, ...missingOptions],
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export function SessionProvider({ children }: { children: ReactNode }) {
|
|
373
|
+
const log = logger.namespace('SessionContext');
|
|
374
|
+
// Conditional verbose logger - only logs when SESSION_STATE debug mode is enabled
|
|
375
|
+
const debugLog = createDebugLogger(debugModes.SESSION_STATE, 'SessionState');
|
|
376
|
+
|
|
377
|
+
const [sessions, setSessions] = useState<Session[]>([]);
|
|
378
|
+
const [activeSessions, setActiveSessions] = useState<Session[]>([]);
|
|
379
|
+
const [currentSession, setCurrentSession] = useState<Session | null>(null);
|
|
380
|
+
const { updateStats: updateGlobalStats } = useGlobalStats();
|
|
381
|
+
|
|
382
|
+
// Ref to store debounce timer
|
|
383
|
+
const persistTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
384
|
+
// Ref to store latest sessions for persistence
|
|
385
|
+
const sessionsRef = useRef(sessions);
|
|
386
|
+
const activeSessionsRef = useRef(activeSessions);
|
|
387
|
+
// RACE CONDITION FIX: Flag to prevent double-loading sessions
|
|
388
|
+
// Prevents re-running loadSessionsFromStorage if component re-mounts
|
|
389
|
+
const hasLoadedRef = useRef(false);
|
|
390
|
+
|
|
391
|
+
const loadSessionsFromStorage = useCallback(async () => {
|
|
392
|
+
// RACE CONDITION FIX: Prevent double-loading if already loaded
|
|
393
|
+
if (hasLoadedRef.current) {
|
|
394
|
+
log.warn(
|
|
395
|
+
'[Session] loadSessionsFromStorage called but already loaded - skipping to prevent race condition'
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
hasLoadedRef.current = true;
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
// CRITICAL RECOVERY: Check for emergency backup from beforeunload
|
|
404
|
+
// This recovers data that may not have been saved to IndexedDB before app close
|
|
405
|
+
const emergencyBackup = localStorage.getItem('sessions_emergency_backup');
|
|
406
|
+
if (emergencyBackup) {
|
|
407
|
+
log.warn('[Session] Found emergency backup - attempting recovery...');
|
|
408
|
+
try {
|
|
409
|
+
const backup = safeJsonParse<{
|
|
410
|
+
sessions: SerializedSession[];
|
|
411
|
+
activeSessions: SerializedSession[];
|
|
412
|
+
timestamp: number;
|
|
413
|
+
reason: string;
|
|
414
|
+
} | null>(emergencyBackup, null, 'SessionContext.emergencyRecover');
|
|
415
|
+
|
|
416
|
+
if (backup && backup.sessions && backup.timestamp) {
|
|
417
|
+
const backupAge = Date.now() - backup.timestamp;
|
|
418
|
+
const backupAgeMinutes = Math.floor(backupAge / 60000);
|
|
419
|
+
|
|
420
|
+
log.info(`[Session] Emergency backup is ${backupAgeMinutes} minutes old`);
|
|
421
|
+
|
|
422
|
+
// Only restore if backup is recent (< 5 minutes old)
|
|
423
|
+
// Older backups are likely stale and shouldn't override IndexedDB
|
|
424
|
+
if (backupAge < 5 * 60 * 1000) {
|
|
425
|
+
log.info(
|
|
426
|
+
`[Session] Restoring ${backup.sessions.length} sessions from emergency backup`
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Save backup sessions to IndexedDB immediately
|
|
430
|
+
// backup.sessions is already in SerializedSession format (strings for dates)
|
|
431
|
+
for (const session of backup.sessions) {
|
|
432
|
+
await saveSessionToDB(session).catch((err) =>
|
|
433
|
+
log.error(`Failed to restore session ${session.id}:`, err)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
log.info('[Session] Emergency backup successfully restored to IndexedDB');
|
|
438
|
+
} else {
|
|
439
|
+
log.info('[Session] Emergency backup too old - using IndexedDB instead');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Clear emergency backup after processing (successful or not)
|
|
444
|
+
localStorage.removeItem('sessions_emergency_backup');
|
|
445
|
+
} catch (err) {
|
|
446
|
+
log.error('[Session] Failed to restore emergency backup:', err);
|
|
447
|
+
// Clear corrupted backup
|
|
448
|
+
localStorage.removeItem('sessions_emergency_backup');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Check if localStorage has sessions that need migration
|
|
453
|
+
const hasLocalStorageSessions = localStorage.getItem('sessions');
|
|
454
|
+
if (hasLocalStorageSessions) {
|
|
455
|
+
log.info('[Session] Found sessions in localStorage, migrating to IndexedDB...');
|
|
456
|
+
await migrateFromLocalStorage();
|
|
457
|
+
// Clear localStorage after migration
|
|
458
|
+
localStorage.removeItem('sessions');
|
|
459
|
+
localStorage.removeItem('activeSessions');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Load sessions from IndexedDB
|
|
463
|
+
const storedSessions = await loadSessions();
|
|
464
|
+
const storedActiveSessions = localStorage.getItem('activeSessions');
|
|
465
|
+
|
|
466
|
+
if (storedSessions && storedSessions.length > 0) {
|
|
467
|
+
const parsed: SerializedSession[] = storedSessions;
|
|
468
|
+
const restored: Session[] = parsed.map((s) => ({
|
|
469
|
+
...s,
|
|
470
|
+
createdAt: new Date(s.createdAt),
|
|
471
|
+
lastModified: new Date(s.lastModified),
|
|
472
|
+
closedAt: s.closedAt ? new Date(s.closedAt) : undefined,
|
|
473
|
+
documents: s.documents.map((d) => ({
|
|
474
|
+
...d,
|
|
475
|
+
processedAt: d.processedAt ? new Date(d.processedAt) : undefined,
|
|
476
|
+
})),
|
|
477
|
+
}));
|
|
478
|
+
|
|
479
|
+
// Clean up sessions older than 30 days (only if closed)
|
|
480
|
+
const thirtyDaysAgo = new Date();
|
|
481
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
482
|
+
|
|
483
|
+
const cleanedSessions = restored.filter((s) => {
|
|
484
|
+
// Keep all active sessions
|
|
485
|
+
if (s.status === 'active') return true;
|
|
486
|
+
|
|
487
|
+
// For closed sessions, check if they're older than 30 days
|
|
488
|
+
if (s.status === 'closed' && s.closedAt) {
|
|
489
|
+
const shouldKeep = s.closedAt > thirtyDaysAgo;
|
|
490
|
+
if (!shouldKeep) {
|
|
491
|
+
// Remove from IndexedDB as well
|
|
492
|
+
deleteSessionFromDB(s.id).catch((err) =>
|
|
493
|
+
log.error(`Failed to delete old session ${s.id}:`, err)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return shouldKeep;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Keep sessions without closedAt (shouldn't happen, but be safe)
|
|
500
|
+
return true;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Log cleanup if any sessions were removed
|
|
504
|
+
const removedCount = restored.length - cleanedSessions.length;
|
|
505
|
+
if (removedCount > 0) {
|
|
506
|
+
log.info(`[Session] Cleaned up ${removedCount} old session(s) (>30 days)`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// BACKFILL FIX: Ensure all loaded sessions have valid listBulletSettings
|
|
510
|
+
// This repairs historical sessions that were created before list settings were added
|
|
511
|
+
const listBackfilledSessions = cleanedSessions.map(ensureListBulletSettings);
|
|
512
|
+
|
|
513
|
+
// Log how many sessions were backfilled for list settings
|
|
514
|
+
const listBackfillCount = listBackfilledSessions.filter(
|
|
515
|
+
(s, idx) => s.listBulletSettings !== cleanedSessions[idx].listBulletSettings
|
|
516
|
+
).length;
|
|
517
|
+
if (listBackfillCount > 0) {
|
|
518
|
+
log.info(
|
|
519
|
+
`[Session] Backfilled ${listBackfillCount} session(s) with default list bullet settings`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// BACKFILL FIX: Ensure all loaded sessions have valid styles
|
|
524
|
+
// This is critical for font color to be applied - without styles, applyStyles() is skipped
|
|
525
|
+
const stylesBackfilledSessions = listBackfilledSessions.map(ensureSessionStyles);
|
|
526
|
+
|
|
527
|
+
// Log how many sessions were backfilled for styles
|
|
528
|
+
const stylesBackfillCount = stylesBackfilledSessions.filter(
|
|
529
|
+
(s, idx) => s.styles !== listBackfilledSessions[idx].styles
|
|
530
|
+
).length;
|
|
531
|
+
if (stylesBackfillCount > 0) {
|
|
532
|
+
log.info(
|
|
533
|
+
`[Session] Backfilled ${stylesBackfillCount} session(s) with default styles`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// BACKFILL FIX: Ensure all loaded sessions have new default-enabled processing options
|
|
538
|
+
// This adds new options (like standardize-cell-borders) to existing sessions
|
|
539
|
+
const backfilledSessions = stylesBackfilledSessions.map(ensureProcessingOptions);
|
|
540
|
+
|
|
541
|
+
// Log how many sessions were backfilled for processing options
|
|
542
|
+
const optionsBackfillCount = backfilledSessions.filter(
|
|
543
|
+
(s, idx) => s.processingOptions?.enabledOperations?.length !== stylesBackfilledSessions[idx].processingOptions?.enabledOperations?.length
|
|
544
|
+
).length;
|
|
545
|
+
if (optionsBackfillCount > 0) {
|
|
546
|
+
log.info(
|
|
547
|
+
`[Session] Backfilled ${optionsBackfillCount} session(s) with default processing options`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
setSessions(backfilledSessions);
|
|
552
|
+
if (storedActiveSessions) {
|
|
553
|
+
const activeIds = safeJsonParse<string[]>(
|
|
554
|
+
storedActiveSessions,
|
|
555
|
+
[],
|
|
556
|
+
'SessionContext.activeSessions'
|
|
557
|
+
);
|
|
558
|
+
const active = backfilledSessions.filter((s) => activeIds.includes(s.id));
|
|
559
|
+
setActiveSessions(active);
|
|
560
|
+
if (active.length > 0) {
|
|
561
|
+
setCurrentSession(active[0]);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch (err) {
|
|
566
|
+
log.error('Failed to load sessions from storage', err);
|
|
567
|
+
}
|
|
568
|
+
}, [log]); // Memoize with log dependency
|
|
569
|
+
|
|
570
|
+
// Load all sessions & active sessions from localStorage on mount
|
|
571
|
+
// CRITICAL: Only run ONCE on mount - do NOT reload when dependencies change
|
|
572
|
+
// Re-loading after mount causes race conditions where new data gets overwritten
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
loadSessionsFromStorage();
|
|
575
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
576
|
+
}, []); // Empty deps - ONLY run on mount
|
|
577
|
+
|
|
578
|
+
// Update refs when sessions change
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
sessionsRef.current = sessions;
|
|
581
|
+
}, [sessions]);
|
|
582
|
+
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
activeSessionsRef.current = activeSessions;
|
|
585
|
+
}, [activeSessions]);
|
|
586
|
+
|
|
587
|
+
// Debounced persist function to reduce database writes
|
|
588
|
+
// PERFORMANCE: Debounces by 1 second to batch rapid state updates
|
|
589
|
+
// Using refs to access latest state without causing dependency issues
|
|
590
|
+
const debouncedPersistSessions = useCallback(async () => {
|
|
591
|
+
try {
|
|
592
|
+
// Critical: Ensure database size limit to prevent quota exceeded errors
|
|
593
|
+
await ensureDBSizeLimit(200); // 200MB limit
|
|
594
|
+
|
|
595
|
+
// Use refs to get latest state values
|
|
596
|
+
const currentSessions = sessionsRef.current;
|
|
597
|
+
const currentActiveSessions = activeSessionsRef.current;
|
|
598
|
+
|
|
599
|
+
const serializedSessions: SerializedSession[] = currentSessions.map((s) => ({
|
|
600
|
+
...s,
|
|
601
|
+
createdAt: s.createdAt.toISOString(),
|
|
602
|
+
lastModified: s.lastModified.toISOString(),
|
|
603
|
+
closedAt: s.closedAt ? s.closedAt.toISOString() : undefined,
|
|
604
|
+
documents: s.documents.map((d) => ({
|
|
605
|
+
...d,
|
|
606
|
+
processedAt: d.processedAt ? d.processedAt.toISOString() : undefined,
|
|
607
|
+
})),
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
// Save each session to IndexedDB with quota error recovery
|
|
611
|
+
for (const session of serializedSessions) {
|
|
612
|
+
// Truncate large change arrays to prevent excessive storage
|
|
613
|
+
const truncatedSession = truncateSessionChanges(session, 100);
|
|
614
|
+
|
|
615
|
+
// Use quota error handler for automatic recovery if needed
|
|
616
|
+
await handleQuotaExceededError(async () => saveSessionToDB(truncatedSession), session.id);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Keep active sessions in localStorage for quick access
|
|
620
|
+
const activeSessionIds = safeJsonStringify(
|
|
621
|
+
currentActiveSessions.map((s) => s.id),
|
|
622
|
+
undefined,
|
|
623
|
+
'SessionContext.saveActiveSessions'
|
|
624
|
+
);
|
|
625
|
+
if (activeSessionIds) {
|
|
626
|
+
localStorage.setItem('activeSessions', activeSessionIds);
|
|
627
|
+
}
|
|
628
|
+
} catch (err) {
|
|
629
|
+
if (err instanceof Error && err.message.includes('DATABASE_QUOTA_EXCEEDED')) {
|
|
630
|
+
// User should be notified about quota issues - this should trigger a UI notification
|
|
631
|
+
log.error(
|
|
632
|
+
'Database quota exceeded - archive old sessions or export data to free up space',
|
|
633
|
+
err
|
|
634
|
+
);
|
|
635
|
+
} else {
|
|
636
|
+
log.error('Failed to persist sessions:', err);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}, [log]); // No dependencies - uses refs instead
|
|
640
|
+
|
|
641
|
+
// Persist sessions and active sessions whenever they change (debounced)
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
if (sessions.length > 0) {
|
|
644
|
+
// Clear existing timer
|
|
645
|
+
if (persistTimerRef.current) {
|
|
646
|
+
clearTimeout(persistTimerRef.current);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// PERFORMANCE FIX: Increased debounce from 1s to 3s for better UI responsiveness
|
|
650
|
+
// This reduces database writes during active editing (drag-drop, processing, etc.)
|
|
651
|
+
// and makes the UI feel much snappier
|
|
652
|
+
persistTimerRef.current = setTimeout(() => {
|
|
653
|
+
debouncedPersistSessions();
|
|
654
|
+
}, 3000); // 3 second debounce
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Cleanup on unmount
|
|
658
|
+
return () => {
|
|
659
|
+
if (persistTimerRef.current) {
|
|
660
|
+
clearTimeout(persistTimerRef.current);
|
|
661
|
+
persistTimerRef.current = null; // Clear the ref
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}, [sessions, activeSessions, debouncedPersistSessions]); // FIX: Removed debouncedPersistSessions - it's stable so doesn't need to be a dependency
|
|
665
|
+
|
|
666
|
+
// CRITICAL FIX: Flush pending saves before window closes
|
|
667
|
+
// Without this, sessions created/modified within the debounce window are lost
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
const handleBeforeUnload = () => {
|
|
670
|
+
// Cancel the debounce timer
|
|
671
|
+
if (persistTimerRef.current) {
|
|
672
|
+
clearTimeout(persistTimerRef.current);
|
|
673
|
+
persistTimerRef.current = null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Immediately save all sessions (synchronous)
|
|
677
|
+
// Note: This must be synchronous - async operations may not complete before unload
|
|
678
|
+
const currentSessions = sessionsRef.current;
|
|
679
|
+
const currentActiveSessions = activeSessionsRef.current;
|
|
680
|
+
|
|
681
|
+
if (currentSessions.length > 0) {
|
|
682
|
+
log.info('[beforeunload] Flushing pending session saves...');
|
|
683
|
+
|
|
684
|
+
// Save active session IDs to localStorage (synchronous)
|
|
685
|
+
const activeSessionIds = safeJsonStringify(
|
|
686
|
+
currentActiveSessions.map((s) => s.id),
|
|
687
|
+
undefined,
|
|
688
|
+
'SessionContext.beforeunload'
|
|
689
|
+
);
|
|
690
|
+
if (activeSessionIds) {
|
|
691
|
+
localStorage.setItem('activeSessions', activeSessionIds);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// CRITICAL DATA LOSS FIX: Add emergency backup to localStorage
|
|
695
|
+
// This ensures data is preserved even if the async IndexedDB save doesn't complete
|
|
696
|
+
// before window closes (within the 3-second debounce window)
|
|
697
|
+
try {
|
|
698
|
+
// Helper function to serialize Session to SerializedSession
|
|
699
|
+
const serializeSession = (session: Session): SerializedSession => ({
|
|
700
|
+
...session,
|
|
701
|
+
createdAt: session.createdAt.toISOString(),
|
|
702
|
+
lastModified: session.lastModified.toISOString(),
|
|
703
|
+
closedAt: session.closedAt?.toISOString(),
|
|
704
|
+
documents: session.documents.map((doc) => ({
|
|
705
|
+
...doc,
|
|
706
|
+
processedAt: doc.processedAt?.toISOString(),
|
|
707
|
+
})),
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const emergencyBackup = safeJsonStringify(
|
|
711
|
+
{
|
|
712
|
+
sessions: currentSessions.map(serializeSession),
|
|
713
|
+
activeSessions: currentActiveSessions.map(serializeSession),
|
|
714
|
+
timestamp: Date.now(),
|
|
715
|
+
reason: 'beforeunload_emergency_backup',
|
|
716
|
+
},
|
|
717
|
+
undefined,
|
|
718
|
+
'SessionContext.emergencyBackup'
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
if (emergencyBackup) {
|
|
722
|
+
localStorage.setItem('sessions_emergency_backup', emergencyBackup);
|
|
723
|
+
log.info('[beforeunload] Emergency backup saved to localStorage');
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
// Silent fail - localStorage might be full or disabled
|
|
727
|
+
log.error('[beforeunload] Failed to create emergency backup:', error);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Trigger the async save (may not complete, but we try)
|
|
731
|
+
debouncedPersistSessions().catch((error) => {
|
|
732
|
+
log.error('[beforeunload] Failed to flush sessions:', error);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
738
|
+
|
|
739
|
+
return () => {
|
|
740
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
741
|
+
};
|
|
742
|
+
}, [debouncedPersistSessions, log]);
|
|
743
|
+
|
|
744
|
+
const createSession = (name: string): Session => {
|
|
745
|
+
// Load custom defaults if user has saved them via "Save as Default"
|
|
746
|
+
const customDefaults = loadCustomDefaults();
|
|
747
|
+
const hasCustomDefaults = customDefaults !== null;
|
|
748
|
+
|
|
749
|
+
if (hasCustomDefaults) {
|
|
750
|
+
log.info('[createSession] Using custom defaults for new session');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const newSession: Session = {
|
|
754
|
+
id: `session-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
755
|
+
name,
|
|
756
|
+
createdAt: new Date(),
|
|
757
|
+
lastModified: new Date(),
|
|
758
|
+
documents: [],
|
|
759
|
+
stats: {
|
|
760
|
+
documentsProcessed: 0,
|
|
761
|
+
hyperlinksChecked: 0,
|
|
762
|
+
feedbackImported: 0,
|
|
763
|
+
timeSaved: 0,
|
|
764
|
+
},
|
|
765
|
+
status: 'active',
|
|
766
|
+
// Use custom defaults if available, otherwise fall back to factory defaults
|
|
767
|
+
styles: customDefaults?.styles
|
|
768
|
+
? [...customDefaults.styles]
|
|
769
|
+
: [...DEFAULT_SESSION_STYLES],
|
|
770
|
+
listBulletSettings: customDefaults?.listBulletSettings
|
|
771
|
+
? { ...customDefaults.listBulletSettings, indentationLevels: [...customDefaults.listBulletSettings.indentationLevels] }
|
|
772
|
+
: createDefaultListBulletSettings(),
|
|
773
|
+
tableShadingSettings: customDefaults?.tableShadingSettings
|
|
774
|
+
? { ...customDefaults.tableShadingSettings }
|
|
775
|
+
: { ...DEFAULT_TABLE_SHADING_SETTINGS },
|
|
776
|
+
processingOptions: customDefaults?.processingOptions
|
|
777
|
+
? {
|
|
778
|
+
...customDefaults.processingOptions,
|
|
779
|
+
enabledOperations: [...(customDefaults.processingOptions.enabledOperations || [])],
|
|
780
|
+
}
|
|
781
|
+
: {
|
|
782
|
+
...DEFAULT_PROCESSING_OPTIONS,
|
|
783
|
+
enabledOperations: [...DEFAULT_PROCESSING_OPTIONS.enabledOperations],
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// DEBUG: Log state transition
|
|
788
|
+
debugLog.debug('Creating session - state before update', {
|
|
789
|
+
sessionCount: sessions.length,
|
|
790
|
+
activeCount: activeSessions.length,
|
|
791
|
+
currentSessionId: currentSession?.id,
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
setSessions((prev) => [...prev, newSession]);
|
|
795
|
+
setActiveSessions((prev) => [...prev, newSession]);
|
|
796
|
+
setCurrentSession(newSession);
|
|
797
|
+
|
|
798
|
+
// DEBUG: Log new state
|
|
799
|
+
debugLog.debug('Session created - state after update', {
|
|
800
|
+
newSessionId: newSession.id,
|
|
801
|
+
sessionCount: sessions.length + 1,
|
|
802
|
+
activeCount: activeSessions.length + 1,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// CRITICAL FIX: Immediately persist new session to prevent loss
|
|
806
|
+
// Don't wait for 3-second debounce - save right away
|
|
807
|
+
const serializedSession: SerializedSession = {
|
|
808
|
+
...newSession,
|
|
809
|
+
createdAt: newSession.createdAt.toISOString(),
|
|
810
|
+
lastModified: newSession.lastModified.toISOString(),
|
|
811
|
+
closedAt: undefined, // New sessions are always active
|
|
812
|
+
documents: [],
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// Fire-and-forget immediate save
|
|
816
|
+
saveSessionToDB(serializedSession).catch((error) => {
|
|
817
|
+
log.error(`[createSession] Failed to immediately save session "${name}":`, error);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
log.info(`[createSession] Created and immediately saved session: ${name} (${newSession.id})`);
|
|
821
|
+
|
|
822
|
+
return newSession;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const loadSession = (id: string) => {
|
|
826
|
+
const session = sessions.find((s) => s.id === id);
|
|
827
|
+
if (session) {
|
|
828
|
+
// Only load if session is already active or explicitly reopening
|
|
829
|
+
if (activeSessions.find((s) => s.id === id)) {
|
|
830
|
+
// Session is already active, just switch to it
|
|
831
|
+
setCurrentSession(session);
|
|
832
|
+
} else if (session.status === 'closed') {
|
|
833
|
+
// CRITICAL FIX: Don't auto-reopen closed sessions
|
|
834
|
+
// User must explicitly reopen via reopenSession() or the Sessions page
|
|
835
|
+
log.warn(
|
|
836
|
+
`[loadSession] Attempted to load closed session: ${id}. Use reopenSession() instead.`
|
|
837
|
+
);
|
|
838
|
+
return; // EXIT without reopening
|
|
839
|
+
} else {
|
|
840
|
+
// Session exists but not in active list (shouldn't happen, but handle gracefully)
|
|
841
|
+
setActiveSessions((prev) => [...prev, session]);
|
|
842
|
+
setCurrentSession(session);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const reopenSession = (id: string) => {
|
|
848
|
+
const session = sessions.find((s) => s.id === id);
|
|
849
|
+
if (session && session.status === 'closed') {
|
|
850
|
+
// Explicitly reopen a closed session
|
|
851
|
+
const updatedSession = { ...session, status: 'active' as const, lastModified: new Date() };
|
|
852
|
+
setSessions((prev) => prev.map((s) => (s.id === id ? updatedSession : s)));
|
|
853
|
+
setActiveSessions((prev) => [...prev, updatedSession]);
|
|
854
|
+
setCurrentSession(updatedSession);
|
|
855
|
+
|
|
856
|
+
log.info(`[reopenSession] Reopened session: ${session.name}`);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const closeSession = (id: string) => {
|
|
861
|
+
// Get session info for logging
|
|
862
|
+
const session = sessions.find((s) => s.id === id);
|
|
863
|
+
const closedAt = new Date();
|
|
864
|
+
|
|
865
|
+
// DEBUG: Log state before close
|
|
866
|
+
debugLog.debug('Closing session - state before', {
|
|
867
|
+
closingSessionId: id,
|
|
868
|
+
closingSessionName: session?.name,
|
|
869
|
+
activeCount: activeSessions.length,
|
|
870
|
+
isCurrentSession: currentSession?.id === id,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// Remove from active sessions (sidebar) but keep in sessions list
|
|
874
|
+
setActiveSessions((prev) => prev.filter((s) => s.id !== id));
|
|
875
|
+
|
|
876
|
+
// Switch to another active session if closing current one
|
|
877
|
+
if (currentSession?.id === id) {
|
|
878
|
+
const remaining = activeSessions.filter((s) => s.id !== id);
|
|
879
|
+
setCurrentSession(remaining.length > 0 ? remaining[0] : null);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Update session status to 'closed' but keep in sessions list for history
|
|
883
|
+
setSessions((prev) =>
|
|
884
|
+
prev.map((s) =>
|
|
885
|
+
s.id === id ? { ...s, status: 'closed' as const, lastModified: new Date(), closedAt } : s
|
|
886
|
+
)
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
// Log session closure
|
|
890
|
+
if (session) {
|
|
891
|
+
log.info('[Session] Closed:', {
|
|
892
|
+
id: session.id,
|
|
893
|
+
name: session.name,
|
|
894
|
+
closedAt: closedAt.toISOString(),
|
|
895
|
+
documentsProcessed: session.documents.length,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const deleteSession = (id: string) => {
|
|
901
|
+
// Get session info for logging before deletion
|
|
902
|
+
const session = sessions.find((s) => s.id === id);
|
|
903
|
+
|
|
904
|
+
// Permanently delete session from storage
|
|
905
|
+
setSessions((prev) => prev.filter((s) => s.id !== id));
|
|
906
|
+
setActiveSessions((prev) => prev.filter((s) => s.id !== id));
|
|
907
|
+
if (currentSession?.id === id) {
|
|
908
|
+
setCurrentSession(null);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Also remove individual session from localStorage if it exists
|
|
912
|
+
localStorage.removeItem(`session_${id}`);
|
|
913
|
+
|
|
914
|
+
// Log session deletion
|
|
915
|
+
if (session) {
|
|
916
|
+
log.info('[Session] Deleted:', {
|
|
917
|
+
id: session.id,
|
|
918
|
+
name: session.name,
|
|
919
|
+
status: session.status,
|
|
920
|
+
createdAt: session.createdAt.toISOString(),
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const switchSession = (id: string) => {
|
|
926
|
+
const session = activeSessions.find((s) => s.id === id);
|
|
927
|
+
if (session) {
|
|
928
|
+
setCurrentSession(session);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const addDocuments = async (sessionId: string, files: File[]) => {
|
|
933
|
+
// Convert files to documents with strict validation
|
|
934
|
+
const newDocuments: Document[] = [];
|
|
935
|
+
const invalidFiles: Array<{ name: string; reason: string }> = [];
|
|
936
|
+
|
|
937
|
+
log.info(`[addDocuments] Processing ${files.length} file(s) for session ${sessionId}`);
|
|
938
|
+
|
|
939
|
+
for (const file of files) {
|
|
940
|
+
const fileWithPath = file as File & { path?: string };
|
|
941
|
+
|
|
942
|
+
// Log detailed file information for debugging
|
|
943
|
+
log.debug(`[addDocuments] File: "${file.name}"`, {
|
|
944
|
+
path: fileWithPath.path,
|
|
945
|
+
size: file.size,
|
|
946
|
+
type: file.type,
|
|
947
|
+
hasPathProperty: 'path' in file,
|
|
948
|
+
pathValue: fileWithPath.path,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// STRICT VALIDATION: Only accept files with valid filesystem paths
|
|
952
|
+
// This is critical for Electron processing which requires absolute paths
|
|
953
|
+
if (!fileWithPath.path || fileWithPath.path.trim() === '') {
|
|
954
|
+
const reason = 'No file path provided';
|
|
955
|
+
log.error(`[addDocuments] File "${file.name}" rejected: ${reason}`);
|
|
956
|
+
invalidFiles.push({ name: file.name, reason });
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Enhanced security validation with path traversal protection
|
|
961
|
+
// Check for .docx/.doc extensions and security threats
|
|
962
|
+
if (!isPathSafe(fileWithPath.path, ['.docx', '.doc'])) {
|
|
963
|
+
const reason = `Failed security validation for path: "${fileWithPath.path}"`;
|
|
964
|
+
log.error(`[addDocuments] File "${file.name}" rejected: ${reason}`);
|
|
965
|
+
invalidFiles.push({ name: file.name, reason });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Create document with validated path
|
|
970
|
+
newDocuments.push({
|
|
971
|
+
id: `doc-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
972
|
+
name: file.name,
|
|
973
|
+
path: fileWithPath.path,
|
|
974
|
+
size: file.size || 0,
|
|
975
|
+
type: file.type,
|
|
976
|
+
status: 'pending' as const,
|
|
977
|
+
// No fileData - will be read by backend using the path
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Log detailed results
|
|
982
|
+
if (invalidFiles.length > 0) {
|
|
983
|
+
log.warn(`[addDocuments] Rejected ${invalidFiles.length} file(s):`);
|
|
984
|
+
invalidFiles.forEach(({ name, reason }) => {
|
|
985
|
+
log.warn(` - ${name}: ${reason}`);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
if (newDocuments.length > 0) {
|
|
989
|
+
log.info(`[addDocuments] ✅ Successfully added ${newDocuments.length} valid document(s)`);
|
|
990
|
+
newDocuments.forEach((doc) => {
|
|
991
|
+
log.debug(` ✓ ${doc.name} (${doc.size} bytes)`);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Only update state if we have valid documents
|
|
996
|
+
if (newDocuments.length === 0) {
|
|
997
|
+
log.error('[addDocuments] ❌ No valid documents to add - all files were rejected');
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
setSessions((prev) =>
|
|
1002
|
+
prev.map((session) =>
|
|
1003
|
+
session.id === sessionId
|
|
1004
|
+
? {
|
|
1005
|
+
...session,
|
|
1006
|
+
documents: [...session.documents, ...newDocuments],
|
|
1007
|
+
lastModified: new Date(),
|
|
1008
|
+
}
|
|
1009
|
+
: session
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
// Update active sessions
|
|
1014
|
+
setActiveSessions((prev) =>
|
|
1015
|
+
prev.map((session) =>
|
|
1016
|
+
session.id === sessionId
|
|
1017
|
+
? {
|
|
1018
|
+
...session,
|
|
1019
|
+
documents: [...session.documents, ...newDocuments],
|
|
1020
|
+
lastModified: new Date(),
|
|
1021
|
+
}
|
|
1022
|
+
: session
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const removeDocument = (sessionId: string, documentId: string) => {
|
|
1028
|
+
setSessions((prev) =>
|
|
1029
|
+
prev.map((session) =>
|
|
1030
|
+
session.id === sessionId
|
|
1031
|
+
? {
|
|
1032
|
+
...session,
|
|
1033
|
+
documents: session.documents.filter((d) => d.id !== documentId),
|
|
1034
|
+
lastModified: new Date(),
|
|
1035
|
+
}
|
|
1036
|
+
: session
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// PERFORMANCE FIX: Wrap in useCallback to prevent child component re-renders
|
|
1042
|
+
// This is critical for StylesEditor and other components that depend on this function
|
|
1043
|
+
const processDocument = useCallback(
|
|
1044
|
+
async (sessionId: string, documentId: string): Promise<void> => {
|
|
1045
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
1046
|
+
const document = session?.documents.find((d) => d.id === documentId);
|
|
1047
|
+
const processingStartTime = Date.now();
|
|
1048
|
+
|
|
1049
|
+
// =========================================================================
|
|
1050
|
+
// COMPREHENSIVE LOGGING - DOCUMENT PROCESSING START
|
|
1051
|
+
// =========================================================================
|
|
1052
|
+
log.info('═══════════════════════════════════════════════════════════════════════');
|
|
1053
|
+
log.info('[SessionContext] DOCUMENT PROCESSING STARTED');
|
|
1054
|
+
log.info('═══════════════════════════════════════════════════════════════════════');
|
|
1055
|
+
log.info(`[SessionContext] Timestamp: ${new Date().toISOString()}`);
|
|
1056
|
+
log.info(`[SessionContext] Session ID: ${sessionId}`);
|
|
1057
|
+
log.info(`[SessionContext] Document ID: ${documentId}`);
|
|
1058
|
+
log.info(`[SessionContext] Document Name: ${document?.name || 'Unknown'}`);
|
|
1059
|
+
log.info(`[SessionContext] Document Path: ${document?.path || 'No path'}`);
|
|
1060
|
+
|
|
1061
|
+
// DEBUG: Log document processing start
|
|
1062
|
+
debugLog.debug('Processing document - starting', {
|
|
1063
|
+
sessionId,
|
|
1064
|
+
documentId,
|
|
1065
|
+
documentName: document?.name,
|
|
1066
|
+
documentPath: document?.path ? '[path exists]' : '[no path]',
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
if (!session || !document || !document.path) {
|
|
1070
|
+
log.error('[SessionContext] ERROR: Session, document, or document path not found');
|
|
1071
|
+
log.error(`[SessionContext] Session exists: ${!!session}`);
|
|
1072
|
+
log.error(`[SessionContext] Document exists: ${!!document}`);
|
|
1073
|
+
log.error(`[SessionContext] Document path exists: ${!!document?.path}`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ENSURE SESSION HAS REQUIRED DATA: Backfill styles and list settings if missing
|
|
1078
|
+
// This ensures font color and other style-dependent features work correctly
|
|
1079
|
+
// even if the session was created before these features were added
|
|
1080
|
+
const ensuredSession = ensureSessionStyles(ensureListBulletSettings(session));
|
|
1081
|
+
if (ensuredSession !== session) {
|
|
1082
|
+
log.info('[Session] Backfilled session with missing styles or list settings during processing');
|
|
1083
|
+
// Update the session in state with the backfilled data
|
|
1084
|
+
setSessions((prev) =>
|
|
1085
|
+
prev.map((s) => (s.id === sessionId ? ensuredSession : s))
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Use the ensured session for processing
|
|
1090
|
+
const sessionToProcess = ensuredSession;
|
|
1091
|
+
|
|
1092
|
+
// PERFORMANCE: Update document status to processing (first setState)
|
|
1093
|
+
setSessions((prev) =>
|
|
1094
|
+
prev.map((s) =>
|
|
1095
|
+
s.id === sessionId
|
|
1096
|
+
? {
|
|
1097
|
+
...s,
|
|
1098
|
+
documents: s.documents.map((d) =>
|
|
1099
|
+
d.id === documentId ? { ...d, status: 'processing' as const, errors: undefined, errorType: undefined } : d
|
|
1100
|
+
),
|
|
1101
|
+
lastModified: new Date(),
|
|
1102
|
+
}
|
|
1103
|
+
: s
|
|
1104
|
+
)
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
// Get user settings from localStorage
|
|
1109
|
+
const userSettings = localStorage.getItem('userSettings');
|
|
1110
|
+
const settings = safeJsonParse<any>(
|
|
1111
|
+
userSettings,
|
|
1112
|
+
{ apiConnections: { powerAutomateUrl: '' }, profile: {} },
|
|
1113
|
+
'SessionContext.processDocument.userSettings'
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
log.debug(
|
|
1117
|
+
'Processing document with PowerAutomate URL:',
|
|
1118
|
+
settings.apiConnections.powerAutomateUrl
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
// Extract profile data from settings for API request
|
|
1122
|
+
const userProfile = settings.profile
|
|
1123
|
+
? {
|
|
1124
|
+
firstName: settings.profile.firstName || '',
|
|
1125
|
+
lastName: settings.profile.lastName || '',
|
|
1126
|
+
email: settings.profile.email || '',
|
|
1127
|
+
}
|
|
1128
|
+
: undefined;
|
|
1129
|
+
|
|
1130
|
+
log.debug('Processing document with user profile:', userProfile);
|
|
1131
|
+
|
|
1132
|
+
// Convert session processing options to hyperlink processing options
|
|
1133
|
+
// Extract style spacing from session styles
|
|
1134
|
+
log.debug('\n=== SESSION CONTEXT: Extracting Style Spacing ===');
|
|
1135
|
+
log.debug('sessionToProcess.styles:', sessionToProcess.styles);
|
|
1136
|
+
|
|
1137
|
+
// Default style spacing (applied when sessionToProcess.styles is undefined/empty)
|
|
1138
|
+
const defaultStyleSpacing = {
|
|
1139
|
+
header1: {
|
|
1140
|
+
spaceBefore: 0,
|
|
1141
|
+
spaceAfter: 12,
|
|
1142
|
+
lineSpacing: 1.0,
|
|
1143
|
+
},
|
|
1144
|
+
header2: {
|
|
1145
|
+
spaceBefore: 6,
|
|
1146
|
+
spaceAfter: 6,
|
|
1147
|
+
lineSpacing: 1.0,
|
|
1148
|
+
},
|
|
1149
|
+
normal: {
|
|
1150
|
+
spaceBefore: 3,
|
|
1151
|
+
spaceAfter: 3,
|
|
1152
|
+
lineSpacing: 1.0,
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// Check if session has configured styles (should always be true after ensureSessionStyles)
|
|
1157
|
+
const hasSessionStyles = sessionToProcess.styles && sessionToProcess.styles.length > 0;
|
|
1158
|
+
|
|
1159
|
+
if (!hasSessionStyles) {
|
|
1160
|
+
log.debug(' No styles configured in session - using default spacing values');
|
|
1161
|
+
log.debug(' Default Header 1: 0pt before, 12pt after, 1.0 line spacing');
|
|
1162
|
+
log.debug(' Default Header 2: 6pt before, 6pt after, 1.0 line spacing');
|
|
1163
|
+
log.debug(' Default Normal: 3pt before, 3pt after, 1.0 line spacing');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const header1Style = sessionToProcess.styles?.find((s: SessionStyle) => s.id === 'header1');
|
|
1167
|
+
const header2Style = sessionToProcess.styles?.find((s: SessionStyle) => s.id === 'header2');
|
|
1168
|
+
const normalStyle = sessionToProcess.styles?.find((s: SessionStyle) => s.id === 'normal');
|
|
1169
|
+
|
|
1170
|
+
log.debug('Found header1Style:', header1Style);
|
|
1171
|
+
log.debug('Found header2Style:', header2Style);
|
|
1172
|
+
log.debug('Found normalStyle:', normalStyle);
|
|
1173
|
+
|
|
1174
|
+
// Define custom style spacing with proper type structure
|
|
1175
|
+
interface CustomStyleSpacing {
|
|
1176
|
+
header1?: { spaceBefore: number; spaceAfter: number; lineSpacing?: number };
|
|
1177
|
+
header2?: { spaceBefore: number; spaceAfter: number; lineSpacing?: number };
|
|
1178
|
+
normal?: {
|
|
1179
|
+
spaceBefore: number;
|
|
1180
|
+
spaceAfter: number;
|
|
1181
|
+
lineSpacing?: number;
|
|
1182
|
+
noSpaceBetweenSame?: boolean;
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
const customStyleSpacing: CustomStyleSpacing = {};
|
|
1186
|
+
|
|
1187
|
+
// Header 1 spacing (use session style or default)
|
|
1188
|
+
if (
|
|
1189
|
+
header1Style &&
|
|
1190
|
+
(header1Style.spaceBefore !== undefined ||
|
|
1191
|
+
header1Style.spaceAfter !== undefined ||
|
|
1192
|
+
header1Style.lineSpacing !== undefined)
|
|
1193
|
+
) {
|
|
1194
|
+
customStyleSpacing.header1 = {
|
|
1195
|
+
spaceBefore: header1Style.spaceBefore ?? 0,
|
|
1196
|
+
spaceAfter: header1Style.spaceAfter ?? 0,
|
|
1197
|
+
lineSpacing: header1Style.lineSpacing ?? 1.0,
|
|
1198
|
+
};
|
|
1199
|
+
log.debug('✓ Added header1 spacing from session:', customStyleSpacing.header1);
|
|
1200
|
+
} else if (!hasSessionStyles) {
|
|
1201
|
+
customStyleSpacing.header1 = defaultStyleSpacing.header1;
|
|
1202
|
+
log.debug('✓ Added header1 spacing from defaults:', customStyleSpacing.header1);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Header 2 spacing (use session style or default)
|
|
1206
|
+
if (
|
|
1207
|
+
header2Style &&
|
|
1208
|
+
(header2Style.spaceBefore !== undefined ||
|
|
1209
|
+
header2Style.spaceAfter !== undefined ||
|
|
1210
|
+
header2Style.lineSpacing !== undefined)
|
|
1211
|
+
) {
|
|
1212
|
+
customStyleSpacing.header2 = {
|
|
1213
|
+
spaceBefore: header2Style.spaceBefore ?? 0,
|
|
1214
|
+
spaceAfter: header2Style.spaceAfter ?? 0,
|
|
1215
|
+
lineSpacing: header2Style.lineSpacing ?? 1.0,
|
|
1216
|
+
};
|
|
1217
|
+
log.debug('✓ Added header2 spacing from session:', customStyleSpacing.header2);
|
|
1218
|
+
} else if (!hasSessionStyles) {
|
|
1219
|
+
customStyleSpacing.header2 = defaultStyleSpacing.header2;
|
|
1220
|
+
log.debug('✓ Added header2 spacing from defaults:', customStyleSpacing.header2);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Normal spacing (use session style or default)
|
|
1224
|
+
if (
|
|
1225
|
+
normalStyle &&
|
|
1226
|
+
(normalStyle.spaceBefore !== undefined ||
|
|
1227
|
+
normalStyle.spaceAfter !== undefined ||
|
|
1228
|
+
normalStyle.lineSpacing !== undefined)
|
|
1229
|
+
) {
|
|
1230
|
+
customStyleSpacing.normal = {
|
|
1231
|
+
spaceBefore: normalStyle.spaceBefore ?? 0,
|
|
1232
|
+
spaceAfter: normalStyle.spaceAfter ?? 0,
|
|
1233
|
+
lineSpacing: normalStyle.lineSpacing ?? 1.0,
|
|
1234
|
+
};
|
|
1235
|
+
log.debug('✓ Added normal spacing from session:', customStyleSpacing.normal);
|
|
1236
|
+
} else if (!hasSessionStyles) {
|
|
1237
|
+
customStyleSpacing.normal = defaultStyleSpacing.normal;
|
|
1238
|
+
log.debug('✓ Added normal spacing from defaults:', customStyleSpacing.normal);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
log.debug('Final customStyleSpacing object:', customStyleSpacing);
|
|
1242
|
+
log.debug(
|
|
1243
|
+
'Will pass to processor:',
|
|
1244
|
+
Object.keys(customStyleSpacing).length > 0 ? customStyleSpacing : undefined
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
// DEBUG: Log enabled operations before processing
|
|
1248
|
+
log.info('\n=== PROCESSING DOCUMENT - OPTIONS DEBUG ===');
|
|
1249
|
+
log.info('Session enabled operations:', sessionToProcess.processingOptions?.enabledOperations || []);
|
|
1250
|
+
|
|
1251
|
+
const processingOptions: HyperlinkProcessingOptions & {
|
|
1252
|
+
// User Profile for API
|
|
1253
|
+
userProfile?: {
|
|
1254
|
+
firstName: string;
|
|
1255
|
+
lastName: string;
|
|
1256
|
+
email: string;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
// Text Formatting Options
|
|
1260
|
+
removeWhitespace?: boolean;
|
|
1261
|
+
removeParagraphLines?: boolean;
|
|
1262
|
+
preserveBlankLinesAfterHeader2Tables?: boolean;
|
|
1263
|
+
preserveUserBlankStructures?: boolean;
|
|
1264
|
+
removeItalics?: boolean;
|
|
1265
|
+
preserveRedFont?: boolean;
|
|
1266
|
+
normalizeDashes?: boolean;
|
|
1267
|
+
standardizeHyperlinkFormatting?: boolean;
|
|
1268
|
+
standardizeListPrefixFormatting?: boolean;
|
|
1269
|
+
correctMisappliedStyles?: boolean;
|
|
1270
|
+
|
|
1271
|
+
// Content Structure Options
|
|
1272
|
+
assignStyles?: boolean;
|
|
1273
|
+
centerAndBorderImages?: boolean;
|
|
1274
|
+
removeHeadersFooters?: boolean;
|
|
1275
|
+
addDocumentWarning?: boolean;
|
|
1276
|
+
|
|
1277
|
+
// Lists & Tables Options
|
|
1278
|
+
listBulletSettings?: ListBulletSettings;
|
|
1279
|
+
bulletUniformity?: boolean;
|
|
1280
|
+
normalizeTableLists?: boolean;
|
|
1281
|
+
tableUniformity?: boolean;
|
|
1282
|
+
smartTables?: boolean;
|
|
1283
|
+
standardizeTableBorders?: boolean;
|
|
1284
|
+
setLandscapeMargins?: boolean;
|
|
1285
|
+
tableShadingSettings?: {
|
|
1286
|
+
header2Shading: string;
|
|
1287
|
+
otherShading: string;
|
|
1288
|
+
// Table cell padding in inches
|
|
1289
|
+
padding1x1Top?: number;
|
|
1290
|
+
padding1x1Bottom?: number;
|
|
1291
|
+
padding1x1Left?: number;
|
|
1292
|
+
padding1x1Right?: number;
|
|
1293
|
+
paddingOtherTop?: number;
|
|
1294
|
+
paddingOtherBottom?: number;
|
|
1295
|
+
paddingOtherLeft?: number;
|
|
1296
|
+
paddingOtherRight?: number;
|
|
1297
|
+
};
|
|
1298
|
+
tableOfContentsSettings?: TableOfContentsSettings;
|
|
1299
|
+
|
|
1300
|
+
// Processing Options Control
|
|
1301
|
+
enabledOperations?: string[];
|
|
1302
|
+
|
|
1303
|
+
// Legacy
|
|
1304
|
+
tableUniformitySettings?: TableUniformitySettings;
|
|
1305
|
+
|
|
1306
|
+
// Word Tracked Changes Handling
|
|
1307
|
+
revisionHandlingMode?: RevisionHandlingMode;
|
|
1308
|
+
revisionAuthor?: string;
|
|
1309
|
+
autoAcceptRevisions?: boolean;
|
|
1310
|
+
|
|
1311
|
+
// Local Dictionary Settings
|
|
1312
|
+
localDictionary?: {
|
|
1313
|
+
enabled: boolean;
|
|
1314
|
+
totalEntries: number;
|
|
1315
|
+
};
|
|
1316
|
+
} = {
|
|
1317
|
+
apiEndpoint: settings.apiConnections.powerAutomateUrl || '',
|
|
1318
|
+
userProfile, // Pass profile data to backend for API request
|
|
1319
|
+
|
|
1320
|
+
// Hyperlink Operations (operations object)
|
|
1321
|
+
operations: {
|
|
1322
|
+
fixContentIds:
|
|
1323
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('fix-content-ids'),
|
|
1324
|
+
updateTitles:
|
|
1325
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('replace-outdated-titles'),
|
|
1326
|
+
replaceOutdatedTitles:
|
|
1327
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('replace-outdated-titles'), // Same flag, standalone fallback
|
|
1328
|
+
fixInternalHyperlinks:
|
|
1329
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('fix-internal-hyperlinks'),
|
|
1330
|
+
updateTopHyperlinks:
|
|
1331
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('update-top-hyperlinks'),
|
|
1332
|
+
updateTocHyperlinks: true, // Always enabled - no UI control
|
|
1333
|
+
standardizeHyperlinkColor: true, // Always enabled - removed from UI
|
|
1334
|
+
validateHeader2Tables:
|
|
1335
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('validate-header2-tables'),
|
|
1336
|
+
validateDocumentStyles: sessionToProcess.processingOptions?.enabledOperations?.includes(
|
|
1337
|
+
'validate-document-styles'
|
|
1338
|
+
),
|
|
1339
|
+
},
|
|
1340
|
+
|
|
1341
|
+
// Text replacements and styles
|
|
1342
|
+
textReplacements: sessionToProcess.replacements?.filter((r) => r.enabled) || [],
|
|
1343
|
+
// Transform session styles array to include all formatting properties
|
|
1344
|
+
// This matches the format expected by WordDocumentProcessor for custom style application
|
|
1345
|
+
styles:
|
|
1346
|
+
sessionToProcess.styles && Array.isArray(sessionToProcess.styles) && sessionToProcess.styles.length > 0
|
|
1347
|
+
? sessionToProcess.styles.map((style: any) => {
|
|
1348
|
+
// DUAL TOGGLE FORMATTING SYSTEM
|
|
1349
|
+
// For formatting properties (bold, italic, underline):
|
|
1350
|
+
// - If preserveBold/preserveItalic/preserveUnderline === true: Don't call setter (preserve existing)
|
|
1351
|
+
// - If preserve flag === false/undefined: Apply bold/italic/underline value (true = apply, false = remove)
|
|
1352
|
+
// WordDocumentProcessor checks preserve flags before calling setters!
|
|
1353
|
+
|
|
1354
|
+
return {
|
|
1355
|
+
id: style.id,
|
|
1356
|
+
name: style.name,
|
|
1357
|
+
fontFamily: style.fontFamily,
|
|
1358
|
+
fontSize: style.fontSize,
|
|
1359
|
+
// Pass through formatting values
|
|
1360
|
+
bold: style.bold ?? false,
|
|
1361
|
+
italic: style.italic ?? false,
|
|
1362
|
+
underline: style.underline ?? false,
|
|
1363
|
+
// Pass through preserve flags
|
|
1364
|
+
preserveBold: style.preserveBold,
|
|
1365
|
+
preserveItalic: style.preserveItalic,
|
|
1366
|
+
preserveUnderline: style.preserveUnderline,
|
|
1367
|
+
preserveCenterAlignment: style.preserveCenterAlignment,
|
|
1368
|
+
alignment: style.alignment,
|
|
1369
|
+
color: style.color,
|
|
1370
|
+
spaceBefore: style.spaceBefore ?? 0,
|
|
1371
|
+
spaceAfter: style.spaceAfter ?? 0,
|
|
1372
|
+
lineSpacing: style.lineSpacing ?? 1.0,
|
|
1373
|
+
noSpaceBetweenSame: style.noSpaceBetweenSame,
|
|
1374
|
+
indentation: style.indentation
|
|
1375
|
+
? {
|
|
1376
|
+
left: style.indentation.left,
|
|
1377
|
+
firstLine: style.indentation.firstLine,
|
|
1378
|
+
}
|
|
1379
|
+
: undefined,
|
|
1380
|
+
};
|
|
1381
|
+
})
|
|
1382
|
+
: [],
|
|
1383
|
+
customStyleSpacing:
|
|
1384
|
+
Object.keys(customStyleSpacing).length > 0 ? customStyleSpacing : undefined,
|
|
1385
|
+
|
|
1386
|
+
// Text Formatting Options (mapped from ProcessingOptions UI)
|
|
1387
|
+
removeWhitespace:
|
|
1388
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('remove-whitespace'),
|
|
1389
|
+
removeParagraphLines:
|
|
1390
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('remove-paragraph-lines'),
|
|
1391
|
+
removeItalics: sessionToProcess.processingOptions?.enabledOperations?.includes('remove-italics'),
|
|
1392
|
+
preserveRedFont: sessionToProcess.processingOptions?.enabledOperations?.includes('preserve-red-font'),
|
|
1393
|
+
normalizeDashes: sessionToProcess.processingOptions?.enabledOperations?.includes('normalize-dashes'),
|
|
1394
|
+
|
|
1395
|
+
// ALWAYS ENABLED: Standardize hyperlink formatting (remove bold/italic from all hyperlinks)
|
|
1396
|
+
// This is intentional and required for the work environment to maintain professional document standards.
|
|
1397
|
+
// Hyperlinks should never be bolded or italicized - they must always use standard blue underlined style.
|
|
1398
|
+
standardizeHyperlinkFormatting: true,
|
|
1399
|
+
|
|
1400
|
+
// ALWAYS ENABLED: Standardize list prefix formatting (Verdana 12pt black for all lists)
|
|
1401
|
+
// This ensures all bullet points and numbered list symbols have consistent professional formatting.
|
|
1402
|
+
standardizeListPrefixFormatting: true,
|
|
1403
|
+
|
|
1404
|
+
// Correct misapplied TOC/Hyperlink paragraph styles (user-togglable)
|
|
1405
|
+
correctMisappliedStyles:
|
|
1406
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('correct-misapplied-styles'),
|
|
1407
|
+
|
|
1408
|
+
// Content Structure Options (ALWAYS ENABLED - automatic processing)
|
|
1409
|
+
// These operations are now always applied when processing documents
|
|
1410
|
+
// UI checkboxes have been removed as these are essential formatting operations
|
|
1411
|
+
assignStyles: true, // Always apply custom styles from Styles tab
|
|
1412
|
+
centerAndBorderImages: true, // Always center and border large images (>1" either dimension)
|
|
1413
|
+
removeHeadersFooters:
|
|
1414
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('remove-headers-footers'),
|
|
1415
|
+
addDocumentWarning:
|
|
1416
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('add-document-warning'),
|
|
1417
|
+
|
|
1418
|
+
// Lists & Tables Options (mapped from ProcessingOptions UI)
|
|
1419
|
+
// Map list-indentation checkbox to listBulletSettings.enabled
|
|
1420
|
+
// This controls Phase 3 (indentation), while bullet-uniformity controls Phases 1+2 (symbols)
|
|
1421
|
+
listBulletSettings: sessionToProcess.processingOptions?.enabledOperations?.includes(
|
|
1422
|
+
'list-indentation'
|
|
1423
|
+
)
|
|
1424
|
+
? {
|
|
1425
|
+
enabled: true,
|
|
1426
|
+
indentationLevels: sessionToProcess.listBulletSettings?.indentationLevels || [],
|
|
1427
|
+
}
|
|
1428
|
+
: undefined,
|
|
1429
|
+
bulletUniformity:
|
|
1430
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('bullet-uniformity'),
|
|
1431
|
+
normalizeTableLists:
|
|
1432
|
+
sessionToProcess.processingOptions?.enabledOperations?.includes('normalize-table-lists'),
|
|
1433
|
+
tableUniformity: sessionToProcess.processingOptions?.enabledOperations?.includes('smart-tables'),
|
|
1434
|
+
smartTables: sessionToProcess.processingOptions?.enabledOperations?.includes('smart-tables'),
|
|
1435
|
+
standardizeTableBorders: sessionToProcess.processingOptions?.enabledOperations?.includes('standardize-table-borders'),
|
|
1436
|
+
setLandscapeMargins: sessionToProcess.processingOptions?.enabledOperations?.includes('set-landscape-margins'),
|
|
1437
|
+
// Table shading settings with values derived from paragraph styles
|
|
1438
|
+
// This ensures table cell formatting inherits from the existing UI controls
|
|
1439
|
+
tableShadingSettings: (() => {
|
|
1440
|
+
// Find relevant paragraph styles to derive table-specific settings
|
|
1441
|
+
const normalStyle = sessionToProcess.styles?.find((s: any) => s.id === 'normal');
|
|
1442
|
+
const heading2Style = sessionToProcess.styles?.find((s: any) => s.id === 'header2');
|
|
1443
|
+
|
|
1444
|
+
return sessionToProcess.tableShadingSettings
|
|
1445
|
+
? {
|
|
1446
|
+
// Settings from TableShadingSettings UI
|
|
1447
|
+
header2Shading: sessionToProcess.tableShadingSettings.header2Shading,
|
|
1448
|
+
otherShading: sessionToProcess.tableShadingSettings.otherShading,
|
|
1449
|
+
imageBorderWidth: sessionToProcess.tableShadingSettings.imageBorderWidth ?? 1.0,
|
|
1450
|
+
// Table cell padding settings (in inches)
|
|
1451
|
+
padding1x1Top: sessionToProcess.tableShadingSettings.padding1x1Top ?? 0,
|
|
1452
|
+
padding1x1Bottom: sessionToProcess.tableShadingSettings.padding1x1Bottom ?? 0,
|
|
1453
|
+
padding1x1Left: sessionToProcess.tableShadingSettings.padding1x1Left ?? 0.08,
|
|
1454
|
+
padding1x1Right: sessionToProcess.tableShadingSettings.padding1x1Right ?? 0.08,
|
|
1455
|
+
paddingOtherTop: sessionToProcess.tableShadingSettings.paddingOtherTop ?? 0,
|
|
1456
|
+
paddingOtherBottom: sessionToProcess.tableShadingSettings.paddingOtherBottom ?? 0,
|
|
1457
|
+
paddingOtherLeft: sessionToProcess.tableShadingSettings.paddingOtherLeft ?? 0.08,
|
|
1458
|
+
paddingOtherRight: sessionToProcess.tableShadingSettings.paddingOtherRight ?? 0.08,
|
|
1459
|
+
// Cell border thickness setting
|
|
1460
|
+
cellBorderThickness: sessionToProcess.tableShadingSettings.cellBorderThickness ?? 0.5,
|
|
1461
|
+
// Derived from Heading 2 paragraph style
|
|
1462
|
+
heading2FontFamily: heading2Style?.fontFamily || 'Verdana',
|
|
1463
|
+
heading2FontSize: heading2Style?.fontSize || 14,
|
|
1464
|
+
// Derived from Normal paragraph style
|
|
1465
|
+
normalAlignment: normalStyle?.alignment || 'left',
|
|
1466
|
+
normalFontFamily: normalStyle?.fontFamily || 'Verdana',
|
|
1467
|
+
normalFontSize: normalStyle?.fontSize || 12,
|
|
1468
|
+
normalSpaceBefore: normalStyle?.spaceBefore ?? 3,
|
|
1469
|
+
normalSpaceAfter: normalStyle?.spaceAfter ?? 3,
|
|
1470
|
+
normalLineSpacing: normalStyle?.lineSpacing ?? 1.0,
|
|
1471
|
+
preserveBold: normalStyle?.preserveBold ?? true,
|
|
1472
|
+
preserveCenterAlignment: normalStyle?.preserveCenterAlignment ?? true,
|
|
1473
|
+
}
|
|
1474
|
+
: undefined;
|
|
1475
|
+
})(),
|
|
1476
|
+
|
|
1477
|
+
// Pass enabled operations array for condition checks in processor
|
|
1478
|
+
enabledOperations: sessionToProcess.processingOptions?.enabledOperations,
|
|
1479
|
+
|
|
1480
|
+
// Table of Contents Settings - Simplified to enabled flag only
|
|
1481
|
+
tableOfContentsSettings: sessionToProcess.tableOfContentsSettings,
|
|
1482
|
+
|
|
1483
|
+
// Word Tracked Changes Handling
|
|
1484
|
+
revisionHandlingMode: sessionToProcess.processingOptions?.revisionHandlingMode || 'accept_all',
|
|
1485
|
+
revisionAuthor: sessionToProcess.processingOptions?.revisionAuthor,
|
|
1486
|
+
autoAcceptRevisions: sessionToProcess.processingOptions?.autoAcceptRevisions ?? false, // Default: false
|
|
1487
|
+
|
|
1488
|
+
// DocHub Change Tracking (for Document Changes UI)
|
|
1489
|
+
trackChanges: true, // Enable hyperlink change tracking for DocumentProcessingComparison
|
|
1490
|
+
|
|
1491
|
+
// Legacy (deprecated, kept for backwards compatibility)
|
|
1492
|
+
tableUniformitySettings: sessionToProcess.tableUniformitySettings,
|
|
1493
|
+
|
|
1494
|
+
// Local Dictionary Settings (for offline hyperlink lookup)
|
|
1495
|
+
localDictionary: settings.localDictionary?.enabled
|
|
1496
|
+
? {
|
|
1497
|
+
enabled: true,
|
|
1498
|
+
totalEntries: settings.localDictionary.totalEntries || 0,
|
|
1499
|
+
}
|
|
1500
|
+
: undefined,
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
// DEBUG: Log final operations object being passed to processor
|
|
1504
|
+
log.info('Operations object being passed to WordDocumentProcessor:');
|
|
1505
|
+
log.info(' - updateTocHyperlinks:', processingOptions.operations?.updateTocHyperlinks);
|
|
1506
|
+
log.info(
|
|
1507
|
+
' - validateDocumentStyles:',
|
|
1508
|
+
processingOptions.operations?.validateDocumentStyles
|
|
1509
|
+
);
|
|
1510
|
+
log.info(' - validateHeader2Tables:', processingOptions.operations?.validateHeader2Tables);
|
|
1511
|
+
log.info(' - styles length:', processingOptions.styles?.length || 0);
|
|
1512
|
+
if (processingOptions.styles && processingOptions.styles.length > 0) {
|
|
1513
|
+
log.info(
|
|
1514
|
+
' - Available style IDs:',
|
|
1515
|
+
processingOptions.styles.map((s: any) => s.id).join(', ')
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
log.info(
|
|
1519
|
+
' - tableOfContentsSettings.enabled:',
|
|
1520
|
+
processingOptions.tableOfContentsSettings?.enabled ?? false
|
|
1521
|
+
);
|
|
1522
|
+
// CRITICAL DEBUG: Log listBulletSettings details
|
|
1523
|
+
log.info(' - listBulletSettings enabled:', processingOptions.listBulletSettings?.enabled);
|
|
1524
|
+
log.info(
|
|
1525
|
+
' - listBulletSettings indentationLevels length:',
|
|
1526
|
+
processingOptions.listBulletSettings?.indentationLevels?.length || 0
|
|
1527
|
+
);
|
|
1528
|
+
if (processingOptions.listBulletSettings?.indentationLevels) {
|
|
1529
|
+
processingOptions.listBulletSettings.indentationLevels.forEach(
|
|
1530
|
+
(level: any, idx: number) => {
|
|
1531
|
+
log.info(
|
|
1532
|
+
` - Level ${level.level}: symbol=${level.symbolIndent}", text=${level.textIndent}", char="${level.bulletChar}"`
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
// Also log session state right before IPC call
|
|
1538
|
+
log.info('Session state right before IPC:');
|
|
1539
|
+
log.info(' - Session has listBulletSettings?', !!sessionToProcess.listBulletSettings);
|
|
1540
|
+
log.info(' - Session listBulletSettings enabled?', sessionToProcess.listBulletSettings?.enabled);
|
|
1541
|
+
log.info(
|
|
1542
|
+
' - Session indentationLevels length:',
|
|
1543
|
+
sessionToProcess.listBulletSettings?.indentationLevels?.length || 0
|
|
1544
|
+
);
|
|
1545
|
+
if (sessionToProcess.listBulletSettings?.indentationLevels) {
|
|
1546
|
+
sessionToProcess.listBulletSettings.indentationLevels.forEach((level, idx) => {
|
|
1547
|
+
log.info(
|
|
1548
|
+
` - Session Level ${level.level}: symbol=${level.symbolIndent}", text=${level.textIndent}", char="${level.bulletChar}"`
|
|
1549
|
+
);
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
// Check enabled operations
|
|
1553
|
+
log.info(' - Enabled operations:', sessionToProcess.processingOptions?.enabledOperations || []);
|
|
1554
|
+
// DEBUG: Show formatting preservation for Normal/ListParagraph styles
|
|
1555
|
+
const normalStyleInOptions = processingOptions.styles?.find((s: any) => s.id === 'normal');
|
|
1556
|
+
const listParaStyleInOptions = processingOptions.styles?.find(
|
|
1557
|
+
(s: any) => s.id === 'listParagraph'
|
|
1558
|
+
);
|
|
1559
|
+
if (normalStyleInOptions || listParaStyleInOptions) {
|
|
1560
|
+
log.info(' - Formatting Preservation (bold & alignment only):');
|
|
1561
|
+
if (normalStyleInOptions) {
|
|
1562
|
+
log.info(
|
|
1563
|
+
` - Normal: bold=${normalStyleInOptions.bold}, alignment=${normalStyleInOptions.alignment} (undefined = preserve), italic=${normalStyleInOptions.italic}, underline=${normalStyleInOptions.underline}`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (listParaStyleInOptions) {
|
|
1567
|
+
log.info(
|
|
1568
|
+
` - ListParagraph: bold=${listParaStyleInOptions.bold}, alignment=${listParaStyleInOptions.alignment} (undefined = preserve), italic=${listParaStyleInOptions.italic}, underline=${listParaStyleInOptions.underline}`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Capture snapshot BEFORE processing for comparison feature
|
|
1574
|
+
// This must happen in the renderer process (IndexedDB not available in main process)
|
|
1575
|
+
try {
|
|
1576
|
+
log.info(`[SessionContext] Capturing pre-processing snapshot for ${document.name}`);
|
|
1577
|
+
|
|
1578
|
+
// Ensure Electron API is available for file operations
|
|
1579
|
+
const electronAPI = requireElectronAPI('snapshot capture');
|
|
1580
|
+
|
|
1581
|
+
// Read original file into buffer
|
|
1582
|
+
const fileBuffer = await electronAPI.readFileAsBuffer(document.path);
|
|
1583
|
+
|
|
1584
|
+
// Extract original text content before processing
|
|
1585
|
+
const textResult = await electronAPI.extractDocumentText(document.path);
|
|
1586
|
+
const originalText = textResult.success && textResult.textContent ? textResult.textContent : [];
|
|
1587
|
+
|
|
1588
|
+
// Store snapshot in IndexedDB (renderer process)
|
|
1589
|
+
await DocumentSnapshotService.captureSnapshot(
|
|
1590
|
+
fileBuffer,
|
|
1591
|
+
sessionId,
|
|
1592
|
+
documentId,
|
|
1593
|
+
originalText,
|
|
1594
|
+
[] // hyperlinks optional
|
|
1595
|
+
);
|
|
1596
|
+
log.info(`[SessionContext] Snapshot captured: ${originalText.length} paragraphs, ${(fileBuffer.byteLength / 1024).toFixed(1)}KB`);
|
|
1597
|
+
} catch (snapshotError) {
|
|
1598
|
+
log.warn('[SessionContext] Failed to capture snapshot (comparison will be unavailable):', snapshotError);
|
|
1599
|
+
// Continue with processing even if snapshot fails
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// =========================================================================
|
|
1603
|
+
// LOGGING - IPC CALL TO MAIN PROCESS
|
|
1604
|
+
// =========================================================================
|
|
1605
|
+
log.info('───────────────────────────────────────────────────────────────────────');
|
|
1606
|
+
log.info('[SessionContext] Sending document to main process for processing');
|
|
1607
|
+
log.info(`[SessionContext] API Endpoint: ${processingOptions.apiEndpoint ? 'Configured' : 'NOT CONFIGURED'}`);
|
|
1608
|
+
log.info(`[SessionContext] Operations enabled: ${sessionToProcess.processingOptions?.enabledOperations?.join(', ') || 'None'}`);
|
|
1609
|
+
log.info(`[SessionContext] IPC Timeout: ${IPC_TIMEOUT_MS}ms`);
|
|
1610
|
+
log.info('───────────────────────────────────────────────────────────────────────');
|
|
1611
|
+
|
|
1612
|
+
// Process the document using Electron IPC with timeout protection
|
|
1613
|
+
const processingAPI = requireElectronAPI('document processing');
|
|
1614
|
+
const rawResult = await withTimeout(
|
|
1615
|
+
processingAPI.processHyperlinkDocument(document.path, processingOptions),
|
|
1616
|
+
IPC_TIMEOUT_MS,
|
|
1617
|
+
'Document processing'
|
|
1618
|
+
);
|
|
1619
|
+
|
|
1620
|
+
// Validate the response structure before using it
|
|
1621
|
+
if (!rawResult || typeof rawResult !== 'object') {
|
|
1622
|
+
throw new Error('Invalid response from document processor: expected an object');
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Type guard to ensure required fields exist
|
|
1626
|
+
const hasRequiredFields =
|
|
1627
|
+
'success' in rawResult &&
|
|
1628
|
+
'totalHyperlinks' in rawResult &&
|
|
1629
|
+
typeof rawResult.success === 'boolean' &&
|
|
1630
|
+
typeof rawResult.totalHyperlinks === 'number';
|
|
1631
|
+
|
|
1632
|
+
if (!hasRequiredFields) {
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
'Invalid response from document processor: missing required fields (success, totalHyperlinks)'
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const result = rawResult as {
|
|
1639
|
+
success: boolean;
|
|
1640
|
+
totalHyperlinks: number;
|
|
1641
|
+
processedHyperlinks: number;
|
|
1642
|
+
modifiedHyperlinks: number;
|
|
1643
|
+
appendedContentIds?: number;
|
|
1644
|
+
backupPath?: string;
|
|
1645
|
+
duration: number;
|
|
1646
|
+
errorMessages?: string[];
|
|
1647
|
+
changes?: import('@/types/session').DocumentChange[];
|
|
1648
|
+
previousRevisions?: import('@/types/session').PreviousRevisionState;
|
|
1649
|
+
wordRevisions?: import('@/types/session').WordRevisionState;
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// PERFORMANCE: Update document status AND stats in single setState (batched)
|
|
1653
|
+
// This reduces re-renders from 2 to 1 per document
|
|
1654
|
+
setSessions((prev) =>
|
|
1655
|
+
prev.map((s) =>
|
|
1656
|
+
s.id === sessionId
|
|
1657
|
+
? {
|
|
1658
|
+
...s,
|
|
1659
|
+
documents: s.documents.map((d) =>
|
|
1660
|
+
d.id === documentId
|
|
1661
|
+
? {
|
|
1662
|
+
...d,
|
|
1663
|
+
status: result.success ? ('completed' as const) : ('error' as const),
|
|
1664
|
+
processedAt: new Date(),
|
|
1665
|
+
errors: result.errorMessages,
|
|
1666
|
+
errorType: !result.success
|
|
1667
|
+
? (result.errorMessages?.some((msg) => msg.toLowerCase().includes('close the file'))
|
|
1668
|
+
? 'file_locked'
|
|
1669
|
+
: result.errorMessages?.some((msg) => msg.toLowerCase().includes('timeout'))
|
|
1670
|
+
? 'api_timeout'
|
|
1671
|
+
: result.errorMessages?.some((msg) =>
|
|
1672
|
+
msg.toLowerCase().includes('compatibility_mode') ||
|
|
1673
|
+
msg.toLowerCase().includes('outdated functions'))
|
|
1674
|
+
? 'word_compatibility'
|
|
1675
|
+
: 'general')
|
|
1676
|
+
: undefined,
|
|
1677
|
+
// Store pre-existing revisions (from before DocHub processing)
|
|
1678
|
+
previousRevisions: result.previousRevisions,
|
|
1679
|
+
// Store Word revisions state from DocHub processing
|
|
1680
|
+
wordRevisions: result.wordRevisions,
|
|
1681
|
+
processingResult: {
|
|
1682
|
+
hyperlinksProcessed: result.processedHyperlinks,
|
|
1683
|
+
hyperlinksModified: result.modifiedHyperlinks,
|
|
1684
|
+
contentIdsAppended:
|
|
1685
|
+
result.appendedContentIds || result.processedHyperlinks,
|
|
1686
|
+
backupPath: result.backupPath,
|
|
1687
|
+
duration: result.duration,
|
|
1688
|
+
// Use the enhanced changes array from processor with full context
|
|
1689
|
+
changes: result.changes || [],
|
|
1690
|
+
},
|
|
1691
|
+
}
|
|
1692
|
+
: d
|
|
1693
|
+
),
|
|
1694
|
+
stats: {
|
|
1695
|
+
...s.stats,
|
|
1696
|
+
documentsProcessed: s.stats.documentsProcessed + (result.success ? 1 : 0),
|
|
1697
|
+
hyperlinksChecked: s.stats.hyperlinksChecked + result.totalHyperlinks,
|
|
1698
|
+
feedbackImported: s.stats.feedbackImported,
|
|
1699
|
+
timeSaved:
|
|
1700
|
+
s.stats.timeSaved +
|
|
1701
|
+
Math.round(
|
|
1702
|
+
(result.totalHyperlinks * TIME_SAVED_SECONDS_PER_HYPERLINK) /
|
|
1703
|
+
SECONDS_PER_MINUTE
|
|
1704
|
+
),
|
|
1705
|
+
},
|
|
1706
|
+
lastModified: new Date(),
|
|
1707
|
+
}
|
|
1708
|
+
: s
|
|
1709
|
+
)
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
// Update global stats if processing was successful
|
|
1713
|
+
if (result.success) {
|
|
1714
|
+
await updateGlobalStats({
|
|
1715
|
+
documentsProcessed: 1,
|
|
1716
|
+
hyperlinksChecked: result.totalHyperlinks,
|
|
1717
|
+
timeSaved: Math.round(
|
|
1718
|
+
(result.totalHyperlinks * TIME_SAVED_SECONDS_PER_HYPERLINK) / SECONDS_PER_MINUTE
|
|
1719
|
+
),
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
const totalDuration = Date.now() - processingStartTime;
|
|
1723
|
+
|
|
1724
|
+
// Enhanced success logging for user visibility
|
|
1725
|
+
log.info('═══════════════════════════════════════════════════════════════════════');
|
|
1726
|
+
log.info('[SessionContext] DOCUMENT PROCESSING COMPLETE - SUCCESS');
|
|
1727
|
+
log.info('═══════════════════════════════════════════════════════════════════════');
|
|
1728
|
+
log.info(`[SessionContext] Document: ${document.name}`);
|
|
1729
|
+
log.info(`[SessionContext] Location: ${document.path}`);
|
|
1730
|
+
log.info(`[SessionContext] Hyperlinks Processed: ${result.totalHyperlinks}`);
|
|
1731
|
+
log.info(`[SessionContext] Hyperlinks Modified: ${result.modifiedHyperlinks}`);
|
|
1732
|
+
log.info(`[SessionContext] Content IDs Appended: ${result.appendedContentIds || 0}`);
|
|
1733
|
+
log.info(`[SessionContext] Processor Duration: ${result.duration}ms`);
|
|
1734
|
+
log.info(`[SessionContext] Total Duration: ${totalDuration}ms`);
|
|
1735
|
+
log.info(
|
|
1736
|
+
`[SessionContext] Time Saved: ${Math.round((result.totalHyperlinks * TIME_SAVED_SECONDS_PER_HYPERLINK) / SECONDS_PER_MINUTE)} seconds`
|
|
1737
|
+
);
|
|
1738
|
+
log.info('═══════════════════════════════════════════════════════════════════════');
|
|
1739
|
+
} else {
|
|
1740
|
+
const totalDuration = Date.now() - processingStartTime;
|
|
1741
|
+
log.error('═══════════════════════════════════════════════════════════════════════');
|
|
1742
|
+
log.error('[SessionContext] DOCUMENT PROCESSING COMPLETE - FAILED');
|
|
1743
|
+
log.error('═══════════════════════════════════════════════════════════════════════');
|
|
1744
|
+
log.error(`[SessionContext] Document: ${document.name}`);
|
|
1745
|
+
log.error(`[SessionContext] Errors: ${result.errorMessages?.join(', ') || 'Unknown error'}`);
|
|
1746
|
+
log.error(`[SessionContext] Duration: ${totalDuration}ms`);
|
|
1747
|
+
log.error('═══════════════════════════════════════════════════════════════════════');
|
|
1748
|
+
}
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
const totalDuration = Date.now() - processingStartTime;
|
|
1751
|
+
log.error('═══════════════════════════════════════════════════════════════════════');
|
|
1752
|
+
log.error('[SessionContext] DOCUMENT PROCESSING EXCEPTION');
|
|
1753
|
+
log.error('═══════════════════════════════════════════════════════════════════════');
|
|
1754
|
+
log.error(`[SessionContext] Document: ${document.name}`);
|
|
1755
|
+
log.error(`[SessionContext] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1756
|
+
if (error instanceof Error && error.stack) {
|
|
1757
|
+
log.error(`[SessionContext] Stack Trace:`);
|
|
1758
|
+
error.stack.split('\n').forEach(line => {
|
|
1759
|
+
log.error(`[SessionContext] ${line}`);
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
log.error(`[SessionContext] Duration: ${totalDuration}ms`);
|
|
1763
|
+
log.error('═══════════════════════════════════════════════════════════════════════');
|
|
1764
|
+
log.error('Error processing document:', error);
|
|
1765
|
+
|
|
1766
|
+
// Update document status to error
|
|
1767
|
+
setSessions((prev) =>
|
|
1768
|
+
prev.map((s) =>
|
|
1769
|
+
s.id === sessionId
|
|
1770
|
+
? {
|
|
1771
|
+
...s,
|
|
1772
|
+
documents: s.documents.map((d) =>
|
|
1773
|
+
d.id === documentId
|
|
1774
|
+
? {
|
|
1775
|
+
...d,
|
|
1776
|
+
status: 'error' as const,
|
|
1777
|
+
errors: [error instanceof Error ? error.message : 'Processing failed'],
|
|
1778
|
+
errorType: (error instanceof Error && error.message.toLowerCase().includes('close the file'))
|
|
1779
|
+
? 'file_locked'
|
|
1780
|
+
: (error instanceof Error && error.message.toLowerCase().includes('timeout'))
|
|
1781
|
+
? 'api_timeout'
|
|
1782
|
+
: (error instanceof Error && (
|
|
1783
|
+
error.message.toLowerCase().includes('compatibility_mode') ||
|
|
1784
|
+
error.message.toLowerCase().includes('outdated functions')))
|
|
1785
|
+
? 'word_compatibility'
|
|
1786
|
+
: 'general',
|
|
1787
|
+
}
|
|
1788
|
+
: d
|
|
1789
|
+
),
|
|
1790
|
+
lastModified: new Date(),
|
|
1791
|
+
}
|
|
1792
|
+
: s
|
|
1793
|
+
)
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
},
|
|
1797
|
+
[sessions, log, updateGlobalStats]
|
|
1798
|
+
); // Dependencies: sessions for finding docs, log for logging, updateGlobalStats for stats
|
|
1799
|
+
|
|
1800
|
+
const revertChange = async (
|
|
1801
|
+
sessionId: string,
|
|
1802
|
+
documentId: string,
|
|
1803
|
+
changeId: string
|
|
1804
|
+
): Promise<void> => {
|
|
1805
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
1806
|
+
const document = session?.documents.find((d) => d.id === documentId);
|
|
1807
|
+
|
|
1808
|
+
if (!session || !document) {
|
|
1809
|
+
log.error('Session or document not found');
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// Remove the change from the tracked changes list
|
|
1814
|
+
setSessions((prev) =>
|
|
1815
|
+
prev.map((s) =>
|
|
1816
|
+
s.id === sessionId
|
|
1817
|
+
? {
|
|
1818
|
+
...s,
|
|
1819
|
+
documents: s.documents.map((d) =>
|
|
1820
|
+
d.id === documentId && d.processingResult
|
|
1821
|
+
? {
|
|
1822
|
+
...d,
|
|
1823
|
+
processingResult: {
|
|
1824
|
+
...d.processingResult,
|
|
1825
|
+
changes: d.processingResult.changes?.filter((c) => c.id !== changeId) || [],
|
|
1826
|
+
},
|
|
1827
|
+
}
|
|
1828
|
+
: d
|
|
1829
|
+
),
|
|
1830
|
+
lastModified: new Date(),
|
|
1831
|
+
}
|
|
1832
|
+
: s
|
|
1833
|
+
)
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1836
|
+
log.info(`[Session] Reverted change ${changeId} from document ${documentId}`);
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
const revertAllChanges = async (sessionId: string, documentId: string): Promise<void> => {
|
|
1840
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
1841
|
+
const document = session?.documents.find((d) => d.id === documentId);
|
|
1842
|
+
|
|
1843
|
+
if (!session || !document || !document.path) {
|
|
1844
|
+
log.error('Session, document, or document path not found');
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
const backupPath = document.processingResult?.backupPath;
|
|
1849
|
+
if (!backupPath) {
|
|
1850
|
+
log.error('No backup path found for document');
|
|
1851
|
+
throw new Error('No backup available for this document');
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
try {
|
|
1855
|
+
// Call Electron IPC to restore from backup
|
|
1856
|
+
const restoreAPI = requireElectronAPI('backup restore');
|
|
1857
|
+
await restoreAPI.restoreFromBackup(backupPath, document.path);
|
|
1858
|
+
|
|
1859
|
+
// Clear all tracked changes and reset processing status
|
|
1860
|
+
setSessions((prev) =>
|
|
1861
|
+
prev.map((s) =>
|
|
1862
|
+
s.id === sessionId
|
|
1863
|
+
? {
|
|
1864
|
+
...s,
|
|
1865
|
+
documents: s.documents.map((d) =>
|
|
1866
|
+
d.id === documentId
|
|
1867
|
+
? {
|
|
1868
|
+
...d,
|
|
1869
|
+
status: 'pending' as const,
|
|
1870
|
+
processedAt: undefined,
|
|
1871
|
+
errors: undefined,
|
|
1872
|
+
processingResult: undefined,
|
|
1873
|
+
}
|
|
1874
|
+
: d
|
|
1875
|
+
),
|
|
1876
|
+
lastModified: new Date(),
|
|
1877
|
+
}
|
|
1878
|
+
: s
|
|
1879
|
+
)
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
log.info(
|
|
1883
|
+
`[Session] Reverted all changes for document ${documentId} from backup ${backupPath}`
|
|
1884
|
+
);
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
log.error('Error reverting all changes:', error);
|
|
1887
|
+
throw error;
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
// UNIFIED STATE UPDATE HELPER
|
|
1892
|
+
// Prevents state synchronization issues by updating all three state variables atomically
|
|
1893
|
+
// This replaces the previous pattern of updating sessions, activeSessions, and currentSession separately
|
|
1894
|
+
const updateSessionById = useCallback(
|
|
1895
|
+
(sessionId: string, updater: (session: Session) => Session) => {
|
|
1896
|
+
const updateFn = (sessions: Session[]) =>
|
|
1897
|
+
sessions.map((s) => (s.id === sessionId ? updater(s) : s));
|
|
1898
|
+
|
|
1899
|
+
setSessions(updateFn);
|
|
1900
|
+
setActiveSessions(updateFn);
|
|
1901
|
+
setCurrentSession((prev) => (prev?.id === sessionId ? updater(prev) : prev));
|
|
1902
|
+
},
|
|
1903
|
+
[]
|
|
1904
|
+
);
|
|
1905
|
+
|
|
1906
|
+
const updateSessionStats = useCallback(
|
|
1907
|
+
(sessionId: string, stats: Partial<SessionStats>) => {
|
|
1908
|
+
updateSessionById(sessionId, (session) => ({
|
|
1909
|
+
...session,
|
|
1910
|
+
stats: { ...session.stats, ...stats },
|
|
1911
|
+
lastModified: new Date(),
|
|
1912
|
+
}));
|
|
1913
|
+
},
|
|
1914
|
+
[updateSessionById]
|
|
1915
|
+
);
|
|
1916
|
+
|
|
1917
|
+
const updateSessionName = useCallback(
|
|
1918
|
+
(sessionId: string, name: string) => {
|
|
1919
|
+
updateSessionById(sessionId, (session) => ({
|
|
1920
|
+
...session,
|
|
1921
|
+
name,
|
|
1922
|
+
lastModified: new Date(),
|
|
1923
|
+
}));
|
|
1924
|
+
},
|
|
1925
|
+
[updateSessionById]
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
const updateSessionOptions = useCallback(
|
|
1929
|
+
(sessionId: string, processingOptions: Session['processingOptions']) => {
|
|
1930
|
+
// DEBUG: Log session options update
|
|
1931
|
+
log.info('[SessionContext] Updating session options for session:', sessionId);
|
|
1932
|
+
log.info(' - Enabled operations:', processingOptions?.enabledOperations || []);
|
|
1933
|
+
log.info(' - Options object:', processingOptions);
|
|
1934
|
+
|
|
1935
|
+
updateSessionById(sessionId, (session) => ({
|
|
1936
|
+
...session,
|
|
1937
|
+
processingOptions,
|
|
1938
|
+
lastModified: new Date(),
|
|
1939
|
+
}));
|
|
1940
|
+
},
|
|
1941
|
+
[updateSessionById, log]
|
|
1942
|
+
);
|
|
1943
|
+
|
|
1944
|
+
const updateSessionReplacements = useCallback(
|
|
1945
|
+
(sessionId: string, replacements: ReplacementRule[]) => {
|
|
1946
|
+
updateSessionById(sessionId, (session) => ({
|
|
1947
|
+
...session,
|
|
1948
|
+
replacements,
|
|
1949
|
+
lastModified: new Date(),
|
|
1950
|
+
}));
|
|
1951
|
+
},
|
|
1952
|
+
[updateSessionById]
|
|
1953
|
+
);
|
|
1954
|
+
|
|
1955
|
+
const updateSessionStyles = useCallback(
|
|
1956
|
+
(sessionId: string, styles: SessionStyle[]) => {
|
|
1957
|
+
updateSessionById(sessionId, (session) => ({
|
|
1958
|
+
...session,
|
|
1959
|
+
styles,
|
|
1960
|
+
lastModified: new Date(),
|
|
1961
|
+
}));
|
|
1962
|
+
},
|
|
1963
|
+
[updateSessionById]
|
|
1964
|
+
);
|
|
1965
|
+
|
|
1966
|
+
const updateSessionListBulletSettings = useCallback(
|
|
1967
|
+
(sessionId: string, listBulletSettings: ListBulletSettings) => {
|
|
1968
|
+
updateSessionById(sessionId, (session) => ({
|
|
1969
|
+
...session,
|
|
1970
|
+
listBulletSettings,
|
|
1971
|
+
lastModified: new Date(),
|
|
1972
|
+
}));
|
|
1973
|
+
},
|
|
1974
|
+
[updateSessionById]
|
|
1975
|
+
);
|
|
1976
|
+
|
|
1977
|
+
const updateSessionTableUniformitySettings = useCallback(
|
|
1978
|
+
(sessionId: string, tableUniformitySettings: TableUniformitySettings) => {
|
|
1979
|
+
updateSessionById(sessionId, (session) => ({
|
|
1980
|
+
...session,
|
|
1981
|
+
tableUniformitySettings,
|
|
1982
|
+
lastModified: new Date(),
|
|
1983
|
+
}));
|
|
1984
|
+
},
|
|
1985
|
+
[updateSessionById]
|
|
1986
|
+
);
|
|
1987
|
+
|
|
1988
|
+
const updateSessionTableShadingSettings = useCallback(
|
|
1989
|
+
(sessionId: string, tableShadingSettings: TableShadingSettings) => {
|
|
1990
|
+
updateSessionById(sessionId, (session) => ({
|
|
1991
|
+
...session,
|
|
1992
|
+
tableShadingSettings,
|
|
1993
|
+
lastModified: new Date(),
|
|
1994
|
+
}));
|
|
1995
|
+
},
|
|
1996
|
+
[updateSessionById]
|
|
1997
|
+
);
|
|
1998
|
+
|
|
1999
|
+
const updateSessionTableOfContentsSettings = useCallback(
|
|
2000
|
+
(sessionId: string, tableOfContentsSettings: TableOfContentsSettings) => {
|
|
2001
|
+
log.info('[SessionContext] Updating Table of Contents settings for session:', sessionId);
|
|
2002
|
+
|
|
2003
|
+
updateSessionById(sessionId, (session) => ({
|
|
2004
|
+
...session,
|
|
2005
|
+
tableOfContentsSettings,
|
|
2006
|
+
lastModified: new Date(),
|
|
2007
|
+
}));
|
|
2008
|
+
},
|
|
2009
|
+
[updateSessionById, log]
|
|
2010
|
+
);
|
|
2011
|
+
|
|
2012
|
+
const saveSession = (session: Session) => {
|
|
2013
|
+
const jsonString = safeJsonStringify(session, undefined, 'SessionContext.saveSession');
|
|
2014
|
+
if (jsonString) {
|
|
2015
|
+
localStorage.setItem(`session_${session.id}`, jsonString);
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
const loadSessionFromStorage = (id: string): Session | null => {
|
|
2020
|
+
const stored = localStorage.getItem(`session_${id}`);
|
|
2021
|
+
if (stored) {
|
|
2022
|
+
const parsed = safeJsonParse<any>(stored, null, 'SessionContext.loadSessionFromStorage');
|
|
2023
|
+
if (parsed) {
|
|
2024
|
+
return {
|
|
2025
|
+
...parsed,
|
|
2026
|
+
createdAt: new Date(parsed.createdAt),
|
|
2027
|
+
lastModified: new Date(parsed.lastModified),
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return null;
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
// Reset session to factory defaults
|
|
2035
|
+
const resetSessionToDefaults = (sessionId: string) => {
|
|
2036
|
+
log.info('[SessionContext] Resetting session to defaults:', sessionId);
|
|
2037
|
+
|
|
2038
|
+
updateSessionById(sessionId, (session) => ({
|
|
2039
|
+
...session,
|
|
2040
|
+
styles: [...DEFAULT_SESSION_STYLES],
|
|
2041
|
+
listBulletSettings: createDefaultListBulletSettings(),
|
|
2042
|
+
processingOptions: {
|
|
2043
|
+
...DEFAULT_PROCESSING_OPTIONS,
|
|
2044
|
+
enabledOperations: [...DEFAULT_PROCESSING_OPTIONS.enabledOperations],
|
|
2045
|
+
},
|
|
2046
|
+
tableShadingSettings: { ...DEFAULT_TABLE_SHADING_SETTINGS },
|
|
2047
|
+
lastModified: new Date(),
|
|
2048
|
+
}));
|
|
2049
|
+
|
|
2050
|
+
log.info('[SessionContext] Session reset to defaults');
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
// Save current session settings as custom defaults for new sessions
|
|
2054
|
+
const saveAsCustomDefaults = (sessionId: string) => {
|
|
2055
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
2056
|
+
if (!session) {
|
|
2057
|
+
log.error('[SessionContext] Session not found for saving defaults:', sessionId);
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
log.info('[SessionContext] Saving session settings as custom defaults:', sessionId);
|
|
2062
|
+
|
|
2063
|
+
const customDefaults = {
|
|
2064
|
+
styles: session.styles,
|
|
2065
|
+
listBulletSettings: session.listBulletSettings,
|
|
2066
|
+
processingOptions: session.processingOptions,
|
|
2067
|
+
tableShadingSettings: session.tableShadingSettings,
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
const jsonString = safeJsonStringify(
|
|
2071
|
+
customDefaults,
|
|
2072
|
+
undefined,
|
|
2073
|
+
'SessionContext.saveAsCustomDefaults'
|
|
2074
|
+
);
|
|
2075
|
+
if (jsonString) {
|
|
2076
|
+
localStorage.setItem(CUSTOM_DEFAULTS_KEY, jsonString);
|
|
2077
|
+
log.info('[SessionContext] Custom defaults saved successfully');
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
const recentSessions = sessions
|
|
2082
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
|
|
2083
|
+
.slice(0, 5);
|
|
2084
|
+
|
|
2085
|
+
return (
|
|
2086
|
+
<SessionContext.Provider
|
|
2087
|
+
value={{
|
|
2088
|
+
sessions,
|
|
2089
|
+
activeSessions,
|
|
2090
|
+
currentSession,
|
|
2091
|
+
recentSessions,
|
|
2092
|
+
createSession,
|
|
2093
|
+
loadSession,
|
|
2094
|
+
reopenSession,
|
|
2095
|
+
closeSession,
|
|
2096
|
+
deleteSession,
|
|
2097
|
+
switchSession,
|
|
2098
|
+
addDocuments,
|
|
2099
|
+
removeDocument,
|
|
2100
|
+
processDocument,
|
|
2101
|
+
revertChange,
|
|
2102
|
+
revertAllChanges,
|
|
2103
|
+
updateSessionStats,
|
|
2104
|
+
updateSessionName,
|
|
2105
|
+
updateSessionOptions,
|
|
2106
|
+
updateSessionReplacements,
|
|
2107
|
+
updateSessionStyles,
|
|
2108
|
+
updateSessionListBulletSettings,
|
|
2109
|
+
updateSessionTableUniformitySettings,
|
|
2110
|
+
updateSessionTableShadingSettings,
|
|
2111
|
+
updateSessionTableOfContentsSettings,
|
|
2112
|
+
saveSession,
|
|
2113
|
+
loadSessionFromStorage,
|
|
2114
|
+
resetSessionToDefaults,
|
|
2115
|
+
saveAsCustomDefaults,
|
|
2116
|
+
}}
|
|
2117
|
+
>
|
|
2118
|
+
{children}
|
|
2119
|
+
</SessionContext.Provider>
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
export function useSession() {
|
|
2124
|
+
const context = useContext(SessionContext);
|
|
2125
|
+
if (context === undefined) {
|
|
2126
|
+
throw new Error('useSession must be used within a SessionProvider');
|
|
2127
|
+
}
|
|
2128
|
+
return context;
|
|
2129
|
+
}
|