Skip to content

Commit 9baab37

Browse files
committed
Fix image import workflow
1 parent d5c6bc7 commit 9baab37

8 files changed

Lines changed: 131 additions & 25 deletions

File tree

App.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const MainApp: React.FC = () => {
127127
const commandPaletteTargetRef = useRef<HTMLDivElement>(null);
128128
const commandPaletteInputRef = useRef<HTMLInputElement>(null);
129129
const dragCounter = useRef(0);
130+
const ensureNodeVisibleRef = useRef<(node: Pick<DocumentOrFolder, 'id' | 'type' | 'parentId'>) => void>();
130131

131132
const llmStatus = useLLMStatus(settings.llmProviderUrl);
132133
const { logs, addLog } = useLogger();
@@ -323,8 +324,23 @@ const MainApp: React.FC = () => {
323324
};
324325
});
325326

326-
await addDocumentsFromFiles(fileEntries, parentId);
327-
}, [addDocumentsFromFiles]);
327+
const importedNodes = await addDocumentsFromFiles(fileEntries, parentId);
328+
329+
if (importedNodes.length > 0) {
330+
const imageNodes = importedNodes.filter(node => node.docType === 'image');
331+
const targetNode = imageNodes[imageNodes.length - 1] ?? importedNodes[importedNodes.length - 1];
332+
if (targetNode) {
333+
const nodeForReveal = { id: targetNode.nodeId, type: 'document' as const, parentId: targetNode.parentId };
334+
setActiveNodeId(targetNode.nodeId);
335+
setSelectedIds(new Set([targetNode.nodeId]));
336+
setLastClickedId(targetNode.nodeId);
337+
setActiveTemplateId(null);
338+
setDocumentView('editor');
339+
setView('editor');
340+
ensureNodeVisibleRef.current?.(nodeForReveal);
341+
}
342+
}
343+
}, [addDocumentsFromFiles, setActiveNodeId, setSelectedIds, setLastClickedId, setActiveTemplateId, setDocumentView, setView]);
328344

329345
useEffect(() => {
330346
const handleDragEnter = (e: DragEvent) => {
@@ -447,6 +463,10 @@ const MainApp: React.FC = () => {
447463
setPendingRevealId(node.id);
448464
}, [items, setPendingRevealId]);
449465

466+
useEffect(() => {
467+
ensureNodeVisibleRef.current = ensureNodeVisible;
468+
}, [ensureNodeVisible]);
469+
450470
const handleNewDocument = useCallback(async (parentId?: string | null) => {
451471
addLog('INFO', 'User action: Create New Document.');
452472
const effectiveParentId = parentId !== undefined ? parentId : getParentIdForNewItem();

components/PromptEditor.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,16 @@ const PREVIEWABLE_LANGUAGES = new Set<string>([
5353
const resolveDefaultViewMode = (mode: ViewMode | null | undefined, languageHint: string | null | undefined): ViewMode => {
5454
if (mode) return mode;
5555
const normalizedHint = languageHint?.toLowerCase();
56-
return normalizedHint === 'pdf' || normalizedHint === 'application/pdf' ? 'preview' : 'edit';
56+
if (!normalizedHint) {
57+
return 'edit';
58+
}
59+
if (normalizedHint === 'pdf' || normalizedHint === 'application/pdf') {
60+
return 'preview';
61+
}
62+
if (normalizedHint === 'image' || normalizedHint.startsWith('image/')) {
63+
return 'preview';
64+
}
65+
return 'edit';
5766
};
5867

5968
const DocumentEditor: React.FC<DocumentEditorProps> = ({ documentNode, onSave, onCommitVersion, onDelete, settings, onShowHistory, onLanguageChange, onViewModeChange, formatTrigger }) => {

electron/database.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { INITIAL_SCHEMA } from './schema';
66
import { v4 as uuidv4 } from 'uuid';
77
import * as crypto from 'crypto';
88
// Fix: Import types to use for casting
9-
import type { Node, Document, DocVersion, DatabaseStats } from '../types';
9+
import type { Node, Document, DocVersion, DatabaseStats, DocType, ViewMode, ImportedNodeSummary } from '../types';
1010

1111
let db: Database.Database;
1212

@@ -43,6 +43,23 @@ const mapExtensionToLanguageId_local = (extension: string | null): string => {
4343
case 'application/pdf':
4444
case 'pdf':
4545
return 'pdf';
46+
case 'png':
47+
case 'jpg':
48+
case 'jpeg':
49+
case 'gif':
50+
case 'bmp':
51+
case 'webp':
52+
case 'svg':
53+
case 'svgz':
54+
case 'image/png':
55+
case 'image/jpg':
56+
case 'image/jpeg':
57+
case 'image/gif':
58+
case 'image/bmp':
59+
case 'image/webp':
60+
case 'image/svg':
61+
case 'image/svg+xml':
62+
return 'image';
4663
default: return 'plaintext';
4764
}
4865
};
@@ -409,7 +426,8 @@ export const databaseService = {
409426
}
410427
},
411428

412-
importFiles(filesData: {path: string, name: string, content: string}[], targetParentId: string | null): { success: boolean, error?: string } {
429+
importFiles(filesData: {path: string; name: string; content: string}[], targetParentId: string | null): { success: boolean; error?: string; createdNodes: ImportedNodeSummary[] } {
430+
const createdNodes: ImportedNodeSummary[] = [];
413431
const transaction = db.transaction(() => {
414432
console.log(`Starting import transaction for ${filesData.length} files.`);
415433
const knownFolderPaths = new Map<string, string>(); // 'parentId/folderName' -> 'node_id'
@@ -461,15 +479,23 @@ export const databaseService = {
461479
const extension = file.name.split('.').pop() || null;
462480
let languageHint = mapExtensionToLanguageId_local(extension);
463481

464-
db.prepare(`INSERT INTO nodes (node_id, parent_id, node_type, title, sort_order, created_at, updated_at) VALUES (?, ?, 'document', ?, ?, ?, ?)`).run(newNodeId, currentParentId, file.name, sortOrder, now, now);
465-
466482
const trimmedContent = file.content.trim();
467-
const isPdf = languageHint === 'pdf' || languageHint === 'application/pdf' || trimmedContent.startsWith('data:application/pdf');
483+
const sample = trimmedContent.slice(0, 64).toLowerCase();
484+
const isPdf = languageHint === 'pdf' || sample.startsWith('data:application/pdf');
485+
const isSvgContent = sample.startsWith('<svg');
486+
const isImageDataUrl = sample.startsWith('data:image/');
487+
const isImage = languageHint === 'image' || isImageDataUrl || isSvgContent;
488+
468489
if (isPdf) {
469490
languageHint = 'pdf';
491+
} else if (isImage) {
492+
languageHint = 'image';
470493
}
471-
const docType = isPdf ? 'pdf' : 'source_code';
472-
const defaultViewMode = isPdf ? 'preview' : null;
494+
495+
const docType: DocType = isPdf ? 'pdf' : isImage ? 'image' : 'source_code';
496+
const defaultViewMode: ViewMode | null = docType === 'pdf' || docType === 'image' ? 'preview' : null;
497+
498+
db.prepare(`INSERT INTO nodes (node_id, parent_id, node_type, title, sort_order, created_at, updated_at) VALUES (?, ?, 'document', ?, ?, ?, ?)`).run(newNodeId, currentParentId, file.name, sortOrder, now, now);
473499

474500
const docResult = db.prepare(`INSERT INTO documents (node_id, doc_type, language_hint, default_view_mode) VALUES (?, ?, ?, ?)`)
475501
.run(newNodeId, docType, languageHint, defaultViewMode);
@@ -480,15 +506,23 @@ export const databaseService = {
480506
const newVersionId = Number(versionResult.lastInsertRowid);
481507
db.prepare('UPDATE documents SET current_version_id = ? WHERE document_id = ?').run(newVersionId, documentId);
482508
console.log(`Created document "${file.name}" with node id ${newNodeId}`);
509+
510+
createdNodes.push({
511+
nodeId: newNodeId,
512+
parentId: currentParentId ?? null,
513+
docType,
514+
languageHint,
515+
defaultViewMode,
516+
});
483517
}
484518
});
485519

486520
try {
487521
transaction();
488-
return { success: true };
522+
return { success: true, createdNodes };
489523
} catch (error) {
490524
console.error('File import transaction failed:', error);
491-
return { success: false, error: error instanceof Error ? error.message : String(error) };
525+
return { success: false, error: error instanceof Error ? error.message : String(error), createdNodes: [] };
492526
}
493527
},
494528

hooks/useNodes.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react';
2-
import type { Node, ViewMode } from '../types';
2+
import type { Node, ViewMode, ImportedNodeSummary } from '../types';
33
import { repository } from '../services/repository';
44
import { useLogger } from './useLogger';
55

@@ -68,9 +68,15 @@ export const useNodes = () => {
6868
addLog('DEBUG', `Content for node ${nodeId} saved.`);
6969
}, [addLog]);
7070

71-
const importFiles = useCallback(async (filesData: {path: string, name: string, content: string}[], targetParentId: string | null) => {
72-
await repository.importFiles(filesData, targetParentId);
73-
}, []);
71+
const importFiles = useCallback(
72+
async (
73+
filesData: { path: string; name: string; content: string }[],
74+
targetParentId: string | null
75+
): Promise<ImportedNodeSummary[]> => {
76+
return repository.importFiles(filesData, targetParentId);
77+
},
78+
[]
79+
);
7480

7581
return { nodes, isLoading, refreshNodes, addNode, updateNode, deleteNode, deleteNodes, moveNodes, updateDocumentContent, duplicateNodes, importFiles, addLog };
7682
};

hooks/usePrompts.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useMemo } from 'react';
2-
import type { Node, DocumentOrFolder, DocType } from '../types';
2+
import type { Node, DocumentOrFolder, DocType, ImportedNodeSummary } from '../types';
33
import { useNodes } from './useNodes';
44
import { mapExtensionToLanguageId } from '../services/languageService';
55

@@ -59,7 +59,8 @@ export const useDocuments = () => {
5959

6060
const addDocument = useCallback(async ({ parentId, title = 'New Document', content = '', doc_type = 'prompt', language_hint = 'markdown' }: { parentId: string | null, title?: string, content?: string, doc_type?: DocType, language_hint?: string | null }) => {
6161
const resolvedLanguage = mapExtensionToLanguageId(language_hint);
62-
const defaultViewMode = doc_type === 'pdf' || resolvedLanguage === 'pdf' ? 'preview' : undefined;
62+
const shouldPreviewByDefault = doc_type === 'pdf' || doc_type === 'image' || resolvedLanguage === 'pdf' || resolvedLanguage === 'image';
63+
const defaultViewMode = shouldPreviewByDefault ? 'preview' : undefined;
6364
const newNode = await addNode({
6465
parent_id: parentId,
6566
node_type: 'document',
@@ -113,7 +114,10 @@ export const useDocuments = () => {
113114
await moveNodes(draggedIds, targetId, position);
114115
}, [moveNodes]);
115116

116-
const addDocumentsFromFiles = useCallback(async (files: { path: string; name: string; file: File }[], targetNodeId: string | null) => {
117+
const addDocumentsFromFiles = useCallback(async (
118+
files: { path: string; name: string; file: File }[],
119+
targetNodeId: string | null
120+
): Promise<ImportedNodeSummary[]> => {
117121
addLog('INFO', `Importing ${files.length} files...`);
118122

119123
const fileReadPromises = files.map(entry => {
@@ -124,7 +128,14 @@ export const useDocuments = () => {
124128

125129
const fileName = entry.name.toLowerCase();
126130
const mimeType = entry.file.type;
127-
const shouldReadAsDataUrl = (mimeType && mimeType.includes('pdf')) || fileName.endsWith('.pdf');
131+
const extension = fileName.split('.').pop() || '';
132+
const isPdf = (mimeType && mimeType.includes('pdf')) || extension === 'pdf';
133+
const isSvg = extension === 'svg' || extension === 'svgz' || mimeType === 'image/svg+xml';
134+
const isImage =
135+
(!isSvg && !!mimeType && mimeType.startsWith('image/')) ||
136+
['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].some(ext => extension === ext);
137+
138+
const shouldReadAsDataUrl = isPdf || isImage;
128139

129140
if (shouldReadAsDataUrl) {
130141
reader.readAsDataURL(entry.file);
@@ -136,12 +147,14 @@ export const useDocuments = () => {
136147

137148
try {
138149
const filesData = await Promise.all(fileReadPromises);
139-
await importFiles(filesData, targetNodeId);
150+
const createdNodes = await importFiles(filesData, targetNodeId);
140151
addLog('INFO', 'File import process completed successfully in the backend.');
141152
await refreshNodes();
153+
return createdNodes;
142154
} catch (error) {
143155
const message = error instanceof Error ? error.message : String(error);
144156
addLog('ERROR', `File import failed: ${message}`);
157+
return [];
145158
}
146159
}, [addLog, importFiles, refreshNodes]);
147160

services/languageService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const mapExtensionToLanguageId = (extension: string | null): string => {
8888
case 'bmp':
8989
case 'webp':
9090
case 'svg':
91+
case 'svgz':
9192
return 'image';
9293
case 'image/png':
9394
case 'image/jpg':

services/repository.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
import type { Node, Document, DocVersion, DocumentOrFolder, DocumentVersion, DocumentTemplate, Settings, ContentStore, ViewMode } from '../types';
1+
import type {
2+
Node,
3+
Document,
4+
DocVersion,
5+
DocumentOrFolder,
6+
DocumentVersion,
7+
DocumentTemplate,
8+
Settings,
9+
ContentStore,
10+
ViewMode,
11+
ImportedNodeSummary,
12+
} from '../types';
213
import { cryptoService } from './cryptoService';
314
import { DEFAULT_SETTINGS, EXAMPLE_TEMPLATES, LOCAL_STORAGE_KEYS } from '../constants';
415
import { v4 as uuidv4 } from 'uuid';
@@ -999,14 +1010,15 @@ export const repository = {
9991010
}
10001011
},
10011012

1002-
async importFiles(filesData: {path: string, name: string, content: string}[], targetParentId: string | null) {
1013+
async importFiles(filesData: {path: string, name: string, content: string}[], targetParentId: string | null): Promise<ImportedNodeSummary[]> {
10031014
if (!window.electronAPI?.dbImportFiles) {
10041015
throw new Error("File import is not supported in this environment.");
10051016
}
10061017
const result = await window.electronAPI.dbImportFiles(filesData, targetParentId);
10071018
if (!result.success) {
10081019
throw new Error(result.error || 'Failed to import files in main process.');
10091020
}
1021+
return result.createdNodes ?? [];
10101022
},
10111023

10121024
async getDbPath(): Promise<string> {

types.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ declare global {
1818
dbGetStats: () => Promise<{ success: boolean; stats?: DatabaseStats; error?: string }>;
1919
dbGetPath: () => Promise<string>;
2020
// FIX: Add missing `dbImportFiles` to the electronAPI type definition.
21-
dbImportFiles: (filesData: {path: string; name: string; content: string}[], targetParentId: string | null) => Promise<{ success: boolean; error?: string }>;
21+
dbImportFiles: (
22+
filesData: { path: string; name: string; content: string }[],
23+
targetParentId: string | null
24+
) => Promise<{ success: boolean; error?: string; createdNodes?: ImportedNodeSummary[] }>;
2225
legacyFileExists: (filename: string) => Promise<boolean>;
2326
readLegacyFile: (filename: string) => Promise<{ success: boolean, data?: string, error?: string }>;
2427
getAppVersion: () => Promise<string>;
@@ -64,9 +67,17 @@ declare global {
6467
// =================================================================
6568

6669
export type NodeType = 'folder' | 'document';
67-
export type DocType = 'prompt' | 'source_code' | 'pdf';
70+
export type DocType = 'prompt' | 'source_code' | 'pdf' | 'image';
6871
export type ViewMode = 'edit' | 'preview' | 'split-vertical' | 'split-horizontal';
6972

73+
export interface ImportedNodeSummary {
74+
nodeId: string;
75+
parentId: string | null;
76+
docType: DocType;
77+
languageHint: string | null;
78+
defaultViewMode: ViewMode | null;
79+
}
80+
7081
export type PythonExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'canceled';
7182

7283
export interface PythonPackageSpec {

0 commit comments

Comments
 (0)