mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-10 03:29:50 +08:00
feat: image capability detection and user-friendly errors; warn and filter images for unsupported models
This commit is contained in:
@@ -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<HTMLInputElement>) => {
|
||||
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"
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
|
||||
+61
-19
@@ -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.`,
|
||||
<footer
|
||||
className={`${isMobile ? "p-2" : "p-4"} border-t border-border/50 bg-card/50`}
|
||||
>
|
||||
<ChatInput
|
||||
input={input}
|
||||
status={status}
|
||||
onSubmit={onFormSubmit}
|
||||
onChange={handleInputChange}
|
||||
onClearChat={handleNewChat}
|
||||
files={files}
|
||||
onFileChange={handleFileChange}
|
||||
pdfData={pdfData}
|
||||
showHistory={showHistory}
|
||||
onToggleHistory={setShowHistory}
|
||||
sessionId={sessionId}
|
||||
error={error}
|
||||
minimalStyle={minimalStyle}
|
||||
onMinimalStyleChange={setMinimalStyle}
|
||||
/>
|
||||
{(() => {
|
||||
const cfg = getAIConfig()
|
||||
const imgCap = resolveImageSupport(
|
||||
cfg.aiProvider,
|
||||
cfg.aiModel,
|
||||
)
|
||||
return (
|
||||
<ChatInput
|
||||
input={input}
|
||||
status={status}
|
||||
onSubmit={onFormSubmit}
|
||||
onChange={handleInputChange}
|
||||
onClearChat={handleNewChat}
|
||||
files={files}
|
||||
onFileChange={handleFileChange}
|
||||
pdfData={pdfData}
|
||||
showHistory={showHistory}
|
||||
onToggleHistory={setShowHistory}
|
||||
sessionId={sessionId}
|
||||
error={error}
|
||||
minimalStyle={minimalStyle}
|
||||
onMinimalStyleChange={setMinimalStyle}
|
||||
imageSupported={imgCap}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</footer>
|
||||
|
||||
<SettingsDialog
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export function supportsImages(provider?: string, modelId?: string): boolean {
|
||||
const p = (provider || "").toLowerCase()
|
||||
const m = (modelId || "").toLowerCase()
|
||||
if (!p && !m) return false
|
||||
if (p === "openai" && m.includes("gpt-4o")) return true
|
||||
if (p === "google" || m.includes("gemini")) return true
|
||||
if (p === "siliconflow") {
|
||||
if (m.includes("vl")) return true
|
||||
if (m.includes("glm-4v")) return true
|
||||
if (m.includes("yi-vl")) return true
|
||||
return false
|
||||
}
|
||||
if (p === "openrouter") {
|
||||
if (m.includes("gpt-4o")) return true
|
||||
if (m.includes("gemini")) return true
|
||||
if (m.includes("vl")) return true
|
||||
if (m.includes("glm-4v")) return true
|
||||
if (m.includes("yi-vl")) return true
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const LS_KEY = "next-ai-draw-io-image-capabilities"
|
||||
|
||||
function readCache(): Record<string, boolean> {
|
||||
if (typeof window === "undefined") return {}
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_KEY)
|
||||
return raw ? (JSON.parse(raw) as Record<string, boolean>) : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(cache: Record<string, boolean>) {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user