From f6189615ee6f12eb569e6da41f8e9c79fcf5dbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sat, 26 Apr 2025 17:02:58 +0800 Subject: [PATCH 1/7] Fix: Add dalle3 as default generation model --- src/components/chat/ChatMessageArea.tsx | 7 ++ src/components/chat/ImageGenerationButton.tsx | 19 ++++- src/components/settings/ApiManagement.tsx | 4 +- src/components/settings/ModelManagement.tsx | 74 ------------------- src/services/providers/openai-service.ts | 6 ++ src/services/settings-service.ts | 30 ++++++++ 6 files changed, 62 insertions(+), 78 deletions(-) delete mode 100644 src/components/settings/ModelManagement.tsx 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/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/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..0795f13 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,26 @@ 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') { + + 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 */ From 6c486a5d79f06316791fb1c9f189815ab601abe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sat, 26 Apr 2025 17:20:54 +0800 Subject: [PATCH 2/7] Fix: Add translation copy button & fix dalle3 model duplicate --- src/components/pages/SettingsPage.tsx | 17 +------ src/components/pages/TranslationPage.tsx | 56 ++++++++++++++---------- src/components/settings/index.ts | 1 - src/locales/en/translation.json | 1 + src/locales/es/translation.json | 1 + src/locales/ja/translation.json | 1 + src/locales/ko/translation.json | 1 + src/locales/zh-CN/translation.json | 1 + src/locales/zh-TW/translation.json | 1 + src/services/settings-service.ts | 19 ++++---- 10 files changed, 49 insertions(+), 50 deletions(-) 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/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..edd7f0f 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", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 7401a7a..0fce122 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", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index fea5ab3..c3c1ac0 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": "英語", diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 783bacf..5df95ee 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": "영어", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 63306d6..213ee47 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": "英语", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index a41b528..289a94e 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": "英文", diff --git a/src/services/settings-service.ts b/src/services/settings-service.ts index 0795f13..d8c8a43 100644 --- a/src/services/settings-service.ts +++ b/src/services/settings-service.ts @@ -312,15 +312,16 @@ export class SettingsService { } if(provider === 'OpenAI') { - - 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(), - }); + 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(), + }); + } } } } From 7df39251f5f7ce0ce2a3befa62e403a5d4e5e3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sun, 27 Apr 2025 18:30:37 +0800 Subject: [PATCH 3/7] Function: Add File Management Page --- src/App.tsx | 2 + src/components/layout/Sidebar.tsx | 17 +- src/components/pages/FileManagementPage.tsx | 448 ++++++++++++++++++++ src/locales/en/translation.json | 21 + src/services/database-integration.ts | 79 +++- src/services/database.ts | 102 ++++- src/services/file-upload-service.ts | 14 +- src/types/file.ts | 2 +- 8 files changed, 674 insertions(+), 11 deletions(-) create mode 100644 src/components/pages/FileManagementPage.tsx 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/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..653b321 --- /dev/null +++ b/src/components/pages/FileManagementPage.tsx @@ -0,0 +1,448 @@ +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + Upload, + Trash2, + Edit2, + Download, + Search, + File as FileIcon, + Code, + Image as ImageIcon, + FileText, + Archive, + ExternalLink, + ChevronDown +} from "lucide-react"; +import { DatabaseIntegrationService } from "../../services/database-integration"; +import { FileData } from "../../types/file"; + +export const FileManagementPage = () => { + const { t } = useTranslation(); + const [files, setFiles] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showRenameModal, setShowRenameModal] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [newFileName, setNewFileName] = useState(""); + const [sortBy, setSortBy] = useState<"name" | "size" | "type">("name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + // 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); + } + }; + + // Filter files by search term + const filteredFiles = files.filter((file) => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // 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); + } + 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 + }; + + // 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); + setShowDeleteModal(false); + setSelectedFile(null); + await loadFiles(); + } catch (error) { + console.error("Error deleting file:", error); + } + }; + + // 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 { + const blob = new Blob([file.data], { type: file.type || 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + console.error("Error exporting file:", error); + } + }; + + // Open file + const handleOpenFile = async (file: FileData) => { + try { + // Use electron API to open the file + const blob = new Blob([file.data], { type: file.type || 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + window.electron.openUrl(url); + } 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]; + }; + + // Get file icon based on type + const getFileIcon = (file: FileData) => { + const extension = file.name.split('.').pop()?.toLowerCase() || ''; + + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + 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']; + + if (imageExtensions.includes(extension)) return ; + if (documentExtensions.includes(extension)) return ; + if (codeExtensions.includes(extension)) return ; + if (archiveExtensions.includes(extension)) return ; + + return ; + }; + + // Toggle sort direction + const handleSortChange = (sortKey: "name" | "size" | "type") => { + if (sortBy === sortKey) { + // Toggle direction if same key + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + // New key, default to ascending + setSortBy(sortKey); + setSortDirection("asc"); + } + }; + + return ( +
+
+ {/* Main content */} +
+
+ {/* Header */} +
+

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

