mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-25 05:54:29 +00:00
test(frontend): cover streaming markdown scheduling
This commit is contained in:
parent
9f8960f222
commit
1b5039e35a
4 changed files with 108 additions and 16 deletions
|
|
@ -35,6 +35,7 @@
|
|||
import { user } from '$lib/stores';
|
||||
|
||||
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
||||
import { createMarkdownUpdateScheduler } from './markdown-update-scheduler';
|
||||
|
||||
export let id = '';
|
||||
export let content;
|
||||
|
|
@ -59,7 +60,6 @@
|
|||
export let onTaskClick = () => {};
|
||||
|
||||
let tokens = [];
|
||||
let pendingUpdate = null;
|
||||
let lastContent = '';
|
||||
let lastParsedContent = '';
|
||||
|
||||
|
|
@ -74,21 +74,12 @@
|
|||
tokens = marked.lexer(processed);
|
||||
};
|
||||
|
||||
const updateHandler = (content) => {
|
||||
if (content && !pendingUpdate) {
|
||||
pendingUpdate = requestAnimationFrame(() => {
|
||||
pendingUpdate = null;
|
||||
parseTokens();
|
||||
});
|
||||
}
|
||||
};
|
||||
const updateScheduler = createMarkdownUpdateScheduler(parseTokens);
|
||||
|
||||
$: updateHandler(content);
|
||||
$: updateScheduler.update(content);
|
||||
|
||||
// Throttle parsing to once per animation frame while streaming
|
||||
onDestroy(() => {
|
||||
cancelAnimationFrame(pendingUpdate);
|
||||
});
|
||||
onDestroy(updateScheduler.cancel);
|
||||
</script>
|
||||
|
||||
{#key id}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
import FullHeightIframe from '$lib/components/common/FullHeightIframe.svelte';
|
||||
import OutputEditView from './OutputEditView.svelte';
|
||||
import { getOutputText, replaceOutputMessageText, type OutputItem } from './structuredOutput';
|
||||
import { resolveMarkdownDone } from './markdown-update-scheduler';
|
||||
|
||||
interface MessageType {
|
||||
id: string;
|
||||
|
|
@ -839,9 +840,7 @@
|
|||
preview={!readOnly}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
done={($settings?.chatFadeStreamingText ?? true)
|
||||
? (message?.done ?? false)
|
||||
: true}
|
||||
done={resolveMarkdownDone($settings?.chatFadeStreamingText, message?.done)}
|
||||
{model}
|
||||
onTaskClick={async (e) => {
|
||||
console.log(e);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createMarkdownUpdateScheduler, resolveMarkdownDone } from './markdown-update-scheduler';
|
||||
|
||||
describe('createMarkdownUpdateScheduler', () => {
|
||||
it('coalesces the fade-disabled done path onto the pending frame', () => {
|
||||
let content = '';
|
||||
let done = resolveMarkdownDone(false, false);
|
||||
const parsed: Array<{ content: string; done: boolean }> = [];
|
||||
const frames: FrameRequestCallback[] = [];
|
||||
const requestFrame = vi.fn((callback: FrameRequestCallback) => {
|
||||
frames.push(callback);
|
||||
return frames.length;
|
||||
});
|
||||
const scheduler = createMarkdownUpdateScheduler(
|
||||
() => parsed.push({ content, done }),
|
||||
requestFrame,
|
||||
vi.fn()
|
||||
);
|
||||
|
||||
scheduler.update(content);
|
||||
expect(requestFrame).not.toHaveBeenCalled();
|
||||
|
||||
content = 'partial';
|
||||
scheduler.update(content);
|
||||
content = 'final';
|
||||
done = resolveMarkdownDone(false, true);
|
||||
scheduler.update(content);
|
||||
|
||||
expect(requestFrame).toHaveBeenCalledTimes(1);
|
||||
expect(parsed).toEqual([]);
|
||||
|
||||
frames[0](16);
|
||||
expect(parsed).toEqual([{ content: 'final', done: true }]);
|
||||
});
|
||||
|
||||
it('cancels a pending frame on teardown', () => {
|
||||
const cancelFrame = vi.fn();
|
||||
const scheduler = createMarkdownUpdateScheduler(
|
||||
vi.fn(),
|
||||
vi.fn(() => 42),
|
||||
cancelFrame
|
||||
);
|
||||
|
||||
scheduler.update('streaming');
|
||||
scheduler.cancel();
|
||||
scheduler.cancel();
|
||||
|
||||
expect(cancelFrame).toHaveBeenCalledOnce();
|
||||
expect(cancelFrame).toHaveBeenCalledWith(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMarkdownDone', () => {
|
||||
it.each([
|
||||
{ fadeStreamingText: false, messageDone: false, expected: true },
|
||||
{ fadeStreamingText: false, messageDone: true, expected: true },
|
||||
{ fadeStreamingText: true, messageDone: false, expected: false },
|
||||
{ fadeStreamingText: true, messageDone: true, expected: true },
|
||||
{ fadeStreamingText: undefined, messageDone: false, expected: false }
|
||||
])(
|
||||
'returns $expected when fadeStreamingText=$fadeStreamingText and messageDone=$messageDone',
|
||||
({ fadeStreamingText, messageDone, expected }) => {
|
||||
expect(resolveMarkdownDone(fadeStreamingText, messageDone)).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
type RequestFrame = (callback: FrameRequestCallback) => number;
|
||||
type CancelFrame = (handle: number) => void;
|
||||
|
||||
const requestBrowserFrame: RequestFrame = (callback) => requestAnimationFrame(callback);
|
||||
const cancelBrowserFrame: CancelFrame = (handle) => cancelAnimationFrame(handle);
|
||||
|
||||
export const resolveMarkdownDone = (
|
||||
fadeStreamingText: boolean | null | undefined,
|
||||
messageDone: boolean | null | undefined
|
||||
) => ((fadeStreamingText ?? true) ? (messageDone ?? false) : true);
|
||||
|
||||
export const createMarkdownUpdateScheduler = (
|
||||
parseTokens: () => void,
|
||||
requestFrame: RequestFrame = requestBrowserFrame,
|
||||
cancelFrame: CancelFrame = cancelBrowserFrame
|
||||
) => {
|
||||
let pendingUpdate: number | null = null;
|
||||
|
||||
return {
|
||||
update(content: unknown) {
|
||||
if (!content || pendingUpdate !== null) return;
|
||||
|
||||
pendingUpdate = requestFrame(() => {
|
||||
pendingUpdate = null;
|
||||
parseTokens();
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
if (pendingUpdate === null) return;
|
||||
|
||||
cancelFrame(pendingUpdate);
|
||||
pendingUpdate = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue