Skip to content

Commit 36198a0

Browse files
author
Tim Sinaeve
committed
docs: release v0.8.7 - RAG improvements and enhanced image support
1 parent 49cad3c commit 36198a0

16 files changed

Lines changed: 289 additions & 56 deletions

App.tsx

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -503,36 +503,10 @@ export const MainApp: React.FC = () => {
503503
});
504504
}, [items, bodySearchMatches, searchTerm]);
505505

506-
const handleApplyToEditor = useCallback((content: string) => {
507-
if (!tabState.activeId) return;
508-
setPendingInsertText(content);
509-
setDocumentCommandTriggers(prev => ({
510-
...prev,
511-
insertText: prev.insertText + 1
512-
}));
513-
addLog('INFO', 'Applying content from chat to editor.');
514-
}, [tabState.activeId, addLog]);
515-
516-
const handleCreateDocumentFromChat = useCallback(async (content: string) => {
517-
try {
518-
// Generate a simple title from the first line or first 20 chars
519-
const firstLine = content.split('\n')[0].substring(0, 30).replace(/[#*`]/g, '').trim();
520-
const title = firstLine || 'New Document from Chat';
521-
522-
const newNode = await addDocument(null, title);
523-
if (newNode) {
524-
await commitVersion(newNode.id, content);
525-
handleNavigateToNode(newNode.id);
526-
addLog('INFO', `Created new document "${title}" from chat.`);
527-
}
528-
} catch (error) {
529-
addLog('ERROR', 'Failed to create document from chat.');
530-
}
531-
}, [addDocument, commitVersion, handleNavigateToNode, addLog]);
532506

533507
const activeNode = useMemo(() => {
534-
return itemsWithSearchMetadata.find(p => p.id === tabState.activeId) || null;
535-
}, [itemsWithSearchMetadata, tabState.activeId]);
508+
return items.find(p => p.id === tabState.activeId) || null;
509+
}, [items, tabState.activeId]);
536510

