Skip to content

Add File Management Support #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion app/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {app, BrowserWindow, ipcMain, shell} from 'electron';
import {app, BrowserWindow, ipcMain, shell, dialog} from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
Expand Down Expand Up @@ -71,6 +71,62 @@ function createWindow(): BrowserWindow {
}
});
});

// Save file handler
ipcMain.handle('save-file', async (event, args) => {
try {
if (!win) return { success: false, error: 'Window not available' };

const { fileBuffer, fileName, fileType } = args;

// Show save dialog
const result = await dialog.showSaveDialog(win, {
title: 'Save File',
defaultPath: fileName,
filters: [
{ name: 'All Files', extensions: ['*'] }
]
});

if (result.canceled || !result.filePath) {
return { success: false, canceled: true };
}

// Convert base64 to buffer if needed
let buffer;
if (typeof fileBuffer === 'string') {
buffer = Buffer.from(fileBuffer, 'base64');
} else {
buffer = Buffer.from(fileBuffer);
}

// Write file to disk
fs.writeFileSync(result.filePath, buffer);

return { success: true, filePath: result.filePath };
} catch (error) {
console.error('Error saving file:', error);
return { success: false, error: String(error) };
}
});

// Open file handler
ipcMain.handle('open-file', async (event, filePath) => {
try {
if (!filePath) return { success: false, error: 'No file path provided' };

const result = await shell.openPath(filePath);

if (result) {
return { success: false, error: result };
}

return { success: true };
} catch (error) {
console.error('Error opening file:', error);
return { success: false, error: String(error) };
}
});

// Window control handlers

Expand Down
3 changes: 3 additions & 0 deletions app/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ contextBridge.exposeInMainWorld('electron', {
// File and URL operations
openUrl: (url: string) => ipcRenderer.send('open-url', url),
openFolderByFile: (path: string) => ipcRenderer.send('open-folder-by-file', path),
saveFile: (fileBuffer: ArrayBuffer | string, fileName: string, fileType: string) =>
ipcRenderer.invoke('save-file', { fileBuffer, fileName, fileType }),
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
});
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { ChatPage } from './components/pages/ChatPage';
import { ImageGenerationPage } from './components/pages/ImageGenerationPage';
import { TranslationPage } from './components/pages/TranslationPage';
import { FileManagementPage } from './components/pages/FileManagementPage';
import MainLayout from './components/layout/MainLayout';
import DatabaseInitializer from './components/core/DatabaseInitializer';

Expand Down Expand Up @@ -36,6 +37,7 @@ function App() {
{activePage === 'chat' && <ChatPage />}
{activePage === 'image' && <ImageGenerationPage />}
{activePage === 'translation' && <TranslationPage />}
{activePage === 'files' && <FileManagementPage />}
</MainLayout>
</DatabaseInitializer>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/chat/ChatMessageArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,13 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
{/* Image generation button */}
<ImageGenerationButton
disabled={isLoading || isCurrentlyStreaming}
onImageGenerate={(prompt, provider, model) => {
// Set special message for image generation
const imageGenPrompt = prompt || `/image Generate an image using ${provider} ${model}`;
setInput(imageGenPrompt);
// Focus the input
inputRef.current?.focus();
}}
/>
</div>

Expand Down
19 changes: 16 additions & 3 deletions src/components/chat/ImageGenerationButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Image } from 'lucide-react';
import { SettingsService } from '../../services/settings-service';
import { SettingsService, SETTINGS_CHANGE_EVENT } from '../../services/settings-service';
import { AIServiceCapability } from '../../types/capabilities';
import ProviderIcon from '../ui/ProviderIcon';
import { useTranslation } from '../../hooks/useTranslation';
Expand All @@ -17,6 +17,7 @@ interface ProviderModel {
}

const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
onImageGenerate,
disabled = false
}) => {
const { t } = useTranslation();
Expand All @@ -42,8 +43,7 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
const providerSettings = settingsService.getProviderSettings(providerId);

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

loadProviders();

// Listen for settings changes to update available providers
window.addEventListener(SETTINGS_CHANGE_EVENT, loadProviders);

return () => {
window.removeEventListener(SETTINGS_CHANGE_EVENT, loadProviders);
};
}, []);

// Handle click outside to close popup
Expand Down Expand Up @@ -96,6 +103,12 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
setSelectedProvider(providerName);
setSelectedModel(modelId);
setIsPopupOpen(false);

// If onImageGenerate is provided, call it with an empty prompt
// The actual prompt will be filled in by the chat message
if (onImageGenerate) {
onImageGenerate("", providerName, modelId);
}
};

const isButtonEnabled = !disabled && providers.length > 0;
Expand Down
17 changes: 16 additions & 1 deletion src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { MessageSquare, Settings, Image, Languages } from 'lucide-react';
import { MessageSquare, Settings, Image, Languages, FileText } from 'lucide-react';

interface SidebarProps {
activePage: string;
Expand Down Expand Up @@ -28,6 +28,9 @@ export const Sidebar: React.FC<SidebarProps> = ({
else if(activePage === 'translation'){
return 'translation';
}
else if(activePage === 'files'){
return 'files';
}

return '';
}
Expand Down Expand Up @@ -72,6 +75,18 @@ export const Sidebar: React.FC<SidebarProps> = ({
>
<Languages size={22} />
</button>

<button
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all duration-200 ${
getActivePage() === 'files'
? 'navigation-item-selected navigation-item-text'
: 'navigation-item navigation-item-text'
}`}
onClick={() => onChangePage('files')}
aria-label="File Management"
>
<FileText size={22} />
</button>
</div>

{/* Settings button at bottom */}
Expand Down
Loading
Loading