@meechi-ai/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +624 -0
  2. package/README.md +59 -0
  3. package/dist/components/CalendarView.d.ts +3 -0
  4. package/dist/components/CalendarView.js +72 -0
  5. package/dist/components/ChatInterface.d.ts +6 -0
  6. package/dist/components/ChatInterface.js +105 -0
  7. package/dist/components/FileExplorer.d.ts +9 -0
  8. package/dist/components/FileExplorer.js +757 -0
  9. package/dist/components/Icon.d.ts +9 -0
  10. package/dist/components/Icon.js +44 -0
  11. package/dist/components/SourceEditor.d.ts +13 -0
  12. package/dist/components/SourceEditor.js +50 -0
  13. package/dist/components/ThemeProvider.d.ts +5 -0
  14. package/dist/components/ThemeProvider.js +105 -0
  15. package/dist/components/ThemeSwitcher.d.ts +1 -0
  16. package/dist/components/ThemeSwitcher.js +16 -0
  17. package/dist/components/voice/VoiceInputArea.d.ts +14 -0
  18. package/dist/components/voice/VoiceInputArea.js +190 -0
  19. package/dist/components/voice/VoiceOverlay.d.ts +7 -0
  20. package/dist/components/voice/VoiceOverlay.js +71 -0
  21. package/dist/hooks/useMeechi.d.ts +16 -0
  22. package/dist/hooks/useMeechi.js +461 -0
  23. package/dist/hooks/useSync.d.ts +8 -0
  24. package/dist/hooks/useSync.js +87 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.js +22 -0
  27. package/dist/lib/ai/embeddings.d.ts +15 -0
  28. package/dist/lib/ai/embeddings.js +128 -0
  29. package/dist/lib/ai/gpu-lock.d.ts +19 -0
  30. package/dist/lib/ai/gpu-lock.js +43 -0
  31. package/dist/lib/ai/llm.worker.d.ts +1 -0
  32. package/dist/lib/ai/llm.worker.js +7 -0
  33. package/dist/lib/ai/local-llm.d.ts +30 -0
  34. package/dist/lib/ai/local-llm.js +211 -0
  35. package/dist/lib/ai/manager.d.ts +20 -0
  36. package/dist/lib/ai/manager.js +51 -0
  37. package/dist/lib/ai/parsing.d.ts +12 -0
  38. package/dist/lib/ai/parsing.js +56 -0
  39. package/dist/lib/ai/prompts.d.ts +2 -0
  40. package/dist/lib/ai/prompts.js +2 -0
  41. package/dist/lib/ai/providers/gemini.d.ts +6 -0
  42. package/dist/lib/ai/providers/gemini.js +88 -0
  43. package/dist/lib/ai/providers/groq.d.ts +6 -0
  44. package/dist/lib/ai/providers/groq.js +42 -0
  45. package/dist/lib/ai/registry.d.ts +29 -0
  46. package/dist/lib/ai/registry.js +52 -0
  47. package/dist/lib/ai/tools.d.ts +2 -0
  48. package/dist/lib/ai/tools.js +106 -0
  49. package/dist/lib/ai/types.d.ts +22 -0
  50. package/dist/lib/ai/types.js +1 -0
  51. package/dist/lib/ai/worker.d.ts +1 -0
  52. package/dist/lib/ai/worker.js +60 -0
  53. package/dist/lib/audio/input.d.ts +13 -0
  54. package/dist/lib/audio/input.js +121 -0
  55. package/dist/lib/audio/stt.d.ts +13 -0
  56. package/dist/lib/audio/stt.js +119 -0
  57. package/dist/lib/audio/tts.d.ts +12 -0
  58. package/dist/lib/audio/tts.js +128 -0
  59. package/dist/lib/audio/vad.d.ts +18 -0
  60. package/dist/lib/audio/vad.js +117 -0
  61. package/dist/lib/colors.d.ts +16 -0
  62. package/dist/lib/colors.js +67 -0
  63. package/dist/lib/extensions.d.ts +35 -0
  64. package/dist/lib/extensions.js +24 -0
  65. package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
  66. package/dist/lib/hooks/use-voice-loop.js +313 -0
  67. package/dist/lib/mcp/McpClient.d.ts +19 -0
  68. package/dist/lib/mcp/McpClient.js +42 -0
  69. package/dist/lib/mcp/McpRegistry.d.ts +47 -0
  70. package/dist/lib/mcp/McpRegistry.js +117 -0
  71. package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
  72. package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
  73. package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
  74. package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
  75. package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
  76. package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
  77. package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
  78. package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
  79. package/dist/lib/mcp/native/index.d.ts +10 -0
  80. package/dist/lib/mcp/native/index.js +10 -0
  81. package/dist/lib/mcp/types.d.ts +35 -0
  82. package/dist/lib/mcp/types.js +1 -0
  83. package/dist/lib/pdf.d.ts +10 -0
  84. package/dist/lib/pdf.js +142 -0
  85. package/dist/lib/settings.d.ts +48 -0
  86. package/dist/lib/settings.js +87 -0
  87. package/dist/lib/storage/db.d.ts +57 -0
  88. package/dist/lib/storage/db.js +45 -0
  89. package/dist/lib/storage/local.d.ts +28 -0
  90. package/dist/lib/storage/local.js +534 -0
  91. package/dist/lib/storage/migrate.d.ts +3 -0
  92. package/dist/lib/storage/migrate.js +122 -0
  93. package/dist/lib/storage/types.d.ts +66 -0
  94. package/dist/lib/storage/types.js +1 -0
  95. package/dist/lib/sync/client-drive.d.ts +9 -0
  96. package/dist/lib/sync/client-drive.js +69 -0
  97. package/dist/lib/sync/engine.d.ts +18 -0
  98. package/dist/lib/sync/engine.js +517 -0
  99. package/dist/lib/sync/google-drive.d.ts +52 -0
  100. package/dist/lib/sync/google-drive.js +183 -0
  101. package/dist/lib/sync/merge.d.ts +1 -0
  102. package/dist/lib/sync/merge.js +68 -0
  103. package/dist/lib/yjs/YjsProvider.d.ts +11 -0
  104. package/dist/lib/yjs/YjsProvider.js +33 -0
  105. package/dist/lib/yjs/graph.d.ts +11 -0
  106. package/dist/lib/yjs/graph.js +7 -0
  107. package/dist/lib/yjs/hooks.d.ts +7 -0
  108. package/dist/lib/yjs/hooks.js +37 -0
  109. package/dist/lib/yjs/store.d.ts +4 -0
  110. package/dist/lib/yjs/store.js +19 -0
  111. package/dist/lib/yjs/syncGraph.d.ts +1 -0
  112. package/dist/lib/yjs/syncGraph.js +38 -0
  113. package/dist/providers/theme-provider.d.ts +3 -0
  114. package/dist/providers/theme-provider.js +18 -0
  115. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  116. package/package.json +69 -0
