diff --git a/app/main.ts b/app/main.ts index 88f074f..833cd81 100644 --- a/app/main.ts +++ b/app/main.ts @@ -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'; @@ -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 diff --git a/app/preload.ts b/app/preload.ts index e5b6916..2f19fba 100644 --- a/app/preload.ts +++ b/app/preload.ts @@ -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), }); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8532843..9f552d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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'; @@ -36,6 +37,7 @@ function App() { {activePage === 'chat' && } {activePage === 'image' && } {activePage === 'translation' && } + {activePage === 'files' && } ); diff --git a/src/components/chat/ChatMessageArea.tsx b/src/components/chat/ChatMessageArea.tsx index 043d407..6bb96c1 100644 --- a/src/components/chat/ChatMessageArea.tsx +++ b/src/components/chat/ChatMessageArea.tsx @@ -537,6 +537,13 @@ export const ChatMessageArea: React.FC = ({ {/* Image generation button */} { + // Set special message for image generation + const imageGenPrompt = prompt || `/image Generate an image using ${provider} ${model}`; + setInput(imageGenPrompt); + // Focus the input + inputRef.current?.focus(); + }} /> diff --git a/src/components/chat/ImageGenerationButton.tsx b/src/components/chat/ImageGenerationButton.tsx index 276f562..aa5080c 100644 --- a/src/components/chat/ImageGenerationButton.tsx +++ b/src/components/chat/ImageGenerationButton.tsx @@ -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'; @@ -17,6 +17,7 @@ interface ProviderModel { } const ImageGenerationButton: React.FC = ({ + onImageGenerate, disabled = false }) => { const { t } = useTranslation(); @@ -42,8 +43,7 @@ const ImageGenerationButton: React.FC = ({ 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)) { @@ -67,6 +67,13 @@ const ImageGenerationButton: React.FC = ({ }; 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 @@ -96,6 +103,12 @@ const ImageGenerationButton: React.FC = ({ 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; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index a77016c..f041c21 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -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; @@ -28,6 +28,9 @@ export const Sidebar: React.FC = ({ else if(activePage === 'translation'){ return 'translation'; } + else if(activePage === 'files'){ + return 'files'; + } return ''; } @@ -72,6 +75,18 @@ export const Sidebar: React.FC = ({ > + + {/* Settings button at bottom */} diff --git a/src/components/pages/FileManagementPage.tsx b/src/components/pages/FileManagementPage.tsx new file mode 100644 index 0000000..57af105 --- /dev/null +++ b/src/components/pages/FileManagementPage.tsx @@ -0,0 +1,783 @@ +import { useState, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + Trash2, + Edit2, + Search, + File as FileIcon, + Code, + Image as ImageIcon, + FileText, + Archive, + ChevronDown, + FolderOpen, + Music, + HardDrive, + AlertTriangle, + FolderUp, + FolderDown +} from "lucide-react"; +import { DatabaseIntegrationService } from "../../services/database-integration"; +import { FileData } from "../../types/file"; +import ConfirmDialog from "../../components/ui/ConfirmDialog"; + +// Define the file type categories +type FileCategory = 'all' | 'document' | 'image' | 'audio' | 'other'; + +export const FileManagementPage = () => { + const { t } = useTranslation(); + const [files, setFiles] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [showRenameModal, setShowRenameModal] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [newFileName, setNewFileName] = useState(""); + const [sortBy, setSortBy] = useState<"name" | "size" | "type" | "updatedAt">("updatedAt"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); + const [activeCategory, setActiveCategory] = useState('all'); + const [isSortOptionsOpen, setIsSortOptionsOpen] = useState(false); + const [confirmDialog, setConfirmDialog] = useState({ + isOpen: false, + title: '', + message: '', + confirmText: '', + cancelText: '', + confirmColor: 'red' as 'red' | 'blue' | 'green' | 'gray' + }); + + // Add refs for sort dropdown and button + const sortOptionsRef = useRef(null); + const sortButtonRef = useRef(null); + + // Load files on mount + useEffect(() => { + loadFiles(); + }, []); + + // Load files from database + const loadFiles = async () => { + setIsLoading(true); + try { + const dbService = DatabaseIntegrationService.getInstance(); + const files = await dbService.getFiles(); + setFiles(files); + } catch (error) { + console.error("Error loading files:", error); + } finally { + setIsLoading(false); + } + }; + + // MIME type helpers to categorize files + const isDocumentType = (type: string): boolean => { + const documentMimeTypes = [ + // Text documents + 'text/plain', 'text/html', 'text/css', 'text/javascript', 'text/markdown', + // Microsoft Office + 'application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + // PDF + 'application/pdf', + // OpenDocument + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.presentation', + // Code files + 'application/json', 'application/xml', 'application/javascript', + // Other documents + 'application/rtf', 'text/csv', 'text/tab-separated-values' + ]; + + // Also check file extensions for common document types + const extension = getFileExtension(type); + const documentExtensions = ['txt', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'md', 'rtf', + 'json', 'xml', 'html', 'htm', 'css', 'js', 'ts', 'tsx', 'jsx', 'csv', 'odt', 'ods', 'odp', 'c', 'cpp', 'h', + 'py', 'java', 'rb', 'php', 'go', 'cs', 'swift', 'kt', 'rust']; + + return documentMimeTypes.some(mime => type.includes(mime)) || + documentExtensions.includes(extension); + }; + + const isImageType = (type: string): boolean => { + const imageMimeTypes = [ + 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/tiff', + 'image/bmp', 'image/svg+xml', 'image/x-icon' + ]; + + // Also check file extensions + const extension = getFileExtension(type); + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif', 'bmp', 'svg', 'ico']; + + return imageMimeTypes.some(mime => type.includes(mime)) || + imageExtensions.includes(extension); + }; + + const isAudioType = (type: string): boolean => { + const audioMimeTypes = [ + 'audio/mpeg', 'audio/mp4', 'audio/wav', 'audio/ogg', 'audio/webm', + 'audio/aac', 'audio/flac', 'audio/x-m4a', 'audio/mp3' + ]; + + // Also check file extensions + const extension = getFileExtension(type); + const audioExtensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus']; + + return audioMimeTypes.some(mime => type.includes(mime)) || + audioExtensions.includes(extension); + }; + + const getFileExtension = (filename: string): string => { + return filename.split('.').pop()?.toLowerCase() || ''; + }; + + const getFileCategory = (file: FileData): FileCategory => { + if (isDocumentType(file.type) || isDocumentType(file.name)) { + return 'document'; + } else if (isImageType(file.type) || isImageType(file.name)) { + return 'image'; + } else if (isAudioType(file.type) || isAudioType(file.name)) { + return 'audio'; + } else { + return 'other'; + } + }; + + // Filter files by category and search term + const filteredFiles = files.filter((file) => { + const matchesCategory = activeCategory === 'all' || getFileCategory(file) === activeCategory; + const matchesSearch = file.name.toLowerCase().includes(searchTerm.toLowerCase()); + return matchesCategory && matchesSearch; + }); + + // Get counts for each category + const getCategoryCount = (category: FileCategory): number => { + if (category === 'all') { + return files.length; + } + return files.filter(file => getFileCategory(file) === category).length; + }; + + // Sort files + const sortedFiles = [...filteredFiles].sort((a, b) => { + let comparison = 0; + if (sortBy === "name") { + comparison = a.name.localeCompare(b.name); + } else if (sortBy === "size") { + comparison = a.size - b.size; + } else if (sortBy === "type") { + comparison = a.type.localeCompare(b.type); + } else if (sortBy === "updatedAt") { + // Sort by date - newest first by default + comparison = new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + } + return sortDirection === "asc" ? comparison : -comparison; + }); + + // Handle file upload + const handleFileUpload = async (event: React.ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + setIsLoading(true); + try { + const dbService = DatabaseIntegrationService.getInstance(); + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + // Read the file + const arrayBuffer = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as ArrayBuffer); + reader.onerror = reject; + reader.readAsArrayBuffer(file); + }); + + // Create file data + const fileData = { + name: file.name, + type: file.type, + size: file.size, + updatedAt: new Date() + }; + + // Save to database + await dbService.saveFile(fileData, arrayBuffer); + } + + // Reload files + await loadFiles(); + } catch (error) { + console.error("Error uploading file:", error); + } finally { + setIsLoading(false); + // Reset input + event.target.value = ""; + } + }; + + // Delete file + const handleDeleteFile = async () => { + if (!selectedFile) return; + + try { + const dbService = DatabaseIntegrationService.getInstance(); + await dbService.deleteFile(selectedFile.fileId); + setConfirmDialog(prev => ({ ...prev, isOpen: false })); + setSelectedFile(null); + await loadFiles(); + } catch (error) { + console.error("Error deleting file:", error); + } + }; + + const handleCancelDelete = () => { + // Just close the dialog + setConfirmDialog(prev => ({ ...prev, isOpen: false })); + }; + + // Rename file + const handleRenameFile = async () => { + if (!selectedFile || !newFileName.trim()) return; + + try { + const dbService = DatabaseIntegrationService.getInstance(); + await dbService.updateFileName(selectedFile.fileId, newFileName); + setShowRenameModal(false); + setSelectedFile(null); + setNewFileName(""); + await loadFiles(); + } catch (error) { + console.error("Error renaming file:", error); + } + }; + + // Export file + const handleExportFile = async (file: FileData) => { + try { + if (!window.electron || !window.electron.saveFile) { + console.error("Electron saveFile API not available"); + return; + } + + const result = await window.electron.saveFile( + file.data, + file.name, + file.type || 'application/octet-stream' + ); + + if (!result.success) { + if (result.canceled) { + // User canceled the save dialog, no need to show error + return; + } + console.error("Error saving file:", result.error); + } + } catch (error) { + console.error("Error exporting file:", error); + } + }; + + // Open file + const handleOpenFile = async (file: FileData) => { + try { + // First save the file to a temporary location + if (!window.electron || !window.electron.saveFile || !window.electron.openFile) { + console.error("Electron API not available"); + return; + } + + // Save file to temp location and then open it + const saveResult = await window.electron.saveFile( + file.data, + file.name, + file.type || 'application/octet-stream' + ); + + if (!saveResult.success || !saveResult.filePath) { + if (!saveResult.canceled) { + console.error("Error saving file:", saveResult.error); + } + return; + } + + // Now open the file with the default application + const openResult = await window.electron.openFile(saveResult.filePath); + + if (!openResult.success) { + console.error("Error opening file:", openResult.error); + } + } catch (error) { + console.error("Error opening file:", error); + } + }; + + // Format file size + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + // Format date for display + const formatDate = (date: Date): string => { + return new Date(date).toLocaleString(); + }; + + // Get file icon based on type + const getFileIcon = (file: FileData) => { + const category = getFileCategory(file); + const extension = file.name.split('.').pop()?.toLowerCase() || ''; + + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']; + const documentExtensions = ['pdf', 'doc', 'docx', 'txt', 'rtf', 'md', 'ppt', 'pptx', 'xls', 'xlsx']; + const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'py', 'java', 'c', 'cpp', 'cs', 'php', 'rb', 'go', 'json']; + const archiveExtensions = ['zip', 'rar', 'tar', 'gz', '7z']; + const audioExtensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']; + + if (imageExtensions.includes(extension) || category === 'image') + return ; + if (documentExtensions.includes(extension)) + return ; + if (codeExtensions.includes(extension)) + return ; + if (archiveExtensions.includes(extension)) + return ; + if (audioExtensions.includes(extension) || category === 'audio') + return ; + + return ; + }; + + // Get category icon + const getCategoryIcon = (category: FileCategory) => { + switch (category) { + case 'all': + return ; + case 'document': + return ; + case 'image': + return ; + case 'audio': + return ; + case 'other': + return ; + } + }; + + // Handle click outside to close sort options dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + sortOptionsRef.current && + !sortOptionsRef.current.contains(event.target as Node) && + sortButtonRef.current && + !sortButtonRef.current.contains(event.target as Node) + ) { + setIsSortOptionsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const toggleSortOptions = () => { + setIsSortOptionsOpen(!isSortOptionsOpen); + }; + + const handleSortChange = (sortKey: "name" | "size" | "type" | "updatedAt") => { + if (sortBy === sortKey) { + // Toggle direction if same key + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + // New key, default to ascending + setSortBy(sortKey); + setSortDirection("asc"); + } + setIsSortOptionsOpen(false); + }; + + const handleDeleteClick = (file: FileData) => { + // Show confirmation dialog + setSelectedFile(file); + setConfirmDialog({ + isOpen: true, + title: t("fileManagement.confirmDelete"), + message: t("fileManagement.confirmDeleteMessage"), + confirmText: t("common.delete"), + cancelText: t("common.cancel"), + confirmColor: 'red' + }); + }; + + return ( +
+
+ {/* Left sidebar - Categories */} +
+ + {/* Upload button */} + + + {/* Category filters */} +
+ {/* All files */} +
setActiveCategory("all")} + > +
+
+ {getCategoryIcon("all")} +
+ {t("fileManagement.categories.all")} +
+ + {getCategoryCount("all")} + +
+ + {/* Documents */} +
setActiveCategory("document")} + > +
+
+ {getCategoryIcon("document")} +
+ {t("fileManagement.categories.document")} +
+ + {getCategoryCount("document")} + +
+ + {/* Images */} +
setActiveCategory("image")} + > +
+
+ {getCategoryIcon("image")} +
+ {t("fileManagement.categories.image")} +
+ + {getCategoryCount("image")} + +
+ + {/* Audio */} +
setActiveCategory("audio")} + > +
+
+ {getCategoryIcon("audio")} +
+ {t("fileManagement.categories.audio")} +
+ + {getCategoryCount("audio")} + +
+ + {/* Others */} +
setActiveCategory("other")} + > +
+
+ {getCategoryIcon("other")} +
+ {t("fileManagement.categories.other")} +
+ + {getCategoryCount("other")} + +
+
+
+ + {/* Main content */} +
+
+ + {/* Search and filters */} +
+
+ + setSearchTerm(e.target.value)} + /> +
+ + {/* Sort dropdown */} +
+ +
+
+ + + + +
+
+
+
+ + {/* File list */} +
+ {isLoading ? ( +
+
+
+ ) : sortedFiles.length > 0 ? ( + + + + + {/* */} + + + + + + + {sortedFiles.map((file) => ( + + + {/* */} + + + + + ))} + +
+ {t("fileManagement.fileName")} + + {t("fileManagement.fileType")} + + {t("fileManagement.fileSize")} + + {t("fileManagement.updatedAt")} + + {t("fileManagement.actions")} +
+
+ {getFileIcon(file)} + {file.name} +
+
+ {file.type.split("/").pop() || "Unknown"} + + {formatFileSize(file.size)} + + {formatDate(file.updatedAt)} + +
+ {/* */} + + + +
+
+ ) : ( +
+ +

+ {t("fileManagement.noFiles")} +

+

+ {t("fileManagement.noFilesDescription")} +

+
+ )} +
+
+
+
+ + {/* Confirmation Dialog */} + } + onConfirm={handleDeleteFile} + onCancel={handleCancelDelete} + /> + + {/* Rename modal */} + {showRenameModal && ( +
+
+

+ {t("fileManagement.renameFile")} +

+
+ + setNewFileName(e.target.value)} + className="w-full p-2 input-box" + autoFocus + /> +
+
+ + +
+
+
+ )} +
+ ); +}; + +export default FileManagementPage; \ No newline at end of file diff --git a/src/components/pages/SettingsPage.tsx b/src/components/pages/SettingsPage.tsx index 2bacb9b..d25bac9 100644 --- a/src/components/pages/SettingsPage.tsx +++ b/src/components/pages/SettingsPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Server, MessageSquare, Languages } from 'lucide-react'; import { SettingsService } from '../../services/settings-service'; import { ProviderSettings } from '../../types/settings'; -import { ApiManagement, ModelManagement, ChatSettings, LanguageSettings } from '../settings'; +import { ApiManagement, ChatSettings, LanguageSettings } from '../settings'; import { DatabaseIntegrationService } from '../../services/database-integration'; import { AIService } from '../../services/ai-service'; import { v4 as uuidv4 } from 'uuid'; @@ -21,7 +21,6 @@ export const SettingsPage: React.FC = ({ const [activeTab, setActiveTab] = useState('api'); const [selectedProvider, setSelectedProvider] = useState('TensorBlock'); const [providerSettings, setProviderSettings] = useState>({}); - const [selectedModel, setSelectedModel] = useState(''); const [useWebSearch, setUseWebSearch] = useState(true); const [isDbInitialized, setIsDbInitialized] = useState(false); const [hasApiKeyChanged, setHasApiKeyChanged] = useState(false); @@ -55,7 +54,6 @@ export const SettingsPage: React.FC = ({ const settings = settingsService.getSettings(); setSelectedProvider(settings.selectedProvider); setProviderSettings(settings.providers); - setSelectedModel(settings.selectedModel); setUseWebSearch(settings.enableWebSearch_Preview); setHasApiKeyChanged(false); lastOpenedSettings.current = true; @@ -73,11 +71,6 @@ export const SettingsPage: React.FC = ({ setSelectedProvider(provider); }; - // Handle model selection change - const handleModelChange = (modelId: string) => { - setSelectedModel(modelId); - }; - // Handle web search setting change const handleWebSearchChange = (enabled: boolean) => { console.log('Web search setting changed to: ', enabled); @@ -308,14 +301,6 @@ export const SettingsPage: React.FC = ({ )} - - {/* Model Management Tab */} - {activeTab === 'models' && ( - - )} diff --git a/src/components/pages/TranslationPage.tsx b/src/components/pages/TranslationPage.tsx index ecd249c..d4bba85 100644 --- a/src/components/pages/TranslationPage.tsx +++ b/src/components/pages/TranslationPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { SettingsService, SETTINGS_CHANGE_EVENT } from '../../services/settings-service'; -import { Orbit, Languages, ArrowRight } from 'lucide-react'; +import { Orbit, Languages, ArrowRight, Copy, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { v4 as uuidv4 } from 'uuid'; import { Message, MessageRole } from '../../types/chat'; @@ -16,6 +16,7 @@ export const TranslationPage: React.FC = () => { const [targetLanguage, setTargetLanguage] = useState('en'); const [error, setError] = useState(null); const [isApiKeyMissing, setIsApiKeyMissing] = useState(true); + const [isCopied, setIsCopied] = useState(false); // Check if API key is available useEffect(() => { @@ -32,6 +33,20 @@ export const TranslationPage: React.FC = () => { }; }, []); + // Handle copy to clipboard + const handleCopy = () => { + if (!translatedText) return; + + navigator.clipboard.writeText(translatedText) + .then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }) + .catch(err => { + console.error('Failed to copy text: ', err); + }); + }; + // Handle translation const handleTranslate = async () => { if (!sourceText.trim()) return; @@ -216,27 +231,6 @@ export const TranslationPage: React.FC = () => { className="flex-1 w-full p-3 mb-4 form-textarea-border input-box" style={{ minHeight: '200px', resize: 'none' }} /> - - {/* Translation button */} - {/*
- -
*/} @@ -272,7 +266,7 @@ export const TranslationPage: React.FC = () => { )} {/* Translation result */} -
+
{isTranslating ? (
@@ -280,7 +274,21 @@ export const TranslationPage: React.FC = () => { ) : (
{translatedText ? ( -

{translatedText}

+ <> +

{translatedText}

+ + ) : (

{t('translation.resultPlaceholder')}

diff --git a/src/components/settings/ApiManagement.tsx b/src/components/settings/ApiManagement.tsx index 8f2eb27..d766c74 100644 --- a/src/components/settings/ApiManagement.tsx +++ b/src/components/settings/ApiManagement.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { ChevronRight, Plus, Trash2, Edit2, Search, X, Brain, Eye, Wrench, Type, Database, EyeOff } from 'lucide-react'; +import { ChevronRight, Plus, Trash2, Edit2, Search, X, Brain, Eye, Wrench, Type, Database, EyeOff, Image } from 'lucide-react'; import { ProviderSettings, ModelSettings } from '../../types/settings'; import { AIServiceCapability } from '../../types/capabilities'; import { v4 as uuidv4 } from 'uuid'; @@ -198,6 +198,8 @@ export const ApiManagement: React.FC = ({ return ; case AIServiceCapability.Embedding: return ; + case AIServiceCapability.ImageGeneration: + return ; default: return null; } diff --git a/src/components/settings/ModelManagement.tsx b/src/components/settings/ModelManagement.tsx deleted file mode 100644 index 272ec69..0000000 --- a/src/components/settings/ModelManagement.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; - -interface ModelManagementProps { - selectedModel: string; - onModelChange: (modelId: string) => void; -} - -export const ModelManagement: React.FC = ({ - selectedModel, - onModelChange, -}) => { - const models = [ - { id: 'gpt-4', name: 'GPT-4', provider: 'OpenAI' }, - { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'OpenAI' }, - { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'OpenAI' }, - { id: 'gpt-3.5-turbo-16k', name: 'GPT-3.5 Turbo (16k)', provider: 'OpenAI' }, - { id: 'claude-3-opus', name: 'Claude 3 Opus', provider: 'Anthropic' }, - { id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'Anthropic' }, - { id: 'claude-3-haiku', name: 'Claude 3 Haiku', provider: 'Anthropic' }, - ]; - - return ( -
-
-

Model Management

-
- -
-

Default Model

-

- Select the model you want to use by default for new conversations. -

- -
- {models.map((model) => ( -
-
- onModelChange(model.id)} - className="w-4 h-4 mt-1 text-blue-600 border-gray-300 focus:ring-blue-500" - /> -
- - - Provider: {model.provider} - -
-
-
- ))} -
-
-
- ); -}; - -export default ModelManagement; \ No newline at end of file diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 02e9a27..1d27d9e 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -1,4 +1,3 @@ export * from './ApiManagement'; -export * from './ModelManagement'; export * from './ChatSettings'; export * from './LanguageSettings'; \ No newline at end of file diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a3a3624..dd8e203 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -40,6 +40,7 @@ "translating": "Translating...", "inputPlaceholder": "Enter text to translate...", "resultPlaceholder": "Translation will appear here", + "copy": "Copy", "apiKeyMissing": "Please set your API key for the selected provider in the settings.", "autoDetect": "Auto-detect", "english": "English", @@ -53,6 +54,35 @@ "japanese": "Japanese", "korean": "Korean" }, + "fileManagement": { + "title": "File Management", + "noFiles": "No files yet", + "noFilesDescription": "Your uploaded files will appear here", + "uploadButton": "Import File", + "uploading": "Uploading...", + "fileName": "File Name", + "fileType": "Type", + "fileSize": "Size", + "updatedAt": "Last Updated", + "actions": "Actions", + "rename": "Rename", + "export": "Export", + "delete": "Delete", + "open": "Open", + "search": "Search files...", + "confirmDelete": "Delete File", + "confirmDeleteMessage": "Are you sure you want to delete this file? This action cannot be undone.", + "renameFile": "Rename File", + "newFileName": "New file name", + "sortBy": "Sort by", + "categories": { + "all": "All Files", + "document": "Documents", + "image": "Images", + "audio": "Audio", + "other": "Others" + } + }, "selectModel": { "selectModel_title": "Select Model", "selectModel_close": "Close", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 7401a7a..0bf7a0c 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -40,6 +40,7 @@ "translating": "Traduciendo...", "inputPlaceholder": "Introduce el texto a traducir...", "resultPlaceholder": "La traducción aparecerá aquí", + "copy": "Copiar", "apiKeyMissing": "Por favor, configura tu clave API para el proveedor seleccionado en la configuración.", "autoDetect": "Detección automática", "english": "Inglés", @@ -53,6 +54,35 @@ "japanese": "Japonés", "korean": "Coreano" }, + "fileManagement": { + "title": "Gestión de Archivos", + "noFiles": "No hay archivos todavía", + "noFilesDescription": "Los archivos cargados aparecerán aquí", + "uploadButton": "Importar Archivo", + "uploading": "Cargando...", + "fileName": "Nombre del Archivo", + "fileType": "Tipo", + "fileSize": "Tamaño", + "updatedAt": "Última Actualización", + "actions": "Acciones", + "rename": "Renombrar", + "export": "Exportar", + "delete": "Eliminar", + "open": "Abrir", + "search": "Buscar archivos...", + "confirmDelete": "Eliminar Archivo", + "confirmDeleteMessage": "¿Estás seguro de que deseas eliminar este archivo? Esta acción no se puede deshacer.", + "renameFile": "Renombrar Archivo", + "newFileName": "Nuevo nombre de archivo", + "sortBy": "Ordenar por", + "categories": { + "all": "Todos los Archivos", + "document": "Documentos", + "image": "Imágenes", + "audio": "Audio", + "other": "Otros" + } + }, "selectModel": { "selectModel_title": "Seleccionar Modelo", "selectModel_close": "Cerrar", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index fea5ab3..49c01c6 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -40,6 +40,7 @@ "translating": "翻訳中...", "inputPlaceholder": "翻訳するテキストを入力...", "resultPlaceholder": "翻訳結果がここに表示されます", + "copy": "コピー", "apiKeyMissing": "選択したプロバイダーのAPIキーを設定で設定してください。", "autoDetect": "自動検出", "english": "英語", @@ -53,6 +54,35 @@ "japanese": "日本語", "korean": "韓国語" }, + "fileManagement": { + "title": "ファイル管理", + "noFiles": "ファイルはまだありません", + "noFilesDescription": "アップロードしたファイルがここに表示されます", + "uploadButton": "ファイルをインポート", + "uploading": "アップロード中...", + "fileName": "ファイル名", + "fileType": "タイプ", + "fileSize": "サイズ", + "updatedAt": "最終更新日", + "actions": "操作", + "rename": "名前の変更", + "export": "エクスポート", + "delete": "削除", + "open": "開く", + "search": "ファイルを検索...", + "confirmDelete": "ファイルの削除", + "confirmDeleteMessage": "このファイルを削除してもよろしいですか?この操作は元に戻せません。", + "renameFile": "ファイル名の変更", + "newFileName": "新しいファイル名", + "sortBy": "並び替え", + "categories": { + "all": "すべてのファイル", + "document": "ドキュメント", + "image": "画像", + "audio": "音声", + "other": "その他" + } + }, "selectModel": { "selectModel_title": "モデルを選択", "selectModel_close": "閉じる", diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 783bacf..952eb2e 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -40,6 +40,7 @@ "translating": "번역 중...", "inputPlaceholder": "번역할 텍스트를 입력하세요...", "resultPlaceholder": "번역 결과가 여기에 표시됩니다", + "copy": "복사", "apiKeyMissing": "선택한 제공자의 API 키를 설정에서 설정하세요.", "autoDetect": "자동 감지", "english": "영어", @@ -53,6 +54,35 @@ "japanese": "일본어", "korean": "한국어" }, + "fileManagement": { + "title": "파일 관리", + "noFiles": "파일 없음", + "noFilesDescription": "업로드한 파일이 여기에 표시됩니다", + "uploadButton": "파일 가져오기", + "uploading": "업로드 중...", + "fileName": "파일 이름", + "fileType": "유형", + "fileSize": "크기", + "updatedAt": "최종 수정일", + "actions": "동작", + "rename": "이름 변경", + "export": "내보내기", + "delete": "삭제", + "open": "열기", + "search": "파일 검색...", + "confirmDelete": "파일 삭제", + "confirmDeleteMessage": "이 파일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "renameFile": "파일 이름 변경", + "newFileName": "새 파일 이름", + "sortBy": "정렬 기준", + "categories": { + "all": "모든 파일", + "document": "문서", + "image": "이미지", + "audio": "오디오", + "other": "기타" + } + }, "selectModel": { "selectModel_title": "모델 선택", "selectModel_close": "닫기", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 63306d6..5069f0c 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -40,6 +40,7 @@ "translating": "翻译中...", "inputPlaceholder": "输入要翻译的文本...", "resultPlaceholder": "翻译结果将显示在这里", + "copy": "复制", "apiKeyMissing": "请在设置中为所选提供商设置您的 API 密钥。", "autoDetect": "自动检测", "english": "英语", @@ -53,6 +54,35 @@ "japanese": "日语", "korean": "韩语" }, + "fileManagement": { + "title": "文件管理", + "noFiles": "暂无文件", + "noFilesDescription": "您上传的文件将会显示在这里", + "uploadButton": "导入文件", + "uploading": "上传中...", + "fileName": "文件名称", + "fileType": "类型", + "fileSize": "大小", + "updatedAt": "最后更新", + "actions": "操作", + "rename": "重命名", + "export": "导出", + "delete": "删除", + "open": "打开", + "search": "搜索文件...", + "confirmDelete": "删除文件", + "confirmDeleteMessage": "确定要删除此文件吗?此操作无法撤销。", + "renameFile": "重命名文件", + "newFileName": "新文件名", + "sortBy": "排序方式", + "categories": { + "all": "所有文件", + "document": "文档", + "image": "图片", + "audio": "音频", + "other": "其他" + } + }, "selectModel": { "selectModel_title": "选择模型", "selectModel_close": "关闭", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index a41b528..5a6389f 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -40,6 +40,7 @@ "translating": "翻譯中...", "inputPlaceholder": "輸入要翻譯的文字...", "resultPlaceholder": "翻譯結果將顯示在這裡", + "copy": "複製", "apiKeyMissing": "請在設定中為所選提供商設置您的 API 金鑰。", "autoDetect": "自動偵測", "english": "英文", @@ -53,6 +54,35 @@ "japanese": "日文", "korean": "韓文" }, + "fileManagement": { + "title": "檔案管理", + "noFiles": "暫無檔案", + "noFilesDescription": "您上傳的檔案將會顯示在這裡", + "uploadButton": "匯入檔案", + "uploading": "上傳中...", + "fileName": "檔案名稱", + "fileType": "類型", + "fileSize": "大小", + "updatedAt": "最後更新", + "actions": "操作", + "rename": "重新命名", + "export": "匯出", + "delete": "刪除", + "open": "開啟", + "search": "搜尋檔案...", + "confirmDelete": "刪除檔案", + "confirmDeleteMessage": "確定要刪除此檔案嗎?此操作無法撤銷。", + "renameFile": "重新命名檔案", + "newFileName": "新檔案名稱", + "sortBy": "排序方式", + "categories": { + "all": "所有檔案", + "document": "文件", + "image": "圖片", + "audio": "音訊", + "other": "其他" + } + }, "selectModel": { "selectModel_title": "選擇模型", "selectModel_close": "關閉", diff --git a/src/services/database-integration.ts b/src/services/database-integration.ts index 51115e4..e358f3b 100644 --- a/src/services/database-integration.ts +++ b/src/services/database-integration.ts @@ -1,7 +1,8 @@ import { DatabaseService } from './database'; import { SettingsService } from './settings-service'; -import { Conversation, Message, ConversationFolder, MessageContent, MessageContentType } from '../types/chat'; +import { Conversation, Message, ConversationFolder, MessageContent, MessageContentType, FileJsonData } from '../types/chat'; import { v4 as uuidv4 } from 'uuid'; +import { FileData } from '../types/file'; const SYSTEM_MESSAGE_CONTENT: MessageContent[] = [ { @@ -204,7 +205,8 @@ export class DatabaseIntegrationService { firstMessageId: conversation.firstMessageId, createdAt: conversation.createdAt, updatedAt: new Date(), - messages: conversation.messages + messages: conversation.messages, + messageInput: conversation.messageInput }; // Update in database @@ -347,4 +349,78 @@ export class DatabaseIntegrationService { ...dbMessage, }; } + + public async saveFile(fileData: FileJsonData, arrayBuffer: ArrayBuffer): Promise { + const fileId = uuidv4(); + const file: FileData = { + fileId: fileId, + updatedAt: new Date(), + name: fileData.name, + type: fileData.type, + size: arrayBuffer.byteLength, + data: arrayBuffer + }; + + await this.dbService.saveFile(file); + + return fileId; + } + + /** + * Get all files from the database + */ + public async getFiles(): Promise { + try { + return await this.dbService.getFiles(); + } catch (error) { + console.error('Error getting files:', error); + return []; + } + } + + /** + * Get a file from the database by ID + */ + public async getFile(fileId: string): Promise { + try { + return await this.dbService.getFile(fileId); + } catch (error) { + console.error(`Error getting file ${fileId}:`, error); + return null; + } + } + + /** + * Update a file name in the database + */ + public async updateFileName(fileId: string, newName: string): Promise { + try { + const file = await this.dbService.getFile(fileId); + if (!file) { + throw new Error(`File with ID ${fileId} not found`); + } + + const updatedFile: FileData = { + ...file, + name: newName + }; + + await this.dbService.updateFile(updatedFile); + } catch (error) { + console.error('Error updating file name:', error); + throw error; + } + } + + /** + * Delete a file from the database + */ + public async deleteFile(fileId: string): Promise { + try { + await this.dbService.deleteFile(fileId); + } catch (error) { + console.error('Error deleting file:', error); + throw error; + } + } } \ No newline at end of file diff --git a/src/services/database.ts b/src/services/database.ts index 6c2346e..9e43ea9 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -2,12 +2,13 @@ import { ProviderSettings } from '../types/settings'; import { Conversation, Message, ConversationFolder } from '../types/chat'; import { v4 as uuidv4 } from 'uuid'; import { UserSettings } from '../types/settings'; +import { FileData } from '../types/file'; // database.ts export class DatabaseService { private db: IDBDatabase | null = null; private readonly DB_NAME = 'tensorblock_db'; - private readonly DB_VERSION = 2; // Increase version to trigger upgrade + private readonly DB_VERSION = 3; // Increase version to trigger upgrade private readonly ENCRYPTION_KEY = 'your-secure-encryption-key'; // In production, use a secure key management system private isInitialized: boolean = false; @@ -82,6 +83,13 @@ export class DatabaseService { keyPath: 'id' }); } + + // Create files store + if (!db.objectStoreNames.contains('files')) { + db.createObjectStore('files', { + keyPath: 'fileId' + }); + } }; }); } @@ -98,7 +106,8 @@ export class DatabaseService { createdAt: new Date(), updatedAt: new Date(), messages: new Map(), - firstMessageId: null + firstMessageId: null, + messageInput: '' }; const transaction = this.db.transaction('conversations', 'readwrite'); @@ -517,4 +526,93 @@ export class DatabaseService { request.onerror = () => reject(request.error); }); } + + /** + * Save a file to the database + * @param file - The file to save + * @returns The file ID + */ + public async saveFile(file: FileData): Promise { + return new Promise((resolve, reject) => { + if (!this.db) throw new Error('Database not initialized'); + + const transaction = this.db.transaction('files', 'readwrite'); + const store = transaction.objectStore('files'); + const request = store.add(file); + + console.log('saveFile', file); + + request.onsuccess = () => resolve(request.result as string); + request.onerror = () => reject(request.error); + }); + } + + /** + * Get all files from the database + * @returns List of files + */ + public async getFiles(): Promise { + return new Promise((resolve, reject) => { + if (!this.db) throw new Error('Database not initialized'); + + const transaction = this.db.transaction('files', 'readonly'); + const store = transaction.objectStore('files'); + const request = store.getAll(); + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + /** + * Get a file from the database by ID + * @param fileId - The file ID + * @returns The file data + */ + public async getFile(fileId: string): Promise { + return new Promise((resolve, reject) => { + if (!this.db) throw new Error('Database not initialized'); + + const transaction = this.db.transaction('files', 'readonly'); + const store = transaction.objectStore('files'); + const request = store.get(fileId); + + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } + + /** + * Update a file in the database + * @param file - The file to update + */ + public async updateFile(file: FileData): Promise { + return new Promise((resolve, reject) => { + if (!this.db) throw new Error('Database not initialized'); + + const transaction = this.db.transaction('files', 'readwrite'); + const store = transaction.objectStore('files'); + const request = store.put(file); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + /** + * Delete a file from the database + * @param fileId - The file ID to delete + */ + public async deleteFile(fileId: string): Promise { + return new Promise((resolve, reject) => { + if (!this.db) throw new Error('Database not initialized'); + + const transaction = this.db.transaction('files', 'readwrite'); + const store = transaction.objectStore('files'); + const request = store.delete(fileId); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } } \ No newline at end of file diff --git a/src/services/file-upload-service.ts b/src/services/file-upload-service.ts index 1a2fa07..f8b4ff7 100644 --- a/src/services/file-upload-service.ts +++ b/src/services/file-upload-service.ts @@ -1,5 +1,6 @@ import { SettingsService } from './settings-service'; import { FileJsonData, MessageContent, MessageContentType } from '../types/chat'; +import { DatabaseIntegrationService } from './database-integration'; // Define provider-specific file size limits export interface ProviderFileLimits { @@ -115,21 +116,24 @@ export class FileUploadService { } const arrayBuffer = await this.readFile(file); - const base64 = FileUploadService.bufferToBase64(arrayBuffer); - const fileData: FileJsonData = { + const fileJsonData: FileJsonData = { name: file.name, type: file.type, size: file.size }; + // Store the file in the database + const dbService = DatabaseIntegrationService.getInstance(); + const fileId = await dbService.saveFile(fileJsonData, arrayBuffer); + // Create a message content object for this file const fileContent: MessageContent = { type: MessageContentType.File, - content: base64, // Store the file path - dataJson: JSON.stringify(fileData) + content: fileId, // Store the file ID + dataJson: JSON.stringify(fileJsonData) }; - + results.push(fileContent); } diff --git a/src/services/providers/openai-service.ts b/src/services/providers/openai-service.ts index 5feca5b..b5205d5 100644 --- a/src/services/providers/openai-service.ts +++ b/src/services/providers/openai-service.ts @@ -71,6 +71,12 @@ export class OpenAIService implements AiServiceProvider { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars getModelCapabilities(model: string): AIServiceCapability[] { + // Add image generation capability for DALL-E 3 + if (model === 'dall-e-3') { + return [AIServiceCapability.ImageGeneration]; + } + + // Default capabilities for chat models return mapModelCapabilities( false, false, diff --git a/src/services/settings-service.ts b/src/services/settings-service.ts index 69386eb..d8c8a43 100644 --- a/src/services/settings-service.ts +++ b/src/services/settings-service.ts @@ -65,6 +65,14 @@ const DEFAULT_SETTINGS: UserSettings = { modelCapabilities: [AIServiceCapability.TextCompletion, AIServiceCapability.WebSearch], modelRefUUID: uuidv4(), }, + { + modelName: 'DALL-E 3', + modelId: 'dall-e-3', + modelCategory: 'Image Generation', + modelDescription: 'DALL-E 3 is OpenAI\'s advanced image generation model.', + modelCapabilities: [AIServiceCapability.ImageGeneration], + modelRefUUID: uuidv4(), + }, ] }, ['Anthropic']: { @@ -277,6 +285,8 @@ export class SettingsService { this.addAllDefaultProviders(); + this.addDefaultModels(); + this.isInitialized = true; } catch (error) { console.error('Error initializing settings service:', error); @@ -295,6 +305,27 @@ export class SettingsService { } } + private addDefaultModels() { + for(const provider in this.settings.providers) { + if(!this.settings.providers[provider].models) { + this.settings.providers[provider].models = DEFAULT_SETTINGS.providers[provider].models; + } + + if(provider === 'OpenAI') { + if(!this.settings.providers[provider].models!.find(model => model.modelId === 'dall-e-3')) { + this.settings.providers[provider].models!.push({ + modelName: 'DALL-E 3', + modelId: 'dall-e-3', + modelCategory: 'Image Generation', + modelDescription: 'DALL-E 3 is OpenAI\'s advanced image generation model.', + modelCapabilities: [AIServiceCapability.ImageGeneration], + modelRefUUID: uuidv4(), + }); + } + } + } + } + /** * Load settings from database */ diff --git a/src/styles/tensorblock-light.css b/src/styles/tensorblock-light.css index 2ebe548..0dcf580 100644 --- a/src/styles/tensorblock-light.css +++ b/src/styles/tensorblock-light.css @@ -637,4 +637,70 @@ .image-generation-provider-selected { background-color: var(--primary-200); } + + /* File Management Page */ + .file-filter-item { + border-radius: 0.5rem; + background-color: transparent; + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + cursor: pointer; + transition: all 0.2s; + } + + .file-filter-item:hover { + background-color: var(--primary-50); + } + + .file-filter-item-active { + background-color: var(--primary-100); + color: var(--primary-700); + } + + .file-filter-item-active:hover { + background-color: var(--primary-200); + } + + .file-filter-count { + background-color: var(--primary-200); + color: var(--primary-600); + border-radius: 9999px; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + } + + .file-type-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 0.375rem; + margin-right: 0.75rem; + } + + .file-type-all { + background-color: var(--primary-100); + color: var(--primary-600); + } + + .file-type-document { + background-color: #D1E7DD; + color: #0F5132; + } + + .file-type-image { + background-color: #CFE2FF; + color: #084298; + } + + .file-type-audio { + background-color: #F8D7DA; + color: #842029; + } + + .file-type-other { + background-color: #E2E3E5; + color: #41464B; + } } \ No newline at end of file diff --git a/src/types/file.ts b/src/types/file.ts index ccfb647..5ed078b 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -1,5 +1,6 @@ -export interface File { +export interface FileData { fileId: string; + updatedAt: Date; name: string; type: string; size: number; diff --git a/src/types/window.d.ts b/src/types/window.d.ts index f82741e..09b725b 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -14,5 +14,15 @@ interface Window { }>; openUrl: (url: string) => Promise; onWindowMaximizedChange: (callback: (event: IpcRendererEvent, maximized: boolean) => void) => void; + saveFile: (fileBuffer: ArrayBuffer | string, fileName: string, fileType: string) => Promise<{ + success: boolean; + filePath?: string; + canceled?: boolean; + error?: string; + }>; + openFile: (filePath: string) => Promise<{ + success: boolean; + error?: string; + }>; }; } \ No newline at end of file