mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-06 08:18:53 +00:00
fix: structured output renderer crashing on an empty output slot (#29250)
A chat could hard-fail in the browser with "TypeError: can't access property content" and stop rendering the assistant message until a reload. Streamed response items and content parts were placed at the index the provider reports. That index is not bounded by the length of the array the client has built up, so an entry could land past the end and leave a gap behind. Spreading the array on the next event turned that gap into a real empty entry, and the renderer then dereferenced it while looking for message text. Output items that would land past the end are now appended, since later events locate them by id anyway. Content and summary parts are padded up to the index instead, because a part carries no id and the text streamed for it is addressed by that same index. The renderer and the structured editor now skip an empty entry as well, so chats already saved in the broken state still display and edit. Fixes #29244
This commit is contained in:
parent
a93c508038
commit
1f529c4eb3
2 changed files with 38 additions and 13 deletions
|
|
@ -138,7 +138,7 @@
|
|||
|
||||
function getMessageText(item: any): string {
|
||||
return (item.content ?? [])
|
||||
.filter((p: any) => p.type === 'output_text' || 'text' in p)
|
||||
.filter((p: any) => p && (p.type === 'output_text' || 'text' in p))
|
||||
.map((p: any) => p.text ?? '')
|
||||
.join('\n');
|
||||
}
|
||||
|
|
@ -146,7 +146,9 @@
|
|||
function updateMessageText(idx: number, text: string) {
|
||||
const next = [...output];
|
||||
const item = { ...next[idx] };
|
||||
const parts = (item.content ?? []).filter((p: any) => p.type === 'output_text' || 'text' in p);
|
||||
const parts = (item.content ?? []).filter(
|
||||
(p: any) => p && (p.type === 'output_text' || 'text' in p)
|
||||
);
|
||||
item.content = [{ ...(parts[0] ?? { type: 'output_text' }), text }];
|
||||
next[idx] = item;
|
||||
output = next;
|
||||
|
|
@ -155,7 +157,7 @@
|
|||
|
||||
function getReasoningText(item: any): string {
|
||||
return (item.summary ?? item.content ?? [])
|
||||
.filter((p: any) => 'text' in p)
|
||||
.filter((p: any) => p && 'text' in p)
|
||||
.map((p: any) => p.text ?? '')
|
||||
.join('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,7 +374,11 @@ export function buildOutputDisplayItems(output: OutputItem[] = []): OutputDispla
|
|||
};
|
||||
|
||||
output.forEach((item, index) => {
|
||||
if (item?.type === 'function_call_output') {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.type === 'function_call_output') {
|
||||
const inlineFile = getInlineFileFromToolOutput(toolCallByCallId[item.call_id ?? ''], item);
|
||||
if (inlineFile) {
|
||||
flushDetails();
|
||||
|
|
@ -388,14 +392,14 @@ export function buildOutputDisplayItems(output: OutputItem[] = []): OutputDispla
|
|||
}
|
||||
|
||||
if (
|
||||
item?.type === 'function_call' &&
|
||||
item.type === 'function_call' &&
|
||||
item.name === 'ask_user' &&
|
||||
(item.status === 'pending' || item.status === 'in_progress')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item?.type && GROUPABLE_OUTPUT_TYPES.has(item.type)) {
|
||||
if (item.type && GROUPABLE_OUTPUT_TYPES.has(item.type)) {
|
||||
const token = buildDetailToken(item, index === output.length - 1, toolOutputByCallId);
|
||||
if (token) {
|
||||
currentDetailTokens.push(token);
|
||||
|
|
@ -403,7 +407,7 @@ export function buildOutputDisplayItems(output: OutputItem[] = []): OutputDispla
|
|||
return;
|
||||
}
|
||||
|
||||
if (item?.type === 'message') {
|
||||
if (item.type === 'message') {
|
||||
const text = getMessageText(item);
|
||||
if (text.trim()) {
|
||||
flushDetails();
|
||||
|
|
@ -481,6 +485,17 @@ function ensurePart(parts: OutputContentPart[], index: number, fallback?: Output
|
|||
return parts[index];
|
||||
}
|
||||
|
||||
function setPart(
|
||||
parts: OutputContentPart[],
|
||||
index: number,
|
||||
part: OutputContentPart,
|
||||
fallback?: OutputContentPart
|
||||
): void {
|
||||
// Assigning past the end leaves a hole that later spreads turn into undefined parts.
|
||||
ensurePart(parts, index, fallback);
|
||||
parts[index] = part;
|
||||
}
|
||||
|
||||
function findOutputItemIndex(output: OutputItem[], item: OutputItem): number {
|
||||
return output.findIndex(
|
||||
(existing) =>
|
||||
|
|
@ -529,7 +544,7 @@ export function applyResponseStreamEvent(
|
|||
} else if (outputIndex < nextOutput.length) {
|
||||
nextOutput.splice(outputIndex, 0, item);
|
||||
} else {
|
||||
nextOutput[outputIndex] = item;
|
||||
nextOutput.push(item);
|
||||
}
|
||||
return nextOutput;
|
||||
}
|
||||
|
|
@ -540,7 +555,13 @@ export function applyResponseStreamEvent(
|
|||
}
|
||||
const item = { ...event.item };
|
||||
const existingIndex = findOutputItemIndex(nextOutput, item);
|
||||
nextOutput[existingIndex >= 0 ? existingIndex : outputIndex] = item;
|
||||
if (existingIndex >= 0) {
|
||||
nextOutput[existingIndex] = item;
|
||||
} else if (outputIndex < nextOutput.length) {
|
||||
nextOutput[outputIndex] = item;
|
||||
} else {
|
||||
nextOutput.push(item);
|
||||
}
|
||||
return nextOutput;
|
||||
}
|
||||
|
||||
|
|
@ -565,7 +586,7 @@ export function applyResponseStreamEvent(
|
|||
return nextOutput;
|
||||
}
|
||||
item.content = [...(item.content ?? [])];
|
||||
item.content[event.content_index ?? item.content.length] = { ...event.part };
|
||||
setPart(item.content, event.content_index ?? item.content.length, { ...event.part });
|
||||
return nextOutput;
|
||||
}
|
||||
|
||||
|
|
@ -574,7 +595,8 @@ export function applyResponseStreamEvent(
|
|||
return nextOutput;
|
||||
}
|
||||
item.summary = [...(item.summary ?? [])];
|
||||
item.summary[event.summary_index ?? item.summary.length] = { ...event.part };
|
||||
const summaryIndex = event.summary_index ?? item.summary.length;
|
||||
setPart(item.summary, summaryIndex, { ...event.part }, { type: 'summary_text', text: '' });
|
||||
return nextOutput;
|
||||
}
|
||||
|
||||
|
|
@ -604,7 +626,8 @@ export function applyResponseStreamEvent(
|
|||
const typeName = eventType.split('.')[1];
|
||||
if (typeName === 'content_part' && event.part) {
|
||||
item.content = [...(item.content ?? [])];
|
||||
item.content[event.content_index ?? Math.max(item.content.length - 1, 0)] = { ...event.part };
|
||||
const contentIndex = event.content_index ?? Math.max(item.content.length - 1, 0);
|
||||
setPart(item.content, contentIndex, { ...event.part });
|
||||
} else if (typeName === 'function_call_arguments' && event.arguments !== undefined) {
|
||||
item.arguments = event.arguments;
|
||||
} else if (
|
||||
|
|
@ -636,7 +659,7 @@ export function replaceOutputMessageText(
|
|||
}
|
||||
|
||||
const partIndex = item.content.findIndex(
|
||||
(part) => typeof part.text === 'string' && part.text.includes(oldContent)
|
||||
(part) => typeof part?.text === 'string' && part.text.includes(oldContent)
|
||||
);
|
||||
if (partIndex === -1) {
|
||||
return item;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue