mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
fix: improve VLM validation with bug fixes and i18n
- Fix race condition in pendingValidationRef (reject previous pending validation) - Fix response format consistency (use streaming for all responses) - Remove dead code (unused lastRequestRef and ValidationRequest interface) - Consolidate duplicate types (re-export from validation-schema.ts) - Add 'success_with_warnings' status for valid diagrams with warnings - Fix tool card auto-collapse (only collapse once, respect user toggle) - Set VLM validation default to disabled - Add i18n support for diagram validation settings (en/zh/ja) - Mark feature as experimental in settings UI
This commit is contained in:
@@ -25,12 +25,30 @@ const DEFAULT_VALID_RESULT: ValidationResult = {
|
||||
suggestions: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a streaming response for useObject compatibility.
|
||||
* useObject expects text stream format, not plain JSON.
|
||||
*/
|
||||
function createStreamingResponse(result: ValidationResult): Response {
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Stream the JSON as text (useObject parses this)
|
||||
controller.enqueue(encoder.encode(JSON.stringify(result)))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
return new Response(stream, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
try {
|
||||
// Check if VLM validation is enabled (default: true)
|
||||
const enableValidation = process.env.ENABLE_VLM_VALIDATION !== "false"
|
||||
if (!enableValidation) {
|
||||
return Response.json(DEFAULT_VALID_RESULT)
|
||||
return createStreamingResponse(DEFAULT_VALID_RESULT)
|
||||
}
|
||||
|
||||
const body: ValidateDiagramRequest = await req.json()
|
||||
@@ -64,7 +82,7 @@ export async function POST(req: Request): Promise<Response> {
|
||||
error,
|
||||
)
|
||||
// Return valid if no vision model is configured
|
||||
return Response.json(DEFAULT_VALID_RESULT)
|
||||
return createStreamingResponse(DEFAULT_VALID_RESULT)
|
||||
}
|
||||
|
||||
// Parse timeout with validation (minimum 1000ms, default 10000ms)
|
||||
@@ -113,6 +131,6 @@ export async function POST(req: Request): Promise<Response> {
|
||||
console.error("[validate-diagram] Error:", errorMessage)
|
||||
|
||||
// On error, return valid to not block the user
|
||||
return Response.json(DEFAULT_VALID_RESULT)
|
||||
return createStreamingResponse(DEFAULT_VALID_RESULT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,11 +435,15 @@ export function ChatMessageDisplay({
|
||||
const toolPart = part as ToolPartLike
|
||||
const { toolCallId, state, input } = toolPart
|
||||
|
||||
// Auto-collapse on completion, but only if user hasn't manually toggled
|
||||
if (state === "output-available") {
|
||||
setExpandedTools((prev) => ({
|
||||
...prev,
|
||||
[toolCallId]: false,
|
||||
}))
|
||||
setExpandedTools((prev) => {
|
||||
// Only auto-collapse if not already set (user hasn't interacted)
|
||||
if (prev[toolCallId] === undefined) {
|
||||
return { ...prev, [toolCallId]: false }
|
||||
}
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function ChatPanel({
|
||||
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
||||
const [tpmLimit, setTpmLimit] = useState(0)
|
||||
const [minimalStyle, setMinimalStyle] = useState(false)
|
||||
const [vlmValidationEnabled, setVlmValidationEnabled] = useState(true)
|
||||
const [vlmValidationEnabled, setVlmValidationEnabled] = useState(false)
|
||||
const [shouldFocusInput, setShouldFocusInput] = useState(false)
|
||||
|
||||
// Restore input from sessionStorage on mount (when ChatPanel remounts due to key change)
|
||||
|
||||
@@ -67,8 +67,8 @@ export function ToolCallCard({
|
||||
}: ToolCallCardProps) {
|
||||
const callId = part.toolCallId
|
||||
const { state, input, output } = part
|
||||
// Default to collapsed if tool is complete, expanded if still streaming
|
||||
const isExpanded = expandedTools[callId] ?? state !== "output-available"
|
||||
// Default to expanded for all states (user can manually collapse if needed)
|
||||
const isExpanded = expandedTools[callId] ?? true
|
||||
const toolName = part.type?.replace("tool-", "")
|
||||
const isCopied = copiedToolCallId === callId
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export type ValidationStatus =
|
||||
| "capturing"
|
||||
| "validating"
|
||||
| "success"
|
||||
| "success_with_warnings"
|
||||
| "failed"
|
||||
| "error"
|
||||
| "skipped"
|
||||
@@ -95,7 +96,9 @@ export function ValidationCard({
|
||||
const showImproveButton =
|
||||
onImproveWithSuggestions &&
|
||||
state.result &&
|
||||
(state.status === "success" || state.status === "skipped") &&
|
||||
(state.status === "success" ||
|
||||
state.status === "success_with_warnings" ||
|
||||
state.status === "skipped") &&
|
||||
(state.result.issues.length > 0 || state.result.suggestions.length > 0)
|
||||
|
||||
const getStatusDisplay = () => {
|
||||
@@ -124,6 +127,14 @@ export function ValidationCard({
|
||||
color: "text-green-600 bg-green-50",
|
||||
icon: <Check className="h-4 w-4" aria-hidden="true" />,
|
||||
}
|
||||
case "success_with_warnings":
|
||||
return {
|
||||
label: "Valid with Warnings",
|
||||
color: "text-amber-600 bg-amber-50",
|
||||
icon: (
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
),
|
||||
}
|
||||
case "failed":
|
||||
return {
|
||||
label: "Issues Found",
|
||||
|
||||
@@ -90,7 +90,7 @@ function SettingsContent({
|
||||
onToggleDarkMode,
|
||||
minimalStyle = false,
|
||||
onMinimalStyleChange = () => {},
|
||||
vlmValidationEnabled = true,
|
||||
vlmValidationEnabled = false,
|
||||
onVlmValidationChange = () => {},
|
||||
}: SettingsDialogProps) {
|
||||
const dict = useDictionary()
|
||||
@@ -416,8 +416,8 @@ function SettingsContent({
|
||||
|
||||
{/* VLM Diagram Validation */}
|
||||
<SettingItem
|
||||
label="Diagram Validation"
|
||||
description="Use AI vision to validate and improve generated diagrams"
|
||||
label={dict.settings.diagramValidation}
|
||||
description={dict.settings.diagramValidationDescription}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
@@ -426,7 +426,9 @@ function SettingsContent({
|
||||
onCheckedChange={onVlmValidationChange}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{vlmValidationEnabled ? "Enabled" : "Disabled"}
|
||||
{vlmValidationEnabled
|
||||
? dict.settings.enabled
|
||||
: dict.settings.disabled}
|
||||
</span>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
@@ -325,9 +325,13 @@ ${finalXml}
|
||||
}
|
||||
|
||||
// Notify UI of success (include the image)
|
||||
// Use "success_with_warnings" if valid but has issues
|
||||
const hasWarnings = result.issues.length > 0
|
||||
updateValidationState(
|
||||
toolCall.toolCallId,
|
||||
"success",
|
||||
hasWarnings
|
||||
? "success_with_warnings"
|
||||
: "success",
|
||||
{ result, imageData: capturedPngData },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,6 @@ interface UseValidateDiagramOptions {
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
interface ValidationRequest {
|
||||
imageData: string
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
// Track pending validation promises for imperative API
|
||||
type PendingValidation = {
|
||||
resolve: (result: ValidationResult) => void
|
||||
@@ -40,7 +35,6 @@ type PendingValidation = {
|
||||
export function useValidateDiagram(options: UseValidateDiagramOptions = {}) {
|
||||
const { onSuccess, onError } = options
|
||||
const pendingValidationRef = useRef<PendingValidation | null>(null)
|
||||
const lastRequestRef = useRef<ValidationRequest | null>(null)
|
||||
|
||||
const { object, submit, isLoading, error, stop } = useObject({
|
||||
api: getApiEndpoint("/api/validate-diagram"),
|
||||
@@ -87,8 +81,13 @@ export function useValidateDiagram(options: UseValidateDiagramOptions = {}) {
|
||||
imageData: string,
|
||||
sessionId?: string,
|
||||
): Promise<ValidationResult> => {
|
||||
// Store request for potential retry
|
||||
lastRequestRef.current = { imageData, sessionId }
|
||||
// Reject any pending validation to prevent promise leaks
|
||||
if (pendingValidationRef.current) {
|
||||
pendingValidationRef.current.reject(
|
||||
new Error("Validation superseded by new request"),
|
||||
)
|
||||
pendingValidationRef.current = null
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Store the promise handlers
|
||||
|
||||
@@ -3,17 +3,10 @@
|
||||
* The actual validation is performed via useValidateDiagram hook using AI SDK's useObject.
|
||||
*/
|
||||
|
||||
export interface ValidationIssue {
|
||||
type: "overlap" | "edge_routing" | "text" | "layout" | "rendering"
|
||||
severity: "critical" | "warning"
|
||||
description: string
|
||||
}
|
||||
// Re-export types from the schema file (single source of truth)
|
||||
export type { ValidationIssue, ValidationResult } from "./validation-schema"
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean
|
||||
issues: ValidationIssue[]
|
||||
suggestions: string[]
|
||||
}
|
||||
import type { ValidationResult } from "./validation-schema"
|
||||
|
||||
/**
|
||||
* Format validation feedback for display to the AI model.
|
||||
|
||||
@@ -115,7 +115,11 @@
|
||||
"httpProxy": "HTTP Proxy",
|
||||
"httpsProxy": "HTTPS Proxy",
|
||||
"applyProxy": "Apply",
|
||||
"proxyApplied": "Proxy settings applied"
|
||||
"proxyApplied": "Proxy settings applied",
|
||||
"diagramValidation": "Diagram Validation (Experimental)",
|
||||
"diagramValidationDescription": "Use a vision language model to validate generated diagrams. Requires a VLM like GPT-5.2 or Sonnet-4.5.",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"save": {
|
||||
"title": "Save Diagram",
|
||||
|
||||
@@ -115,7 +115,11 @@
|
||||
"httpProxy": "HTTP プロキシ",
|
||||
"httpsProxy": "HTTPS プロキシ",
|
||||
"applyProxy": "適用",
|
||||
"proxyApplied": "プロキシ設定が適用されました"
|
||||
"proxyApplied": "プロキシ設定が適用されました",
|
||||
"diagramValidation": "ダイアグラム検証(実験的)",
|
||||
"diagramValidationDescription": "視覚言語モデルを使用して生成されたダイアグラムを検証します。GPT-5.2 や Sonnet-4.5 などの VLM が必要です。",
|
||||
"enabled": "有効",
|
||||
"disabled": "無効"
|
||||
},
|
||||
"save": {
|
||||
"title": "ダイアグラムを保存",
|
||||
|
||||
@@ -115,7 +115,11 @@
|
||||
"httpProxy": "HTTP 代理",
|
||||
"httpsProxy": "HTTPS 代理",
|
||||
"applyProxy": "应用",
|
||||
"proxyApplied": "代理设置已应用"
|
||||
"proxyApplied": "代理设置已应用",
|
||||
"diagramValidation": "图表验证(实验性)",
|
||||
"diagramValidationDescription": "使用视觉语言模型验证生成的图表。需要支持视觉的模型,如 GPT-5.2 或 Sonnet-4.5。",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用"
|
||||
},
|
||||
"save": {
|
||||
"title": "保存图表",
|
||||
|
||||
@@ -35,3 +35,4 @@ export const ValidationResultSchema = z.object({
|
||||
})
|
||||
|
||||
export type ValidationResult = z.infer<typeof ValidationResultSchema>
|
||||
export type ValidationIssue = ValidationResult["issues"][number]
|
||||
|
||||
Reference in New Issue
Block a user