diff --git a/package-lock.json b/package-lock.json index c03411e..995dcd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,13 @@ "name": "tensorblock-studio", "version": "0.0.1", "dependencies": { - "@ai-sdk/fireworks": "^0.2.5", - "@ai-sdk/google": "^1.2.5", - "@ai-sdk/openai": "^1.3.6", - "@ai-sdk/togetherai": "^0.2.5", - "@anthropic-ai/sdk": "^0.39.0", + "@ai-sdk/fireworks": "^0.2.13", + "@ai-sdk/google": "^1.2.13", + "@ai-sdk/openai": "^1.3.20", + "@ai-sdk/togetherai": "^0.2.13", + "@anthropic-ai/sdk": "^0.40.0", "@openrouter/ai-sdk-provider": "^0.4.5", - "ai": "^4.2.10", + "ai": "^4.3.10", "axios": "^1.8.4", "dotenv": "^16.4.7", "electron-builder": "26.0.12", @@ -63,13 +63,13 @@ } }, "node_modules/@ai-sdk/fireworks": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@ai-sdk/fireworks/-/fireworks-0.2.6.tgz", - "integrity": "sha512-2aqKIJ9A6ioRcANxxI6CpxWSS7kYUJDwHyxQiQNw3Ng/rz4u6oa0bOEUWHistP8uKzalveXjooLPZxa/Ie2iFg==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/fireworks/-/fireworks-0.2.13.tgz", + "integrity": "sha512-LYS0gof7dgJ2sfOU8WmKobHVV8gHfoFtLmiKfhNJjhOnF0a8EA5mX6uWmN///j8mtst7lBLqOw4Q3DNJFoqS3g==", "dependencies": { - "@ai-sdk/openai-compatible": "0.2.6", - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4" + "@ai-sdk/openai-compatible": "0.2.13", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" }, "engines": { "node": ">=18" @@ -79,12 +79,12 @@ } }, "node_modules/@ai-sdk/google": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.7.tgz", - "integrity": "sha512-swGJ4nPRB83ZQgR9W5fF7usvZmZsBN61+dm03Hz/dRVVLLLTVZq/0YtpFZ8Yj0utNs9K+NvY0456e7cN3Ff8TQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.13.tgz", + "integrity": "sha512-nnHDzbX1Zst28AjP3718xSWsEqx++qmFuqmnDc2Htelc02HyO6WkWOXMH+YVK3W8zdIyZEKpHL9KKlql7pa10A==", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4" + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" }, "engines": { "node": ">=18" @@ -94,12 +94,12 @@ } }, "node_modules/@ai-sdk/openai": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.7.tgz", - "integrity": "sha512-JjjEulfMgH5kGyCWKdyhxNLSIs2qBkUr6LtzNJtYlV23e5RbOHA5qgbbxn6IbGxeZs4xpPhpGmOnUVzh7GZwcg==", + "version": "1.3.20", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.20.tgz", + "integrity": "sha512-/DflUy7ROG9k6n6YTXMBFPbujBKnbGY58f3CwvicLvDar9nDAloVnUWd3LUoOxpSVnX8vtQ7ngxF52SLWO6RwQ==", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4" + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" }, "engines": { "node": ">=18" @@ -109,12 +109,12 @@ } }, "node_modules/@ai-sdk/openai-compatible": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-0.2.6.tgz", - "integrity": "sha512-UOPEWIqG3l5K9O+p7gqiCOWzx66JtmG9v9Mab+S4E7WE34EN6u1QS1pX+RDlRDhZ0/8gNJif0r4Xlc+Ti03yNA==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-0.2.13.tgz", + "integrity": "sha512-tB+lL8Z3j0qDod/mvxwjrPhbLUHp/aQW+NvMoJaqeTtP+Vmv5qR800pncGczxn5WN0pllQm+7aIRDnm69XeSbg==", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4" + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" }, "engines": { "node": ">=18" @@ -124,9 +124,9 @@ } }, "node_modules/@ai-sdk/provider": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz", - "integrity": "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", "dependencies": { "json-schema": "^0.4.0" }, @@ -135,11 +135,11 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.4.tgz", - "integrity": "sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.7.tgz", + "integrity": "sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==", "dependencies": { - "@ai-sdk/provider": "1.1.0", + "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, @@ -151,12 +151,12 @@ } }, "node_modules/@ai-sdk/react": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.6.tgz", - "integrity": "sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.9.tgz", + "integrity": "sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==", "dependencies": { - "@ai-sdk/provider-utils": "2.2.4", - "@ai-sdk/ui-utils": "1.2.5", + "@ai-sdk/provider-utils": "2.2.7", + "@ai-sdk/ui-utils": "1.2.8", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -174,13 +174,13 @@ } }, "node_modules/@ai-sdk/togetherai": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@ai-sdk/togetherai/-/togetherai-0.2.6.tgz", - "integrity": "sha512-AV3CABMKlIniCe5owr6H/kSirfk3Y/MeBAetrNJxRDhmrxa5VXzbWeMxS5xeS8crqFXWJPPLcwPiwOuFtpQMrA==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/togetherai/-/togetherai-0.2.13.tgz", + "integrity": "sha512-DmcH+5aeLPpVCQYZgmM1nnKNMkqvJR8lWStFze5+PbKJsUHGqli8zdGEEOJ7iGWGOFONo7EpdW/tJeGP1xNXdw==", "dependencies": { - "@ai-sdk/openai-compatible": "0.2.6", - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4" + "@ai-sdk/openai-compatible": "0.2.13", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" }, "engines": { "node": ">=18" @@ -190,12 +190,12 @@ } }, "node_modules/@ai-sdk/ui-utils": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.5.tgz", - "integrity": "sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.8.tgz", + "integrity": "sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7", "zod-to-json-schema": "^3.24.1" }, "engines": { @@ -231,9 +231,9 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", - "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.40.0.tgz", + "integrity": "sha512-NkIgtZxa4nWBzXtDuYe6Wumu1+cgEuEQtol8b1bVSjtS7Dvqex6iNdxaddV8PkRC8Z8mhbNw2m7j+9AD9uRRKw==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -2926,14 +2926,14 @@ } }, "node_modules/ai": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.1.tgz", - "integrity": "sha512-6RSRE0x0FAUZxWpLOq6yrh1IFXakvvJgHs8xPHtt8VmsTqhgjP5GClguaGs+KCpVbVfdygwgji7YJjOwq80suQ==", - "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.4", - "@ai-sdk/react": "1.2.6", - "@ai-sdk/ui-utils": "1.2.5", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.10.tgz", + "integrity": "sha512-jw+ahNu+T4SHj9gtraIKtYhanJI6gj2IZ5BFcfEHgoyQVMln5a5beGjzl/nQSX6FxyLqJ/UBpClRa279EEKK/Q==", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7", + "@ai-sdk/react": "1.2.9", + "@ai-sdk/ui-utils": "1.2.8", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, diff --git a/package.json b/package.json index b6cc7fe..e8783c0 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,13 @@ "preview": "vite preview" }, "dependencies": { - "@ai-sdk/fireworks": "^0.2.5", - "@ai-sdk/google": "^1.2.5", - "@ai-sdk/openai": "^1.3.6", - "@ai-sdk/togetherai": "^0.2.5", - "@anthropic-ai/sdk": "^0.39.0", + "@ai-sdk/fireworks": "^0.2.13", + "@ai-sdk/google": "^1.2.13", + "@ai-sdk/openai": "^1.3.20", + "@ai-sdk/togetherai": "^0.2.13", + "@anthropic-ai/sdk": "^0.40.0", "@openrouter/ai-sdk-provider": "^0.4.5", - "ai": "^4.2.10", + "ai": "^4.3.10", "axios": "^1.8.4", "dotenv": "^16.4.7", "electron-builder": "26.0.12", diff --git a/src/App.tsx b/src/App.tsx index 15404fe..8532843 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,13 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { ChatPage } from './components/pages/ChatPage'; +import { ImageGenerationPage } from './components/pages/ImageGenerationPage'; +import { TranslationPage } from './components/pages/TranslationPage'; import MainLayout from './components/layout/MainLayout'; import DatabaseInitializer from './components/core/DatabaseInitializer'; function App() { + const [activePage, setActivePage] = useState('chat'); + const [showSettings, setShowSettings] = useState(false); // Handle link clicks useEffect(() => { @@ -22,11 +26,16 @@ function App() { }; }, []); + const handlePageChange = (page: string) => { + setActivePage(page); + }; return ( - - + + {activePage === 'chat' && } + {activePage === 'image' && } + {activePage === 'translation' && } ); diff --git a/src/components/chat/ChatMessageArea.tsx b/src/components/chat/ChatMessageArea.tsx index f090cb0..043d407 100644 --- a/src/components/chat/ChatMessageArea.tsx +++ b/src/components/chat/ChatMessageArea.tsx @@ -12,6 +12,7 @@ import ProviderIcon from '../ui/ProviderIcon'; import { useTranslation } from '../../hooks/useTranslation'; import FileUploadButton from './FileUploadButton'; import FileAttachmentDisplay from './FileAttachmentDisplay'; +import ImageGenerationButton from './ImageGenerationButton'; interface ChatMessageAreaProps { activeConversation: Conversation | null; @@ -319,7 +320,7 @@ export const ChatMessageArea: React.FC = ({ :<>; return ( -
+
{/* Messages area */}
{getMessagesList().map((message) => { @@ -527,11 +528,16 @@ export const ChatMessageArea: React.FC = ({
{/* File upload button */} {onSendMessageWithFiles && ( - - )} + + )} + + {/* Image generation button */} +
{/* Web search element */} diff --git a/src/components/chat/ImageGenerationButton.tsx b/src/components/chat/ImageGenerationButton.tsx new file mode 100644 index 0000000..276f562 --- /dev/null +++ b/src/components/chat/ImageGenerationButton.tsx @@ -0,0 +1,158 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Image } from 'lucide-react'; +import { SettingsService } from '../../services/settings-service'; +import { AIServiceCapability } from '../../types/capabilities'; +import ProviderIcon from '../ui/ProviderIcon'; +import { useTranslation } from '../../hooks/useTranslation'; + +interface ImageGenerationButtonProps { + onImageGenerate?: (prompt: string, provider: string, model: string) => void; + disabled?: boolean; +} + +interface ProviderModel { + providerName: string; + modelId: string; + modelName: string; +} + +const ImageGenerationButton: React.FC = ({ + disabled = false +}) => { + const { t } = useTranslation(); + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [providers, setProviders] = useState([]); + const [selectedProvider, setSelectedProvider] = useState(null); + const [selectedModel, setSelectedModel] = useState(null); + const popupRef = useRef(null); + const buttonRef = useRef(null); + + // Load available image generation providers and models + useEffect(() => { + const loadProviders = () => { + const settingsService = SettingsService.getInstance(); + const availableProviders: ProviderModel[] = []; + + // Get all providers from settings + const settings = settingsService.getSettings(); + const providerIds = Object.keys(settings.providers); + + for (const providerId of providerIds) { + // Get the provider's settings + 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 + for (const model of providerSettings.models) { + // Check if model has image generation capability + if (model.modelCapabilities?.includes(AIServiceCapability.ImageGeneration)) { + availableProviders.push({ + providerName: providerId, + modelId: model.modelId, + modelName: model.modelName + }); + } + } + } + } + + setProviders(availableProviders); + + // Set default selected provider and model if available + if (availableProviders.length > 0) { + setSelectedProvider(availableProviders[0].providerName); + setSelectedModel(availableProviders[0].modelId); + } + }; + + loadProviders(); + }, []); + + // Handle click outside to close popup + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + popupRef.current && + !popupRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setIsPopupOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const togglePopup = () => { + setIsPopupOpen(!isPopupOpen); + }; + + const handleProviderModelSelect = (providerName: string, modelId: string) => { + setSelectedProvider(providerName); + setSelectedModel(modelId); + setIsPopupOpen(false); + }; + + const isButtonEnabled = !disabled && providers.length > 0; + + return ( +
+ + + {isPopupOpen && ( +
+
+
+ {t('chat.selectImageProvider')} +
+
+ {providers.map((provider) => ( +
handleProviderModelSelect(provider.providerName, provider.modelId)} + > + +
+ {provider.providerName} + {provider.modelName} +
+
+ ))} + + {providers.length === 0 && ( +
+ {t('chat.noImageProvidersAvailable')} +
+ )} +
+
+
+ )} +
+ ); +}; + +export default ImageGenerationButton; \ No newline at end of file diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 1d82268..1ea8f53 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from 'react'; +import React, { ReactNode } from 'react'; import Sidebar from './Sidebar'; import SettingsPage from '../pages/SettingsPage'; import TopBar from './TopBar'; @@ -6,32 +6,35 @@ import { SettingsService } from '../../services/settings-service'; interface MainLayoutProps { children: ReactNode; + activePage: string; + onChangePage: (page: string) => void; + showSettings: boolean; + setShowSettings: (showSettings: boolean) => void; } -const MainLayout: React.FC = ({ children }) => { - const [activePage, setActivePage] = useState('chat'); - const [showSettings, setShowSettings] = useState(false); +const MainLayout: React.FC = ({ + children, + activePage, + onChangePage, + showSettings, + setShowSettings +}) => { + + // Handle settings dialog + const handleOpenSettingsDialog = () => { + setShowSettings(true); + }; // Handle page changes const handlePageChange = (page: string) => { - if(activePage === 'settings' && page !== activePage){ - // Save settings - } - if (page === 'settings') { setShowSettings(true); - setActivePage('settings'); } else { setShowSettings(false); - setActivePage(page); + onChangePage(page); } }; - // Handle page changes - const handleOpenSettingsDialog = () => { - handlePageChange('settings'); - }; - // Handle selecting a model const handleSelectModel = (modelId: string, provider: string) => { SettingsService.getInstance().setSelectedModel(modelId); @@ -46,17 +49,19 @@ const MainLayout: React.FC = ({ children }) => { onOpenSettingsDialog={handleOpenSettingsDialog} /> -
+
-
+
{/* Main content */} -
-
+
+
{children}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 76e5e8b..a77016c 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,20 +1,45 @@ import React from 'react'; -import { MessageSquare, Settings } from 'lucide-react'; +import { MessageSquare, Settings, Image, Languages } from 'lucide-react'; interface SidebarProps { activePage: string; onChangePage: (page: string) => void; + showSettings: boolean; + setShowSettings: (showSettings: boolean) => void; } -export const Sidebar: React.FC = ({ activePage, onChangePage }) => { +export const Sidebar: React.FC = ({ + activePage, + onChangePage, + showSettings, + setShowSettings +}) => { + + const getActivePage = () => { + if(showSettings){ + return 'settings'; + } + else if(activePage === 'chat'){ + return 'chat'; + } + else if(activePage === 'image'){ + return 'image'; + } + else if(activePage === 'translation'){ + return 'translation'; + } + + return ''; + } + return (
{/* Navigation buttons */} -
+
- {/* Add more navigation buttons here as needed */} + + +
{/* Settings button at bottom */}