From 691c7e34bf67c30fad77c4ba242be7ae0ad94515 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 19 Dec 2025 14:12:31 +0800 Subject: [PATCH] feat: image capability detection and user-friendly errors; warn and filter images for unsupported models --- components/chat-input.tsx | 21 ++++++++-- components/chat-panel.tsx | 80 +++++++++++++++++++++++++++++---------- lib/model-capabilities.ts | 76 +++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 lib/model-capabilities.ts diff --git a/components/chat-input.tsx b/components/chat-input.tsx index b63bbd12..95d22193 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -58,6 +58,7 @@ interface ValidationResult { function validateFiles( newFiles: File[], existingCount: number, + imageSupported: boolean, ): ValidationResult { const errors: string[] = [] const validFiles: File[] = [] @@ -74,7 +75,10 @@ function validateFiles( errors.push(`Only ${availableSlots} more file(s) allowed`) break } - if (!isValidFileType(file)) { + if ( + !isValidFileType(file) || + (!imageSupported && file.type.startsWith("image/")) + ) { errors.push(`"${file.name}" is not a supported file type`) continue } @@ -137,6 +141,7 @@ interface ChatInputProps { error?: Error | null minimalStyle?: boolean onMinimalStyleChange?: (value: boolean) => void + imageSupported?: boolean } export function ChatInput({ @@ -154,6 +159,7 @@ export function ChatInput({ error = null, minimalStyle = false, onMinimalStyleChange = () => {}, + imageSupported = true, }: ChatInputProps) { const { diagramHistory, @@ -224,6 +230,7 @@ export function ChatInput({ const { validFiles, errors } = validateFiles( imageFiles, files.length, + imageSupported, ) showValidationErrors(errors) if (validFiles.length > 0) { @@ -234,7 +241,11 @@ export function ChatInput({ const handleFileChange = (e: React.ChangeEvent) => { const newFiles = Array.from(e.target.files || []) - const { validFiles, errors } = validateFiles(newFiles, files.length) + const { validFiles, errors } = validateFiles( + newFiles, + files.length, + imageSupported, + ) showValidationErrors(errors) if (validFiles.length > 0) { onFileChange([...files, ...validFiles]) @@ -428,7 +439,11 @@ export function ChatInput({ size="sm" onClick={triggerFileInput} disabled={isDisabled} - tooltipContent="Upload file (image, PDF, text)" + tooltipContent={ + imageSupported + ? "Upload file (image, PDF, text)" + : "Upload file (PDF, text)" + } className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" > diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index b7408b0b..ccd0fa00 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -23,6 +23,10 @@ import { SettingsDialog } from "@/components/settings-dialog" import { useDiagram } from "@/contexts/diagram-context" import { getAIConfig } from "@/lib/ai-config" import { findCachedResponse } from "@/lib/cached-responses" +import { + resolveImageSupport, + setCachedImageCapability, +} from "@/lib/model-capabilities" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { useQuotaManager } from "@/lib/use-quota-manager" @@ -581,9 +585,15 @@ Continue from EXACTLY where you stopped.`, "Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength." } - // Translate image not supported error - if (friendlyMessage.includes("image content block")) { + if ( + friendlyMessage.includes("image content block") || + friendlyMessage.toLowerCase().includes("image_url") || + friendlyMessage.toLowerCase().includes("unknown variant") + ) { friendlyMessage = "This model doesn't support image input." + // Cache capability as unsupported for current provider/model + const cfg = getAIConfig() + setCachedImageCapability(cfg.aiProvider, cfg.aiModel, false) } // Add system message for error so it can be cleared @@ -959,7 +969,29 @@ Continue from EXACTLY where you stopped.`, // Check all quota limits if (!checkAllQuotaLimits()) return - sendChatMessage(parts, chartXml, previousXml, sessionId) + { + const config = getAIConfig() + const provider = config.aiProvider + const modelId = config.aiModel + const canUseImages = resolveImageSupport(provider, modelId) + const hasImage = + files.some((f) => !isPdfFile(f) && !isTextFile(f)) && + parts.some((p) => p.type === "file") + if (hasImage && !canUseImages) { + const filtered = parts.filter((p) => p.type !== "file") + sendChatMessage( + filtered, + chartXml, + previousXml, + sessionId, + ) + toast.warning( + "当前模型不支持图片输入,已忽略上传的图片", + ) + } else { + sendChatMessage(parts, chartXml, previousXml, sessionId) + } + } // Token count is tracked in onFinish with actual server usage setInput("") @@ -1387,22 +1419,32 @@ Continue from EXACTLY where you stopped.`,
- + {(() => { + const cfg = getAIConfig() + const imgCap = resolveImageSupport( + cfg.aiProvider, + cfg.aiModel, + ) + return ( + + ) + })()}
{ + if (typeof window === "undefined") return {} + try { + const raw = localStorage.getItem(LS_KEY) + return raw ? (JSON.parse(raw) as Record) : {} + } catch { + return {} + } +} + +function writeCache(cache: Record) { + if (typeof window === "undefined") return + try { + localStorage.setItem(LS_KEY, JSON.stringify(cache)) + } catch { + // ignore + } +} + +function capabilityKey(provider?: string, modelId?: string): string { + return `${(provider || "").toLowerCase()}::${(modelId || "").toLowerCase()}` +} + +export function getCachedImageCapability( + provider?: string, + modelId?: string, +): boolean | undefined { + const cache = readCache() + const key = capabilityKey(provider, modelId) + return key in cache ? cache[key] : undefined +} + +export function setCachedImageCapability( + provider?: string, + modelId?: string, + supported?: boolean, +) { + if (supported === undefined) return + const cache = readCache() + cache[capabilityKey(provider, modelId)] = supported + writeCache(cache) +} + +export function resolveImageSupport( + provider?: string, + modelId?: string, +): boolean { + const cached = getCachedImageCapability(provider, modelId) + if (cached !== undefined) return cached + return supportsImages(provider, modelId) +}