Skip to content

Commit 42b7215

Browse files
committed
Function: Adding support Image generation in chat
1 parent 01398d2 commit 42b7215

File tree

5 files changed

+203
-5
lines changed

5 files changed

+203
-5
lines changed

src/components/chat/ChatMessageArea.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ProviderIcon from '../ui/ProviderIcon';
1212
import { useTranslation } from '../../hooks/useTranslation';
1313
import FileUploadButton from './FileUploadButton';
1414
import FileAttachmentDisplay from './FileAttachmentDisplay';
15+
import ImageGenerationButton from './ImageGenerationButton';
1516

1617
interface ChatMessageAreaProps {
1718
activeConversation: Conversation | null;
@@ -319,9 +320,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
319320
:<></>;
320321

321322
return (
322-
<div className="flex flex-col w-full h-full max-w-full">
323+
<div className="flex flex-col w-full h-full">
323324
{/* Messages area */}
324-
<div className="p-4 space-y-4 overflow-y-auto">
325+
<div className="flex-1 p-4 space-y-4 overflow-y-auto">
325326
{getMessagesList().map((message) => {
326327
if(message.role === 'system') return null;
327328

@@ -532,6 +533,11 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
532533
disabled={isLoading || isCurrentlyStreaming}
533534
/>
534535
)}
536+
537+
{/* Image generation button */}
538+
<ImageGenerationButton
539+
disabled={isLoading || isCurrentlyStreaming}
540+
/>
535541
</div>
536542

537543
{/* Web search element */}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import { Image } from 'lucide-react';
3+
import { SettingsService } from '../../services/settings-service';
4+
import { AIServiceCapability } from '../../types/capabilities';
5+
import ProviderIcon from '../ui/ProviderIcon';
6+
import { useTranslation } from '../../hooks/useTranslation';
7+
8+
interface ImageGenerationButtonProps {
9+
onImageGenerate?: (prompt: string, provider: string, model: string) => void;
10+
disabled?: boolean;
11+
}
12+
13+
interface ProviderModel {
14+
providerName: string;
15+
modelId: string;
16+
modelName: string;
17+
}
18+
19+
const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
20+
disabled = false
21+
}) => {
22+
const { t } = useTranslation();
23+
const [isPopupOpen, setIsPopupOpen] = useState(false);
24+
const [providers, setProviders] = useState<ProviderModel[]>([]);
25+
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
26+
const [selectedModel, setSelectedModel] = useState<string | null>(null);
27+
const popupRef = useRef<HTMLDivElement>(null);
28+
const buttonRef = useRef<HTMLButtonElement>(null);
29+
30+
// Load available image generation providers and models
31+
useEffect(() => {
32+
const loadProviders = () => {
33+
const settingsService = SettingsService.getInstance();
34+
const availableProviders: ProviderModel[] = [];
35+
36+
// Get all providers from settings
37+
const settings = settingsService.getSettings();
38+
const providerIds = Object.keys(settings.providers);
39+
40+
for (const providerId of providerIds) {
41+
// Get the provider's settings
42+
const providerSettings = settingsService.getProviderSettings(providerId);
43+
44+
if (providerSettings.models) {
45+
// For now, just assume all models have image generation capability
46+
// This would need to be updated once proper capability detection is implemented
47+
for (const model of providerSettings.models) {
48+
// Check if model has image generation capability
49+
if (model.modelCapabilities?.includes(AIServiceCapability.ImageGeneration)) {
50+
availableProviders.push({
51+
providerName: providerId,
52+
modelId: model.modelId,
53+
modelName: model.modelName
54+
});
55+
}
56+
}
57+
}
58+
}
59+
60+
setProviders(availableProviders);
61+
62+
// Set default selected provider and model if available
63+
if (availableProviders.length > 0) {
64+
setSelectedProvider(availableProviders[0].providerName);
65+
setSelectedModel(availableProviders[0].modelId);
66+
}
67+
};
68+
69+
loadProviders();
70+
}, []);
71+
72+
// Handle click outside to close popup
73+
useEffect(() => {
74+
const handleClickOutside = (event: MouseEvent) => {
75+
if (
76+
popupRef.current &&
77+
!popupRef.current.contains(event.target as Node) &&
78+
buttonRef.current &&
79+
!buttonRef.current.contains(event.target as Node)
80+
) {
81+
setIsPopupOpen(false);
82+
}
83+
};
84+
85+
document.addEventListener('mousedown', handleClickOutside);
86+
return () => {
87+
document.removeEventListener('mousedown', handleClickOutside);
88+
};
89+
}, []);
90+
91+
const togglePopup = () => {
92+
setIsPopupOpen(!isPopupOpen);
93+
};
94+
95+
const handleProviderModelSelect = (providerName: string, modelId: string) => {
96+
setSelectedProvider(providerName);
97+
setSelectedModel(modelId);
98+
setIsPopupOpen(false);
99+
};
100+
101+
const isButtonEnabled = !disabled && providers.length > 0;
102+
103+
return (
104+
<div className="relative">
105+
<button
106+
ref={buttonRef}
107+
type="button"
108+
onClick={togglePopup}
109+
disabled={!isButtonEnabled}
110+
className="flex items-center justify-center w-8 h-8 rounded-full image-generation-button focus:outline-none"
111+
title={isButtonEnabled ? t('chat.generateImage') : t('chat.imageGenerationNotAvailable')}
112+
>
113+
<Image size={20} />
114+
</button>
115+
116+
{isPopupOpen && (
117+
<div
118+
ref={popupRef}
119+
className="absolute z-10 mt-2 image-generation-popup"
120+
style={{ bottom: '100%', left: 0, minWidth: '220px' }}
121+
>
122+
<div className="p-2">
123+
<div className="mb-2 text-sm font-medium text-gray-700">
124+
{t('chat.selectImageProvider')}
125+
</div>
126+
<div className="overflow-y-auto max-h-60">
127+
{providers.map((provider) => (
128+
<div
129+
key={`${provider.providerName}-${provider.modelId}`}
130+
className={`flex items-center px-3 py-2 cursor-pointer rounded-md ${
131+
selectedProvider === provider.providerName && selectedModel === provider.modelId
132+
? 'image-generation-provider-selected'
133+
: 'image-generation-provider-item'
134+
}`}
135+
onClick={() => handleProviderModelSelect(provider.providerName, provider.modelId)}
136+
>
137+
<ProviderIcon providerName={provider.providerName} className="w-5 h-5 mr-2" />
138+
<div className="flex flex-col">
139+
<span className="text-sm font-medium">{provider.providerName}</span>
140+
<span className="text-xs text-gray-500">{provider.modelName}</span>
141+
</div>
142+
</div>
143+
))}
144+
145+
{providers.length === 0 && (
146+
<div className="px-3 py-2 text-sm text-gray-500">
147+
{t('chat.noImageProvidersAvailable')}
148+
</div>
149+
)}
150+
</div>
151+
</div>
152+
</div>
153+
)}
154+
</div>
155+
);
156+
};
157+
158+
export default ImageGenerationButton;

src/components/pages/ChatPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ export const ChatPage = () => {
372372
<div className="flex flex-col w-full h-full bg-white">
373373

374374
{/* Main content with chat history and messages */}
375-
<div className="flex w-full overflow-hidden">
375+
<div className="flex w-full h-full overflow-hidden">
376376
<ChatHistoryList
377377
conversations={conversations}
378378
folders={folders}
@@ -387,7 +387,7 @@ export const ChatPage = () => {
387387
onMoveConversation={handleMoveConversation}
388388
/>
389389

390-
<div className="flex flex-col w-full min-w-0 overflow-hidden">
390+
<div className="flex flex-col w-full h-full overflow-hidden">
391391
{isApiKeyMissing && (
392392
<div className="p-2 text-sm text-center text-yellow-800 bg-yellow-100">
393393
Please set your API key for the selected provider in the settings.

src/components/pages/TranslationPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const TranslationPage: React.FC = () => {
1313
const [sourceText, setSourceText] = useState('');
1414
const [translatedText, setTranslatedText] = useState('');
1515
const [isTranslating, setIsTranslating] = useState(false);
16-
const [sourceLanguage, setSourceLanguage] = useState('auto');
1716
const [targetLanguage, setTargetLanguage] = useState('en');
1817
const [error, setError] = useState<Error | null>(null);
1918
const [isApiKeyMissing, setIsApiKeyMissing] = useState(true);

src/styles/tensorblock-light.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,4 +602,39 @@
602602
.text-light-hint{
603603
color: var(--surface-400);
604604
}
605+
606+
.image-generation-button {
607+
background-color: transparent;
608+
color: var(--primary-500);
609+
}
610+
611+
.image-generation-button:hover {
612+
background-color: var(--primary-100);
613+
color: var(--primary-600);
614+
}
615+
616+
.image-generation-button:disabled {
617+
background-color: transparent;
618+
color: var(--surface-400);
619+
cursor: not-allowed;
620+
}
621+
622+
.image-generation-popup {
623+
background-color: var(--surface-0);
624+
border: 1px solid var(--primary-200);
625+
border-radius: 0.5rem;
626+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
627+
}
628+
629+
.image-generation-provider-item {
630+
background-color: transparent;
631+
}
632+
633+
.image-generation-provider-item:hover {
634+
background-color: var(--primary-100);
635+
}
636+
637+
.image-generation-provider-selected {
638+
background-color: var(--primary-200);
639+
}
605640
}

0 commit comments

Comments
 (0)