@@ -0,0 +1,757 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React, { useState, useEffect, useRef } from 'react';
4
+ import styles from '../app/app/page.module.css';
5
+ import { useLiveQuery } from 'dexie-react-hooks';
6
+ import { db } from '../lib/storage/db';
7
+ import { extractTextFromPdf } from '../lib/pdf';
8
+ import Icon from './Icon';
9
+ import { extensions } from '../lib/extensions';
10
+ export default function FileExplorer(props) {
11
+ const { storage, onClose, onOpenFile } = props;
12
+ const [currentPath, setCurrentPath] = useState(() => {
13
+ if (typeof window !== 'undefined') {
14
+ return localStorage.getItem('meechi_explorer_path') || 'misc';
15
+ }
16
+ return 'misc';
17
+ });
18
+ useEffect(() => {
19
+ localStorage.setItem('meechi_explorer_path', currentPath);
20
+ }, [currentPath]);
21
+ // Actions State
22
+ const [isUploading, setIsUploading] = useState(false);
23
+ const fileInputRef = useRef(null);
24
+ const [activeMenuId, setActiveMenuId] = useState(null);
25
+ const [isBulkMode, setIsBulkMode] = useState(false);
26
+ const [selectedIds, setSelectedIds] = useState(new Set());
27
+ // Close menu on click outside - Use CAPTURE to handle before React bubbles
28
+ useEffect(() => {
29
+ const handleClickOutside = (e) => {
30
+ // Don't close if clicking the kebab button OR inside the menu (let item handlers manage closure)
31
+ if (e.target.closest(`.${styles.kebabButton}`) || e.target.closest(`.${styles.dropdownMenu}`))
32
+ return;
33
+ setActiveMenuId(null);
34
+ };
35
+ document.addEventListener('click', handleClickOutside, true);
36
+ return () => document.removeEventListener('click', handleClickOutside, true);
37
+ }, []);
38
+ const lastInteractionIndex = useRef(-1);
39
+ const [dialogAction, setDialogAction] = useState(null);
40
+ // Link Dialog State
41
+ const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
42
+ const [linkUrl, setLinkUrl] = useState("");
43
+ const [isFetchingLink, setIsFetchingLink] = useState(false);
44
+ const showAlert = (title, message, onConfirm) => {
45
+ setDialogAction({ type: 'alert', title, message, onConfirm });
46
+ };
47
+ const handleAddLink = async () => {
48
+ if (!linkUrl)
49
+ return;
50
+ setIsFetchingLink(true);
51
+ try {
52
+ const res = await fetch('/api/utils/fetch-url', {
53
+ method: 'POST',
54
+ body: JSON.stringify({ url: linkUrl }),
55
+ headers: { 'Content-Type': 'application/json' }
56
+ });
57
+ const data = await res.json();
58
+ if (!res.ok)
59
+ throw new Error(data.message);
60
+ // Create file
61
+ const safeTitle = data.title.replace(/[^a-zA-Z0-9 \-_]/g, '').trim() || 'Untitled Source';
62
+ const fileName = `${safeTitle}.source.md`;
63
+ const path = `${currentPath}/${fileName}`;
64
+ // Add Summary Wrapper if content is long?
65
+ // The prompt says: "create a md file with the parsed text, and summary in the beggining"
66
+ // Generate Summary
67
+ let finalContent = data.content;
68
+ try {
69
+ const sumRes = await fetch('/api/ai/summarize', {
70
+ method: 'POST',
71
+ body: JSON.stringify({ content: data.content }),
72
+ headers: { 'Content-Type': 'application/json' }
73
+ });
74
+ const sumData = await sumRes.json();
75
+ if (sumData.summary) {
76
+ finalContent = `> **Summary**: ${sumData.summary}\n\n> **Source**: ${linkUrl}\n\n---\n\n${data.content}`;
77
+ }
78
+ else {
79
+ finalContent = `> **Source**: ${linkUrl}\n\n---\n\n${data.content}`;
80
+ }
81
+ }
82
+ catch (e) {
83
+ console.error("Summary failed", e);
84
+ finalContent = `> **Source**: ${linkUrl}\n\n---\n\n${data.content}`;
85
+ }
86
+ await storage.saveFile(path, finalContent);
87
+ setIsLinkDialogOpen(false);
88
+ setLinkUrl("");
89
+ showAlert("Success", "Link added successfully!");
90
+ await storage.saveFile(path, finalContent);
91
+ setIsLinkDialogOpen(false);
92
+ setLinkUrl("");
93
+ showAlert("Success", "Link added successfully!");
94
+ }
95
+ catch (e) {
96
+ showAlert("Error", "Failed to add link: " + e.message);
97
+ }
98
+ finally {
99
+ setIsFetchingLink(false);
100
+ }
101
+ };
102
+ // Initial Migration Trigger
103
+ useEffect(() => {
104
+ storage.init().catch(err => console.error("Migration failed", err));
105
+ }, [storage]);
106
+ // REACTIVE DATA FETCHING (Dexie Magic 🪄)
107
+ const items = useLiveQuery(async () => {
108
+ // Dynamic Query based on path
109
+ // optimization: if path is "root", show top level folders?
110
+ // Our structure implies everything starts with "misc/" or "history/".
111
+ // Let's query EVERYTHING for now to ensure folders appear?
112
+ // No, querying everything (startsWith("")) is better for discovery if we don't strictly enforce 'misc' root.
113
+ let collection;
114
+ const isRoot = currentPath === 'root';
115
+ const queryPath = isRoot ? '' : currentPath;
116
+ // simple prefix query
117
+ const allFiles = await db.files.where('path').startsWith(queryPath).toArray();
118
+ console.log(`[FileExplorer] Query '${queryPath}' returned ${allFiles.length} files.`, allFiles.map(f => f.path));
119
+ const folders = new Set();
120
+ const currentLevelFiles = [];
121
+ allFiles.filter(f => !f.deleted).forEach(f => {
122
+ // Filter: Must start with currentPath + '/'
123
+ // Special case: if isRoot, just look for top level dirs?
124
+ let relative = "";
125
+ if (isRoot) {
126
+ relative = f.path;
127
+ }
128
+ else {
129
+ if (!f.path.startsWith(currentPath + '/'))
130
+ return;
131
+ relative = f.path.substring(currentPath.length + 1);
132
+ }
133
+ if (relative.includes('/')) {
134
+ folders.add(relative.split('/')[0]);
135
+ }
136
+ else {
137
+ // It's a file right here
138
+ currentLevelFiles.push({
139
+ id: f.path,
140
+ name: f.path.split('/').pop() || f.path,
141
+ path: f.path,
142
+ updatedAt: f.updatedAt,
143
+ type: f.type,
144
+ remoteId: f.remoteId,
145
+ tags: f.tags,
146
+ metadata: f.metadata
147
+ });
148
+ }
149
+ });
150
+ // NotebookLM Style: Group "Source" files
151
+ // If we have "foo.pdf" and "foo.pdf.source.md", hide "foo.pdf" and rename "foo.pdf.source.md" to "foo.pdf (Source)"
152
+ // 1. Find all sources
153
+ // 1. Find all sources and potential wrapper notes
154
+ const sources = currentLevelFiles.filter(f => f.name.endsWith('.source.md'));
155
+ const sourceMap = new Set(sources.map(s => s.name));
156
+ const allFileNames = new Set(currentLevelFiles.map(f => f.name));
157
+ // 2. Filter out raw PDFs if they have a source
158
+ const finalFiles = currentLevelFiles.filter(f => {
159
+ const isSource = f.name.endsWith('.source.md');
160
+ if (isSource)
161
+ return true;
162
+ const potentialSourceName = f.name + '.source.md';
163
+ if (sourceMap.has(potentialSourceName))
164
+ return false; // Hide raw PDF if Source exists
165
+ const potentialNoteName = f.name + '.md';
166
+ if (allFileNames.has(potentialNoteName))
167
+ return false; // Hide raw PDF if Converted Note exists
168
+ return true;
169
+ }).map(f => {
170
+ if (f.name.endsWith('.source.md')) {
171
+ return Object.assign(Object.assign({}, f), { name: f.name.replace('.pdf.source.md', ' (Source)'), type: 'source' // New visual type?
172
+ });
173
+ }
174
+ return f;
175
+ });
176
+ const folderItems = Array.from(folders)
177
+ .filter(name => !currentLevelFiles.some(f => f.name === name && f.type === 'folder'))
178
+ .map(name => ({
179
+ id: isRoot ? name : `${currentPath}/${name}`,
180
+ name: name,
181
+ path: isRoot ? name : `${currentPath}/${name}`,
182
+ updatedAt: Date.now(),
183
+ type: 'folder'
184
+ }));
185
+ return [...folderItems, ...finalFiles].sort((a, b) => {
186
+ if (a.type === b.type)
187
+ return a.name.localeCompare(b.name);
188
+ return a.type === 'folder' ? -1 : 1;
189
+ });
190
+ }, [currentPath]) || [];
191
+ // Loading deleted (Dexie handles it)
192
+ // ... existing handlers ...
193
+ // Note: We no longer need to call loadFiles() manually after actions!
194
+ // saveFile -> DB Update -> useLiveQuery triggers -> UI Updates automatically.
195
+ const handleNavigate = (folderName) => {
196
+ setCurrentPath(`${currentPath}/${folderName}`);
197
+ };
198
+ const handleBack = () => {
199
+ if (currentPath === 'misc')
200
+ return;
201
+ const parts = currentPath.split('/');
202
+ parts.pop();
203
+ setCurrentPath(parts.join('/'));
204
+ };
205
+ const handleCreateFolder = async () => {
206
+ const name = prompt("Topic Name:");
207
+ if (!name)
208
+ return;
209
+ // Explicitly create folder record for sync
210
+ const path = `${currentPath}/${name}`;
211
+ await storage.createFolder(path);
212
+ };
213
+ const processFile = async (file, targetPath) => {
214
+ const path = `${targetPath}/${file.name}`;
215
+ let content = "";
216
+ if (file.type.startsWith('text/') || file.name.endsWith('.md') || file.name.endsWith('.txt')) {
217
+ content = await file.text();
218
+ }
219
+ else if (file.type === 'application/pdf') {
220
+ try {
221
+ const buffer = await file.arrayBuffer();
222
+ const extractedText = await extractTextFromPdf(buffer.slice(0));
223
+ let finalContent = `## Source: ${file.name}\n\n${extractedText}`;
224
+ try {
225
+ const res = await fetch('/api/ai/summarize', {
226
+ method: 'POST',
227
+ body: JSON.stringify({ content: extractedText }),
228
+ headers: { 'Content-Type': 'application/json' }
229
+ });
230
+ const data = await res.json();
231
+ if (data.summary) {
232
+ finalContent = `> **Summary**: ${data.summary}\n\n---\n\n${finalContent}`;
233
+ }
234
+ }
235
+ catch (e) {
236
+ console.error("Auto-summary failed", e);
237
+ }
238
+ const sourcePath = `${path}.source.md`;
239
+ await storage.saveFile(sourcePath, finalContent);
240
+ await storage.saveFile(path, buffer);
241
+ return;
242
+ }
243
+ catch (e) {
244
+ console.error("PDF Upload Trace", e);
245
+ alert("Failed to parse PDF");
246
+ return;
247
+ }
248
+ }
249
+ else {
250
+ alert(`Skipped ${file.name}: Only text/markdown/pdf supported`);
251
+ return;
252
+ }
253
+ let finalContent = content;
254
+ try {
255
+ const res = await fetch('/api/ai/summarize', {
256
+ method: 'POST',
257
+ body: JSON.stringify({ content }),
258
+ headers: { 'Content-Type': 'application/json' }
259
+ });
260
+ const data = await res.json();
261
+ if (data.summary) {
262
+ finalContent = `> **Summary**: ${data.summary}\n\n---\n\n${content}`;
263
+ }
264
+ }
265
+ catch (e) {
266
+ console.error("Auto-summary failed", e);
267
+ }
268
+ await storage.saveFile(path, finalContent);
269
+ };
270
+ const handleUpload = async (e) => {
271
+ var _a;
272
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
273
+ if (!file)
274
+ return;
275
+ await processFile(file, currentPath);
276
+ }; // loadFiles(); // Removed: Reactive
277
+ // Bulk Delete
278
+ const handleBulkDelete = async () => {
279
+ if (selectedIds.size === 0)
280
+ return;
281
+ if (!confirm(`Delete ${selectedIds.size} items?`))
282
+ return;
283
+ // Use db transaction for bulk delete?
284
+ // For now, loop.
285
+ for (const id of Array.from(selectedIds)) {
286
+ const item = items.find(i => i.id === id);
287
+ if (item)
288
+ await performDelete(item);
289
+ }
290
+ setSelectedIds(new Set()); // Clear selection
291
+ };
292
+ const handleDeleteClick = (file) => {
293
+ setDialogAction({ type: 'delete', file });
294
+ };
295
+ const confirmDelete = async () => {
296
+ if (!dialogAction || dialogAction.type !== 'delete')
297
+ return;
298
+ const file = dialogAction.file;
299
+ setDialogAction(null); // Close immediately
300
+ try {
301
+ await performDelete(file);
302
+ }
303
+ catch (e) {
304
+ console.error("Delete failed:", e);
305
+ alert("Delete Error: " + e.message);
306
+ }
307
+ };
308
+ const performDelete = async (file) => {
309
+ if (file.type === 'folder') {
310
+ const all = await storage.listFiles(file.path);
311
+ for (const f of all) {
312
+ await storage.deleteFile(f.path);
313
+ }
314
+ }
315
+ else if (file.type === 'source') {
316
+ // Delete Source AND Original PDF
317
+ await storage.deleteFile(file.path);
318
+ const originalPath = file.path.replace('.source.md', '');
319
+ try {
320
+ await storage.deleteFile(originalPath);
321
+ }
322
+ catch (e) {
323
+ console.warn("Could not delete original PDF (might not exist):", e);
324
+ }
325
+ }
326
+ else {
327
+ await storage.deleteFile(file.path);
328
+ // Also check if there's a source for this file (if we deleted the raw PDF manually?)
329
+ // Usually we hide raw PDF, but if we delete from search results or something.
330
+ // Safe to check.
331
+ if (file.path.endsWith('.pdf')) {
332
+ const sourcePath = `${file.path}.source.md`;
333
+ await storage.deleteFile(sourcePath);
334
+ }
335
+ }
336
+ };
337
+ const handleRenameClick = (file) => {
338
+ setDialogAction({
339
+ type: 'rename',
340
+ file,
341
+ value: file.type === 'source' ? file.name.replace(' (Source)', '') : file.name
342
+ });
343
+ };
344
+ const confirmRename = async () => {
345
+ if (!dialogAction || dialogAction.type !== 'rename')
346
+ return;
347
+ const { file, value: newName } = dialogAction;
348
+ setDialogAction(null);
349
+ if (!newName || newName === file.name)
350
+ return;
351
+ // Handle Source Rename
352
+ if (file.type === 'source') {
353
+ const oldSourcePath = file.path;
354
+ const oldPdfPath = oldSourcePath.replace('.source.md', '');
355
+ const oldPdfName = oldPdfPath.split('/').pop() || '';
356
+ let newPdfName = newName;
357
+ if (!newPdfName.endsWith('.pdf'))
358
+ newPdfName += '.pdf';
359
+ if (newPdfName === oldPdfName)
360
+ return;
361
+ const parentDir = oldPdfPath.substring(0, oldPdfPath.lastIndexOf('/'));
362
+ const newPdfPath = `${parentDir}/${newPdfName}`;
363
+ const newSourcePath = `${newPdfPath}.source.md`;
364
+ try {
365
+ // Try rename PDF first
366
+ try {
367
+ await storage.renameFile(oldPdfPath, newPdfPath);
368
+ }
369
+ catch (err) {
370
+ console.warn("Could not rename original PDF (might not exist):", err);
371
+ }
372
+ // Always rename Source
373
+ await storage.renameFile(oldSourcePath, newSourcePath);
374
+ }
375
+ catch (e) {
376
+ alert("Rename failed: " + e.message);
377
+ }
378
+ return;
379
+ }
380
+ const oldPath = file.path;
381
+ const parts = oldPath.split('/');
382
+ parts.pop();
383
+ const newPath = `${parts.join('/')}/${newName}`;
384
+ try {
385
+ await storage.renameFile(oldPath, newPath);
386
+ }
387
+ catch (e) {
388
+ alert("Rename failed: " + e.message);
389
+ }
390
+ };
391
+ const handleToggleSourceStatus = async (file) => {
392
+ try {
393
+ // Fetch FRESH metadata from storage to ensure we don't lose 'edited' status
394
+ const freshFile = await storage.getFile(file.path);
395
+ if (!freshFile) {
396
+ showAlert("Error", "File not found locally.");
397
+ return;
398
+ }
399
+ const currentMeta = freshFile.metadata || {};
400
+ if (file.type === 'source') {
401
+ // Convert to Note: Rename .source.md -> .md
402
+ const newPath = file.path.replace('.source.md', '.md');
403
+ await storage.renameFile(file.path, newPath);
404
+ // Preserve existing metadata (including 'edited'), just flip isSource
405
+ await storage.updateMetadata(newPath, { type: 'file', metadata: Object.assign(Object.assign({}, currentMeta), { isSource: false }) });
406
+ showAlert("Success", `Converted "${file.name}" to Note.`);
407
+ }
408
+ else {
409
+ // Convert to Source: Rename .md -> .source.md
410
+ let newPath = file.path;
411
+ if (!newPath.endsWith('.source.md')) {
412
+ if (newPath.endsWith('.md')) {
413
+ newPath = newPath.replace(/\.md$/, '.source.md');
414
+ }
415
+ else {
416
+ newPath = newPath + '.source.md'; // Fallback
417
+ }
418
+ }
419
+ await storage.renameFile(file.path, newPath);
420
+ await storage.updateMetadata(newPath, { type: 'source', metadata: Object.assign(Object.assign({}, currentMeta), { isSource: true }) });
421
+ showAlert("Success", `Converted "${file.name}" to Source.`);
422
+ }
423
+ }
424
+ catch (e) {
425
+ console.error(e);
426
+ showAlert("Error", "Conversion failed: " + e.message);
427
+ }
428
+ };
429
+ const handleResetSource = async (file) => {
430
+ if (file.type !== 'source')
431
+ return;
432
+ const performReset = async () => {
433
+ setDialogAction(null);
434
+ try {
435
+ let pdfContent;
436
+ const originalPath = file.path.replace('.source.md', '');
437
+ const rawFile = await storage.readFile(originalPath);
438
+ if (!rawFile)
439
+ throw new Error("Original PDF file not found. Cannot reset.");
440
+ if (rawFile instanceof ArrayBuffer) {
441
+ pdfContent = rawFile;
442
+ }
443
+ else if (typeof rawFile === 'string') {
444
+ throw new Error("Expected binary PDF, got text.");
445
+ }
446
+ if (!pdfContent)
447
+ throw new Error("Could not read binary content.");
448
+ let content = await extractTextFromPdf(pdfContent);
449
+ const displayName = file.name.replace(' (Source)', '');
450
+ content = `## Source: ${displayName}\n\n${content}`;
451
+ const meta = file.metadata || {};
452
+ const newMeta = Object.assign(Object.assign({}, meta), { edited: false, comments: [] });
453
+ await storage.saveFile(file.path, content, undefined, file.tags, newMeta);
454
+ showAlert("Success", "Source reset to original content.");
455
+ }
456
+ catch (e) {
457
+ console.error("Reset failed", e);
458
+ showAlert("Error", "Reset failed: " + e.message);
459
+ }
460
+ };
461
+ setDialogAction({
462
+ type: 'confirm',
463
+ title: 'Reset Source',
464
+ message: `Reset "${file.name}" to original? This will discard all edits and re-extract text from the PDF.`,
465
+ confirmLabel: 'Reset',
466
+ onConfirm: performReset
467
+ });
468
+ };
469
+ const handleSelectionClick = (item, index, event) => {
470
+ event.stopPropagation();
471
+ const newSelected = new Set(selectedIds);
472
+ if (event.shiftKey && lastInteractionIndex.current !== -1) {
473
+ // Range Select
474
+ const start = Math.min(lastInteractionIndex.current, index);
475
+ const end = Math.max(lastInteractionIndex.current, index);
476
+ for (let i = start; i <= end; i++) {
477
+ newSelected.add(items[i].id);
478
+ }
479
+ }
480
+ else if (event.ctrlKey || event.metaKey) {
481
+ // Toggle
482
+ if (newSelected.has(item.id))
483
+ newSelected.delete(item.id);
484
+ else
485
+ newSelected.add(item.id);
486
+ lastInteractionIndex.current = index;
487
+ }
488
+ else {
489
+ // Single Select (clears others) - Standard Logic
490
+ // If user just clicks a checkbox, they usually expect "Add to selection" or "Toggle"
491
+ // But if they click the *row*, they expect "Select Only This".
492
+ // Since this is triggered by the checkbox click (mostly), let's keep it as Toggle logic if strictly checking box?
493
+ // Actually, Windows Explorer: Checkbox click = Toggle. Row Click = Select Only This.
494
+ // Let's assume this handles the Checkbox Click for now.
495
+ if (newSelected.has(item.id))
496
+ newSelected.delete(item.id);
497
+ else
498
+ newSelected.add(item.id);
499
+ lastInteractionIndex.current = index;
500
+ }
501
+ setSelectedIds(newSelected);
502
+ };
503
+ const handleRowClick = (item, index, event) => {
504
+ // Desktop Standard: Row Click = Select Only This (unless Cmd/Ctrl/Shift)
505
+ const newSelected = new Set(selectedIds);
506
+ if (event.shiftKey && lastInteractionIndex.current !== -1) {
507
+ const start = Math.min(lastInteractionIndex.current, index);
508
+ const end = Math.max(lastInteractionIndex.current, index);
509
+ // Clear prior if Shift click? Usually Shift+Click keeps others?
510
+ // Standard: Shift+Click extends selection from anchor.
511
+ // Simplified: Add range.
512
+ for (let i = start; i <= end; i++) {
513
+ newSelected.add(items[i].id);
514
+ }
515
+ }
516
+ else if (event.ctrlKey || event.metaKey) {
517
+ if (newSelected.has(item.id))
518
+ newSelected.delete(item.id);
519
+ else
520
+ newSelected.add(item.id);
521
+ lastInteractionIndex.current = index;
522
+ }
523
+ else {
524
+ // Single Click -> Select ONLY this
525
+ newSelected.clear();
526
+ newSelected.add(item.id);
527
+ lastInteractionIndex.current = index;
528
+ }
529
+ setSelectedIds(newSelected);
530
+ };
531
+ const handleSelectAll = () => {
532
+ if (selectedIds.size === items.length) {
533
+ setSelectedIds(new Set());
534
+ }
535
+ else {
536
+ setSelectedIds(new Set(items.map(i => i.id)));
537
+ }
538
+ };
539
+ const toggleSelect = (id) => {
540
+ const next = new Set(selectedIds);
541
+ if (next.has(id))
542
+ next.delete(id);
543
+ else
544
+ next.add(id);
545
+ setSelectedIds(next);
546
+ };
547
+ // DRAG AND DROP
548
+ const handleDragStart = (e, item) => {
549
+ // ... (lines 258-268)
550
+ // If the item is in current selection, drag ALL selected
551
+ // Else just drag this one
552
+ let dragIds = [item.id];
553
+ if (selectedIds.has(item.id)) {
554
+ dragIds = Array.from(selectedIds);
555
+ }
556
+ e.dataTransfer.setData('application/json', JSON.stringify(dragIds));
557
+ e.dataTransfer.effectAllowed = 'move';
558
+ };
559
+ const handleDragOver = (e) => {
560
+ e.preventDefault(); // Allow drop
561
+ e.dataTransfer.dropEffect = 'move';
562
+ };
563
+ const handleDrop = async (e, targetFolder) => {
564
+ e.preventDefault();
565
+ e.stopPropagation();
566
+ if (targetFolder.type !== 'folder')
567
+ return;
568
+ const raw = e.dataTransfer.getData('application/json');
569
+ // 1. External File Drop
570
+ if (!raw) {
571
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
572
+ const files = Array.from(e.dataTransfer.files);
573
+ for (const file of files) {
574
+ await processFile(file, targetFolder.path);
575
+ }
576
+ }
577
+ return;
578
+ }
579
+ // 2. Internal Move
580
+ const ids = JSON.parse(raw);
581
+ // Filter out if trying to drop into self
582
+ if (ids.includes(targetFolder.id))
583
+ return;
584
+ // Move Logic
585
+ for (const id of ids) {
586
+ const item = items.find(i => i.id === id);
587
+ if (!item)
588
+ continue;
589
+ // Folder Move or File Move?
590
+ // Simplified: We assume flat structure support for now in `renameFile`
591
+ // Old Path: misc/A.txt
592
+ // New Path: misc/Folder/A.txt
593
+ const newPath = `${targetFolder.path}/${item.name}`;
594
+ try {
595
+ // If it's a folder, we need recursive move?
596
+ // IndexedDB 'folder' is virtual.
597
+ // We actually need to find ALL files starting with item.path and replace prefix.
598
+ // Does storage.renameFile support directory rename?
599
+ // Our implementation in local.ts handles Single File rename.
600
+ // We need to upgrade performMove to handle folders.
601
+ await performMove(item, newPath);
602
+ }
603
+ catch (err) {
604
+ console.error("Move failed", err);
605
+ }
606
+ }
607
+ // loadFiles(); // Removed: Reactive
608
+ };
609
+ const performMove = async (item, newPath) => {
610
+ if (item.type === 'file') {
611
+ await storage.renameFile(item.path, newPath);
612
+ }
613
+ else {
614
+ // Folder Move: Rename prefix for all children
615
+ // Get all files with prefix `item.path/`
616
+ // e.g. misc/OldFolder/... -> misc/NewFolder/...
617
+ const allChilds = await storage.listFiles(item.path); // uses startsWith
618
+ // Target Folder Path is derived from newPath (which includes the folder name)
619
+ // item.path = misc/OldFolder
620
+ // newPath = misc/Target/OldFolder
621
+ for (const child of allChilds) {
622
+ const relative = child.path.substring(item.path.length); // /file.txt
623
+ const childNewPath = `${newPath}${relative}`;
624
+ await storage.renameFile(child.path, childNewPath);
625
+ }
626
+ // If local.ts implementation of renameFile handles copy+delete, this works.
627
+ }
628
+ };
629
+ return (_jsx("div", { className: styles.modalOverlay, onClick: onClose, children: _jsxs("div", { className: styles.modalContent, onClick: e => e.stopPropagation(), style: { width: '80%', maxWidth: 800, height: '80vh', display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }, children: [_jsx("h2", { style: { fontSize: '1.5rem', fontWeight: 600 }, children: "Topic Explorer" }), _jsx("button", { onClick: onClose, style: { background: 'none', border: 'none', fontSize: '1.5rem', cursor: 'pointer', color: 'var(--foreground)' }, children: "\u00D7" })] }), _jsxs("div", { style: { display: 'flex', gap: '1rem', marginBottom: '1rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border)', alignItems: 'center' }, children: [_jsxs("button", { onClick: handleBack, disabled: currentPath === 'misc', style: {
630
+ cursor: currentPath === 'misc' ? 'default' : 'pointer',
631
+ opacity: currentPath === 'misc' ? 0 : 1,
632
+ background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4,
633
+ color: 'var(--foreground)'
634
+ }, children: [_jsx(Icon, { name: "ArrowLeft", size: 18 }), " Back"] }), _jsxs("button", { onClick: () => {
635
+ setIsBulkMode(!isBulkMode);
636
+ if (isBulkMode)
637
+ setSelectedIds(new Set());
638
+ }, style: {
639
+ cursor: 'pointer',
640
+ background: isBulkMode ? 'var(--info)' : 'transparent',
641
+ color: isBulkMode ? 'var(--surface)' : 'var(--foreground)',
642
+ border: '1px solid var(--border)',
643
+ borderRadius: 4, padding: '4px 8px', display: 'flex', alignItems: 'center', gap: 4
644
+ }, children: [isBulkMode ? _jsx(Icon, { name: "CheckSquare", size: 16 }) : _jsx(Icon, { name: "Square", size: 16 }), " Select"] }), isBulkMode && (_jsx("button", { onClick: handleSelectAll, style: { cursor: 'pointer' }, children: selectedIds.size === items.length && items.length > 0 ? 'Deselect All' : 'Select All' })), isBulkMode && selectedIds.size > 0 && (_jsxs("button", { onClick: handleBulkDelete, style: { color: 'var(--error)', cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Icon, { name: "Trash2", size: 16 }), " Delete (", selectedIds.size, ")"] })), _jsx("div", { style: { flex: 1 } }), _jsx("button", { onClick: async () => {
645
+ if (confirm("Reset connection to Google Drive? This will re-upload all files to 'Meechi Journal'.")) {
646
+ await storage.resetSyncState();
647
+ alert("Sync Reset. Please Sign Out and Sign In again to refresh permissions.");
648
+ }
649
+ }, style: { cursor: 'pointer', marginRight: '1rem', color: 'var(--error)', border: '1px solid var(--error)', background: 'transparent', borderRadius: 4, padding: '4px 8px', fontSize: '0.8rem' }, children: "Reset Cloud" }), props.syncLogs && (_jsxs("button", { onClick: async () => {
650
+ if (storage.forceSync) {
651
+ await storage.forceSync();
652
+ await storage.forceSync();
653
+ await storage.forceSync(); // Triple sync hack
654
+ showAlert("Sync", "Sync triggered!");
655
+ }
656
+ else {
657
+ showAlert("Sync", "Sync not available");
658
+ }
659
+ }, style: { cursor: 'pointer', marginRight: '1rem', color: 'var(--info)', border: '1px solid var(--info)', background: 'transparent', borderRadius: 4, padding: '4px 8px', display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Icon, { name: "RefreshCw", size: 14 }), " Sync Now"] })), _jsxs("button", { onClick: handleCreateFolder, style: { cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4, color: 'var(--foreground)' }, children: [_jsx(Icon, { name: "FolderPlus", size: 18 }), " New Topic"] }), _jsxs("button", { onClick: () => setIsLinkDialogOpen(true), style: { cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4, color: 'var(--foreground)' }, children: [_jsx(Icon, { name: "Link", size: 18 }), " Add Link"] }), _jsxs("button", { onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, style: { cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4, color: 'var(--foreground)' }, children: [_jsx(Icon, { name: "Upload", size: 18 }), " Upload Logic"] }), _jsx("input", { type: "file", ref: fileInputRef, onChange: handleUpload, style: { display: 'none' } })] }), _jsx("div", { style: { padding: '0.5rem', background: 'var(--background)', border: '1px solid var(--border)', borderRadius: 4, marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--secondary)', display: 'flex', gap: '0.5rem' }, children: currentPath.split('/').map((part, index, arr) => {
660
+ // ... breadcrumb logic ...
661
+ const pathSoFar = arr.slice(0, index + 1).join('/');
662
+ const isLast = index === arr.length - 1;
663
+ const targetFolder = { id: pathSoFar, name: part, path: pathSoFar, updatedAt: Date.now(), type: 'folder' };
664
+ return (_jsxs(React.Fragment, { children: [_jsx("span", { onDragOver: (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }, onDrop: (e) => handleDrop(e, targetFolder), onClick: () => !isLast && setCurrentPath(pathSoFar), style: {
665
+ cursor: isLast ? 'default' : 'pointer',
666
+ fontWeight: isLast ? 600 : 400,
667
+ textDecoration: isLast ? 'none' : 'underline'
668
+ }, children: part === 'misc' ? 'Home' : part }), !isLast && _jsx(Icon, { name: "ChevronRight", size: 14, style: { color: 'var(--muted)' } })] }, pathSoFar));
669
+ }) }), (dialogAction || isLinkDialogOpen) && (_jsx("div", { style: {
670
+ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
671
+ background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center',
672
+ zIndex: 1000
673
+ }, onClick: (e) => { e.stopPropagation(); setDialogAction(null); setIsLinkDialogOpen(false); }, children: _jsxs("div", { style: { background: 'var(--surface)', color: 'var(--foreground)', padding: '1.5rem', borderRadius: 8, width: 300, border: '1px solid var(--border)' }, onClick: e => e.stopPropagation(), children: [isLinkDialogOpen && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Add Link Source" }), _jsx("input", { autoFocus: true, placeholder: "https://example.com/article", value: linkUrl, onChange: e => setLinkUrl(e.target.value), onKeyDown: e => e.key === 'Enter' && handleAddLink(), style: { width: '100%', padding: '0.5rem', marginBottom: '1.5rem' } }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setIsLinkDialogOpen(false), style: { color: 'var(--foreground)', background: 'none', border: '1px solid var(--border)', padding: '0.5rem 1rem', borderRadius: 4 }, children: "Cancel" }), _jsx("button", { onClick: handleAddLink, disabled: isFetchingLink, style: { background: 'var(--accent)', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: isFetchingLink ? 'Fetching...' : 'Add' })] })] })), dialogAction && dialogAction.type === 'delete' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Confirm Delete" }), _jsxs("p", { style: { marginBottom: '1.5rem' }, children: ["Delete ", _jsx("b", { children: dialogAction.file.name }), "?"] }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setDialogAction(null), children: "Cancel" }), _jsx("button", { onClick: confirmDelete, style: { background: '#ff4444', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: "Delete" })] })] })), dialogAction && dialogAction.type === 'alert' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: dialogAction.title }), _jsx("p", { style: { marginBottom: '1.5rem', whiteSpace: 'pre-wrap' }, children: dialogAction.message }), _jsx("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: _jsx("button", { onClick: () => {
674
+ if (dialogAction.onConfirm)
675
+ dialogAction.onConfirm();
676
+ setDialogAction(null);
677
+ }, style: { background: '#007bff', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: "OK" }) })] })), dialogAction && dialogAction.type === 'confirm' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: dialogAction.title }), _jsx("p", { style: { marginBottom: '1.5rem', whiteSpace: 'pre-wrap' }, children: dialogAction.message }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setDialogAction(null), children: "Cancel" }), _jsx("button", { onClick: () => {
678
+ dialogAction.onConfirm();
679
+ // Dialog close handling is up to the caller usually?
680
+ // But for consistent UI, we might want to auto-close if the caller doesn't?
681
+ // The caller of confirm usually sets state.
682
+ // But looking at performSummarise logic I added "setDialogAction(null)" inside.
683
+ // However, let's keep it safe.
684
+ }, style: { background: '#007bff', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: dialogAction.confirmLabel || 'Confirm' })] })] })), dialogAction && dialogAction.type === 'rename' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Rename File" }), _jsx("input", { autoFocus: true, value: dialogAction.value, onChange: e => setDialogAction(Object.assign(Object.assign({}, dialogAction), { value: e.target.value })), onKeyDown: e => e.key === 'Enter' && confirmRename(), style: { width: '100%', padding: '0.5rem', marginBottom: '1.5rem' } }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setDialogAction(null), children: "Cancel" }), _jsx("button", { onClick: confirmRename, style: { background: '#007bff', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: "Rename" })] })] }))] }) })), _jsxs("div", { style: { flex: 1, overflowY: 'auto' }, onDragOver: (e) => {
685
+ e.preventDefault();
686
+ e.dataTransfer.dropEffect = 'move';
687
+ }, onDrop: async (e) => {
688
+ e.preventDefault();
689
+ // Target is currentPath
690
+ await handleDrop(e, { id: currentPath, name: currentPath.split('/').pop() || 'root', path: currentPath, type: 'folder', updatedAt: 0 });
691
+ }, children: [items.length === 0 && _jsx("div", { style: { textAlign: 'center', color: '#999', padding: '2rem' }, children: "Empty Topic (Drop files here)" }), items.map((item, index) => {
692
+ var _a, _b;
693
+ return (_jsxs("div", { className: `${styles.fileRow} ${selectedIds.has(item.id) ? styles.selected : ''}`, draggable: true, onDragStart: (e) => handleDragStart(e, item), onDragOver: (e) => item.type === 'folder' ? handleDragOver(e) : undefined, onDrop: (e) => item.type === 'folder' ? handleDrop(e, item) : undefined, onClick: (e) => handleRowClick(item, index, e), style: {
694
+ background: selectedIds.has(item.id) ? 'var(--accent)' : undefined,
695
+ color: selectedIds.has(item.id) ? '#fff' : undefined, // Force white text on accent
696
+ borderRadius: 6
697
+ }, onDoubleClick: () => {
698
+ if (item.type === 'folder') {
699
+ handleNavigate(item.name);
700
+ }
701
+ else {
702
+ // Open file
703
+ if (onOpenFile) {
704
+ onOpenFile(item.path);
705
+ }
706
+ else {
707
+ onClose();
708
+ window.open(`/q?file=${encodeURIComponent(item.path)}`, '_self');
709
+ }
710
+ }
711
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '1rem', flex: 1 }, children: [isBulkMode && (_jsx("input", { type: "checkbox", checked: selectedIds.has(item.id), readOnly: true, onClick: (e) => handleSelectionClick(item, index, e) })), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '1rem', flex: 1 }, children: [_jsx("span", { style: { color: selectedIds.has(item.id) ? 'currentColor' : '#666', display: 'flex', alignItems: 'center' }, children: item.type === 'folder' ?
712
+ _jsx(Icon, { name: "Folder", size: 20, fill: "currentColor", fillOpacity: selectedIds.has(item.id) ? 0.3 : 0.2 }) :
713
+ (item.type === 'source' ?
714
+ (((_a = item.metadata) === null || _a === void 0 ? void 0 : _a.edited) ? _jsx(Icon, { name: "BookOpen", size: 20 }) : _jsx(Icon, { name: "Book", size: 20 })) :
715
+ _jsx(Icon, { name: "FileText", size: 20 })) }), _jsx("span", { style: { fontWeight: item.type === 'folder' ? 600 : 400 }, children: item.name })] })] }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { className: `${styles.kebabButton} ${activeMenuId === item.id ? styles.active : ''}`, onClick: (e) => { e.stopPropagation(); setActiveMenuId(activeMenuId === item.id ? null : item.id); }, children: _jsx(Icon, { name: "MoreVertical", size: 16 }) }), activeMenuId === item.id && (_jsxs("div", { className: styles.dropdownMenu, onClick: e => e.stopPropagation(), style: { left: 'auto', right: 0 }, children: [_jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleRenameClick(item); }, children: "Rename" }), extensions.getFileActions().map(action => ((!action.shouldShow || action.shouldShow(item)) && (_jsxs("button", { className: styles.dropdownItem, onClick: async () => {
716
+ setActiveMenuId(null);
717
+ try {
718
+ await action.handler(item, { storage });
719
+ }
720
+ catch (err) {
721
+ showAlert("Error", err.message || String(err));
722
+ }
723
+ }, children: [action.icon && _jsx(Icon, { name: action.icon.name, size: 16, style: { marginRight: 8 } }), action.label] }, action.id)))), item.type === 'source' ? (_jsxs(_Fragment, { children: [((_b = item.metadata) === null || _b === void 0 ? void 0 : _b.edited) && (_jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleResetSource(item); }, children: "Reset Source" })), _jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleToggleSourceStatus(item); }, children: "Convert to Note" })] })) : (_jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleToggleSourceStatus(item); }, children: "Make Source" })), _jsx("button", { className: `${styles.dropdownItem} ${styles.delete}`, onClick: () => { setActiveMenuId(null); handleDeleteClick(item); }, children: "Delete" })] }))] })] }, item.id));
724
+ })] }), _jsxs("div", { style: { marginTop: 'auto', borderTop: '1px solid #eee', paddingTop: '1rem' }, children: [_jsxs("div", { style: { marginBottom: '1rem' }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '5px' }, children: [_jsx("h4", { style: { margin: 0, fontSize: '0.8rem', color: '#666' }, children: "Live Sync Log" }), _jsx("button", { onClick: () => navigator.clipboard.writeText((props.syncLogs || []).join('\n')), style: { border: 'none', background: 'none', color: '#007bff', cursor: 'pointer', fontSize: '0.7rem' }, children: "Copy" })] }), _jsxs("div", { style: {
725
+ background: '#f8f8f8',
726
+ padding: '8px',
727
+ borderRadius: '4px',
728
+ height: '100px',
729
+ overflowY: 'auto',
730
+ fontSize: '0.7rem',
731
+ fontFamily: 'monospace',
732
+ border: '1px solid #eee',
733
+ display: 'flex', flexDirection: 'column', gap: '2px'
734
+ }, children: [(props.syncLogs || []).length === 0 && _jsx("span", { style: { color: '#999', fontStyle: 'italic' }, children: "Waiting for logs..." }), (props.syncLogs || []).slice().reverse().map((log, i) => (_jsx("div", { style: { borderBottom: '1px solid #f0f0f0' }, children: log }, i)))] })] }), _jsxs("details", { children: [_jsx("summary", { style: { cursor: 'pointer', color: '#666', fontSize: '0.85rem' }, children: "Storage Maintenance" }), _jsx("div", { style: { padding: '1rem', background: '#f9f9f9', borderRadius: 4, marginTop: '0.5rem', border: '1px solid #eee' }, children: _jsxs("div", { style: { display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }, children: [_jsx("button", { onClick: async () => {
735
+ const all = await db.files.count();
736
+ const deleted = await db.files.where('deleted').equals(1).count();
737
+ const dirty = await db.files.where('dirty').equals(1).count();
738
+ showAlert("Stats", `Total: ${all}\nTrash: ${deleted}\nUnsynced: ${dirty}`);
739
+ }, style: { padding: '4px 8px' }, children: "Check Stats" }), _jsx("button", { onClick: async () => {
740
+ if (confirm("Restore ALL items from trash?")) {
741
+ await db.transaction('rw', db.files, async () => {
742
+ await db.files.where('deleted').equals(1).modify({ deleted: 0, dirty: 1 });
743
+ });
744
+ alert("Items restored.");
745
+ }
746
+ }, style: { padding: '4px 8px' }, children: "Empty Trash" }), _jsx("button", { onClick: async () => {
747
+ if (confirm("PERMANENTLY delete all items in trash?")) {
748
+ await db.files.where('deleted').equals(1).delete();
749
+ alert("Trash purged.");
750
+ }
751
+ }, style: { padding: '4px 8px', color: 'red' }, children: "Purge Trash" }), _jsx("button", { onClick: async () => {
752
+ if (prompt("Type 'DELETE' to confirm Factory Reset. This wipes ALL local data.") === 'DELETE') {
753
+ await storage.factoryReset();
754
+ window.location.reload();
755
+ }
756
+ }, style: { padding: '4px 8px', color: 'white', background: 'red', fontWeight: 'bold' }, children: "FACTORY RESET" })] }) })] })] })] }) }));
757
+ }