Compare commits

..

No commits in common. "c5c16ee893987e0ae2aa61d12f6a355b0615268f" and "4efee3a06a2f1c3a2ae1373de8d01f2933675812" have entirely different histories.

10 changed files with 11 additions and 1983 deletions

View File

@ -1,6 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { db } from '@/drizzle/db'; import { db } from '@/drizzle/db';
import { conversations, messages, userSettings, type GeneratedImageData } from '@/drizzle/schema'; import { conversations, messages, userSettings } from '@/drizzle/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { executeTool } from '@/services/tools'; import { executeTool } from '@/services/tools';
@ -94,19 +94,6 @@ function isCodexModel(modelId: string): boolean {
return modelId.startsWith('gpt-') && modelId.includes('codex'); return modelId.startsWith('gpt-') && modelId.includes('codex');
} }
// 判断是否为 Gemini 图片生成模型
function isGeminiImageModel(modelId: string): boolean {
// 支持的图片生成模型列表
const imageModels = [
'gemini-2.0-flash-preview-image-generation',
'gemini-3-pro-image-preview',
'imagen-3.0-generate-002',
];
return imageModels.some(model => modelId.includes(model)) ||
modelId.includes('image-generation') ||
modelId.includes('imagen');
}
// 规范化 URL移除末尾斜杠避免拼接时出现双斜杠 // 规范化 URL移除末尾斜杠避免拼接时出现双斜杠
function normalizeBaseUrl(url: string): string { function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, ''); return url.replace(/\/+$/, '');
@ -372,7 +359,6 @@ export async function POST(request: Request) {
// 判断使用的模型类型 // 判断使用的模型类型
const useModel = model || conversation.model; const useModel = model || conversation.model;
const isCodex = isCodexModel(useModel); const isCodex = isCodexModel(useModel);
const isGeminiImage = isGeminiImageModel(useModel);
// 创建 SSE 响应 // 创建 SSE 响应
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@ -434,7 +420,6 @@ export async function POST(request: Request) {
let totalInputTokens = 0; let totalInputTokens = 0;
let totalOutputTokens = 0; let totalOutputTokens = 0;
let usedTools: string[] = []; // 收集使用过的工具名称 let usedTools: string[] = []; // 收集使用过的工具名称
let generatedImages: GeneratedImageData[] = []; // Gemini 生成的图片
// 如果有文档解析失败,将警告添加到内容开头 // 如果有文档解析失败,将警告添加到内容开头
if (documentParseErrors.length > 0) { if (documentParseErrors.length > 0) {
@ -448,35 +433,13 @@ export async function POST(request: Request) {
} }
// 【重要】处理器选择优先级说明: // 【重要】处理器选择优先级说明:
// 0. 首先检查 isGeminiImageGemini 图片生成模型有专门的处理逻辑 // 1. 首先检查 apiFormat === 'openai':如果用户选择了 "OpenAI 兼容" 格式,
// 1. 然后检查 apiFormat === 'openai':如果用户选择了 "OpenAI 兼容" 格式,
// 则所有模型(包括 Codex 模型)都统一使用 /v1/chat/completions 端点 // 则所有模型(包括 Codex 模型)都统一使用 /v1/chat/completions 端点
// 这是因为第三方中转站通常只支持 OpenAI 兼容的 /v1/chat/completions 端点 // 这是因为第三方中转站通常只支持 OpenAI 兼容的 /v1/chat/completions 端点
// 2. 然后检查 isCodex如果是 Claude 原生格式 + Codex 模型,才使用 /v1/responses 端点 // 2. 然后检查 isCodex如果是 Claude 原生格式 + Codex 模型,才使用 /v1/responses 端点
// 3. 最后是普通的 Claude 原生格式,使用 /v1/messages 端点 // 3. 最后是普通的 Claude 原生格式,使用 /v1/messages 端点
if (isGeminiImage) { if (apiFormat === 'openai') {
// ==================== Gemini 图片生成模型处理 ====================
// Gemini 图片生成模型使用专门的 Gemini API 端点
console.log('[API/chat] 使用 Gemini 图片生成模型:', useModel);
const result = await handleGeminiImageChat({
cchUrl,
apiKey: decryptedApiKey,
model: useModel,
systemPrompt,
temperature,
historyMessages,
message,
controller,
encoder,
images,
});
fullContent = result.fullContent;
generatedImages = result.generatedImages;
totalInputTokens = result.inputTokens;
totalOutputTokens = result.outputTokens;
} else if (apiFormat === 'openai') {
// ==================== OpenAI 兼容格式处理 ==================== // ==================== OpenAI 兼容格式处理 ====================
// 当用户选择 "OpenAI 兼容" 时,无论什么模型都走这个分支 // 当用户选择 "OpenAI 兼容" 时,无论什么模型都走这个分支
// 第三方中转站统一使用 /v1/chat/completions 端点 // 第三方中转站统一使用 /v1/chat/completions 端点
@ -565,7 +528,6 @@ export async function POST(request: Request) {
content: fullContent, content: fullContent,
thinkingContent: thinkingContent || null, thinkingContent: thinkingContent || null,
usedTools: usedTools.length > 0 ? usedTools : null, usedTools: usedTools.length > 0 ? usedTools : null,
generatedImages: generatedImages.length > 0 ? generatedImages : null,
inputTokens: totalInputTokens, inputTokens: totalInputTokens,
outputTokens: totalOutputTokens, outputTokens: totalOutputTokens,
status: 'completed', status: 'completed',
@ -2012,203 +1974,6 @@ async function handleOpenAICompatibleChat(params: OpenAICompatibleChatParams): P
}; };
} }
// ==================== Gemini 图片生成模型处理函数 ====================
interface GeminiImageChatParams {
cchUrl: string;
apiKey: string;
model: string;
systemPrompt: string;
temperature: number;
historyMessages: { role: string; content: string }[];
message: string;
controller: ReadableStreamDefaultController;
encoder: TextEncoder;
// 用户上传的图片
images?: {
type: 'image';
media_type: string;
data: string;
}[];
}
async function handleGeminiImageChat(params: GeminiImageChatParams): Promise<{
fullContent: string;
generatedImages: GeneratedImageData[];
inputTokens: number;
outputTokens: number;
}> {
const {
cchUrl,
apiKey,
model,
systemPrompt,
temperature,
historyMessages,
message,
controller,
encoder,
images,
} = params;
// 创建安全的 stream 写入器
const safeWriter = createSafeStreamWriter(controller, encoder);
// 发送生成开始事件
safeWriter.write({
type: 'image_generation_start',
model,
});
// 构建 Gemini API 请求内容
// Gemini 使用 contents 数组格式
const contents: Array<{
role: 'user' | 'model';
parts: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }>;
}> = [];
// 添加历史消息
for (const msg of historyMessages) {
if (msg.content && msg.content.trim() !== '') {
contents.push({
role: msg.role === 'assistant' ? 'model' : 'user',
parts: [{ text: msg.content }],
});
}
}
// 构建当前用户消息(支持多模态)
const currentParts: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }> = [];
// 如果有图片,先添加图片
if (images && images.length > 0) {
for (const img of images) {
currentParts.push({
inlineData: {
mimeType: img.media_type,
data: img.data,
},
});
}
}
// 添加文本消息
if (message) {
currentParts.push({ text: message });
}
contents.push({
role: 'user',
parts: currentParts,
});
// 构建请求体
const requestBody = {
contents,
systemInstruction: systemPrompt ? { parts: [{ text: systemPrompt }] } : undefined,
generationConfig: {
temperature,
responseModalities: ['TEXT', 'IMAGE'], // 请求同时返回文本和图片
},
};
console.log('[handleGeminiImageChat] Sending request to Gemini API:', {
model,
contentsCount: contents.length,
hasImages: images && images.length > 0,
});
// 发送请求到 Gemini API
// Gemini API 端点格式: /v1beta/models/{model}:generateContent
const geminiEndpoint = `${normalizeBaseUrl(cchUrl)}/v1beta/models/${model}:generateContent`;
const response = await fetch(geminiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': apiKey,
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
}
// 解析 Gemini 响应
const geminiResponse = await response.json();
let fullContent = '';
const generatedImages: GeneratedImageData[] = [];
let totalInputTokens = 0;
let totalOutputTokens = 0;
// 提取 usage 信息
if (geminiResponse.usageMetadata) {
totalInputTokens = geminiResponse.usageMetadata.promptTokenCount || 0;
totalOutputTokens = geminiResponse.usageMetadata.candidatesTokenCount || 0;
}
// 解析候选响应
if (geminiResponse.candidates && geminiResponse.candidates.length > 0) {
const candidate = geminiResponse.candidates[0];
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
// 处理文本内容
if (part.text) {
fullContent += part.text;
safeWriter.write({
type: 'text',
content: part.text,
});
}
// 处理生成的图片
if (part.inlineData) {
const imageData: GeneratedImageData = {
mimeType: part.inlineData.mimeType,
data: part.inlineData.data,
};
generatedImages.push(imageData);
// 发送生成图片事件
safeWriter.write({
type: 'generated_image',
image: imageData,
index: generatedImages.length - 1,
});
console.log('[handleGeminiImageChat] Generated image:', {
mimeType: imageData.mimeType,
dataLength: imageData.data.length,
});
}
}
}
}
// 发送生成完成事件
safeWriter.write({
type: 'image_generation_complete',
imageCount: generatedImages.length,
});
console.log('[handleGeminiImageChat] Response processed:', {
textLength: fullContent.length,
imageCount: generatedImages.length,
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
});
return {
fullContent,
generatedImages,
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
};
}
// 构建 Claude 工具定义 // 构建 Claude 工具定义
function buildClaudeToolDefinitions(toolIds: string[]) { function buildClaudeToolDefinitions(toolIds: string[]) {
const toolMap: Record<string, object> = { const toolMap: Record<string, object> = {

View File

@ -112,8 +112,6 @@ export default function ChatPage({ params }: PageProps) {
uploadedDocuments: (msg.uploadedDocuments as { name: string; size: number; type: string; content: string }[]) || undefined, uploadedDocuments: (msg.uploadedDocuments as { name: string; size: number; type: string; content: string }[]) || undefined,
// 从数据库加载使用的工具列表 // 从数据库加载使用的工具列表
usedTools: (msg.usedTools as string[]) || undefined, usedTools: (msg.usedTools as string[]) || undefined,
// 从数据库加载 AI 生成的图片Gemini 等图片生成模型)
generatedImages: (msg.generatedImages as { mimeType: string; data: string; width?: number; height?: number }[]) || undefined,
})); }));
setInitialMessages(historyMessages); setInitialMessages(historyMessages);
} }
@ -395,31 +393,12 @@ export default function ChatPage({ params }: PageProps) {
setLinkPreviewOpen(true); setLinkPreviewOpen(true);
}; };
// 判断是否为图片生成模型
const isImageGenerationModel = (modelId: string): boolean => {
const imageModels = [
'gemini-2.0-flash-preview-image-generation',
'gemini-3-pro-image-preview',
'imagen-3.0-generate-002',
];
return imageModels.some(model => modelId.includes(model)) ||
modelId.includes('image-generation') ||
modelId.includes('imagen');
};
// 获取模型标签
const getModelTag = (modelId: string, supportsThinking: boolean): string => {
if (isImageGenerationModel(modelId)) return '图片';
if (supportsThinking) return 'Thinking';
return '';
};
// 转换模型格式 // 转换模型格式
const modelOptions = models.map((m) => ({ const modelOptions = models.map((m) => ({
id: m.modelId, id: m.modelId,
name: m.modelId, name: m.modelId,
displayName: m.displayName, displayName: m.displayName,
tag: getModelTag(m.modelId, m.supportsThinking), tag: m.supportsThinking ? 'Thinking' : '',
})); }));
const selectedModel = modelOptions.find((m) => m.id === selectedModelId) || modelOptions[0]; const selectedModel = modelOptions.find((m) => m.id === selectedModelId) || modelOptions[0];
@ -644,8 +623,6 @@ export default function ChatPage({ params }: PageProps) {
uploadedDocuments={message.uploadedDocuments} uploadedDocuments={message.uploadedDocuments}
usedTools={message.usedTools} usedTools={message.usedTools}
pyodideStatus={message.pyodideStatus} pyodideStatus={message.pyodideStatus}
generatedImages={message.generatedImages}
isGeneratingImage={message.isGeneratingImage}
onRegenerate={message.role === 'assistant' && !isStreaming ? handleRegenerate : undefined} onRegenerate={message.role === 'assistant' && !isStreaming ? handleRegenerate : undefined}
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined} onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
onLinkClick={handleLinkClick} onLinkClick={handleLinkClick}

