mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-10 19:49:52 +08:00
Chats: - New Chat right after an answer saves that chat once. Saves run one at a time and read the chat on screen when their turn comes; a save scheduled for a chat that is no longer on screen is dropped. A chat whose id was still on its way to the URL no longer comes back after New Chat (the next answer went into it). - Crossing the 768 px breakpoint keeps the chat panel: a streaming answer, unsaved messages and attachments stay. The panel gets the sizes of each side, and a panel collapsed on desktop opens on mobile. - The chat's export waits for its own reply: an edit's history export still on its way no longer answers it with the older diagram, and two file saves at once no longer swap results. - A second edit in one answer is previewed on the first edit's result. - Stop also ends a running screenshot check; a chat that cannot be saved (storage full) can be left with "Continue without saving". - Small diagrams with shapes count as diagrams; the tool card no longer crashes on malformed operations. Quota and providers: - Requests that reach the server's own endpoints count toward the quota: EdgeOne (always its own endpoint now), a private base URL whatever key header is sent, keyless Ollama without a URL. With the quota on, a redirect is followed only to a public address. The output cap applies to these requests too. - Stop records the tokens of the steps that finished; the screenshot check counts its tokens without counting a request. - EdgeOne configured only by AI_PROVIDER works, also in the admin Test, which forwards the access code. Azure set up only in the admin panel works in chat. The Test sends a Bedrock session token. - The admin panel's Test of an entry without a URL uses the server's URL as the server does (no private address check for it); the admin panel no longer writes an Ollama URL. MCP server: - Write tools and start_session run one at a time, so two at once never drop each other's change; a cancelled call waiting its turn is skipped. get_diagram and export_diagram keep the session they started with. - Export to .drawio first gets the user's latest edits from the browser. - History thumbnails: one that arrives after the next AI write is dropped; a sync reply keeps the image; a version that changed only page settings is its own entry. - A diagram over the 10 MB limit is saved without its image, or the user is told to download it (the server now answers 413 instead of cutting the connection). - Labels holding text like id='1' or parent='1' are no longer read as attributes (a layer or a parent was deleted). A broken bare <mxGraphModel> file is refused. - After a sync reply the tab no longer sends its autosave copy again. Desktop and files: - A newer switch of the same preset is not rolled back by an older one that failed. .env values with escaped quotes are read whole. - MCP saved files: a file that could not be read stays protected while a folder without permission hides it, and is saved again once deleted. - The desktop app reports "no chats" only when the count was read and no model settings are stored.
155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
"use client"
|
|
|
|
/**
|
|
* Hook for VLM-based diagram validation using AI SDK's useObject.
|
|
*/
|
|
|
|
import { experimental_useObject as useObject } from "@ai-sdk/react"
|
|
import { useCallback, useRef } from "react"
|
|
import { getSelectedAIConfig } from "@/hooks/use-model-config"
|
|
import { getApiEndpoint } from "@/lib/base-path"
|
|
import {
|
|
type ValidationResult,
|
|
ValidationResultSchema,
|
|
} from "@/lib/validation-schema"
|
|
|
|
export type { ValidationResult }
|
|
|
|
// Default valid result for fallback cases
|
|
const DEFAULT_VALID_RESULT: ValidationResult = {
|
|
valid: true,
|
|
issues: [],
|
|
suggestions: [],
|
|
}
|
|
|
|
interface UseValidateDiagramOptions {
|
|
onSuccess?: (result: ValidationResult) => void
|
|
onError?: (error: Error) => void
|
|
}
|
|
|
|
// Track pending validation promises for imperative API
|
|
type PendingValidation = {
|
|
resolve: (result: ValidationResult) => void
|
|
reject: (error: Error) => void
|
|
}
|
|
|
|
export function useValidateDiagram(options: UseValidateDiagramOptions = {}) {
|
|
const { onSuccess, onError } = options
|
|
const pendingValidationRef = useRef<PendingValidation | null>(null)
|
|
|
|
const { object, submit, isLoading, error, stop } = useObject({
|
|
api: getApiEndpoint("/api/validate-diagram"),
|
|
schema: ValidationResultSchema,
|
|
// Resolved per request so a changed access code is picked up
|
|
headers: () => ({ "x-access-code": getSelectedAIConfig().accessCode }),
|
|
onFinish: ({
|
|
object,
|
|
error: finishError,
|
|
}: {
|
|
object: ValidationResult | undefined
|
|
error: Error | undefined
|
|
}) => {
|
|
if (finishError) {
|
|
console.error(
|
|
"[useValidateDiagram] Validation error:",
|
|
finishError,
|
|
)
|
|
onError?.(finishError)
|
|
pendingValidationRef.current?.reject(finishError)
|
|
pendingValidationRef.current = null
|
|
return
|
|
}
|
|
|
|
if (object) {
|
|
const result = object as ValidationResult
|
|
onSuccess?.(result)
|
|
pendingValidationRef.current?.resolve(result)
|
|
pendingValidationRef.current = null
|
|
}
|
|
},
|
|
onError: (err: Error) => {
|
|
console.error("[useValidateDiagram] Stream error:", err)
|
|
onError?.(err)
|
|
pendingValidationRef.current?.reject(err)
|
|
pendingValidationRef.current = null
|
|
},
|
|
})
|
|
|
|
/**
|
|
* Validate a diagram image.
|
|
* Returns a promise that resolves with the validation result.
|
|
*/
|
|
const validate = useCallback(
|
|
async (
|
|
imageData: string,
|
|
sessionId?: string,
|
|
): Promise<ValidationResult> => {
|
|
// 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
|
|
pendingValidationRef.current = { resolve, reject }
|
|
|
|
// Submit the validation request
|
|
submit({ imageData, sessionId })
|
|
})
|
|
},
|
|
[submit],
|
|
)
|
|
|
|
/**
|
|
* End a running check (the user pressed Stop): its promise rejects with
|
|
* an AbortError, so the tool handler can finish at once.
|
|
*/
|
|
const cancel = useCallback(() => {
|
|
const pending = pendingValidationRef.current
|
|
if (!pending) return
|
|
pendingValidationRef.current = null
|
|
stop()
|
|
pending.reject(new DOMException("Validation cancelled", "AbortError"))
|
|
}, [stop])
|
|
|
|
/**
|
|
* Validate with fallback - returns default valid result on error.
|
|
* Use this to avoid blocking the user on validation failures.
|
|
* A cancelled check is passed on as its AbortError.
|
|
*/
|
|
const validateWithFallback = useCallback(
|
|
async (
|
|
imageData: string,
|
|
sessionId?: string,
|
|
): Promise<ValidationResult> => {
|
|
try {
|
|
return await validate(imageData, sessionId)
|
|
} catch (error) {
|
|
if ((error as Error)?.name === "AbortError") throw error
|
|
console.warn(
|
|
"[useValidateDiagram] Validation failed, using fallback:",
|
|
error,
|
|
)
|
|
return DEFAULT_VALID_RESULT
|
|
}
|
|
},
|
|
[validate],
|
|
)
|
|
|
|
return {
|
|
// Validation functions
|
|
validate,
|
|
validateWithFallback,
|
|
stop,
|
|
cancel,
|
|
|
|
// State
|
|
isValidating: isLoading,
|
|
partialResult: object as ValidationResult | undefined,
|
|
error,
|
|
}
|
|
}
|