+ + {/* Upload button */} + +
+ + {/* 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.actions")}
+
+ {getFileIcon(file)} + {file.name} +
+
{file.type.split("/").pop() || "Unknown"}{formatFileSize(file.size)} +
+ + + + +
+
+ ) : ( +
+ +

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

+

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

+
+ )} +
+
+
+
+ + {/* Delete confirmation modal */} + {showDeleteModal && ( +
+
+

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

+

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

+
+ + +
+
+
+ )} + + {/* 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/locales/en/translation.json b/src/locales/en/translation.json index edd7f0f..02e45fc 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -54,6 +54,27 @@ "japanese": "Japanese", "korean": "Korean" }, + "fileManagement": { + "title": "File Management", + "noFiles": "No files yet", + "noFilesDescription": "Your uploaded files will appear here", + "uploadButton": "Upload File", + "uploading": "Uploading...", + "fileName": "File Name", + "fileType": "Type", + "fileSize": "Size", + "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" + }, "selectModel": { "selectModel_title": "Select Model", "selectModel_close": "Close", diff --git a/src/services/database-integration.ts b/src/services/database-integration.ts index 51115e4..e8bb0fc 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,77 @@ export class DatabaseIntegrationService { ...dbMessage, }; } + + public async saveFile(fileData: FileJsonData, arrayBuffer: ArrayBuffer): Promise { + const fileId = uuidv4(); + const file: FileData = { + fileId: fileId, + 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/types/file.ts b/src/types/file.ts index ccfb647..f3d67f9 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -1,4 +1,4 @@ -export interface File { +export interface FileData { fileId: string; name: string; type: string; From d9393c45d587903a636b7674658e0c5d68b69bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sun, 27 Apr 2025 19:51:27 +0800 Subject: [PATCH 4/7] Fix: Add modal filter categories --- src/components/pages/FileManagementPage.tsx | 298 +++++++++++++++++--- src/locales/en/translation.json | 9 +- src/locales/es/translation.json | 28 ++ src/locales/ja/translation.json | 28 ++ src/locales/ko/translation.json | 28 ++ src/locales/zh-CN/translation.json | 28 ++ src/locales/zh-TW/translation.json | 28 ++ src/styles/tensorblock-light.css | 66 +++++ 8 files changed, 466 insertions(+), 47 deletions(-) diff --git a/src/components/pages/FileManagementPage.tsx b/src/components/pages/FileManagementPage.tsx index 653b321..8ffcab5 100644 --- a/src/components/pages/FileManagementPage.tsx +++ b/src/components/pages/FileManagementPage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { - Upload, +import { + Upload, Trash2, Edit2, Download, @@ -12,22 +12,38 @@ import { FileText, Archive, ExternalLink, - ChevronDown + ChevronDown, + FolderOpen, + Music, + HardDrive, + AlertTriangle } 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 [showDeleteModal, setShowDeleteModal] = useState(false); const [showRenameModal, setShowRenameModal] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [newFileName, setNewFileName] = useState(""); const [sortBy, setSortBy] = useState<"name" | "size" | "type">("name"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [activeCategory, setActiveCategory] = useState('all'); + const [confirmDialog, setConfirmDialog] = useState({ + isOpen: false, + title: '', + message: '', + confirmText: '', + cancelText: '', + confirmColor: 'red' as 'red' | 'blue' | 'green' | 'gray' + }); // Load files on mount useEffect(() => { @@ -48,10 +64,96 @@ export const FileManagementPage = () => { } }; - // Filter files by search term - const filteredFiles = files.filter((file) => - file.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + // 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) => { @@ -115,7 +217,7 @@ export const FileManagementPage = () => { try { const dbService = DatabaseIntegrationService.getInstance(); await dbService.deleteFile(selectedFile.fileId); - setShowDeleteModal(false); + setConfirmDialog(prev => ({ ...prev, isOpen: false })); setSelectedFile(null); await loadFiles(); } catch (error) { @@ -123,6 +225,11 @@ export const FileManagementPage = () => { } }; + const handleCancelDelete = () => { + // Just close the dialog + setConfirmDialog(prev => ({ ...prev, isOpen: false })); + }; + // Rename file const handleRenameFile = async () => { if (!selectedFile || !newFileName.trim()) return; @@ -182,21 +289,45 @@ export const FileManagementPage = () => { // 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']; + 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)) return ; - if (documentExtensions.includes(extension)) return ; - if (codeExtensions.includes(extension)) return ; - if (archiveExtensions.includes(extension)) return ; + 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 ; + } + }; + // Toggle sort direction const handleSortChange = (sortKey: "name" | "size" | "type") => { if (sortBy === sortKey) { @@ -209,16 +340,109 @@ export const FileManagementPage = () => { } }; + 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 */} +
+

