mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 04:59:51 +08:00
fix(chat): undo rejected or stopped previews, and more fixes from the third review
- A call the server runs (get_shape_library) still reaches the browser's tool handler, and it dropped the stored diagram of an earlier broken edit, whose preview then stayed. Only the tools that draw take it now. - A display_diagram whose final XML fails the checks loads the diagram from before its preview again, as a failed edit does. - After Stop, a tool result that arrives later (a screenshot check still running) no longer sends a new request; Stop also skips calls the tool handler already took. - New chat and opening another chat kept nothing of a diagram drawn without messages when it could not be saved; now they stay on it. - The settings dialog drops a model list or test result whose provider credentials changed meanwhile, also in another tab. - A saved provider this version does not know crashed the whole page on load; it is skipped. - The input emptied a moment after the message showed in the chat, so it briefly appeared twice (seen as a flaky e2e test). - The desktop app's preset switch on the same port refetches the server models instead of reloading the page, which lost unsent attachments.
This commit is contained in:
+39
-14
@@ -281,6 +281,9 @@ export default function ChatPanel({
|
||||
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
|
||||
const processedToolCallsRef = useRef<Set<string>>(new Set())
|
||||
|
||||
// Set by Stop until the user sends the next message
|
||||
const stoppedRef = useRef(false)
|
||||
|
||||
// Store original XML for display_diagram and edit_diagram streaming -
|
||||
// shared between streaming preview and tool handler
|
||||
// Key: toolCallId, Value: XML before the call's preview was drawn
|
||||
@@ -458,6 +461,10 @@ export default function ChatPanel({
|
||||
// diagram preview draws on each update, so this also limits redraws
|
||||
experimental_throttle: 150,
|
||||
sendAutomaticallyWhen: ({ messages }) => {
|
||||
// The user stopped: a tool result that arrives later (a VLM
|
||||
// check still running) must not start a new request
|
||||
if (stoppedRef.current) return false
|
||||
|
||||
const isInContinuationMode = partialXmlRef.current.length > 0
|
||||
|
||||
const shouldRetry = hasToolErrors(messages)
|
||||
@@ -869,12 +876,12 @@ export default function ChatPanel({
|
||||
// Add the combined text as the first part
|
||||
parts.unshift({ type: "text", text: userText })
|
||||
|
||||
await sendWithCurrentDiagram(parts)
|
||||
|
||||
setInput("")
|
||||
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||
setFiles([])
|
||||
setUrlData(new Map())
|
||||
await sendWithCurrentDiagram(parts, () => {
|
||||
setInput("")
|
||||
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||
setFiles([])
|
||||
setUrlData(new Map())
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error fetching chart data:", error)
|
||||
toast.error(dict.errors.failedToExport)
|
||||
@@ -882,8 +889,13 @@ export default function ChatPanel({
|
||||
}
|
||||
}
|
||||
|
||||
// Export the current diagram, snapshot it for this message, and send
|
||||
const sendWithCurrentDiagram = async (parts: any[]) => {
|
||||
// Export the current diagram, snapshot it for this message, and send.
|
||||
// onSent runs right after sending, so the input empties as the message
|
||||
// shows in the chat
|
||||
const sendWithCurrentDiagram = async (
|
||||
parts: any[],
|
||||
onSent?: () => void,
|
||||
) => {
|
||||
const chartXml = formatXML(await onFetchChart())
|
||||
const previousXml = getPreviousXml(messages.length)
|
||||
|
||||
@@ -895,6 +907,7 @@ export default function ChatPanel({
|
||||
)
|
||||
|
||||
sendChatMessage(parts, chartXml, previousXml, sessionId)
|
||||
onSent?.()
|
||||
}
|
||||
|
||||
// Send VLM validation feedback as a new user message through the normal send path
|
||||
@@ -913,12 +926,15 @@ export default function ChatPanel({
|
||||
async (sessionId: string) => {
|
||||
if (!sessionManager.isAvailable) return
|
||||
|
||||
// Save current session before switching
|
||||
if (messages.length > 0) {
|
||||
// Save current session before switching (also a diagram drawn
|
||||
// without messages); if that failed (storage full), stay on it
|
||||
if (messages.length > 0 || isRealDiagram(chartXMLRef.current)) {
|
||||
const sessionData = await buildSessionData({
|
||||
withThumbnail: true,
|
||||
})
|
||||
await sessionManager.saveCurrentSession(sessionData)
|
||||
if (!(await sessionManager.saveCurrentSession(sessionData))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to selected session
|
||||
@@ -961,8 +977,12 @@ export default function ChatPanel({
|
||||
)
|
||||
|
||||
const handleNewChat = useCallback(async () => {
|
||||
// Save current session before creating new one
|
||||
if (sessionManager.isAvailable && messages.length > 0) {
|
||||
// Save current session before creating new one (also a diagram
|
||||
// drawn without messages)
|
||||
if (
|
||||
sessionManager.isAvailable &&
|
||||
(messages.length > 0 || isRealDiagram(chartXMLRef.current))
|
||||
) {
|
||||
const sessionData = await buildSessionData({ withThumbnail: true })
|
||||
// Not saved (storage full): keep the chat on screen
|
||||
if (!(await sessionManager.saveCurrentSession(sessionData))) return
|
||||
@@ -1068,11 +1088,15 @@ export default function ChatPanel({
|
||||
|
||||
// Handle stop button click
|
||||
const handleStop = useCallback(() => {
|
||||
stoppedRef.current = true
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
// Calls the tool handler already took can still show as streaming:
|
||||
// the messages update at most every 150 ms (useChat throttle)
|
||||
const toolParts = lastMessage?.parts?.filter(
|
||||
(part: any) =>
|
||||
part.type?.startsWith("tool-") &&
|
||||
part.state === "input-streaming",
|
||||
part.state === "input-streaming" &&
|
||||
!processedToolCallsRef.current.has(part.toolCallId),
|
||||
)
|
||||
|
||||
toolParts?.forEach((part: any) => {
|
||||
@@ -1101,6 +1125,7 @@ export default function ChatPanel({
|
||||
continuationRetryCountRef.current = 0
|
||||
validationRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
stoppedRef.current = false
|
||||
|
||||
const config = getSelectedAIConfig()
|
||||
|
||||
|
||||
@@ -157,9 +157,6 @@ export function ModelConfigDialog({
|
||||
id: string
|
||||
value: string
|
||||
} | null>(null)
|
||||
// Bumped on every credential edit so a running test can tell that its
|
||||
// results belong to the old credentials
|
||||
const credentialsVersionRef = useRef(0)
|
||||
// Models fetched from the provider, per provider config
|
||||
const [fetchedModels, setFetchedModels] = useState<
|
||||
Record<string, ListedModel[]>
|
||||
@@ -192,6 +189,20 @@ export function ModelConfigDialog({
|
||||
selectedProviderIdRef.current = selectedProviderId
|
||||
const configRef = useRef(config)
|
||||
configRef.current = config
|
||||
// A model list or test result belongs to the credentials it was asked
|
||||
// with; they can change meanwhile, here or in another tab
|
||||
const credentialsOf = (providerId: string) => {
|
||||
const p = configRef.current.providers.find((x) => x.id === providerId)
|
||||
return JSON.stringify([
|
||||
p?.provider,
|
||||
p?.apiKey,
|
||||
p?.baseUrl,
|
||||
p?.awsAccessKeyId,
|
||||
p?.awsSecretAccessKey,
|
||||
p?.awsRegion,
|
||||
p?.vertexApiKey,
|
||||
])
|
||||
}
|
||||
|
||||
// Discard an unfinished model ID edit when the dialog closes
|
||||
useEffect(() => {
|
||||
@@ -217,6 +228,7 @@ export function ModelConfigDialog({
|
||||
const handleFetchModels = async () => {
|
||||
if (!selectedProvider) return
|
||||
const providerId = selectedProvider.id
|
||||
const askedWith = credentialsOf(providerId)
|
||||
setFetchingModels(true)
|
||||
setFetchModelsError("")
|
||||
try {
|
||||
@@ -237,6 +249,7 @@ export function ModelConfigDialog({
|
||||
},
|
||||
)
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (credentialsOf(providerId) !== askedWith) return
|
||||
// The picker and the error belong to the provider shown
|
||||
const stillShown = selectedProviderIdRef.current === providerId
|
||||
if (Array.isArray(data.models)) {
|
||||
@@ -253,7 +266,10 @@ export function ModelConfigDialog({
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
if (selectedProviderIdRef.current === providerId) {
|
||||
if (
|
||||
selectedProviderIdRef.current === providerId &&
|
||||
credentialsOf(providerId) === askedWith
|
||||
) {
|
||||
setFetchModelsError(dict.errors.networkError)
|
||||
}
|
||||
} finally {
|
||||
@@ -313,7 +329,6 @@ export function ModelConfigDialog({
|
||||
"vertexApiKey",
|
||||
]
|
||||
if (credentialFields.includes(field)) {
|
||||
credentialsVersionRef.current++
|
||||
setValidationStatus("idle")
|
||||
setValidatingModelIds(new Set())
|
||||
setFetchedModels(({ [selectedProviderId]: _, ...rest }) => rest)
|
||||
@@ -398,7 +413,7 @@ export function ModelConfigDialog({
|
||||
let allValid = true
|
||||
let errorCount = 0
|
||||
let idChanged = false
|
||||
const credentialsVersion = credentialsVersionRef.current
|
||||
const askedWith = credentialsOf(selectedProviderId)
|
||||
|
||||
// For EdgeOne, construct baseUrl from current origin
|
||||
const baseUrl = isEdgeOne
|
||||
@@ -474,7 +489,7 @@ export function ModelConfigDialog({
|
||||
}
|
||||
}
|
||||
// Credentials changed during the test: drop the result
|
||||
if (credentialsVersionRef.current !== credentialsVersion) return
|
||||
if (credentialsOf(selectedProviderId) !== askedWith) return
|
||||
// So did this model's id: the result is for the old one
|
||||
const current = configRef.current.providers
|
||||
.find((p) => p.id === selectedProviderId)
|
||||
@@ -500,7 +515,7 @@ export function ModelConfigDialog({
|
||||
})
|
||||
}),
|
||||
)
|
||||
if (credentialsVersionRef.current !== credentialsVersion) return
|
||||
if (credentialsOf(selectedProviderId) !== askedWith) return
|
||||
|
||||
// A model whose id changed was not tested
|
||||
if (allValid && !idChanged) {
|
||||
|
||||
Reference in New Issue
Block a user