This is an automated email from the ASF dual-hosted git repository. voidmatcha pushed a commit to branch branch-voidmatcha-assistant-integration-temp in repository https://gitbox.apache.org/repos/asf/zeppelin.git
commit 9fd079b214112ffe4dc13897da445ebac18049ef Author: YONGJAE LEE <[email protected]> AuthorDate: Fri Oct 16 12:42:19 2026 +0900 [ZEPPELIN-6745] Align answer UI with segmented history --- .../entities/assistant/ui/AssistantAnswer.spec.tsx | 53 ++++++++++++++++++++-- .../assistant/ui/AssistantAnswer.stories.tsx | 19 +++++--- .../src/entities/assistant/ui/AssistantAnswer.tsx | 13 ++++-- .../assistant/ui/AssistantMessageItem.stories.tsx | 6 ++- .../src/entities/assistant/ui/AssistantToolLog.tsx | 9 +++- .../src/entities/assistant/ui/answerView.ts | 6 +-- 6 files changed, 85 insertions(+), 21 deletions(-) diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx index 7531cf429e..e1a3ed4253 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx @@ -42,8 +42,10 @@ describe('AssistantAnswer', () => { message: { id: 'm1', role: 'assistant', - content: 'Revenue is grouped by region.', - toolCalls: [{ id: 't1', name: 'list_paragraphs' }] + segments: [ + { id: 's1', content: '', toolCalls: [{ id: 't1', name: 'list_paragraphs' }] }, + { id: 's2', content: 'Revenue is grouped by region.' } + ] } }} /> @@ -97,8 +99,7 @@ describe('AssistantAnswer', () => { message: { id: 'm1', role: 'assistant', - content: 'Revenue', - toolCalls: [{ id: 't1', name: 'list_paragraphs' }] + segments: [{ id: 's1', content: 'Revenue', toolCalls: [{ id: 't1', name: 'list_paragraphs' }] }] } }} /> @@ -111,6 +112,28 @@ describe('AssistantAnswer', () => { expect(screen.getByRole('status').textContent).toBe('Running Tool…'); }); + it('keeps completed answer rounds in order and shows tools from every round', () => { + render( + <AssistantAnswer + view={{ + status: 'done', + message: { + id: 'm2', + role: 'assistant', + segments: [ + { id: 'm1', content: 'First finding', toolCalls: [{ id: 't1', name: 'list_paragraphs' }] }, + { id: 'm2', content: 'Second finding', toolCalls: [{ id: 't2', name: 'read_results' }] } + ] + } + }} + /> + ); + + expect(answer().textContent).toContain('First finding\n\nSecond finding'); + expect(screen.getByText('Used 2 tools')).toBeTruthy(); + expect(screen.getAllByRole('listitem').map(item => item.textContent)).toEqual(['list_paragraphs', 'read_results']); + }); + it('keeps the partial answer and replaces the progress line with the error', () => { const view: AssistantAnswerView = { status: 'error', @@ -125,10 +148,30 @@ describe('AssistantAnswer', () => { expect(screen.getByRole('alert').textContent).toContain('stopped responding'); }); + it('does not imply a tool is still observed after the connection is lost', () => { + render( + <AssistantAnswer + view={{ + status: 'error', + kind: 'connection', + message: 'Connection lost', + tools: [{ id: 't1', name: 'read_results', status: 'running' }] + }} + /> + ); + + expect(screen.getByText('Used 1 tool (status unknown)')).toBeTruthy(); + expect(screen.queryByRole('img', { name: 'Running' })).toBeNull(); + expect(screen.getByRole('img', { name: 'Status unknown' })).toBeTruthy(); + }); + it('renders answer bodies through the given renderer', () => { render( <AssistantAnswer - view={{ status: 'done', message: { id: 'm1', role: 'assistant', content: '**bold**' } }} + view={{ + status: 'done', + message: { id: 'm1', role: 'assistant', segments: [{ id: 's1', content: '**bold**' }] } + }} renderContent={content => <strong>{content.replaceAll('*', '')}</strong>} /> ); diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx index cedeff5d6e..dc472e2696 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx @@ -30,16 +30,22 @@ const fixtures = { { id: 't2', name: 'list_paragraphs', status: 'running' } ] }, - completed: { status: 'done', message: { id: 'a1', role: 'assistant', content: reply } }, + completed: { status: 'done', message: { id: 'a1', role: 'assistant', segments: [{ id: 's1', content: reply }] } }, completedWithTools: { status: 'done', message: { id: 'a1', role: 'assistant', - content: reply, - toolCalls: [ - { id: 't1', name: 'list_paragraphs' }, - { id: 't2', name: 'list_paragraphs' } + segments: [ + { + id: 's1', + content: '', + toolCalls: [ + { id: 't1', name: 'list_paragraphs' }, + { id: 't2', name: 'list_paragraphs' } + ] + }, + { id: 's2', content: reply } ] } }, @@ -55,7 +61,8 @@ const fixtures = { status: 'error', kind: 'connection', message: new AssistantConnectionError().message, - content: partial + content: partial, + tools: [{ id: 't1', name: 'read_results', status: 'running' }] } } satisfies Record<string, AssistantAnswerView>; diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx index b0f6fbecbc..1139c73b2e 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx @@ -11,6 +11,7 @@ */ import { Skeleton } from 'antd'; +import { answerText, answerToolCalls } from '../model/messageHistory'; import type { AssistantContentRenderer, AssistantAnswerView, AssistantToolActivity } from './answerView'; import { AssistantAnswerError } from './AssistantAnswerError'; import { AssistantMessageItem } from './AssistantMessageItem'; @@ -61,19 +62,23 @@ export const AssistantAnswer = ({ view, onRetry, renderContent }: AssistantAnswe ); } case 'done': { - const tools = (view.message.toolCalls ?? []).map(call => ({ ...call, status: 'done' as const })); + const tools = answerToolCalls(view.message).map(call => ({ ...call, status: 'done' as const })); return ( - <AssistantMessageItem role="assistant" content={view.message.content} renderContent={renderContent}> + <AssistantMessageItem role="assistant" content={answerText(view.message)} renderContent={renderContent}> <AssistantToolLog tools={tools} /> </AssistantMessageItem> ); } - case 'error': + case 'error': { + const tools = (view.tools ?? []).map(tool => + tool.status === 'running' ? { ...tool, status: 'unknown' as const } : tool + ); return ( <AssistantMessageItem role="assistant" content={view.content ?? ''} renderContent={renderContent}> - <AssistantToolLog tools={view.tools ?? []} /> + <AssistantToolLog tools={tools} /> <AssistantAnswerError kind={view.kind} message={view.message} onRetry={onRetry} /> </AssistantMessageItem> ); + } } }; diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx index 75931eb2b7..69ba1ccff4 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx @@ -54,7 +54,11 @@ export const Conversation: Story = { <AssistantAnswer view={{ status: 'done', - message: { id: 'a1', role: 'assistant', content: reply, toolCalls: [{ id: 't1', name: 'list_paragraphs' }] } + message: { + id: 'a1', + role: 'assistant', + segments: [{ id: 's1', content: reply, toolCalls: [{ id: 't1', name: 'list_paragraphs' }] }] + } }} /> </div> diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx index 46a0a111a6..aa1359733a 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx @@ -10,7 +10,7 @@ * limitations under the License. */ -import { CheckOutlined } from '@ant-design/icons'; +import { CheckOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { Spin } from 'antd'; import type { AssistantToolActivity } from './answerView'; import * as styles from './AssistantAnswer.css'; @@ -25,10 +25,13 @@ export interface AssistantToolLogProps { export const AssistantToolLog = ({ tools }: AssistantToolLogProps) => { if (!tools.length) return null; const running = tools.some(tool => tool.status === 'running'); + const unknown = tools.some(tool => tool.status === 'unknown'); const count = `${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`; return ( <details className={styles.toolLog} open={running}> - <summary className={styles.toolSummary}>{running ? `Using ${count}` : `Used ${count}`}</summary> + <summary className={styles.toolSummary}> + {running ? `Using ${count}` : unknown ? `Used ${count} (status unknown)` : `Used ${count}`} + </summary> <ul className={styles.toolList}> {tools.map(tool => ( <li key={tool.id} className={styles.toolItem} data-status={tool.status}> @@ -36,6 +39,8 @@ export const AssistantToolLog = ({ tools }: AssistantToolLogProps) => { <span className={styles.toolIcon} role="img" aria-label="Running"> <Spin size="small" /> </span> + ) : tool.status === 'unknown' ? ( + <QuestionCircleOutlined className={styles.toolIcon} aria-label="Status unknown" /> ) : ( <CheckOutlined className={`${styles.toolIcon} ${styles.toolDone}`} aria-label="Done" /> )} diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts index b11e7ebcfc..f154658c79 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts @@ -11,13 +11,13 @@ */ import type { ReactNode } from 'react'; -import type { AssistantMessage } from '../model/assistantContract'; +import type { AssistantAnswerMessage } from '../model/assistantContract'; /** A tool the assistant called during a live run. `tool_call.done` may omit the name, so it is optional. */ export interface AssistantToolActivity { id: string; name?: string; - status: 'running' | 'done'; + status: 'running' | 'done' | 'unknown'; } /** @@ -30,7 +30,7 @@ export type AssistantAnswerErrorKind = 'failed' | 'stream' | 'connection'; export type AssistantAnswerView = | { status: 'loading' } | { status: 'streaming'; content: string; tools?: AssistantToolActivity[] } - | { status: 'done'; message: AssistantMessage } + | { status: 'done'; message: AssistantAnswerMessage } | { status: 'error'; kind: AssistantAnswerErrorKind;
