Skip to content

Commit 040c300

Browse files
committed
fix: enhance rendering event handling with new types and SSE integration
1 parent f48402e commit 040c300

6 files changed

Lines changed: 118 additions & 9 deletions

File tree

agentEvents.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export type AgentEvent =
1717
type: "tool-call";
1818
data: ToolCallEvent;
1919
}
20+
| {
21+
type: "rendering";
22+
phase: "start" | "end";
23+
label: string;
24+
}
2025
| {
2126
type: "transcript";
2227
text: string;

custom/conversation_area/ProcessingTimeline.vue

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
<template v-for="(part, index) in ToolOrReasoningParts" :key="index">
4040
<ReasoningRenderer v-if="part.type === 'reasoning'" :state="part.state" :text="part.text" />
4141
<ToolsGroup v-else-if="part.type==='data-tool-call'" :toolGroup="groupToolCallParts(message, part)" />
42+
<li v-else-if="part.type === 'data-rendering'" class="mb-6 mx-2 mt-2 px-2 z-50 overflow-hidden">
43+
<span class="bg-lightNavbar dark:bg-darkNavbar absolute flex items-center text-listTableHeadingText dark:text-darkListTableHeadingText justify-center w-5 h-5 rounded-full -start-[0.68rem] ring-4 ring-lightNavbar dark:ring-darkNavbar">
44+
<div class="w-2 h-2 rounded-full bg-current animate-pulse"></div>
45+
</span>
46+
<h3 class="flex items-center mb-1 text-sm ml-3 gap-1 text-listTableHeadingText dark:text-darkListTableHeadingText">
47+
<span class="font-semibold">{{ part.data?.label ?? 'Rendering...' }}</span>
48+
<ThreeDotsAnimation />
49+
</h3>
50+
</li>
4251
</template>
4352
</ol>
4453
</CustomAutoScrollContainer>
@@ -73,7 +82,11 @@
7382
const isExpanded = ref(true);
7483
let isUserScrolled = false;
7584
const ToolOrReasoningParts = computed(() => {
76-
return props.message.parts.filter((part: IPart) => part.type === 'data-tool-call' || part.type === 'reasoning');
85+
return props.message.parts.filter((part: IPart) => {
86+
return part.type === 'data-tool-call'
87+
|| part.type === 'reasoning'
88+
|| isActiveRenderingPart(part);
89+
});
7790
});
7891
const isResponseInProgress = computed(() =>{
7992
return props.isLastMessageInChat && agentStore.isResponseInProgress;
@@ -157,6 +170,14 @@
157170
});
158171
};
159172
173+
function isActiveRenderingPart(part: IPart) {
174+
return part.type === 'data-rendering'
175+
&& part.data?.phase === 'start'
176+
&& !props.message.parts.some((candidate: IPart) => {
177+
return candidate.type === 'data-rendering' && candidate.data?.phase === 'end';
178+
});
179+
}
180+
160181
const groupToolCallParts = (message: IMessage, currentPart: IPart): IToolGroup[] => {
161182
if (currentPart.type !== 'data-tool-call') {
162183
return [];
@@ -259,4 +280,4 @@
259280
}
260281
}
261282
262-
</style>
283+
</style>

custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ let highlightModulePromise: Promise<typeof import('./incremarkCodeHighlight')> |
7272
const sourceCode = computed(() => props.node.value ?? '');
7373
const language = computed(() => props.node.lang?.trim().toLowerCase() || 'text');
7474
const languageLabel = computed(() => language.value === 'vega-lite' ? '' : props.node.lang?.trim() || 'text');
75-
const shouldRenderVega = computed(() => language.value === 'vega-lite' && props.blockStatus === 'completed');
75+
const shouldRenderVega = computed(() => language.value === 'vega-lite');
7676
const codeTheme = computed<IncremarkCodeTheme>(() => {
7777
const requestedTheme = props.theme ?? (prefersDarkMode.value ? props.darkTheme : props.lightTheme);
7878
@@ -390,4 +390,4 @@ function clearVega() {
390390
:deep(.incremark-vega){
391391
padding: 0;
392392
}
393-
</style>
393+
</style>

custom/types.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
export interface IPartData {
2-
toolCallId: string;
3-
toolName: string;
4-
phase: 'start' | 'end';
2+
toolCallId?: string;
3+
toolName?: string;
4+
phase?: 'start' | 'end';
5+
label?: string;
56
input?: any;
67
output?: any;
78
durationMs?: number;
89
toolInfo?: string;
910
}
1011
export interface IPart {
11-
type: 'reasoning' | 'data-tool-call' | 'text';
12+
type: 'reasoning' | 'data-tool-call' | 'data-rendering' | 'text';
1213
text?: string;
1314
state?: 'started' | 'thinking' | 'processing' | 'streaming' | 'done';
1415
data?: IPartData;

index.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ const createSessionBodySchema = z.object({
8989
triggerMessage: z.string().optional(),
9090
}).strict();
9191

92+
const VEGA_LITE_FENCE_START = "```vega-lite";
93+
const COMPLETE_VEGA_LITE_BLOCK_RE = /```vega-lite[\s\S]*?```/;
94+
9295
function isAbortError(error: unknown): boolean {
9396
return (
9497
error instanceof DOMException && error.name === "AbortError"
@@ -279,6 +282,8 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
279282

280283
private async runAgentTurn(input: AgentTurnRunInput) {
281284
let fullResponse = "";
285+
let bufferedTextDelta = "";
286+
let isRenderingVegaLite = false;
282287
const maxTokens = this.options.maxTokens ?? 1000;
283288
const selectedMode = this.options.modes.find((mode) => mode.name === input.modeName) ?? this.options.modes[0];
284289
const [primaryModelSpec, summaryModelSpec] = await Promise.all([
@@ -386,13 +391,63 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
386391

387392
if (textDelta) {
388393
fullResponse += textDelta;
394+
bufferedTextDelta += textDelta;
395+
396+
if (
397+
bufferedTextDelta.includes(VEGA_LITE_FENCE_START) &&
398+
!COMPLETE_VEGA_LITE_BLOCK_RE.test(bufferedTextDelta)
399+
) {
400+
if (!isRenderingVegaLite) {
401+
isRenderingVegaLite = true;
402+
await input.emit?.({
403+
type: "rendering",
404+
phase: "start",
405+
label: "Rendering...",
406+
});
407+
}
408+
continue;
409+
}
410+
411+
if (isRenderingVegaLite) {
412+
isRenderingVegaLite = false;
413+
await input.emit?.({
414+
type: "rendering",
415+
phase: "end",
416+
label: "Rendering...",
417+
});
418+
}
419+
420+
const streamableLength = bufferedTextDelta.includes(VEGA_LITE_FENCE_START)
421+
? bufferedTextDelta.length
422+
: bufferedTextDelta.length - getPartialVegaLiteFenceStartLength(bufferedTextDelta);
423+
424+
if (!streamableLength) {
425+
continue;
426+
}
427+
389428
await input.emit?.({
390429
type: "text-delta",
391-
delta: textDelta,
430+
delta: bufferedTextDelta.slice(0, streamableLength),
392431
});
432+
bufferedTextDelta = bufferedTextDelta.slice(streamableLength);
393433
}
394434
}
395435

436+
if (isRenderingVegaLite) {
437+
await input.emit?.({
438+
type: "rendering",
439+
phase: "end",
440+
label: "Rendering...",
441+
});
442+
}
443+
444+
if (bufferedTextDelta) {
445+
await input.emit?.({
446+
type: "text-delta",
447+
delta: bufferedTextDelta,
448+
});
449+
}
450+
396451
return {
397452
text: fullResponse,
398453
};
@@ -956,3 +1011,13 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
9561011
})
9571012
}
9581013
}
1014+
1015+
function getPartialVegaLiteFenceStartLength(text: string): number {
1016+
for (let length = Math.min(text.length, VEGA_LITE_FENCE_START.length - 1); length > 0; length -= 1) {
1017+
if (VEGA_LITE_FENCE_START.startsWith(text.slice(-length))) {
1018+
return length;
1019+
}
1020+
}
1021+
1022+
return 0;
1023+
}

surfaces/web-sse/createSseEventEmitter.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ function createAgentEventStream(
108108
});
109109
},
110110

111+
rendering(phase: "start" | "end", label: string) {
112+
if (phase === "start") {
113+
stream.endActiveBlock();
114+
}
115+
116+
stream.send({
117+
type: "data-rendering",
118+
data: {
119+
phase,
120+
label,
121+
},
122+
});
123+
},
124+
111125
transcript(text: string, language?: string) {
112126
stream.send({
113127
type: "transcript",
@@ -226,6 +240,9 @@ export function createSseEventEmitter(
226240
case "tool-call":
227241
stream.toolCall(event.data);
228242
break;
243+
case "rendering":
244+
stream.rendering(event.phase, event.label);
245+
break;
229246
case "transcript":
230247
stream.transcript(event.text, event.language);
231248
break;

0 commit comments

Comments
 (0)