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,319 @@
|
|
|
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 { SessionManager } from '@/components/sessions/SessionManager';
|
|
10
|
+
import { useGlobalStats } from '@/contexts/GlobalStatsContext';
|
|
11
|
+
import { useSession } from '@/contexts/SessionContext';
|
|
12
|
+
import { cn } from '@/utils/cn';
|
|
13
|
+
import { motion } from 'framer-motion';
|
|
14
|
+
import {
|
|
15
|
+
ArrowUpRight,
|
|
16
|
+
Calendar,
|
|
17
|
+
Clock,
|
|
18
|
+
FileCheck,
|
|
19
|
+
FileText,
|
|
20
|
+
FolderOpen,
|
|
21
|
+
Link,
|
|
22
|
+
MessageSquare,
|
|
23
|
+
Minus,
|
|
24
|
+
Plus,
|
|
25
|
+
TrendingDown,
|
|
26
|
+
TrendingUp,
|
|
27
|
+
} from 'lucide-react';
|
|
28
|
+
import { useState } from 'react';
|
|
29
|
+
import { useNavigate } from 'react-router-dom';
|
|
30
|
+
|
|
31
|
+
const containerVariants = {
|
|
32
|
+
hidden: { opacity: 0 },
|
|
33
|
+
visible: {
|
|
34
|
+
opacity: 1,
|
|
35
|
+
transition: {
|
|
36
|
+
staggerChildren: 0.1,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const itemVariants = {
|
|
42
|
+
hidden: { opacity: 0, y: 20 },
|
|
43
|
+
visible: {
|
|
44
|
+
opacity: 1,
|
|
45
|
+
y: 0,
|
|
46
|
+
transition: {
|
|
47
|
+
duration: 0.5,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function Dashboard() {
|
|
53
|
+
const { recentSessions, loadSession } = useSession();
|
|
54
|
+
const { stats: globalStats, getTodayStats, getTodayChange } = useGlobalStats();
|
|
55
|
+
const navigate = useNavigate();
|
|
56
|
+
const [showSessionManager, setShowSessionManager] = useState(false);
|
|
57
|
+
const [sessionManagerMode, setSessionManagerMode] = useState<'new' | 'load'>('new');
|
|
58
|
+
|
|
59
|
+
// Get today's stats and changes from yesterday
|
|
60
|
+
const todayStats = getTodayStats();
|
|
61
|
+
const todayChange = getTodayChange();
|
|
62
|
+
|
|
63
|
+
const stats = [
|
|
64
|
+
{
|
|
65
|
+
title: 'Documents Processed',
|
|
66
|
+
value: globalStats.allTime.documentsProcessed.toString(),
|
|
67
|
+
todayValue: todayStats.documentsProcessed,
|
|
68
|
+
change: todayChange.documentsProcessed || 0,
|
|
69
|
+
icon: FileCheck,
|
|
70
|
+
gradient: 'from-green-400 to-emerald-600',
|
|
71
|
+
bgGradient: 'from-green-500/20 to-emerald-500/10',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: 'Hyperlinks Checked',
|
|
75
|
+
value: globalStats.allTime.hyperlinksChecked.toString(),
|
|
76
|
+
todayValue: todayStats.hyperlinksChecked,
|
|
77
|
+
change: todayChange.hyperlinksChecked || 0,
|
|
78
|
+
icon: Link,
|
|
79
|
+
gradient: 'from-blue-400 to-indigo-600',
|
|
80
|
+
bgGradient: 'from-blue-500/20 to-indigo-500/10',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: 'Feedback Imported',
|
|
84
|
+
value: globalStats.allTime.feedbackImported.toString(),
|
|
85
|
+
todayValue: todayStats.feedbackImported,
|
|
86
|
+
change: todayChange.feedbackImported || 0,
|
|
87
|
+
icon: MessageSquare,
|
|
88
|
+
gradient: 'from-purple-400 to-pink-600',
|
|
89
|
+
bgGradient: 'from-purple-500/20 to-pink-500/10',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: 'Time Saved',
|
|
93
|
+
value: `${globalStats.allTime.timeSaved}m`,
|
|
94
|
+
todayValue: todayStats.timeSaved,
|
|
95
|
+
change: todayChange.timeSaved || 0,
|
|
96
|
+
icon: Clock,
|
|
97
|
+
gradient: 'from-orange-400 to-red-600',
|
|
98
|
+
bgGradient: 'from-orange-500/20 to-red-500/10',
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const handleNewSession = () => {
|
|
103
|
+
setSessionManagerMode('new');
|
|
104
|
+
setShowSessionManager(true);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleLoadSession = () => {
|
|
108
|
+
setSessionManagerMode('load');
|
|
109
|
+
setShowSessionManager(true);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleSessionCreated = (sessionId: string) => {
|
|
113
|
+
navigate(`/session/${sessionId}`);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleRecentSessionClick = (sessionId: string) => {
|
|
117
|
+
loadSession(sessionId);
|
|
118
|
+
navigate(`/session/${sessionId}`);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const formatDate = (date: Date) => {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const diff = now.getTime() - date.getTime();
|
|
124
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
125
|
+
|
|
126
|
+
if (hours < 1) {
|
|
127
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
128
|
+
return `${minutes} minutes ago`;
|
|
129
|
+
} else if (hours < 24) {
|
|
130
|
+
return `${hours} hours ago`;
|
|
131
|
+
} else {
|
|
132
|
+
const days = Math.floor(hours / 24);
|
|
133
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<motion.div
|
|
139
|
+
className="p-6 space-y-6"
|
|
140
|
+
variants={containerVariants}
|
|
141
|
+
initial="hidden"
|
|
142
|
+
animate="visible"
|
|
143
|
+
>
|
|
144
|
+
<motion.div className="flex justify-end" variants={itemVariants}>
|
|
145
|
+
<div className="flex gap-2">
|
|
146
|
+
<Button
|
|
147
|
+
onClick={handleNewSession}
|
|
148
|
+
variant="default"
|
|
149
|
+
size="sm"
|
|
150
|
+
icon={<Plus className="w-4 h-4" />}
|
|
151
|
+
>
|
|
152
|
+
New Session
|
|
153
|
+
</Button>
|
|
154
|
+
<Button
|
|
155
|
+
onClick={handleLoadSession}
|
|
156
|
+
variant="outline"
|
|
157
|
+
size="sm"
|
|
158
|
+
icon={<FolderOpen className="w-4 h-4" />}
|
|
159
|
+
>
|
|
160
|
+
Load Session
|
|
161
|
+
</Button>
|
|
162
|
+
</div>
|
|
163
|
+
</motion.div>
|
|
164
|
+
|
|
165
|
+
<motion.div
|
|
166
|
+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
|
167
|
+
variants={itemVariants}
|
|
168
|
+
>
|
|
169
|
+
{stats.map((stat) => {
|
|
170
|
+
const Icon = stat.icon;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<motion.div key={stat.title} whileHover={{ y: -4 }} transition={{ duration: 0.2 }}>
|
|
174
|
+
<Card className="relative overflow-hidden group cursor-pointer border-0 shadow-lg hover:shadow-2xl transition-all">
|
|
175
|
+
<div
|
|
176
|
+
className={cn(
|
|
177
|
+
'absolute inset-0 bg-linear-to-br opacity-5 group-hover:opacity-10 transition-opacity',
|
|
178
|
+
stat.bgGradient
|
|
179
|
+
)}
|
|
180
|
+
/>
|
|
181
|
+
<CardContent className="p-6 relative">
|
|
182
|
+
<div className="flex items-center justify-between mb-4">
|
|
183
|
+
<div
|
|
184
|
+
className={cn('p-3 rounded-xl bg-linear-to-br', stat.gradient, 'shadow-lg')}
|
|
185
|
+
>
|
|
186
|
+
<Icon className="w-6 h-6 text-white" />
|
|
187
|
+
</div>
|
|
188
|
+
<motion.button
|
|
189
|
+
whileHover={{ scale: 1.1 }}
|
|
190
|
+
whileTap={{ scale: 0.95 }}
|
|
191
|
+
className="p-2 rounded-lg hover:bg-muted transition-colors"
|
|
192
|
+
>
|
|
193
|
+
<ArrowUpRight className="w-4 h-4 text-muted-foreground" />
|
|
194
|
+
</motion.button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="space-y-1">
|
|
198
|
+
<p className="text-xs font-medium text-foreground uppercase tracking-wide">
|
|
199
|
+
{stat.title}
|
|
200
|
+
</p>
|
|
201
|
+
<p className="text-3xl font-bold tracking-tight">{stat.value}</p>
|
|
202
|
+
<div className="flex items-center gap-2 pt-2">
|
|
203
|
+
{stat.todayValue > 0 ? (
|
|
204
|
+
<>
|
|
205
|
+
<div className="flex items-center gap-1">
|
|
206
|
+
{stat.change > 0 ? (
|
|
207
|
+
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
|
208
|
+
<TrendingUp className="w-3 h-3" />
|
|
209
|
+
<span>+{stat.change}</span>
|
|
210
|
+
</div>
|
|
211
|
+
) : stat.change < 0 ? (
|
|
212
|
+
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
|
213
|
+
<TrendingDown className="w-3 h-3" />
|
|
214
|
+
<span>{stat.change}</span>
|
|
215
|
+
</div>
|
|
216
|
+
) : (
|
|
217
|
+
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-muted text-muted-foreground">
|
|
218
|
+
<Minus className="w-3 h-3" />
|
|
219
|
+
<span>No change</span>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
<span className="text-xs text-muted-foreground">vs yesterday</span>
|
|
224
|
+
</>
|
|
225
|
+
) : (
|
|
226
|
+
<span className="text-xs text-muted-foreground">No activity today</span>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</CardContent>
|
|
231
|
+
</Card>
|
|
232
|
+
</motion.div>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</motion.div>
|
|
236
|
+
|
|
237
|
+
<motion.div variants={itemVariants}>
|
|
238
|
+
<Card>
|
|
239
|
+
<CardHeader>
|
|
240
|
+
<CardTitle>Recent Sessions</CardTitle>
|
|
241
|
+
<CardDescription>Your recently accessed document processing sessions</CardDescription>
|
|
242
|
+
</CardHeader>
|
|
243
|
+
<CardContent>
|
|
244
|
+
{recentSessions.length === 0 ? (
|
|
245
|
+
<div className="text-center py-8">
|
|
246
|
+
<FileText className="w-12 h-12 mx-auto text-muted-foreground mb-3" />
|
|
247
|
+
<p className="text-muted-foreground">No sessions yet</p>
|
|
248
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
249
|
+
Create a new session to start processing documents
|
|
250
|
+
</p>
|
|
251
|
+
<Button
|
|
252
|
+
onClick={handleNewSession}
|
|
253
|
+
variant="outline"
|
|
254
|
+
size="sm"
|
|
255
|
+
className="mt-4"
|
|
256
|
+
icon={<Plus className="w-4 h-4" />}
|
|
257
|
+
>
|
|
258
|
+
Create First Session
|
|
259
|
+
</Button>
|
|
260
|
+
</div>
|
|
261
|
+
) : (
|
|
262
|
+
<div className="space-y-3">
|
|
263
|
+
{recentSessions.map((session) => (
|
|
264
|
+
<motion.div
|
|
265
|
+
key={session.id}
|
|
266
|
+
className="flex items-center justify-between p-4 rounded-lg border border-border hover:bg-muted/50 transition-all cursor-pointer group"
|
|
267
|
+
whileHover={{ x: 4 }}
|
|
268
|
+
onClick={() => handleRecentSessionClick(session.id)}
|
|
269
|
+
>
|
|
270
|
+
<div className="flex items-center gap-4">
|
|
271
|
+
<div className="p-2 rounded-lg bg-primary/10">
|
|
272
|
+
<FileText className="w-5 h-5 text-primary" />
|
|
273
|
+
</div>
|
|
274
|
+
<div>
|
|
275
|
+
<p className="text-lg font-medium">{session.name}</p>
|
|
276
|
+
<div className="flex items-center gap-4 text-sm text-muted-foreground mt-1">
|
|
277
|
+
<span className="flex items-center gap-1">
|
|
278
|
+
<FileCheck className="w-3 h-3" />
|
|
279
|
+
{session.documents.length} documents
|
|
280
|
+
</span>
|
|
281
|
+
<span className="flex items-center gap-1">
|
|
282
|
+
<Calendar className="w-3 h-3" />
|
|
283
|
+
{formatDate(session.lastModified)}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center gap-2">
|
|
289
|
+
<span
|
|
290
|
+
className={cn(
|
|
291
|
+
'text-xs px-2 py-1 rounded-full',
|
|
292
|
+
session.status === 'active'
|
|
293
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
294
|
+
: 'bg-muted text-muted-foreground'
|
|
295
|
+
)}
|
|
296
|
+
>
|
|
297
|
+
{session.status}
|
|
298
|
+
</span>
|
|
299
|
+
<ArrowUpRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
300
|
+
</div>
|
|
301
|
+
</motion.div>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</CardContent>
|
|
306
|
+
</Card>
|
|
307
|
+
</motion.div>
|
|
308
|
+
|
|
309
|
+
{showSessionManager && (
|
|
310
|
+
<SessionManager
|
|
311
|
+
mode={sessionManagerMode}
|
|
312
|
+
onClose={() => setShowSessionManager(false)}
|
|
313
|
+
onSessionCreated={handleSessionCreated}
|
|
314
|
+
onSessionLoaded={handleSessionCreated}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
</motion.div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { useSession } from '@/contexts/SessionContext';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import {
|
|
5
|
+
FileText,
|
|
6
|
+
FolderOpen,
|
|
7
|
+
CheckCircle,
|
|
8
|
+
AlertCircle,
|
|
9
|
+
Clock,
|
|
10
|
+
Search,
|
|
11
|
+
Filter,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/common/Card';
|
|
14
|
+
import { Input } from '@/components/common/Input';
|
|
15
|
+
import { Button } from '@/components/common/Button';
|
|
16
|
+
import { cn } from '@/utils/cn';
|
|
17
|
+
import logger from '@/utils/logger';
|
|
18
|
+
import type { Document } from '@/types/session';
|
|
19
|
+
|
|
20
|
+
interface DocumentWithSession extends Document {
|
|
21
|
+
sessionName: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Documents() {
|
|
26
|
+
const { sessions } = useSession();
|
|
27
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
28
|
+
const [statusFilter, setStatusFilter] = useState<'all' | 'completed' | 'pending' | 'error'>(
|
|
29
|
+
'all'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Collect all documents from all sessions
|
|
33
|
+
const allDocuments = useMemo(() => {
|
|
34
|
+
const docs: DocumentWithSession[] = [];
|
|
35
|
+
sessions.forEach((session) => {
|
|
36
|
+
session.documents.forEach((doc) => {
|
|
37
|
+
docs.push({
|
|
38
|
+
...doc,
|
|
39
|
+
sessionName: session.name,
|
|
40
|
+
sessionId: session.id,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return docs;
|
|
45
|
+
}, [sessions]);
|
|
46
|
+
|
|
47
|
+
// Filter documents
|
|
48
|
+
const filteredDocuments = useMemo(() => {
|
|
49
|
+
return allDocuments.filter((doc) => {
|
|
50
|
+
// Filter by search query
|
|
51
|
+
const matchesSearch =
|
|
52
|
+
searchQuery === '' ||
|
|
53
|
+
doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
54
|
+
doc.sessionName.toLowerCase().includes(searchQuery.toLowerCase());
|
|
55
|
+
|
|
56
|
+
// Filter by status
|
|
57
|
+
const matchesStatus = statusFilter === 'all' || doc.status === statusFilter;
|
|
58
|
+
|
|
59
|
+
return matchesSearch && matchesStatus;
|
|
60
|
+
});
|
|
61
|
+
}, [allDocuments, searchQuery, statusFilter]);
|
|
62
|
+
|
|
63
|
+
const handleOpenLocation = async (path?: string) => {
|
|
64
|
+
if (!path) {
|
|
65
|
+
logger.warn('No path available for document');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await window.electronAPI.showInFolder(path);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.error('Failed to open file location:', err);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleOpenDocument = async (path?: string) => {
|
|
77
|
+
if (!path) {
|
|
78
|
+
logger.warn('No path available for document');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await window.electronAPI.openDocument(path);
|
|
84
|
+
logger.info('Document opened successfully');
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logger.error('Failed to open document:', err);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getStatusIcon = (status: Document['status']) => {
|
|
91
|
+
switch (status) {
|
|
92
|
+
case 'completed':
|
|
93
|
+
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
94
|
+
case 'error':
|
|
95
|
+
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
|
96
|
+
case 'pending':
|
|
97
|
+
return <Clock className="w-4 h-4 text-muted-foreground" />;
|
|
98
|
+
default:
|
|
99
|
+
return <FileText className="w-4 h-4 text-muted-foreground" />;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getStatusBadge = (status: Document['status']) => {
|
|
104
|
+
const styles = {
|
|
105
|
+
completed: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
|
106
|
+
error: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
|
107
|
+
pending: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
|
108
|
+
processing: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<span className={cn('px-2 py-1 text-xs rounded-full font-medium', styles[status])}>
|
|
113
|
+
{status.charAt(0).toUpperCase() + status.slice(1)}
|
|
114
|
+
</span>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const formatDate = (date?: Date) => {
|
|
119
|
+
if (!date) return 'N/A';
|
|
120
|
+
return new Date(date).toLocaleString();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="p-6 max-w-7xl mx-auto space-y-6">
|
|
125
|
+
{/* Header */}
|
|
126
|
+
<div>
|
|
127
|
+
<h1 className="text-3xl font-bold mb-2">Documents</h1>
|
|
128
|
+
<p className="text-muted-foreground">
|
|
129
|
+
View and manage all processed documents across all sessions
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Stats Cards */}
|
|
134
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
135
|
+
<Card>
|
|
136
|
+
<CardContent className="p-4">
|
|
137
|
+
<div className="flex items-center gap-3">
|
|
138
|
+
<FileText className="w-8 h-8 text-blue-500" />
|
|
139
|
+
<div>
|
|
140
|
+
<p className="text-sm text-foreground">Total</p>
|
|
141
|
+
<p className="text-2xl font-bold">{allDocuments.length}</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</CardContent>
|
|
145
|
+
</Card>
|
|
146
|
+
|
|
147
|
+
<Card>
|
|
148
|
+
<CardContent className="p-4">
|
|
149
|
+
<div className="flex items-center gap-3">
|
|
150
|
+
<CheckCircle className="w-8 h-8 text-green-500" />
|
|
151
|
+
<div>
|
|
152
|
+
<p className="text-sm text-foreground">Completed</p>
|
|
153
|
+
<p className="text-2xl font-bold">
|
|
154
|
+
{allDocuments.filter((d) => d.status === 'completed').length}
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</CardContent>
|
|
159
|
+
</Card>
|
|
160
|
+
|
|
161
|
+
<Card>
|
|
162
|
+
<CardContent className="p-4">
|
|
163
|
+
<div className="flex items-center gap-3">
|
|
164
|
+
<Clock className="w-8 h-8 text-yellow-500" />
|
|
165
|
+
<div>
|
|
166
|
+
<p className="text-sm text-foreground">Pending</p>
|
|
167
|
+
<p className="text-2xl font-bold">
|
|
168
|
+
{allDocuments.filter((d) => d.status === 'pending').length}
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</CardContent>
|
|
173
|
+
</Card>
|
|
174
|
+
|
|
175
|
+
<Card>
|
|
176
|
+
<CardContent className="p-4">
|
|
177
|
+
<div className="flex items-center gap-3">
|
|
178
|
+
<AlertCircle className="w-8 h-8 text-red-500" />
|
|
179
|
+
<div>
|
|
180
|
+
<p className="text-sm text-foreground">Errors</p>
|
|
181
|
+
<p className="text-2xl font-bold">
|
|
182
|
+
{allDocuments.filter((d) => d.status === 'error').length}
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</CardContent>
|
|
187
|
+
</Card>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* Filters */}
|
|
191
|
+
<Card>
|
|
192
|
+
<CardHeader>
|
|
193
|
+
<CardTitle>Filters</CardTitle>
|
|
194
|
+
</CardHeader>
|
|
195
|
+
<CardContent className="space-y-4">
|
|
196
|
+
<div className="flex flex-col sm:flex-row gap-4">
|
|
197
|
+
<div className="flex-1">
|
|
198
|
+
<Input
|
|
199
|
+
type="search"
|
|
200
|
+
placeholder="Search documents..."
|
|
201
|
+
value={searchQuery}
|
|
202
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
203
|
+
leftIcon={<Search className="w-4 h-4" />}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="flex gap-2">
|
|
207
|
+
<Button
|
|
208
|
+
variant={statusFilter === 'all' ? 'default' : 'outline'}
|
|
209
|
+
size="sm"
|
|
210
|
+
onClick={() => setStatusFilter('all')}
|
|
211
|
+
>
|
|
212
|
+
All
|
|
213
|
+
</Button>
|
|
214
|
+
<Button
|
|
215
|
+
variant={statusFilter === 'completed' ? 'default' : 'outline'}
|
|
216
|
+
size="sm"
|
|
217
|
+
onClick={() => setStatusFilter('completed')}
|
|
218
|
+
>
|
|
219
|
+
Completed
|
|
220
|
+
</Button>
|
|
221
|
+
<Button
|
|
222
|
+
variant={statusFilter === 'pending' ? 'default' : 'outline'}
|
|
223
|
+
size="sm"
|
|
224
|
+
onClick={() => setStatusFilter('pending')}
|
|
225
|
+
>
|
|
226
|
+
Pending
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
variant={statusFilter === 'error' ? 'default' : 'outline'}
|
|
230
|
+
size="sm"
|
|
231
|
+
onClick={() => setStatusFilter('error')}
|
|
232
|
+
>
|
|
233
|
+
Errors
|
|
234
|
+
</Button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</CardContent>
|
|
238
|
+
</Card>
|
|
239
|
+
|
|
240
|
+
{/* Documents List */}
|
|
241
|
+
<Card>
|
|
242
|
+
<CardHeader>
|
|
243
|
+
<CardTitle>
|
|
244
|
+
{filteredDocuments.length} Document{filteredDocuments.length !== 1 ? 's' : ''}
|
|
245
|
+
</CardTitle>
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent>
|
|
248
|
+
{filteredDocuments.length === 0 ? (
|
|
249
|
+
<div className="text-center py-12">
|
|
250
|
+
<FileText className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
|
251
|
+
<h3 className="text-lg font-medium mb-2">No documents found</h3>
|
|
252
|
+
<p className="text-muted-foreground">
|
|
253
|
+
{searchQuery || statusFilter !== 'all'
|
|
254
|
+
? 'Try adjusting your filters'
|
|
255
|
+
: 'Start by creating a session and adding documents'}
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
) : (
|
|
259
|
+
<div className="space-y-2">
|
|
260
|
+
{filteredDocuments.map((doc) => (
|
|
261
|
+
<motion.div
|
|
262
|
+
key={`${doc.sessionId}-${doc.id}`}
|
|
263
|
+
initial={{ opacity: 0, y: 20 }}
|
|
264
|
+
animate={{ opacity: 1, y: 0 }}
|
|
265
|
+
className="flex items-center justify-between p-4 rounded-lg border border-border hover:bg-muted/50 transition-all group"
|
|
266
|
+
>
|
|
267
|
+
<div className="flex items-center gap-4 flex-1 min-w-0">
|
|
268
|
+
{getStatusIcon(doc.status)}
|
|
269
|
+
<div className="flex-1 min-w-0">
|
|
270
|
+
<p className="font-medium truncate">{doc.name}</p>
|
|
271
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
272
|
+
<span className="truncate">{doc.sessionName}</span>
|
|
273
|
+
{doc.processedAt && (
|
|
274
|
+
<>
|
|
275
|
+
<span>•</span>
|
|
276
|
+
<span>{formatDate(doc.processedAt)}</span>
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="flex items-center gap-3">
|
|
284
|
+
{getStatusBadge(doc.status)}
|
|
285
|
+
{doc.status === 'completed' && doc.path && (
|
|
286
|
+
<Button
|
|
287
|
+
variant="ghost"
|
|
288
|
+
size="xs"
|
|
289
|
+
icon={<FileText className="w-4 h-4" />}
|
|
290
|
+
onClick={() => handleOpenDocument(doc.path)}
|
|
291
|
+
title="Open document in Word"
|
|
292
|
+
className="text-green-600 hover:text-green-700 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-950"
|
|
293
|
+
>
|
|
294
|
+
Open Document
|
|
295
|
+
</Button>
|
|
296
|
+
)}
|
|
297
|
+
{doc.path && (
|
|
298
|
+
<Button
|
|
299
|
+
variant="ghost"
|
|
300
|
+
size="xs"
|
|
301
|
+
icon={<FolderOpen className="w-4 h-4" />}
|
|
302
|
+
onClick={() => handleOpenLocation(doc.path)}
|
|
303
|
+
title="Open file location"
|
|
304
|
+
>
|
|
305
|
+
Open Location
|
|
306
|
+
</Button>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
</motion.div>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</CardContent>
|
|
314
|
+
</Card>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|