Add VLM-based diagram validation (#602)

* [Feature] Add VLM-based diagram validation

Add automatic VLM (Vision Language Model) validation after display_diagram
tool execution. The system captures a screenshot of the rendered diagram,
sends it to a VLM for visual analysis, and uses feedback to improve
diagram quality through the existing retry mechanism.

Changes:
- Add /api/validate-diagram endpoint for VLM validation
- Add diagram-validator.ts for client-side validation orchestration
- Add validation-prompts.ts for VLM system prompts
- Add ValidationCard component to display validation status in chat
- Add PNG capture functionality to diagram context
- Integrate validation into tool handlers with retry support (max 3)
- Add "Improve with Suggestions" button for manual regeneration
- Add settings toggle to enable/disable VLM validation
- Add getValidationModel() helper in ai-providers.ts

* refactor(validation): use AI SDK structured outputs and address review feedback

- Replace generateText + manual JSON parsing with generateObject and Zod schema
  for type-safe structured validation output
- Use AbortSignal.timeout() instead of Promise.race for cleaner timeout handling
- Add timeout validation with minimum 1000ms to handle malformed env values
- Remove unused xml parameter from validateRenderedDiagram API
- Remove parseValidationResponse function (now handled by schema)
- Clear validationStates on session switch and new chat to prevent memory leak
- Update 100ms render delay comment to clarify best-effort heuristic
- Remove unused useEffect import from ValidationCard
- Fix optional chaining lint warning in ValidationCard
- Add unit tests for formatValidationFeedback function

* refactor(validation): use AI SDK experimental_useObject hook instead of raw fetch

- Change API endpoint from generateObject to streamObject for useObject compatibility
- Create useValidateDiagram hook using AI SDK's experimental_useObject for reactive validation
- Update useDiagramToolHandlers to accept validation function as parameter
- Update chat-panel to use new useValidateDiagram hook
- Remove validateRenderedDiagram function from lib/diagram-validator.ts (now in hook)
- Export ValidationResultSchema from API route for client-side use

* fix(validation): extract schema to shared file for client/server compatibility

Move ValidationResultSchema to lib/validation-schema.ts to avoid importing
server-side modules (ai-providers) into client-side code. This fixes the
Turbopack build error caused by the hook importing from the API route.

* fix(validation): use 'Valid' instead of 'Complete' for validation success

Change ValidationCard success label from 'Complete' to 'Valid' to avoid
conflicting with ToolCallCard's 'Complete' badge in E2E tests. This fixes
the diagram-generation E2E test that expects a specific count of 'Complete'
badges.

* fix(validation): add aria-hidden to icons to prevent duplicate ID warning

* 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

* fix: resolve TypeScript errors in electron-standalone

- Add forwardRef support to ChatInput component with ChatInputRef type
- Copy electron.d.ts to electron-standalone/electron folder
- Exclude electron-standalone from root tsconfig type checking

* fix: return empty string for valid result with no issues in formatValidationFeedback

* feat(i18n): add validation strings for ValidationCard component

- Add validation section to en.json, zh.json, ja.json dictionaries
- Update ValidationCard to use useDictionary hook
- Replace all hardcoded English strings with i18n keys

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
This commit is contained in:
yujinze
2026-01-20 20:52:04 +09:00
committed by GitHub
parent b386dc45e6
commit afddba364b
20 changed files with 1741 additions and 399 deletions

View File

@@ -28,6 +28,8 @@ import {
import { ChatLobby } from "@/components/chat/ChatLobby"
import { ToolCallCard } from "@/components/chat/ToolCallCard"
import type { DiagramOperation, ToolPartLike } from "@/components/chat/types"
import type { ValidationState } from "@/components/chat/ValidationCard"
import { ValidationCard } from "@/components/chat/ValidationCard"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path"
@@ -148,6 +150,8 @@ interface ChatMessageDisplayProps {
onSelectSession?: (id: string) => void
onDeleteSession?: (id: string) => void
loadedMessageIdsRef?: MutableRefObject<Set<string>>
validationStates?: Record<string, ValidationState>
onImproveWithSuggestions?: (feedback: string) => void
}
export function ChatMessageDisplay({
@@ -165,6 +169,8 @@ export function ChatMessageDisplay({
onSelectSession,
onDeleteSession,
loadedMessageIdsRef,
validationStates = {},
onImproveWithSuggestions,
}: ChatMessageDisplayProps) {
const dict = useDictionary()
const { chartXML, loadDiagram: onDisplayChart } = useDiagram()
@@ -429,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 (
@@ -911,30 +921,56 @@ export function ChatMessageDisplay({
return groups.map(
(group, groupIndex) => {
if (group.type === "tool") {
const toolPart = group
.parts[0] as ToolPartLike
const toolCallId =
toolPart.toolCallId
const isDisplayDiagram =
toolPart.type ===
"tool-display_diagram"
const validationState =
validationStates[
toolCallId
]
return (
<ToolCallCard
<div
key={`${message.id}-tool-${group.startIndex}`}
part={
group
.parts[0] as ToolPartLike
}
expandedTools={
expandedTools
}
setExpandedTools={
setExpandedTools
}
onCopy={
copyMessageToClipboard
}
copiedToolCallId={
copiedToolCallId
}
copyFailedToolCallId={
copyFailedToolCallId
}
dict={dict}
/>
>
<ToolCallCard
part={
toolPart
}
expandedTools={
expandedTools
}
setExpandedTools={
setExpandedTools
}
onCopy={
copyMessageToClipboard
}
copiedToolCallId={
copiedToolCallId
}
copyFailedToolCallId={
copyFailedToolCallId
}
dict={dict}
/>
{/* Show validation card for display_diagram tools */}
{isDisplayDiagram &&
validationState && (
<ValidationCard
state={
validationState
}
onImproveWithSuggestions={
onImproveWithSuggestions
}
/>
)}
</div>
)
}