{t("fileManagement.title")}

+ + {/* 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 */}
{/* Header */}

- {t("fileManagement.title")} + {activeCategory === 'all' + ? t("fileManagement.title") + : t(`fileManagement.categories.${activeCategory}`)}

{/* Upload button */} @@ -345,10 +569,7 @@ export const FileManagementPage = () => {
- {/* Delete confirmation modal */} - {showDeleteModal && ( -
-
-

- {t("fileManagement.confirmDelete")} -

-

- {t("fileManagement.confirmDeleteMessage")} -

-
- - -
-
-
- )} + {/* Confirmation Dialog */} + } + onConfirm={handleDeleteFile} + onCancel={handleCancelDelete} + /> {/* Rename modal */} {showRenameModal && ( diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 02e45fc..fe83329 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -73,7 +73,14 @@ "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" + "sortBy": "Sort by", + "categories": { + "all": "All Files", + "document": "Documents", + "image": "Images", + "audio": "Audio", + "other": "Others" + } }, "selectModel": { "selectModel_title": "Select Model", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 0fce122..da93eba 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -54,6 +54,34 @@ "japanese": "Japonés", "korean": "Coreano" }, + "fileManagement": { + "title": "Gestión de Archivos", + "noFiles": "Aún no hay archivos", + "noFilesDescription": "Tus archivos subidos aparecerán aquí", + "uploadButton": "Subir Archivo", + "uploading": "Subiendo...", + "fileName": "Nombre del Archivo", + "fileType": "Tipo", + "fileSize": "Tamaño", + "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 c3c1ac0..444a14f 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -54,6 +54,34 @@ "japanese": "日本語", "korean": "韓国語" }, + "fileManagement": { + "title": "ファイル管理", + "noFiles": "ファイルがありません", + "noFilesDescription": "アップロードしたファイルがここに表示されます", + "uploadButton": "ファイルをアップロード", + "uploading": "アップロード中...", + "fileName": "ファイル名", + "fileType": "種類", + "fileSize": "サイズ", + "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 5df95ee..832420d 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -54,6 +54,34 @@ "japanese": "일본어", "korean": "한국어" }, + "fileManagement": { + "title": "파일 관리", + "noFiles": "파일 없음", + "noFilesDescription": "업로드한 파일이 여기에 표시됩니다", + "uploadButton": "파일 업로드", + "uploading": "업로드 중...", + "fileName": "파일 이름", + "fileType": "유형", + "fileSize": "크기", + "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 213ee47..639ad89 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -54,6 +54,34 @@ "japanese": "日语", "korean": "韩语" }, + "fileManagement": { + "title": "文件管理", + "noFiles": "暂无文件", + "noFilesDescription": "您上传的文件将会显示在这里", + "uploadButton": "上传文件", + "uploading": "上传中...", + "fileName": "文件名称", + "fileType": "类型", + "fileSize": "大小", + "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 289a94e..e23ac6e 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -54,6 +54,34 @@ "japanese": "日文", "korean": "韓文" }, + "fileManagement": { + "title": "檔案管理", + "noFiles": "暫無檔案", + "noFilesDescription": "您上傳的檔案將會顯示在這裡", + "uploadButton": "上傳檔案", + "uploading": "上傳中...", + "fileName": "檔案名稱", + "fileType": "類型", + "fileSize": "大小", + "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/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 From 6e77c833d15f7e7a0dd5fb8acf2579b19b9a62af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sun, 27 Apr 2025 20:30:07 +0800 Subject: [PATCH 5/7] Fix: Add updatedAt date to fileData --- src/components/pages/FileManagementPage.tsx | 68 +++++++++++++++++---- src/locales/en/translation.json | 1 + src/locales/es/translation.json | 9 +-- src/locales/ja/translation.json | 5 +- src/locales/ko/translation.json | 1 + src/locales/zh-CN/translation.json | 1 + src/locales/zh-TW/translation.json | 1 + src/services/database-integration.ts | 1 + src/types/file.ts | 1 + 9 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/components/pages/FileManagementPage.tsx b/src/components/pages/FileManagementPage.tsx index 8ffcab5..07e9106 100644 --- a/src/components/pages/FileManagementPage.tsx +++ b/src/components/pages/FileManagementPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Upload, @@ -33,9 +33,10 @@ export const FileManagementPage = () => { const [showRenameModal, setShowRenameModal] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [newFileName, setNewFileName] = useState(""); - const [sortBy, setSortBy] = useState<"name" | "size" | "type">("name"); - const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + 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: '', @@ -44,6 +45,10 @@ export const FileManagementPage = () => { 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(() => { @@ -164,6 +169,9 @@ export const FileManagementPage = () => { 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; }); @@ -192,7 +200,8 @@ export const FileManagementPage = () => { const fileData = { name: file.name, type: file.type, - size: file.size + size: file.size, + updatedAt: new Date() }; // Save to database @@ -287,6 +296,11 @@ export const FileManagementPage = () => { 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); @@ -328,8 +342,30 @@ export const FileManagementPage = () => { } }; - // Toggle sort direction - const handleSortChange = (sortKey: "name" | "size" | "type") => { + // 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"); @@ -338,6 +374,7 @@ export const FileManagementPage = () => { setSortBy(sortKey); setSortDirection("asc"); } + setIsSortOptionsOpen(false); }; const handleDeleteClick = (file: FileData) => { @@ -475,19 +512,26 @@ export const FileManagementPage = () => { {/* Sort dropdown */}
-
+
+ -
- - - -
@@ -565,17 +602,27 @@ export const FileManagementPage = () => { - - - - - + + {/* */} + + + {sortedFiles.map((file) => ( - - - - + {/* */} + +
{t("fileManagement.fileName")}{t("fileManagement.fileType")}{t("fileManagement.fileSize")}{t("fileManagement.updatedAt")}{t("fileManagement.actions")} + {t("fileManagement.fileName")} + + {t("fileManagement.fileType")} + + {t("fileManagement.fileSize")} + + {t("fileManagement.updatedAt")} + + {t("fileManagement.actions")} +
@@ -584,11 +631,24 @@ export const FileManagementPage = () => { {file.name} {file.type.split("/").pop() || "Unknown"}{formatFileSize(file.size)}{formatDate(file.updatedAt)} + {file.type.split("/").pop() || "Unknown"} + + {formatFileSize(file.size)} + + {formatDate(file.updatedAt)} +
+ -
- + */}