View File

@ -649,48 +649,3 @@ pre[class*="language-"] {
background-color: transparent; background-color: transparent;
} }
} }
/* ========================================
AI 图片生成加载动画
用于 Gemini 等图片生成模型
======================================== */
/* 闪光效果动画 - 从左到右的光线扫过 */
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.animate-shimmer {
animation: shimmer 2s infinite;
}
/* 进度条动画 - 模拟加载进度 */
@keyframes progress {
0% {
width: 0%;
}
20% {
width: 15%;
}
40% {
width: 35%;
}
60% {
width: 55%;
}
80% {
width: 75%;
}
100% {
width: 90%;
}
}
.animate-progress {
animation: progress 8s ease-in-out infinite;
}

View File

@ -13,7 +13,7 @@ import { ImageLightbox } from '@/components/ui/ImageLightbox';
import { DocumentPreview, type DocumentData } from '@/components/ui/DocumentPreview'; import { DocumentPreview, type DocumentData } from '@/components/ui/DocumentPreview';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { Message, User, ToolResult } from '@/types'; import type { Message, User, ToolResult } from '@/types';
import type { UploadedDocument, GeneratedImageData } from '@/hooks/useStreamChat'; import type { UploadedDocument } from '@/hooks/useStreamChat';
// 工具名称中文映射 // 工具名称中文映射
const TOOL_DISPLAY_NAMES: Record<string, string> = { const TOOL_DISPLAY_NAMES: Record<string, string> = {
@ -48,10 +48,6 @@ interface MessageBubbleProps {
message: string; message: string;
progress?: number; progress?: number;
}; };
/** AI 生成的图片Gemini 等图片生成模型) */
generatedImages?: GeneratedImageData[];
/** 是否正在生成图片 */
isGeneratingImage?: boolean;
/** 重新生成回调(仅对 AI 消息有效),传入消息 ID */ /** 重新生成回调(仅对 AI 消息有效),传入消息 ID */
onRegenerate?: (messageId: string) => void; onRegenerate?: (messageId: string) => void;
/** 保存到笔记回调(仅对 AI 消息有效),传入消息内容 */ /** 保存到笔记回调(仅对 AI 消息有效),传入消息内容 */
@ -81,7 +77,7 @@ function getDocumentIcon(type: string) {
return FileText; return FileText;
} }
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, generatedImages, isGeneratingImage, onRegenerate, onSaveToNote, onLinkClick, conversationId, isHighlighted }: MessageBubbleProps) { export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId, isHighlighted }: MessageBubbleProps) {
const isUser = message.role === 'user'; const isUser = message.role === 'user';
const [thinkingExpanded, setThinkingExpanded] = useState(false); const [thinkingExpanded, setThinkingExpanded] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -300,21 +296,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
</div> </div>
)} )}
{/* 检查是否有需要显示在白色卡片内的内容 */} {/* 主要内容 */}
{(() => {
const hasCardContent =
message.content ||
isStreaming ||
(searchImages && searchImages.length > 0) ||
(searchVideos && searchVideos.length > 0) ||
(message.toolResults && message.toolResults.length > 0) ||
pyodideStatus ||
(images && images.length > 0) ||
isGeneratingImage;
if (!hasCardContent) return null;
return (
<div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-md px-5 py-4 shadow-sm"> <div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-md px-5 py-4 shadow-sm">
{/* 搜索到的图片(图片搜索工具结果)- 显示在最上面 */} {/* 搜索到的图片(图片搜索工具结果)- 显示在最上面 */}
{searchImages && searchImages.length > 0 && ( {searchImages && searchImages.length > 0 && (
@ -391,37 +373,6 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
); );
})()} })()}
{/* AI 生成的图片加载状态 */}
{isGeneratingImage && (
<div className="mt-4">
<div className="relative w-full max-w-[400px] aspect-video bg-[var(--color-bg-tertiary)] rounded-lg overflow-hidden">
{/* 闪光动画背景 */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-[var(--color-primary)]/10 to-transparent animate-shimmer" />
{/* 加载内容 */}
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4">
{/* 旋转加载器 */}
<div className="w-12 h-12 border-3 border-[var(--color-border-light)] border-t-[var(--color-primary)] rounded-full animate-spin" />
{/* 加载文字 */}
<div className="flex items-center gap-2 text-[var(--color-text-secondary)]">
<span></span>
<div className="flex gap-1">
<span className="w-1.5 h-1.5 bg-[var(--color-primary)] rounded-full animate-bounce" style={{ animationDelay: '0s' }} />
<span className="w-1.5 h-1.5 bg-[var(--color-primary)] rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
<span className="w-1.5 h-1.5 bg-[var(--color-primary)] rounded-full animate-bounce" style={{ animationDelay: '0.4s' }} />
</div>
</div>
{/* 进度条 */}
<div className="w-48 h-1 bg-[var(--color-border)] rounded-full overflow-hidden">
<div className="h-full bg-[var(--color-primary)] rounded-full animate-progress" />
</div>
</div>
</div>
</div>
)}
{/* 流式状态指示器 */} {/* 流式状态指示器 */}
{isStreaming && message.content && ( {isStreaming && message.content && (
@ -474,105 +425,6 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
</> </>
)} )}
</div> </div>
);
})()}
{/* AI 生成的图片Gemini 等模型)- 单独显示,不包在白色卡片内 */}
{generatedImages && generatedImages.length > 0 && (() => {
// 将 GeneratedImageData 转换为 data URL
const generatedImageUrls = generatedImages.map(
(img) => `data:${img.mimeType};base64,${img.data}`
);
// 复制图片到剪贴板
const handleCopyImage = async (imgUrl: string) => {
try {
const response = await fetch(imgUrl);
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({ [blob.type]: blob })
]);
} catch (error) {
console.error('Failed to copy image:', error);
}
};
// 下载图片
const handleDownloadImage = (imgUrl: string, index: number) => {
const link = document.createElement('a');
link.href = imgUrl;
link.download = `generated-image-${index + 1}.png`;
link.click();
};
return (
<div className="mt-4">
{generatedImageUrls.map((imgUrl, index) => (
<div
key={index}
className="group"
>
{/* 图片 */}
<div
className="relative cursor-pointer rounded overflow-hidden transition-all duration-300 hover:shadow-lg inline-block"
onClick={() => openLightbox(generatedImageUrls, index)}
>
<img
src={imgUrl}
alt={`生成的图片 ${index + 1}`}
className="rounded max-w-full h-auto"
style={{ maxWidth: '450px', maxHeight: '400px', objectFit: 'contain' }}
/>
</div>
{/* 底部操作按钮栏 - 悬停时显示,横向排列 */}
<div className="flex items-center gap-1 mt-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<Tooltip content="复制图片">
<button
onClick={() => handleCopyImage(imgUrl)}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<Copy size={16} />
</button>
</Tooltip>
{onRegenerate && (
<Tooltip content="重新生成">
<button
onClick={() => onRegenerate(message.id)}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<RefreshCw size={16} />
</button>
</Tooltip>
)}
<Tooltip content="下载">
<button
onClick={() => handleDownloadImage(imgUrl, index)}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</button>
</Tooltip>
<Tooltip content="放大">
<button
onClick={() => openLightbox(generatedImageUrls, index)}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
</svg>
</button>
</Tooltip>
</div>
</div>
))}
</div>
);
})()}
</div> </div>
{/* 图片 Lightbox */} {/* 图片 Lightbox */}

