Skip to content

Commit 24ee6a6

Browse files
Merge pull request #13 from TensorBlock/image-generation-support
Add File Management Support
2 parents afc1331 + 9085284 commit 24ee6a6

26 files changed

+1402
-131
lines changed

app/main.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
2-
import {app, BrowserWindow, ipcMain, shell} from 'electron';
2+
import {app, BrowserWindow, ipcMain, shell, dialog} from 'electron';
33
import * as path from 'path';
44
import * as fs from 'fs';
55
import * as os from 'os';
@@ -71,6 +71,62 @@ function createWindow(): BrowserWindow {
7171
}
7272
});
7373
});
74+
75+
// Save file handler
76+
ipcMain.handle('save-file', async (event, args) => {
77+
try {
78+
if (!win) return { success: false, error: 'Window not available' };
79+
80+
const { fileBuffer, fileName, fileType } = args;
81+
82+
// Show save dialog
83+
const result = await dialog.showSaveDialog(win, {
84+
title: 'Save File',
85+
defaultPath: fileName,
86+
filters: [
87+
{ name: 'All Files', extensions: ['*'] }
88+
]
89+
});
90+
91+
if (result.canceled || !result.filePath) {
92+
return { success: false, canceled: true };
93+
}
94+
95+
// Convert base64 to buffer if needed
96+
let buffer;
97+
if (typeof fileBuffer === 'string') {
98+
buffer = Buffer.from(fileBuffer, 'base64');
99+
} else {
100+
buffer = Buffer.from(fileBuffer);
101+
}
102+
103+
// Write file to disk
104+
fs.writeFileSync(result.filePath, buffer);
105+
106+
return { success: true, filePath: result.filePath };
107+
} catch (error) {
108+
console.error('Error saving file:', error);
109+
return { success: false, error: String(error) };
110+
}
111+
});
112+
113+
// Open file handler
114+
ipcMain.handle('open-file', async (event, filePath) => {
115+
try {
116+
if (!filePath) return { success: false, error: 'No file path provided' };
117+
118+
const result = await shell.openPath(filePath);
119+
120+
if (result) {
121+
return { success: false, error: result };
122+
}
123+
124+
return { success: true };
125+
} catch (error) {
126+
console.error('Error opening file:', error);
127+
return { success: false, error: String(error) };
128+
}
129+
});
74130

75131
// Window control handlers
76132

app/preload.ts

+3
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@ contextBridge.exposeInMainWorld('electron', {
2626
// File and URL operations
2727
openUrl: (url: string) => ipcRenderer.send('open-url', url),
2828
openFolderByFile: (path: string) => ipcRenderer.send('open-folder-by-file', path),
29+
saveFile: (fileBuffer: ArrayBuffer | string, fileName: string, fileType: string) =>
30+
ipcRenderer.invoke('save-file', { fileBuffer, fileName, fileType }),
31+
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
2932
});

src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
22
import { ChatPage } from './components/pages/ChatPage';
33
import { ImageGenerationPage } from './components/pages/ImageGenerationPage';
44
import { TranslationPage } from './components/pages/TranslationPage';
5+
import { FileManagementPage } from './components/pages/FileManagementPage';
56
import MainLayout from './components/layout/MainLayout';
67
import DatabaseInitializer from './components/core/DatabaseInitializer';
78

@@ -36,6 +37,7 @@ function App() {
3637
{activePage === 'chat' && <ChatPage />}
3738
{activePage === 'image' && <ImageGenerationPage />}
3839
{activePage === 'translation' && <TranslationPage />}
40+
{activePage === 'files' && <FileManagementPage />}
3941
</MainLayout>
4042
</DatabaseInitializer>
4143
);

src/components/chat/ChatMessageArea.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,13 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
537537
{/* Image generation button */}
538538
<ImageGenerationButton
539539
disabled={isLoading || isCurrentlyStreaming}
540+
onImageGenerate={(prompt, provider, model) => {
541+
// Set special message for image generation
542+
const imageGenPrompt = prompt || `/image Generate an image using ${provider} ${model}`;
543+
setInput(imageGenPrompt);
544+
// Focus the input
545+
inputRef.current?.focus();
546+
}}
540547
/>
541548
</div>
542549

src/components/chat/ImageGenerationButton.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useRef, useEffect } from 'react';
22
import { Image } from 'lucide-react';
3-
import { SettingsService } from '../../services/settings-service';
3+
import { SettingsService, SETTINGS_CHANGE_EVENT } from '../../services/settings-service';
44
import { AIServiceCapability } from '../../types/capabilities';
55
import ProviderIcon from '../ui/ProviderIcon';
66
import { useTranslation } from '../../hooks/useTranslation';
@@ -17,6 +17,7 @@ interface ProviderModel {
1717
}
1818

1919
const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
20+
onImageGenerate,
2021
disabled = false
2122
}) => {
2223
const { t } = useTranslation();
@@ -42,8 +43,7 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
4243
const providerSettings = settingsService.getProviderSettings(providerId);
4344

4445
if (providerSettings.models) {
45-
// For now, just assume all models have image generation capability
46-
// This would need to be updated once proper capability detection is implemented
46+
// Find models with image generation capability
4747
for (const model of providerSettings.models) {
4848
// Check if model has image generation capability
4949
if (model.modelCapabilities?.includes(AIServiceCapability.ImageGeneration)) {
@@ -67,6 +67,13 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
6767
};
6868

6969
loadProviders();
70+
71+
// Listen for settings changes to update available providers
72+
window.addEventListener(SETTINGS_CHANGE_EVENT, loadProviders);
73+
74+
return () => {
75+
window.removeEventListener(SETTINGS_CHANGE_EVENT, loadProviders);
76+
};
7077
}, []);
7178

7279
// Handle click outside to close popup
@@ -96,6 +103,12 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
96103
setSelectedProvider(providerName);
97104
setSelectedModel(modelId);
98105
setIsPopupOpen(false);
106+
107+
// If onImageGenerate is provided, call it with an empty prompt
108+
// The actual prompt will be filled in by the chat message
109+
if (onImageGenerate) {
110+
onImageGenerate("", providerName, modelId);
111+
}
99112
};
100113

101114
const isButtonEnabled = !disabled && providers.length > 0;

src/components/layout/Sidebar.tsx

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { MessageSquare, Settings, Image, Languages } from 'lucide-react';
2+
import { MessageSquare, Settings, Image, Languages, FileText } from 'lucide-react';
33

44
interface SidebarProps {
55
activePage: string;
@@ -28,6 +28,9 @@ export const Sidebar: React.FC<SidebarProps> = ({
2828
else if(activePage === 'translation'){
2929
return 'translation';
3030
}
31+
else if(activePage === 'files'){
32+
return 'files';
33+
}
3134

3235
return '';
3336
}
@@ -72,6 +75,18 @@ export const Sidebar: React.FC<SidebarProps> = ({
7275
>
7376
<Languages size={22} />
7477
</button>
78+
79+
<button
80+
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all duration-200 ${
81+
getActivePage() === 'files'
82+
? 'navigation-item-selected navigation-item-text'
83+
: 'navigation-item navigation-item-text'
84+
}`}
85+
onClick={() => onChangePage('files')}
86+
aria-label="File Management"
87+
>
88+
<FileText size={22} />
89+
</button>
7590
</div>
7691

7792
{/* Settings button at bottom */}

0 commit comments

Comments
 (0)