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