View File

@ -48,18 +48,8 @@ const CODEX_MODEL_CARDS = [
}, },
]; ];
// Gemini 图片模型卡片配置
const GEMINI_MODEL_CARDS = [
{
id: 'gemini-3-pro-image-preview',
name: 'Gemini 图片',
description: '文生图',
modelIdPattern: 'gemini',
},
];
// 模型类型 // 模型类型
type ModelType = 'claude' | 'codex' | 'gemini'; type ModelType = 'claude' | 'codex';
interface ModelCardSelectorProps { interface ModelCardSelectorProps {
value: string; value: string;
@ -83,9 +73,6 @@ export function ModelCardSelector({
if (modelId.startsWith('gpt-') && modelId.includes('codex')) { if (modelId.startsWith('gpt-') && modelId.includes('codex')) {
return 'codex'; return 'codex';
} }
if (modelId.includes('gemini') || modelId.includes('imagen')) {
return 'gemini';
}
return 'claude'; return 'claude';
}; };
@ -120,22 +107,9 @@ export function ModelCardSelector({
return model?.modelId || cardId; return model?.modelId || cardId;
}; };
// 根据当前选中的模型ID判断选中的卡片Gemini
const getSelectedGeminiCard = (modelId: string): string | null => {
const matchedCard = GEMINI_MODEL_CARDS.find((card) => modelId.includes(card.modelIdPattern));
return matchedCard?.id || null;
};
// 根据卡片类型找到对应的实际模型IDGemini
const findGeminiModelIdByCard = (cardId: string): string => {
const model = models.find((m) => m.modelId === cardId);
return model?.modelId || cardId;
};
const currentModelType = getModelType(value); const currentModelType = getModelType(value);
const selectedClaudeCard = getSelectedClaudeCard(value); const selectedClaudeCard = getSelectedClaudeCard(value);
const selectedCodexCard = getSelectedCodexCard(value); const selectedCodexCard = getSelectedCodexCard(value);
const selectedGeminiCard = getSelectedGeminiCard(value);
// 处理模型选择 // 处理模型选择
const handleModelSelect = (modelId: string, modelType: ModelType) => { const handleModelSelect = (modelId: string, modelType: ModelType) => {
@ -163,9 +137,6 @@ export function ModelCardSelector({
// 检查是否有 Codex 模型可用 // 检查是否有 Codex 模型可用
const hasCodexModels = models.some(m => m.modelType === 'codex' || (m.modelId.startsWith('gpt-') && m.modelId.includes('codex'))); const hasCodexModels = models.some(m => m.modelType === 'codex' || (m.modelId.startsWith('gpt-') && m.modelId.includes('codex')));
// 检查是否有 Gemini 模型可用
const hasGeminiModels = models.some(m => m.modelType === 'gemini' || m.modelId.includes('gemini') || m.modelId.includes('imagen'));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* 模型切换提示 */} {/* 模型切换提示 */}
@ -177,8 +148,6 @@ export function ModelCardSelector({
<p className="text-amber-600 dark:text-amber-300 mt-1"> <p className="text-amber-600 dark:text-amber-300 mt-1">
{currentModelType === 'codex' {currentModelType === 'codex'
? 'Codex 模型不支持思考模式Thinking但支持工具调用。' ? 'Codex 模型不支持思考模式Thinking但支持工具调用。'
: currentModelType === 'gemini'
? 'Gemini 图片模型专注于图片生成,不支持思考模式和工具调用。'
: 'Claude 模型支持思考模式和工具调用。'} : 'Claude 模型支持思考模式和工具调用。'}
</p> </p>
</div> </div>
@ -302,70 +271,6 @@ export function ModelCardSelector({
</div> </div>
</div> </div>
)} )}
{/* Gemini 图片模型组 */}
{hasGeminiModels && (
<div>
<div className="text-xs font-medium text-[var(--color-text-tertiary)] mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
Gemini
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
Google
</span>
</div>
<div className="flex gap-3">
{GEMINI_MODEL_CARDS.map((card) => {
const isSelected = selectedGeminiCard === card.id;
// 检查该模型是否在可用模型列表中
const isAvailable = models.some(m => m.modelId === card.id || m.modelId.includes(card.modelIdPattern));
if (!isAvailable) return null;
return (
<button
key={card.id}
onClick={() => {
const newModelId = findGeminiModelIdByCard(card.id);
handleModelSelect(newModelId, 'gemini');
}}
disabled={disabled}
className={cn(
'flex flex-col items-center justify-center',
'w-[120px] h-[72px] rounded-md',
'border-2 transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500',
isSelected
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
: 'bg-[var(--color-bg-tertiary)] border-transparent hover:border-blue-300 dark:hover:border-blue-700',
disabled && 'opacity-50 cursor-not-allowed'
)}
>
<span
className={cn(
'text-sm font-semibold',
isSelected
? 'text-blue-600 dark:text-blue-400'
: 'text-[var(--color-text-primary)]'
)}
>
{card.name}
</span>
<span
className={cn(
'text-xs mt-1',
isSelected
? 'text-blue-500 dark:text-blue-400'
: 'text-[var(--color-text-tertiary)]'
)}
>
{card.description}
</span>
</button>
);
})}
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -1,2 +0,0 @@
ALTER TABLE "messages" ADD COLUMN "generated_images" jsonb;--> statement-breakpoint
ALTER TABLE "shared_conversations" ADD COLUMN "selected_message_ids" jsonb;

File diff suppressed because it is too large Load Diff

View File

@ -106,13 +106,6 @@
"when": 1766546111189, "when": 1766546111189,
"tag": "0014_naive_kronos", "tag": "0014_naive_kronos",
"breakpoints": true "breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1766812096044,
"tag": "0015_milky_anthem",
"breakpoints": true
} }
] ]
} }

View File

@ -207,8 +207,6 @@ export const messages = pgTable('messages', {
usedTools: jsonb('used_tools').$type<string[]>().default([]), usedTools: jsonb('used_tools').$type<string[]>().default([]),
// 搜索到的图片(图片搜索工具返回) // 搜索到的图片(图片搜索工具返回)
searchImages: jsonb('search_images').$type<SearchImageData[]>(), searchImages: jsonb('search_images').$type<SearchImageData[]>(),
// AI 生成的图片Gemini 等图片生成模型返回)
generatedImages: jsonb('generated_images').$type<GeneratedImageData[]>(),
// Token 统计 // Token 统计
inputTokens: integer('input_tokens').default(0), inputTokens: integer('input_tokens').default(0),
outputTokens: integer('output_tokens').default(0), outputTokens: integer('output_tokens').default(0),
@ -481,14 +479,6 @@ export interface SearchImageData {
sourceUrl?: string; sourceUrl?: string;
} }
// AI 生成的图片数据(用于 Gemini 等图片生成模型)
export interface GeneratedImageData {
mimeType: string; // 图片 MIME 类型,如 "image/png"、"image/jpeg"
data: string; // Base64 编码的图片数据
width?: number; // 图片宽度(可选)
height?: number; // 图片高度(可选)
}
// 导出类型 // 导出类型
export type User = typeof users.$inferSelect; export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert; export type NewUser = typeof users.$inferInsert;

View File

@ -14,7 +14,7 @@ import {
} from '@/utils/document-utils'; } from '@/utils/document-utils';
export interface StreamMessage { export interface StreamMessage {
type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'tool_search_images' | 'tool_search_videos' | 'pyodide_execution_required' | 'tool_used' | 'done' | 'error' | 'image_generation_start' | 'generated_image' | 'image_generation_complete'; type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'tool_search_images' | 'tool_search_videos' | 'pyodide_execution_required' | 'tool_used' | 'done' | 'error';
content?: string; content?: string;
id?: string; id?: string;
name?: string; name?: string;
@ -36,11 +36,6 @@ export interface StreamMessage {
// 工具使用相关 // 工具使用相关
toolName?: string; toolName?: string;
usedTools?: string[]; usedTools?: string[];
// AI 图片生成相关
model?: string;
image?: GeneratedImageData;
index?: number;
imageCount?: number;
} }
// 搜索图片数据类型 // 搜索图片数据类型
@ -67,14 +62,6 @@ export interface SearchVideoData {
coverImage: string; coverImage: string;
} }
// AI 生成的图片数据类型Gemini 等图片生成模型返回)
export interface GeneratedImageData {
mimeType: string; // 图片 MIME 类型,如 "image/png"、"image/jpeg"
data: string; // Base64 编码的图片数据
width?: number; // 图片宽度(可选)
height?: number; // 图片高度(可选)
}
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
role: 'user' | 'assistant'; role: 'user' | 'assistant';
@ -102,10 +89,6 @@ export interface ChatMessage {
message: string; message: string;
progress?: number; progress?: number;
}; };
// AI 生成的图片Gemini 等图片生成模型)
generatedImages?: GeneratedImageData[];
// 是否正在生成图片
isGeneratingImage?: boolean;
} }
/** /**
@ -633,55 +616,6 @@ export function useStreamChat() {
return updated; return updated;
}); });
} }
} else if (event.type === 'image_generation_start') {
// 图片生成开始
console.log('[useStreamChat] Image generation started, model:', event.model);
setMessages((prev) => {
const updated = [...prev];
const lastIndex = updated.length - 1;
if (updated[lastIndex]?.role === 'assistant') {
updated[lastIndex] = {
...updated[lastIndex],
isGeneratingImage: true,
};
}
return updated;
});
} else if (event.type === 'generated_image') {
// 收到一张生成的图片
if (event.image) {
console.log('[useStreamChat] Received generated image:', {
mimeType: event.image.mimeType,
dataLength: event.image.data?.length || 0,
index: event.index,
});
setMessages((prev) => {
const updated = [...prev];
const lastIndex = updated.length - 1;
if (updated[lastIndex]?.role === 'assistant') {
const existingGeneratedImages = updated[lastIndex].generatedImages || [];
updated[lastIndex] = {
...updated[lastIndex],
generatedImages: [...existingGeneratedImages, event.image!],
};
}
return updated;
});
}
} else if (event.type === 'image_generation_complete') {
// 图片生成完成
console.log('[useStreamChat] Image generation complete, count:', event.imageCount);
setMessages((prev) => {
const updated = [...prev];
const lastIndex = updated.length - 1;
if (updated[lastIndex]?.role === 'assistant') {
updated[lastIndex] = {
...updated[lastIndex],
isGeneratingImage: false,
};
}
return updated;
});
} else if (event.type === 'pyodide_execution_required') { } else if (event.type === 'pyodide_execution_required') {
// 需要在浏览器端执行 Python 图形代码 // 需要在浏览器端执行 Python 图形代码
const code = event.code || ''; const code = event.code || '';