537511
useEffect(() => {
538512
setFolderSearchTerm('');
@@ -1828,6 +1802,42 @@ export const MainApp: React.FC = () => {
18281802
setLastClickedId(nodeId);
18291803
}, [items, ensureNodeVisible, activateDocumentTab, setActiveItem]);
18301804

1805+
const handleApplyToEditor = useCallback((content: string) => {
1806+
if (!tabState.activeId) return;
1807+
setPendingInsertText(content);
1808+
setDocumentCommandTriggers(prev => ({
1809+
...prev,
1810+
insertText: prev.insertText + 1
1811+
}));
1812+
addLog('INFO', 'Applying content from chat to editor.');
1813+
}, [tabState.activeId, addLog, setPendingInsertText, setDocumentCommandTriggers]);
1814+
1815+
const handleCreateDocumentFromChat = useCallback(async (content: string) => {
1816+
try {
1817+
// Generate a simple title from the first line or first 20 chars
1818+
const firstLine = content.split('\n')[0].substring(0, 30).replace(/[#*`]/g, '').trim();
1819+
const title = firstLine || 'New Document from Chat';
1820+
1821+
const newNode = await addDocument({ parentId: null, title });
1822+
if (newNode) {
1823+
await commitVersion(newNode.id, content);
1824+
1825+
// Navigate to the new document directly since it might not be in 'items' yet
1826+
ensureNodeVisible(newNode);
1827+
activateDocumentTab(newNode.id);
1828+
setSelectedIds(new Set([newNode.id]));
1829+
setLastClickedId(newNode.id);
1830+
setActiveTemplateId(null);
1831+
setDocumentView('editor');
1832+
setView('editor');
1833+
1834+
addLog('INFO', `Created new document "${title}" from chat.`);
1835+
}
1836+
} catch (error) {
1837+
addLog('ERROR', 'Failed to create document from chat.');
1838+
}
1839+
}, [addDocument, commitVersion, ensureNodeVisible, activateDocumentTab, addLog]);
1840+
18311841
const handleNewDocument = useCallback(async (parentId?: string | null) => {
18321842
addLog('INFO', 'User action: Create New Document.');
18331843
const effectiveParentId = parentId !== undefined ? parentId : getParentIdForNewItem();
@@ -1847,14 +1857,29 @@ export const MainApp: React.FC = () => {
18471857
const effectiveParentId = parentId !== undefined ? parentId : getParentIdForNewItem();
18481858
try {
18491859
const { text, warnings: clipboardWarnings, mimeType } = await readClipboardText();
1850-
if (!text || text.trim().length === 0) {
1851-
const message = 'Clipboard is empty or does not contain text content to import.';
1852-
addLog('WARNING', message);
1853-
setClipboardNotice({ title: 'Clipboard Empty', message });
1854-
return;
1860+
1861+
let contentToImport = text;
1862+
let importTitle: string | undefined = undefined;
1863+
1864+
if (!contentToImport || contentToImport.trim().length === 0) {
1865+
// Try reading image if text is empty
1866+
const imgResult = await window.electronAPI!.readClipboardImage();
1867+
if (imgResult.success && imgResult.dataUrl) {
1868+
contentToImport = imgResult.dataUrl;
1869+
importTitle = `Pasted Image ${new Date().toLocaleTimeString()}`;
1870+
} else {
1871+
const message = 'Clipboard is empty or does not contain text or image content to import.';
1872+
addLog('WARNING', message);
1873+
setClipboardNotice({ title: 'Clipboard Empty', message });
1874+
return;
1875+
}
18551876
}
18561877

1857-
const result = await createDocumentFromClipboard({ parentId: effectiveParentId, content: text });
1878+
const result = await createDocumentFromClipboard({
1879+
parentId: effectiveParentId,
1880+
content: contentToImport,
1881+
title: importTitle
1882+
});
18581883
const { summary } = result;
18591884
const newDoc = result.item;
18601885

@@ -2162,7 +2187,11 @@ export const MainApp: React.FC = () => {
21622187

21632188
if (normalizedLanguage === 'html') {
21642189
nextDocType = 'rich_text';
2165-
} else if (activeNode.doc_type === 'rich_text') {
2190+
} else if (normalizedLanguage === 'image' || normalizedLanguage.startsWith('image/')) {
2191+
nextDocType = 'image';
2192+
} else if (normalizedLanguage === 'pdf' || normalizedLanguage === 'application/pdf') {
2193+
nextDocType = 'pdf';
2194+
} else if (activeNode.doc_type === 'rich_text' || activeNode.doc_type === 'image' || activeNode.doc_type === 'pdf') {
21662195
nextDocType = 'prompt';
21672196
}
21682197

FUNCTIONAL_MANUAL.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,25 @@ The document editor is powered by Monaco, the same editor core used in VS Code,
136136
Import PDFs by dragging `.pdf` files from your operating system into the sidebar, using the "Import" options in context menus, or converting clipboard data through **New from Clipboard** when it contains PDF bytes or data URLs. Opening a PDF switches the editor to **Preview Only** mode automatically so you can focus on the embedded viewer. The preview displays the document in an inline reader with Chromium's native toolbar (page navigation, zoom, and print controls) and honors keyboard shortcuts exposed by the viewer. DocForge synchronizes the viewer with the global preview zoom so the zoom buttons, scroll wheel shortcuts, and reset commands behave consistently across file types. Because PDFs are stored as binary data, editing is disabled by default; switching to the editor view will show the raw payload, and changing it can corrupt the file.
137137

138138
#### Image Documents
139-
140-
PNG, JPEG, GIF, BMP, WEBP, and SVG assets are detected when you import them from disk, drop them into the sidebar, or pipe image data through **New from Clipboard**. Image documents also open directly in **Preview Only** mode. The preview uses DocForge's zoom and pan surface so you can scroll, drag to reposition, double-click to zoom, or press the on-screen controls to reset the view. Image metadata—such as pixel dimensions and MIME type—appears in the status bar while the preview is active, and the renderer clamps the image inside the workspace with padding so large assets remain manageable. Editing the underlying binary/text data is optional but discouraged unless you are intentionally replacing the encoded image contents.
139+
140+
PNG, JPEG, GIF, BMP, WEBP, and SVG assets are detected when you import them from disk, drop them into the sidebar, or pipe image data through **New from Clipboard**. Image documents also open directly in **Preview Only** mode. The preview uses DocForge's zoom and pan surface so you can scroll, drag to reposition, double-click to zoom, or press the on-screen controls to reset the view. Image metadata—such as pixel dimensions and MIME type—appears in the status bar while the preview is active.
141+
142+
**Interacting with Images**:
143+
- **Pasting**: When an image document is open and unlocked, you can paste an image directly from your clipboard (**Ctrl+V**) to replace its content.
144+
- **Drag-and-Drop**: Drop any image file from your computer onto the open image editor to update the document.
145+
- **Manual Creation**: Create a new document, change its language to **Image**, and then paste or drop your content.
146+
147+
Editing the underlying binary/text data is optional but discouraged unless you are intentionally replacing the encoded image contents.
148+
149+
#### RAG (Chat with Workspace)
150+
151+
The "Chat with Workspace" feature allows you to query your entire collection of documents using a local AI model.
152+
153+
- **Indexing**: Before you can chat, DocForge needs to index your workspace. This process creates a semantic representation of your documents, allowing the AI to find relevant context even if the exact keywords don't match.
154+
- **Configurable Retrieval**: In Settings, you can adjust the **RAG Similarity Threshold**. A lower value (e.g., 0.8) makes the search more restrictive, returning only highly relevant matches, while a higher value (e.g., 1.5) returns more context.
155+
- **Context Limit**: You can configure the chat to analyze up to **500 documents** at once for deep workspace insights.
156+
- **Source Attribution**: When the AI answers a question, it lists the specific documents it used as context. These sources are streamed immediately as soon as the search completes.
157+
- **Smart Privacy**: If the AI determines that none of the retrieved documents contain the answer to your question, the source list is automatically hidden to keep the chat interface clean.
141158

142159
#### Python Execution Panel
143160

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ DocForge is a desktop application designed to streamline the process of creating
3535
- **Comprehensive Action Logging**: Every user action is logged, and the logger panel now supports range selections, modifier-aware drag selection, and configurable copy-to-clipboard exports with or without timestamps and levels.
3636
- **Offline First:** All your data is stored locally on your machine.
3737
- **Auto-Update:** Control automatic startup checks, opt into pre-release builds, and trigger manual "Check for Updates" scans that report success or errors inline.
38+
- **RAG-Powered "Chat with Workspace":** Semantic search across your entire workspace. Adjust the similarity threshold to tune the relevance of retrieved context and analyze up to 500 documents at once.
39+
- **Smart Source Attribution:** Chat responses include streamed sources that automatically hide if no relevant information is found, keeping your context clean and focused.
40+
- **Enhanced Image Clipboard & Drag-Drop:** Create image documents directly from the clipboard, or update existing image nodes by simply pasting (**Ctrl+V**) or dropping an image file onto the editor.
3841
- **Resizable Layout:** The sidebar, templates panel, and logger panel are all fully resizable to customize your workspace.
3942

4043
## Getting Started

VERSION_LOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1-
# Version Log
1+
## v0.8.7 - The RAG & Clipboard Image Update
2+
3+
### ✨ Features
4+
5+
- **RAG-Powered "Chat with Workspace"**:
6+
- Finalized the workspace analysis tool with configurable **Similarity Threshold** and increased context limits (up to **500 documents**).
7+
- Intelligent source attribution: Sources are now streamed immediately and automatically hidden if the AI determines no relevant information was found.
8+
- **Enhanced Image Clipboard Support**:
9+
- **New from Clipboard**: Now detects images on the system clipboard and automatically creates a new image document.
10+
- **Direct Paste/Drop**: Drag-and-drop or paste images directly into existing image nodes to update their content.
11+
- **Navigation Polish**: Refined the "Create New Document" action in chat to navigate directly to the new file, ensuring a seamless transition from AI response to workspace document.
12+
13+
### 🐛 Fixes
14+
15+
- **RAG Reliability**: Optimized the retrieval logic to trigger search and streaming in parallel, reducing latency.
16+
- **Copyright Maintenance**: Updated all internal About boxes and legal notices to **2026**.
217

318
## v0.8.6 - The Usability & Drag-Drop Fixes
419

components/AboutModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const AboutModal: React.FC<AboutModalProps> = ({ onClose }) => {
3030
</div>
3131
<div className="space-y-1">
3232
<p className="text-lg font-semibold">DocForge</p>
33-
<p className="text-sm text-text-secondary">© 2025 Tim Sinaeve. All rights reserved.</p>
33+
<p className="text-sm text-text-secondary">© 2026 Tim Sinaeve. All rights reserved.</p>
3434
</div>
3535
</div>
3636
<div className="flex flex-col items-center space-y-2">

components/ChatPanel.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
9898
addLog('INFO', `RAG: Current index status: ${statusResult.indexedDocuments}/${statusResult.totalDocuments} documents indexed.`);
9999
} else {
100100
// Find if there was an error in the status call
101-
const rawResult = await window.electronAPI!.ragGetIndexStatus();
102-
if (!rawResult.success && rawResult.error) {
103-
addLog('ERROR', `RAG: Status check failed - ${rawResult.error}`);
101+
if (window.electronAPI) {
102+
const rawResult = await window.electronAPI.ragGetIndexStatus();
103+
if (!rawResult.success && rawResult.error) {
104+
addLog('ERROR', `RAG: Status check failed - ${rawResult.error}`);
105+
}
104106
}
105107
}
106108
}
@@ -158,9 +160,27 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
158160
)
159161
);
160162
},
163+
onSources: (retrievedSources) => {
164+
sources = retrievedSources;
165+
setMessages(prev =>
166+
prev.map(msg =>
167+
msg.id === assistantMessageId
168+
? { ...msg, sources: retrievedSources }
169+
: msg
170+
)
171+
);
172+
},
161173
onDone: (fullText) => {
162174
// If the AI says it couldn't find information, don't show sources
163-
const noInfoFound = fullText.toLowerCase().includes("couldn't find information");
175+
const noInfoPhrases = [
176+
"couldn't find information",
177+
"i don't have information",
178+
"no information found",
179+
"could not find",
180+
"don't have any information"
181+
];
182+
const lowerText = fullText.toLowerCase();
183+
const noInfoFound = noInfoPhrases.some(phrase => lowerText.includes(phrase));
164184
const finalSources = noInfoFound ? [] : sources;
165185

166186
setMessages(prev =>
@@ -383,6 +403,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
383403
))}
384404
</div>
385405
</div>
406+
)}
386407
{/* Actions */}
387408
{msg.role === 'assistant' && !msg.isStreaming && (
388409
<div className="mt-3 flex gap-2">

components/CodeEditor.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,13 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({
378378
return new Promise(resolve => {
379379
if (monacoInstanceRef.current) {
380380
const editor = monacoInstanceRef.current;
381+
resolve({
382+
scrollTop: editor.getScrollTop(),
383+
scrollHeight: editor.getScrollHeight(),
384+
clientHeight: editor.getLayoutInfo().height,
381385
});
386+
} else {
387+
resolve({ scrollTop: 0, scrollHeight: 0, clientHeight: 0 });
382388
}
383389
});
384390
},

components/PromptEditor.tsx

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
163163
previewZoomStep,
164164
previewInitialScale,
165165
previewResetSignal,
166-
onPreviewMetadataChange,
166+
onPreviewVisibilityChange,
167167
onPreviewZoomAvailabilityChange,
168-
onPreviewMetadataChange: onPreviewMetadataChangeProp,
168+
onPreviewMetadataChange,
169169
onZoomTargetChange,
170170
onSelectionChange,
171171
pendingInsertText,
@@ -615,6 +615,59 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
615615
}
616616
}, [isLocked, addLog, title, settings]);
617617

618+
// Support pasting/dropping images into image nodes
619+
useEffect(() => {
620+
const handlePaste = async (e: ClipboardEvent) => {
621+
if (documentNode.doc_type !== 'image' || isLocked) return;
622+
if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) return;
623+
624+
const items = e.clipboardData?.items;
625+
if (!items) return;
626+
627+
for (const item of Array.from(items)) {
628+
if (item.type.startsWith('image/')) {
629+
e.preventDefault();
630+
const file = item.getAsFile();
631+
if (file) {
632+
const reader = new FileReader();
633+
reader.onload = (event) => {
634+
const dataUrl = event.target?.result as string;
635+
if (dataUrl) {
636+
onCommitVersion(documentNode.id, dataUrl);
637+
addLog('INFO', `Image pasted into document "${title}".`);
638+
}
639+
};
640+
reader.readAsDataURL(file);
641+
break;
642+
}
643+
}
644+
}
645+
};
646+
647+
window.addEventListener('paste', handlePaste);
648+
return () => window.removeEventListener('paste', handlePaste);
649+
}, [documentNode.id, documentNode.doc_type, isLocked, onCommitVersion, addLog, title]);
650+
651+
const handleDrop = useCallback(async (e: React.DragEvent) => {
652+
if (documentNode.doc_type !== 'image' || isLocked) return;
653+
654+
const files = Array.from(e.dataTransfer.files);
655+
const imageFile = files.find(f => f.type.startsWith('image/'));
656+
657+
if (imageFile) {
658+
e.preventDefault();
659+
const reader = new FileReader();
660+
reader.onload = (event) => {
661+
const dataUrl = event.target?.result as string;
662+
if (dataUrl) {
663+
onCommitVersion(documentNode.id, dataUrl);
664+
addLog('INFO', `Image dropped into document "${title}".`);
665+
}
666+
};
667+
reader.readAsDataURL(imageFile);
668+
}
669+
}, [documentNode.id, documentNode.doc_type, isLocked, onCommitVersion, addLog, title]);
670+
618671
const updateTitleSelection = useCallback(() => {
619672
const input = titleInputRef.current;
620673
if (!input) {
@@ -1139,7 +1192,17 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
11391192
<IconButton onClick={handleDeleteDocument} tooltip="Delete Document" size="xs" variant="destructive"><TrashIcon className="w-4 h-4" /></IconButton>
11401193
</div>
11411194
</div>
1142-
<div className="flex-1 flex flex-col bg-secondary overflow-hidden">{renderContent()}</div>
1195+
<div
1196+
className="flex-1 flex flex-col bg-secondary overflow-hidden"
1197+
onDrop={handleDrop}
1198+
onDragOver={(e) => {
1199+
if (documentNode.doc_type === 'image' && !isLocked) {
1200+
e.preventDefault();
1201+
}
1202+
}}
1203+
>
1204+
{renderContent()}
1205+
</div>
11431206
{isPythonDocument && (
11441207
<div
11451208
className="flex-shrink-0 flex flex-col bg-secondary"

0 commit comments

Comments
 (0)