Skip to content

Commit de0c700

Browse files
committed
Fix: Add image generation display to markdown message
1 parent 17aa471 commit de0c700

File tree

10 files changed

+280
-41
lines changed

10 files changed

+280
-41
lines changed

src/components/chat/ImageGenerationButton.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
141141
});
142142
};
143143

144-
const isButtonEnabled = !disabled && providers.length > 0 && isEnabled;
145144
const buttonClass = `flex items-center justify-center w-8 h-8 rounded-full focus:outline-none ${
146145
isEnabled ? 'image-generation-button' : 'text-gray-400 bg-gray-100'
147146
}`;
@@ -152,10 +151,10 @@ const ImageGenerationButton: React.FC<ImageGenerationButtonProps> = ({
152151
ref={buttonRef}
153152
type="button"
154153
onClick={togglePopup}
155-
disabled={!providers.length > 0 || disabled}
154+
disabled={providers.length === 0 || disabled}
156155
className={buttonClass}
157156
title={
158-
!providers.length > 0
157+
providers.length === 0
159158
? t('chat.imageGenerationNotAvailable')
160159
: isEnabled
161160
? t('chat.generateImage')

src/components/chat/MarkdownContent.tsx

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { HTMLProps } from 'react';
1111
import { MessageContent, MessageContentType } from '../../types/chat';
1212
import { MessageHelper } from '../../services/message-helper';
1313
import FileAttachmentDisplay from './FileAttachmentDisplay';
14+
import { Loader2 } from 'lucide-react';
15+
import { useTranslation } from '../../hooks/useTranslation';
1416

1517
interface MarkdownContentProps {
1618
content: MessageContent[];
@@ -23,28 +25,38 @@ type CodeProps = React.ClassAttributes<HTMLElement> &
2325
};
2426

2527
export const MarkdownContent: React.FC<MarkdownContentProps> = ({ content, isUserMessage = false }) => {
28+
const { t } = useTranslation();
2629
const [processedContent, setProcessedContent] = useState('');
2730
const [thinkContent, setThinkContent] = useState<string | null>(null);
2831
const [isThinkExpanded, setIsThinkExpanded] = useState(true);
2932
const [fileContents, setFileContents] = useState<MessageContent[]>([]);
33+
const [imageContents, setImageContents] = useState<MessageContent[]>([]);
34+
const [isProcessingImage, setIsProcessingImage] = useState(false);
35+
const [imageGenerationError, setImageGenerationError] = useState<string | null>(null);
3036

31-
// Process content and check for thinking blocks and files
37+
// Process content and check for thinking blocks, files, and image generation
3238
useEffect(() => {
33-
// Extract text and file contents
39+
// Extract text, file, and image contents
3440
const textContents: MessageContent[] = [];
3541
const files: MessageContent[] = [];
42+
const images: MessageContent[] = [];
3643

3744
content.forEach(item => {
3845
if (item.type === MessageContentType.Text) {
3946
textContents.push(item);
4047
} else if (item.type === MessageContentType.File) {
4148
files.push(item);
49+
} else if (item.type === MessageContentType.Image) {
50+
images.push(item);
4251
}
4352
});
4453

4554
// Save file contents for rendering
4655
setFileContents(files);
4756

57+
// Save image contents for rendering
58+
setImageContents(images);
59+
4860
// Create a function for a safer replacement
4961
function safeReplace(str: string, search: string, replace: string): string {
5062
// Split the string by the search term
@@ -55,6 +67,32 @@ export const MarkdownContent: React.FC<MarkdownContentProps> = ({ content, isUse
5567

5668
let processed = MessageHelper.MessageContentToText(textContents);
5769

70+
// Detect image generation in progress
71+
const imageGenInProgressMatch = processed.match(
72+
/(?:generating|creating|processing)\s+(?:an\s+)?image(?:s)?\s+(?:with|using|for|from)?(?:\s+prompt)?(?::|;)?\s*["']?([^"']+)["']?/i
73+
) || processed.match(/\bimage\s+generation\s+in\s+progress\b/i);
74+
75+
if ((imageGenInProgressMatch && images.length === 0) ||
76+
(processed.includes('generate_image') && processed.includes('tool call') && images.length === 0)) {
77+
setIsProcessingImage(true);
78+
setImageGenerationError(null);
79+
} else {
80+
setIsProcessingImage(false);
81+
}
82+
83+
// Detect image generation errors
84+
const imageGenErrorMatch = processed.match(
85+
/(?:error|failed|couldn't|unable)\s+(?:in\s+)?(?:generating|creating|processing)\s+(?:an\s+)?image(?:s)?(?::|;)?\s*["']?([^"']+)["']?/i
86+
) || processed.match(/\bimage\s+generation\s+(?:error|failed)\b:?\s*["']?([^"']+)["']?/i);
87+
88+
if (imageGenErrorMatch || (processed.includes('error') && processed.includes('generate_image'))) {
89+
const errorMessage = imageGenErrorMatch ? (imageGenErrorMatch[1] || "Unknown error occurred") : "Failed to generate image";
90+
setImageGenerationError(errorMessage);
91+
setIsProcessingImage(false);
92+
} else if (images.length > 0) {
93+
setImageGenerationError(null);
94+
}
95+
5896
// Check if content contains thinking block
5997
const thinkMatch = processed.match(/<think>([\s\S]*?)<\/think>([\s\S]*)/);
6098

@@ -134,6 +172,46 @@ export const MarkdownContent: React.FC<MarkdownContentProps> = ({ content, isUse
134172
</div>
135173
)}
136174

175+
{/* Image Generation In Progress */}
176+
{isProcessingImage && (
177+
<div className="p-4 mb-3 border border-gray-200 rounded-md">
178+
<div className="flex items-center gap-2 mb-2">
179+
<Loader2 size={20} className="text-blue-500 animate-spin" />
180+
<span className="font-medium">{t('imageGeneration.generating')}</span>
181+
</div>
182+
<div className="flex items-center justify-center w-full h-40 bg-gray-100 rounded-md">
183+
<span className="text-sm text-gray-400">{t('imageGeneration.creatingImage')}</span>
184+
</div>
185+
</div>
186+
)}
187+
188+
{/* Image Generation Error */}
189+
{imageGenerationError && (
190+
<div className="p-4 mb-3 border border-red-200 rounded-md bg-red-50">
191+
<div className="flex items-center gap-2 mb-2">
192+
<span className="font-medium text-red-600">{t('imageGeneration.generationFailed')}</span>
193+
</div>
194+
<div className="text-sm text-red-600">
195+
{imageGenerationError}
196+
</div>
197+
</div>
198+
)}
199+
200+
{/* Generated Images */}
201+
{imageContents.length > 0 && (
202+
<div className="grid grid-cols-1 gap-4 mb-3 md:grid-cols-2">
203+
{imageContents.map((image, index) => (
204+
<div key={index} className="overflow-hidden border border-gray-200 rounded-md">
205+
<img
206+
src={`data:image/png;base64,${image.content}`}
207+
alt={t('imageGeneration.generatedImage')}
208+
className="w-full h-auto"
209+
/>
210+
</div>
211+
))}
212+
</div>
213+
)}
214+
137215
{thinkContent && (
138216
<div className="mb-4">
139217
<div

src/locales/en/translation.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@
103103
"generationCount": "Generation Count",
104104
"randomSeed": "Random Seed",
105105
"generateButton": "Generate",
106-
"generating": "Generating...",
106+
"generating": "Generating image...",
107+
"creatingImage": "AI is creating your image",
108+
"generationFailed": "Image generation failed",
109+
"generatedImage": "Generated image",
107110
"prompt": "Prompt",
108111
"promptPlaceholder": "Describe the image you want to create, e.g.: a peaceful lake at sunset with mountains in the background",
109112
"results": "Generated Results",

src/locales/es/translation.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,19 @@
9696
"selectModel_search_placeholder": "Buscar modelos..."
9797
},
9898
"imageGeneration": {
99-
"title": "Imagen Generación",
99+
"title": "Generación de Imágenes",
100100
"provider": "Proveedor",
101101
"model": "Modelo",
102102
"imageSize": "Tamaño de Imagen",
103103
"generationCount": "Cantidad de Generaciones",
104104
"randomSeed": "Semilla Aleatoria",
105105
"generateButton": "Generar",
106-
"generating": "Generando...",
106+
"generating": "Generando imagen...",
107+
"creatingImage": "La IA está creando tu imagen",
108+
"generationFailed": "Falló la generación de imagen",
109+
"generatedImage": "Imagen generada",
107110
"prompt": "Prompt",
108-
"promptPlaceholder": "Describe la imagen que quieres crear, ej.: un lago tranquilo al atardecer con montañas en el fondo",
111+
"promptPlaceholder": "Describe la imagen que quieres crear, p.ej.: un lago tranquilo al atardecer con montañas en el fondo",
109112
"results": "Resultados Generados",
110113
"placeholderText": "Ingresa un prompt y haz clic en generar para crear imágenes",
111114
"apiKeyMissing": "Por favor, configura tu clave API para el proveedor seleccionado en la configuración.",

src/locales/ja/translation.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,16 @@
103103
"generationCount": "生成数",
104104
"randomSeed": "ランダムシード",
105105
"generateButton": "生成",
106-
"generating": "生成中...",
106+
"generating": "画像を生成中...",
107+
"creatingImage": "AIが画像を作成しています",
108+
"generationFailed": "画像生成に失敗しました",
109+
"generatedImage": "生成された画像",
107110
"prompt": "プロンプト",
108-
"promptPlaceholder": "作成したい画像を説明してください。例:夕日が沈む静かな湖、背景には山々",
111+
"promptPlaceholder": "作成したい画像を説明してください。例:夕暮れの穏やかな湖と背景の山々",
109112
"results": "生成結果",
110-
"placeholderText": "プロンプトを入力して生成ボタンをクリックして画像を作成",
111-
"apiKeyMissing": "選択したプロバイダーのAPIキーを設定で設定してください",
112-
"seedHelp": "再現可能な結果のためのシード値"
113+
"placeholderText": "プロンプトを入力して生成ボタンをクリックすると画像が作成されます",
114+
"apiKeyMissing": "設定で選択したプロバイダーのAPIキーを設定してください",
115+
"seedHelp": "再現可能な結果のためのシード"
113116
},
114117
"mcpServer": {
115118
"title": "MCPサーバー",

src/locales/ko/translation.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,18 @@
100100
"provider": "제공자",
101101
"model": "모델",
102102
"imageSize": "이미지 크기",
103-
"generationCount": "생성 수량",
103+
"generationCount": "생성 ",
104104
"randomSeed": "랜덤 시드",
105105
"generateButton": "생성",
106-
"generating": "생성 중...",
106+
"generating": "이미지 생성 중...",
107+
"creatingImage": "AI가 이미지를 만들고 있습니다",
108+
"generationFailed": "이미지 생성 실패",
109+
"generatedImage": "생성된 이미지",
107110
"prompt": "프롬프트",
108-
"promptPlaceholder": "만들고 싶은 이미지를 설명하세요. 예: 석양이 지는 고요한 호수, 배경에는 산맥",
109-
"results": "생성 결과",
110-
"placeholderText": "프롬프트를 입력하고 생성 버튼을 클릭하여 이미지 생성",
111-
"apiKeyMissing": "선택한 제공자의 API 키를 설정에서 설정하세요.",
111+
"promptPlaceholder": "원하는 이미지를 설명하세요. 예: 일몰 시 평화로운 호수와 배경에 산이 있는 풍경",
112+
"results": "생성된 결과",
113+
"placeholderText": "프롬프트를 입력하고 생성 버튼을 클릭하여 이미지를 만드세요",
114+
"apiKeyMissing": "설정에서 선택한 제공자의 API 키를 설정해 주세요.",
112115
"seedHelp": "재현 가능한 결과를 위한 시드"
113116
},
114117
"mcpServer": {

src/locales/zh-CN/translation.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,22 @@
9696
"selectModel_search_placeholder": "搜索模型..."
9797
},
9898
"imageGeneration": {
99-
"title": "图片生成",
99+
"title": "图像生成",
100100
"provider": "提供商",
101101
"model": "模型",
102-
"imageSize": "图片尺寸",
102+
"imageSize": "图像尺寸",
103103
"generationCount": "生成数量",
104104
"randomSeed": "随机种子",
105105
"generateButton": "生成",
106-
"generating": "生成中...",
106+
"generating": "正在生成图像...",
107+
"creatingImage": "AI正在创建您的图像",
108+
"generationFailed": "图像生成失败",
109+
"generatedImage": "生成的图像",
107110
"prompt": "提示词",
108-
"promptPlaceholder": "描述你想创建的图片,例如:一个宁静的湖泊,夕阳西下,远处是群山",
111+
"promptPlaceholder": "描述您想要创建的图像,例如:日落时分的平静湖泊,背景是山脉",
109112
"results": "生成结果",
110-
"placeholderText": "输入提示词并点击生成按钮来创建图片",
111-
"apiKeyMissing": "请在设置中为所选提供商设置您的 API 密钥",
113+
"placeholderText": "输入提示词并点击生成按钮创建图像",
114+
"apiKeyMissing": "请在设置中为所选提供商设置API密钥",
112115
"seedHelp": "用于可重现结果的种子"
113116
},
114117
"mcpServer": {

src/locales/zh-TW/translation.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,22 @@
9696
"selectModel_search_placeholder": "搜尋模型..."
9797
},
9898
"imageGeneration": {
99-
"title": "圖片生成",
100-
"provider": "提供商",
99+
"title": "圖像生成",
100+
"provider": "提供者",
101101
"model": "模型",
102-
"imageSize": "圖片尺寸",
102+
"imageSize": "圖像大小",
103103
"generationCount": "生成數量",
104104
"randomSeed": "隨機種子",
105105
"generateButton": "生成",
106-
"generating": "生成中...",
106+
"generating": "正在生成圖像...",
107+
"creatingImage": "AI正在創建您的圖像",
108+
"generationFailed": "圖像生成失敗",
109+
"generatedImage": "生成的圖像",
107110
"prompt": "提示詞",
108-
"promptPlaceholder": "描述你想創建的圖片,例如:一個寧靜的湖泊,夕陽西下,遠處是群山",
111+
"promptPlaceholder": "描述您想要創建的圖像,例如:日落時分的平靜湖泊,背景是山脈",
109112
"results": "生成結果",
110-
"placeholderText": "輸入提示詞並點擊生成按鈕來創建圖片",
111-
"apiKeyMissing": "請在設定中為所選提供商設置您的 API 金鑰",
113+
"placeholderText": "輸入提示詞並點擊生成按鈕創建圖像",
114+
"apiKeyMissing": "請在設定中為所選提供者設置API金鑰",
112115
"seedHelp": "用於可重現結果的種子"
113116
},
114117
"mcpServer": {

src/services/providers/common-provider-service.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { generateText, LanguageModelV1, LanguageModelUsage, Provider, streamText, ToolSet, ToolChoice } from 'ai';
2-
import { v4 as uuidv4 } from 'uuid';
1+
import { generateText, streamText, Provider, type LanguageModelUsage } from 'ai';
32
import { Message, MessageRole } from '../../types/chat';
43
import { AiServiceProvider, CompletionOptions } from '../core/ai-service-provider';
54
import { SettingsService } from '../settings-service';
65
import { StreamControlHandler } from '../streaming-control';
7-
import { AIServiceCapability } from '../../types/capabilities';
8-
import { mapModelCapabilities } from '../../types/capabilities';
9-
import { ModelSettings } from '../../types/settings';
6+
import { v4 as uuidv4 } from 'uuid';
107
import { MessageHelper } from '../message-helper';
8+
import { AIServiceCapability, mapModelCapabilities } from '../../types/capabilities';
9+
import { ModelSettings } from '../../types/settings';
10+
import { LanguageModelV1, ToolChoice, ToolSet } from 'ai';
1111
/**
1212
* Implementation of OpenAI service provider using the AI SDK
1313
*/
@@ -165,6 +165,18 @@ export class CommonProviderHelper implements AiServiceProvider {
165165
presencePenalty: options.presence_penalty,
166166
tools: tools,
167167
toolChoice: toolChoice,
168+
onToolCall: (toolCall) => {
169+
console.log('Tool call:', toolCall);
170+
streamController.onToolCall(toolCall);
171+
},
172+
onToolCallResult: (toolCallId, result) => {
173+
console.log('Tool call result:', toolCallId, result);
174+
streamController.onToolCallResult(toolCallId, result);
175+
},
176+
onToolCallError: (toolCallId, error) => {
177+
console.error('Tool call error:', toolCallId, error);
178+
streamController.onToolCallError(toolCallId, error);
179+
},
168180
onFinish: (result: { usage: LanguageModelUsage }) => {
169181
console.log('OpenAI streaming chat completion finished');
170182
streamController.onFinish(result.usage);
@@ -192,9 +204,26 @@ export class CommonProviderHelper implements AiServiceProvider {
192204
presencePenalty: options.presence_penalty,
193205
tools: tools,
194206
toolChoice: toolChoice,
207+
maxSteps: 3, // Allow multiple steps for tool calls
195208
});
196209

197210
console.log('toolResults: ', toolResults);
211+
212+
// Process tool results for images
213+
if (toolResults && toolResults.length > 0) {
214+
for (const toolResult of toolResults) {
215+
if (toolResult.name === 'generate_image' && toolResult.result?.images) {
216+
const images = toolResult.result.images;
217+
if (Array.isArray(images)) {
218+
for (const imageData of images) {
219+
if (typeof imageData === 'string') {
220+
streamController.onToolCallResult(toolResult.id, { images: [imageData] });
221+
}
222+
}
223+
}
224+
}
225+
}
226+
}
198227

199228
fullText = text;
200229
streamController.onChunk(fullText);

0 commit comments

Comments
 (0)