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 69dbc89df31fc7d94e1075a3ada32e4e3e966ba7 Author: kimyenac <[email protected]> AuthorDate: Mon Oct 12 12:53:50 2026 +0900 [ZEPPELIN-6745] Add stories for AI Assistant messages and answer states --- .../assistant/ui/AssistantAnswer.stories.tsx | 88 ++++++++++++++++++++++ .../assistant/ui/AssistantMessageItem.stories.tsx | 62 +++++++++++++++ 2 files changed, 150 insertions(+) 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 new file mode 100644 index 0000000000..cedeff5d6e --- /dev/null +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx @@ -0,0 +1,88 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { AssistantConnectionError, AssistantStreamError } from '../model/assistantContract'; +import type { AssistantAnswerView } from './answerView'; +import { AssistantAnswer } from './AssistantAnswer'; + +const partial = 'The notebook groups orders by region and'; +const reply = 'The notebook groups orders by region and ranks them by total revenue.'; + +/** One fixture per state, in the order a run passes through them. */ +const fixtures = { + loading: { status: 'loading' }, + streaming: { status: 'streaming', content: partial }, + toolRunning: { + status: 'streaming', + content: '', + tools: [ + { id: 't1', name: 'list_paragraphs', status: 'done' }, + { id: 't2', name: 'list_paragraphs', status: 'running' } + ] + }, + completed: { status: 'done', message: { id: 'a1', role: 'assistant', content: reply } }, + completedWithTools: { + status: 'done', + message: { + id: 'a1', + role: 'assistant', + content: reply, + toolCalls: [ + { id: 't1', name: 'list_paragraphs' }, + { id: 't2', name: 'list_paragraphs' } + ] + } + }, + failed: { status: 'error', kind: 'failed', message: 'The model request failed.' }, + streamError: { + status: 'error', + kind: 'stream', + message: new AssistantStreamError().message, + content: partial, + tools: [{ id: 't1', name: 'list_paragraphs', status: 'done' }] + }, + connectionLost: { + status: 'error', + kind: 'connection', + message: new AssistantConnectionError().message, + content: partial + } +} satisfies Record<string, AssistantAnswerView>; + +type FixtureName = keyof typeof fixtures; + +const meta = { + title: 'Assistant/Status', + component: AssistantAnswer, + args: { view: fixtures.loading, onRetry: () => {} } +} satisfies Meta<typeof AssistantAnswer>; + +export default meta; +type Story = StoryObj<typeof meta>; + +export const Loading: Story = {}; +export const Streaming: Story = { args: { view: fixtures.streaming } }; +export const ToolRunning: Story = { args: { view: fixtures.toolRunning } }; +export const Completed: Story = { args: { view: fixtures.completed } }; +export const CompletedWithTools: Story = { args: { view: fixtures.completedWithTools } }; +export const Failed: Story = { args: { view: fixtures.failed } }; +export const StreamError: Story = { args: { view: fixtures.streamError } }; +export const ConnectionLost: Story = { args: { view: fixtures.connectionLost } }; +export const ErrorWithoutRetry: Story = { args: { view: fixtures.failed, onRetry: undefined } }; + +/** Switch the fixture from the controls panel to check that each state fully replaces the last. */ +export const StateSwitcher: StoryObj<{ state: FixtureName }> = { + args: { state: 'loading' }, + argTypes: { state: { control: 'select', options: Object.keys(fixtures) } }, + render: ({ state }) => <AssistantAnswer view={fixtures[state]} onRetry={() => {}} /> +}; 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 new file mode 100644 index 0000000000..75931eb2b7 --- /dev/null +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { AssistantAnswer } from './AssistantAnswer'; +import { AssistantMessageItem } from './AssistantMessageItem'; + +const question = 'Explain how this notebook calculates revenue.'; +const reply = 'The notebook groups orders by region and ranks them by total revenue.'; + +const meta = { + title: 'Assistant/Message', + component: AssistantMessageItem, + args: { role: 'user', content: question } +} satisfies Meta<typeof AssistantMessageItem>; + +export default meta; +type Story = StoryObj<typeof meta>; + +export const User: Story = {}; + +export const CompletedReply: Story = { + args: { role: 'assistant', content: reply } +}; + +export const StreamingReply: Story = { + args: { role: 'assistant', content: 'The notebook groups orders by region and', streaming: true } +}; + +export const LongContent: Story = { + args: { + role: 'assistant', + content: [ + 'Paragraph 2 loads `orders` and joins it with `regions` on region_id.', + 'Paragraph 3 sums amount per region and sorts the totals in descending order.', + 'A very long identifier such as orders_by_region_with_currency_normalisation_applied_v2 wraps instead of overflowing.' + ].join('\n\n') + } +}; + +export const Conversation: Story = { + render: () => ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> + <AssistantMessageItem role="user" content={question} /> + <AssistantAnswer + view={{ + status: 'done', + message: { id: 'a1', role: 'assistant', content: reply, toolCalls: [{ id: 't1', name: 'list_paragraphs' }] } + }} + /> + </div> + ) +};
