This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new f801b2e4c2 [ZEPPELIN-6745] Add AI Assistant message and answer state UI
f801b2e4c2 is described below

commit f801b2e4c2f0fae5e5e47ed6e12a2eda9069fd14
Author: 김예나 <[email protected]>
AuthorDate: Tue Oct 20 22:50:00 2026 +0900

    [ZEPPELIN-6745] Add AI Assistant message and answer state UI
    
    ### What is this PR for?
    
    Add the AI Assistant message and answer state UI: user and Assistant 
messages are shown distinctly, and an answer displays its loading, streaming, 
tool-use, completed and error states. Everything is driven by fixture props. 
There is no server, transport or panel state. The components live in 
`projects/zeppelin-react/src/entities/assistant/ui`.
    
    This PR adds new files under `entities/assistant/ui` and removes the 
temporary `Assistant/Setup` story. It builds on the shared contract from 
ZEPPELIN-6741 (#5561) and the Markdown answer from ZEPPELIN-6744 (#5570), both 
on `master`.
    
    Commits:
    1. **Message item and answer view types**
       * `AssistantMessageItem` renders a question or an answer:
         * A question is a right-aligned bubble, always shown as typed. It 
never goes through the content renderer.
         * An answer is a full-width body under an "AI / Assistant" label. 
While it streams, it is marked `aria-busy` and shows a caret where the text 
ends.
       * `AssistantAnswerView` is the display union (`loading` / `streaming` / 
`done` / `error`). It reuses `AssistantAnswerMessage` and 
`AssistantToolCallRef` from the ZEPPELIN-6741 contract.
    2. **Answer status, tool log and error displays**
       * `AssistantAnswer` renders one view at a time. Each state fully 
replaces the last.
       * The progress line (`role="status"`) reads "Thinking…", "Writing 
answer…" or "Running \<tool\>…".
       * `AssistantToolLog` is open while a tool runs and collapses once the 
answer completes. A tool without a name is labelled "Tool".
       * `AssistantAnswerError` is an antd `Alert` (`role="alert"`):
         * `failed` (`run.failed`) and `stream` (`AssistantStreamError`) are 
errors with **Retry**.
         * `connection` (`AssistantConnectionError`) is a warning with **Reload 
conversation**, because the answer may have finished on the server.
         * With no `onRetry`, no button is shown.
         * A partial answer that arrived before the error is kept.
    3. **Stories**
       * `Assistant/Message`: User, CompletedReply, StreamingReply, 
LongContent, Conversation.
       * `Assistant/Status`: Loading, Streaming, ToolRunning, Completed, 
CompletedWithTools, Failed, StreamError, ConnectionLost, ErrorWithoutRetry.
       * `Assistant/Status/StateSwitcher` switches the fixture from the 
Controls panel, so you can check that the states stay consistent in both themes.
    4. **Review: remove the temporary theme check story**
       * `Assistant/Setup › TemporaryThemeCheck` (`AssistantScope.stories.tsx`) 
was a placeholder until the Assistant UI stories landed. Nothing else 
referenced it.
    5. **Review: align the answer UI with segmented history** (by 
<at>voidmatcha)
       * A completed answer is an `AssistantAnswerMessage` with ordered 
`segments`. Its text joins the rounds in order, and its tool log lists the 
tools from every round.
       * Tools still marked running when an error arrives are shown as "status 
unknown", not as running or done.
    6. **Review: render answers as Markdown by default**
       * `renderContent` now defaults to `MarkdownAnswer` from ZEPPELIN-6744. A 
parent can still pass its own, for example one that resolves paragraph 
references.
       * `MarkdownAnswer` wraps the text in blocks, so the streaming caret is 
now drawn inside the last block (or the last item of a closing list). It stays 
on the line where the text ends instead of dropping below it.
    
    Styling uses `<at>vanilla-extract/css` through the Assistant theme contract 
only, so light and dark need no per-component rules. The HTML/CSS screens in 
`voidmatcha/assistant-storybook-share` were used as a design reference. The 
code is written from scratch.
    
    **Out of scope** (per the issue / ZEPPELIN-6748): run event → view 
derivation, panel mounting in `AssistantWorkspace`, real tool calls, approval, 
persistence and WebSocket handling. Showing failed tool calls (raised in 
review) is tracked in ZEPPELIN-6754.
    
    ### What type of PR is it?
    Feature
    
    ### Todos
    * [x] Use the ZEPPELIN-6744 Markdown component as the default 
`renderContent` once it merges
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6745
    
    ### How should this be tested?
    In `zeppelin-web-angular/projects/zeppelin-react`:
    * `npm ci`, `npm test` (254 passed), `npm run typecheck`, `npm run lint` 
and `npm run build:storybook` all pass.
      * `typecheck` needs `npm ci` in `zeppelin-web-angular` first, because the 
SDK resolves `rxjs` from there.
      * The new files also pass the frontend's `prettier --check`.
    * `npm run storybook`: both themes checked manually for every story above, 
including StateSwitcher.
    * The specs were mutation-checked: making the connection error say "Retry", 
keeping the progress line on error, dropping the "Tool" fallback, rendering a 
question through `renderContent`, keeping the tool log open, or going back to a 
plain-text default each fails exactly the intended test.
    
    **`npm run test:storybook`:**
    * With `--workers=1`, all 68 tests pass.
    * With the default config (3 workers), 1–5 tests time out between runs.
      * The traces show asset requests to the `python3 -m http.server` web 
server failing (status -1) and a blank page.
      * On the earlier temporary base (`4599ad2d9`), its own story alone 
reproduced this: with `--repeat-each=8`, 1–2 of 16 time out.
      * So it is a problem with the existing smoke-test server, not with these 
stories. I left it untouched here.
    
    ### Screenshots (if appropriate)
    See the `Assistant/Message` and `Assistant/Status` stories.
    
    ### Questions:
    * Does the license files need to update? No. There are no new dependencies.
    * Is there breaking changes for older versions? No. Only new files are 
added, plus the removal of a temporary story.
    * Does this needs documentation? No.
    
    
    
    Closes #5567 from kimyenac/ZEPPELIN-6745.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../entities/assistant/ui/AssistantAnswer.css.ts   |  91 +++++++++++
 .../entities/assistant/ui/AssistantAnswer.spec.tsx | 180 +++++++++++++++++++++
 .../assistant/ui/AssistantAnswer.stories.tsx       |  95 +++++++++++
 .../src/entities/assistant/ui/AssistantAnswer.tsx  |  84 ++++++++++
 .../assistant/ui/AssistantAnswerError.spec.tsx     |  49 ++++++
 .../entities/assistant/ui/AssistantAnswerError.tsx |  46 ++++++
 .../assistant/ui/AssistantMessageItem.css.ts       |  93 +++++++++++
 .../assistant/ui/AssistantMessageItem.spec.tsx     |  42 +++++
 .../assistant/ui/AssistantMessageItem.stories.tsx  |  66 ++++++++
 .../entities/assistant/ui/AssistantMessageItem.tsx |  67 ++++++++
 .../src/entities/assistant/ui/AssistantToolLog.tsx |  53 ++++++
 .../src/entities/assistant/ui/answerView.ts        |  44 +++++
 .../ui/assistant-theme/AssistantScope.stories.tsx  |  35 ----
 13 files changed, 910 insertions(+), 35 deletions(-)

diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.css.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.css.ts
new file mode 100644
index 0000000000..044307eb43
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.css.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 { globalStyle, keyframes, style } from '@vanilla-extract/css';
+import { vars } from '@/shared/ui/assistant-theme/AssistantScope.css';
+
+export const progress = style({
+  display: 'flex',
+  alignItems: 'center',
+  gap: vars.space2,
+  color: vars.textSecondary,
+  fontSize: vars.fontSizeSm
+});
+
+const pulse = keyframes({
+  '0%, 80%, 100%': { opacity: 0.25 },
+  '40%': { opacity: 1 }
+});
+
+export const dots = style({
+  display: 'inline-flex',
+  gap: 3
+});
+
+globalStyle(`${dots} > i`, {
+  width: 4,
+  height: 4,
+  borderRadius: '50%',
+  background: vars.accent,
+  animation: `${pulse} 1.2s ${vars.easeStandard} infinite`
+});
+globalStyle(`${dots} > i:nth-child(2)`, { animationDelay: '0.15s' });
+globalStyle(`${dots} > i:nth-child(3)`, { animationDelay: '0.3s' });
+
+export const skeleton = style({
+  marginTop: vars.space1
+});
+
+export const toolLog = style({
+  border: `1px solid ${vars.border}`,
+  borderRadius: vars.radius,
+  background: vars.fill,
+  fontSize: vars.fontSizeSm
+});
+
+export const toolSummary = style({
+  padding: `${vars.space1} ${vars.space2}`,
+  color: vars.textSecondary,
+  cursor: 'pointer'
+});
+
+export const toolList = style({
+  listStyle: 'none',
+  margin: 0,
+  padding: `0 ${vars.space2} ${vars.space2}`,
+  display: 'flex',
+  flexDirection: 'column',
+  gap: vars.space1
+});
+
+export const toolItem = style({
+  display: 'flex',
+  alignItems: 'center',
+  gap: vars.space2,
+  color: vars.text
+});
+
+export const toolIcon = style({
+  display: 'inline-flex',
+  width: 14,
+  justifyContent: 'center',
+  color: vars.textSecondary
+});
+
+export const toolDone = style({
+  color: vars.success
+});
+
+export const toolName = style({
+  fontFamily: vars.fontMono,
+  overflowWrap: 'anywhere'
+});
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
new file mode 100644
index 0000000000..50edf03585
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx
@@ -0,0 +1,180 @@
+/*
+ * 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 { render, screen, within } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import type { AssistantAnswerView } from './answerView';
+import { AssistantAnswer } from './AssistantAnswer';
+
+const answer = () => screen.getByRole('article', { name: 'Assistant answer' });
+
+describe('AssistantAnswer', () => {
+  it('shows only the current state as the run moves from loading to done', () 
=> {
+    const { rerender } = render(<AssistantAnswer view={{ status: 'loading' }} 
/>);
+    expect(screen.getByRole('status').textContent).toBe('Thinking…');
+    expect(answer().getAttribute('aria-busy')).toBe('true');
+
+    rerender(
+      <AssistantAnswer
+        view={{
+          status: 'streaming',
+          content: 'Revenue is grouped',
+          tools: [{ id: 't1', name: 'list_paragraphs', status: 'done' }]
+        }}
+      />
+    );
+    expect(screen.getByRole('status').textContent).toBe('Writing answer…');
+    expect(answer().textContent).toContain('Revenue is grouped');
+
+    rerender(
+      <AssistantAnswer
+        view={{
+          status: 'done',
+          message: {
+            id: 'm1',
+            role: 'assistant',
+            segments: [
+              { id: 's1', content: '', toolCalls: [{ id: 't1', name: 
'list_paragraphs' }] },
+              { id: 's2', content: 'Revenue is grouped by region.' }
+            ]
+          }
+        }}
+      />
+    );
+    expect(screen.queryByRole('status')).toBeNull();
+    expect(screen.queryByRole('alert')).toBeNull();
+    expect(answer().hasAttribute('aria-busy')).toBe(false);
+    expect(answer().textContent).toContain('Revenue is grouped by region.');
+    expect(screen.getByText('Used 1 tool')).toBeTruthy();
+  });
+
+  it('names the running tool in the progress line and marks each tool with its 
state', () => {
+    render(
+      <AssistantAnswer
+        view={{
+          status: 'streaming',
+          content: '',
+          tools: [
+            { id: 't1', name: 'list_paragraphs', status: 'done' },
+            { id: 't2', name: 'read_results', status: 'running' }
+          ]
+        }}
+      />
+    );
+    expect(screen.getByRole('status').textContent).toBe('Running 
read_results…');
+    expect(screen.getByText('Using 2 tools')).toBeTruthy();
+    const [first, second] = screen.getAllByRole('listitem');
+    expect(within(first).getByRole('img', { name: 'Done' })).toBeTruthy();
+    expect(within(second).getByRole('img', { name: 'Running' })).toBeTruthy();
+  });
+
+  it('opens the tool log while a tool runs and collapses it when the answer 
completes', () => {
+    const running: AssistantAnswerView = {
+      status: 'streaming',
+      content: '',
+      tools: [{ id: 't1', name: 'list_paragraphs', status: 'running' }]
+    };
+    const toolLog = () => screen.getByText(/tool$/).closest('details')!;
+    const { rerender } = render(<AssistantAnswer view={running} />);
+    expect(toolLog().open).toBe(true);
+
+    // A user who collapses the log keeps it collapsed while more of the 
answer arrives.
+    toolLog().open = false;
+    rerender(<AssistantAnswer view={{ ...running, content: 'Revenue' }} />);
+    expect(toolLog().open).toBe(false);
+
+    rerender(
+      <AssistantAnswer
+        view={{
+          status: 'done',
+          message: {
+            id: 'm1',
+            role: 'assistant',
+            segments: [{ id: 's1', content: 'Revenue', toolCalls: [{ id: 't1', 
name: 'list_paragraphs' }] }]
+          }
+        }}
+      />
+    );
+    expect(toolLog().open).toBe(false);
+  });
+
+  it('labels a tool whose completion event carried no name', () => {
+    render(<AssistantAnswer view={{ status: 'streaming', content: '', tools: 
[{ id: 't1', status: 'running' }] }} />);
+    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(Array.from(answer().querySelectorAll('p'), p => 
p.textContent)).toEqual(['First finding', 'Second 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',
+      kind: 'stream',
+      message: 'The assistant stopped responding before the reply finished. 
Retry the request.',
+      content: 'Revenue is grouped'
+    };
+    render(<AssistantAnswer view={view} />);
+    expect(screen.queryByRole('status')).toBeNull();
+    expect(answer().hasAttribute('aria-busy')).toBe(false);
+    expect(answer().textContent).toContain('Revenue is grouped');
+    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', segments: [{ id: 's1', 
content: '**bold**' }] }
+        }}
+        renderContent={content => <strong>{content.replaceAll('*', 
'')}</strong>}
+      />
+    );
+    expect(answer().querySelector('strong')?.textContent).toBe('bold');
+  });
+});
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..dc472e2696
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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', 
segments: [{ id: 's1', content: reply }] } },
+  completedWithTools: {
+    status: 'done',
+    message: {
+      id: 'a1',
+      role: 'assistant',
+      segments: [
+        {
+          id: 's1',
+          content: '',
+          toolCalls: [
+            { id: 't1', name: 'list_paragraphs' },
+            { id: 't2', name: 'list_paragraphs' }
+          ]
+        },
+        { id: 's2', content: reply }
+      ]
+    }
+  },
+  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,
+    tools: [{ id: 't1', name: 'read_results', status: 'running' }]
+  }
+} 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/AssistantAnswer.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx
new file mode 100644
index 0000000000..1139c73b2e
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { Skeleton } from 'antd';
+import { answerText, answerToolCalls } from '../model/messageHistory';
+import type { AssistantContentRenderer, AssistantAnswerView, 
AssistantToolActivity } from './answerView';
+import { AssistantAnswerError } from './AssistantAnswerError';
+import { AssistantMessageItem } from './AssistantMessageItem';
+import { AssistantToolLog, toolLabel } from './AssistantToolLog';
+import * as styles from './AssistantAnswer.css';
+
+const Progress = ({ text }: { text: string }) => (
+  <div role="status" className={styles.progress}>
+    <span className={styles.dots} aria-hidden="true">
+      <i />
+      <i />
+      <i />
+    </span>
+    {text}
+  </div>
+);
+
+const progressText = (content: string, tools: AssistantToolActivity[]) => {
+  const running = tools.find(tool => tool.status === 'running');
+  if (running) return `Running ${toolLabel(running)}…`;
+  return content ? 'Writing answer…' : 'Thinking…';
+};
+
+export interface AssistantAnswerProps {
+  view: AssistantAnswerView;
+  /** Called from the error display. */
+  onRetry?: () => void;
+  renderContent?: AssistantContentRenderer;
+}
+
+/** One assistant answer in whichever state its run is in. Each state replaces 
the last, so only one shows at a time. */
+export const AssistantAnswer = ({ view, onRetry, renderContent }: 
AssistantAnswerProps) => {
+  switch (view.status) {
+    case 'loading':
+      return (
+        <AssistantMessageItem role="assistant" content="" streaming>
+          <Progress text="Thinking…" />
+          <Skeleton className={styles.skeleton} active title={false} 
paragraph={{ rows: 2, width: ['92%', '64%'] }} />
+        </AssistantMessageItem>
+      );
+    case 'streaming': {
+      const tools = view.tools ?? [];
+      return (
+        <AssistantMessageItem role="assistant" content={view.content} 
streaming renderContent={renderContent}>
+          <AssistantToolLog tools={tools} />
+          <Progress text={progressText(view.content, tools)} />
+        </AssistantMessageItem>
+      );
+    }
+    case 'done': {
+      const tools = answerToolCalls(view.message).map(call => ({ ...call, 
status: 'done' as const }));
+      return (
+        <AssistantMessageItem role="assistant" 
content={answerText(view.message)} renderContent={renderContent}>
+          <AssistantToolLog tools={tools} />
+        </AssistantMessageItem>
+      );
+    }
+    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={tools} />
+          <AssistantAnswerError kind={view.kind} message={view.message} 
onRetry={onRetry} />
+        </AssistantMessageItem>
+      );
+    }
+  }
+};
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswerError.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswerError.spec.tsx
new file mode 100644
index 0000000000..644505c632
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswerError.spec.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { AssistantConnectionError, AssistantStreamError } from 
'../model/assistantContract';
+import { AssistantAnswerError } from './AssistantAnswerError';
+
+describe('AssistantAnswerError', () => {
+  it('shows a server failure with a retry action', () => {
+    const onRetry = vi.fn();
+    render(<AssistantAnswerError kind="failed" message="The model request 
failed." onRetry={onRetry} />);
+    expect(screen.getByRole('alert').textContent).toContain('The model request 
failed.');
+    fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
+    expect(onRetry).toHaveBeenCalledOnce();
+  });
+
+  it('offers retry when the stream ended early', () => {
+    render(<AssistantAnswerError kind="stream" message={new 
AssistantStreamError().message} onRetry={() => {}} />);
+    expect(screen.getByRole('alert').textContent).toContain('stopped 
responding before the reply finished');
+    expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy();
+  });
+
+  it('asks for a reload, not a retry, when the connection dropped', () => {
+    const onRetry = vi.fn();
+    render(
+      <AssistantAnswerError kind="connection" message={new 
AssistantConnectionError().message} onRetry={onRetry} />
+    );
+    expect(screen.getByRole('alert').textContent).toContain('connection to the 
server was lost');
+    expect(screen.queryByRole('button', { name: 'Retry' })).toBeNull();
+    fireEvent.click(screen.getByRole('button', { name: 'Reload conversation' 
}));
+    expect(onRetry).toHaveBeenCalledOnce();
+  });
+
+  it('shows no button when there is nothing to retry', () => {
+    render(<AssistantAnswerError kind="failed" message="The model request 
failed." />);
+    expect(screen.getByRole('alert')).toBeTruthy();
+    expect(screen.queryByRole('button')).toBeNull();
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswerError.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswerError.tsx
new file mode 100644
index 0000000000..ebfb6e80fa
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswerError.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 { Alert, Button } from 'antd';
+import type { AssistantAnswerErrorKind } from './answerView';
+
+const presentation: Record<AssistantAnswerErrorKind, { type: 'error' | 
'warning'; action: string }> = {
+  failed: { type: 'error', action: 'Retry' },
+  stream: { type: 'error', action: 'Retry' },
+  // The answer may have finished on the server; only reloading the 
conversation shows where it got to.
+  connection: { type: 'warning', action: 'Reload conversation' }
+};
+
+export interface AssistantAnswerErrorProps {
+  kind: AssistantAnswerErrorKind;
+  message: string;
+  /** Without it there is nothing to offer, so no button is shown. */
+  onRetry?: () => void;
+}
+
+export const AssistantAnswerError = ({ kind, message, onRetry }: 
AssistantAnswerErrorProps) => {
+  const { type, action } = presentation[kind];
+  return (
+    <Alert
+      type={type}
+      showIcon
+      title={message}
+      action={
+        onRetry && (
+          <Button size="small" onClick={onRetry}>
+            {action}
+          </Button>
+        )
+      }
+    />
+  );
+};
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.css.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.css.ts
new file mode 100644
index 0000000000..3ec1583712
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.css.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { globalStyle, keyframes, style } from '@vanilla-extract/css';
+import { enter, vars } from '@/shared/ui/assistant-theme/AssistantScope.css';
+
+export const message = style({
+  display: 'flex',
+  flexDirection: 'column',
+  gap: vars.space1,
+  minWidth: 0,
+  animation: `${enter} ${vars.durationModerate} ${vars.easeEntrance}`
+});
+
+export const user = style({
+  alignSelf: 'flex-end',
+  alignItems: 'flex-end',
+  maxWidth: '85%'
+});
+
+export const assistant = style({
+  alignSelf: 'stretch'
+});
+
+export const label = style({
+  display: 'flex',
+  alignItems: 'center',
+  gap: vars.space2,
+  color: vars.textSecondary,
+  fontSize: vars.fontSizeSm
+});
+
+export const badge = style({
+  display: 'inline-flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+  width: 20,
+  height: 20,
+  borderRadius: vars.radiusSm,
+  background: vars.accent,
+  color: vars.bg,
+  fontSize: vars.fontSizeXs,
+  fontWeight: 600
+});
+
+export const bubble = style({
+  padding: `${vars.space2} ${vars.space3}`,
+  borderRadius: vars.radiusLg,
+  border: `1px solid ${vars.accentBorder}`,
+  background: vars.accentBg,
+  overflowWrap: 'anywhere'
+});
+
+export const body = style({
+  minWidth: 0,
+  overflowWrap: 'anywhere'
+});
+
+export const plainText = style({
+  whiteSpace: 'pre-wrap'
+});
+
+const blink = keyframes({
+  '50%': { opacity: 0 }
+});
+
+/**
+ * Trails the partial answer while it streams. The renderer wraps the answer 
in blocks, so the caret goes inside the
+ * last block (or the last item of a closing list) to stay on the line where 
the text ends.
+ */
+globalStyle(
+  `${body}[data-streaming] > :last-child > :last-child::after, 
${body}[data-streaming] > :last-child > :is(ul, ol):last-child > 
li:last-child::after`,
+  {
+    content: '""',
+    display: 'inline-block',
+    width: 2,
+    height: '1em',
+    marginLeft: 2,
+    verticalAlign: 'text-bottom',
+    background: vars.accent,
+    animation: `${blink} 1s steps(1) infinite`
+  }
+);
+globalStyle(`${body}[data-streaming] > :last-child > :is(ul, 
ol):last-child::after`, { content: 'none' });
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.spec.tsx
new file mode 100644
index 0000000000..d436f22164
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.spec.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { AssistantMessageItem } from './AssistantMessageItem';
+
+describe('AssistantMessageItem', () => {
+  it('tells a question from an answer', () => {
+    render(
+      <>
+        <AssistantMessageItem role="user" content="How is revenue calculated?" 
/>
+        <AssistantMessageItem role="assistant" content="By region." />
+      </>
+    );
+    expect(screen.getByRole('article', { name: 'Your question' 
}).textContent).toBe('YouHow is revenue calculated?');
+    expect(screen.getByRole('article', { name: 'Assistant answer' 
}).textContent).toContain('By region.');
+  });
+
+  it('shows a question as typed instead of rendering it', () => {
+    render(<AssistantMessageItem role="user" content="<b>not bold</b>" 
renderContent={() => 'rendered'} />);
+    const question = screen.getByRole('article', { name: 'Your question' });
+    expect(question.textContent).toContain('<b>not bold</b>');
+    expect(question.querySelector('b')).toBeNull();
+  });
+
+  it('renders an answer as Markdown by default', () => {
+    render(<AssistantMessageItem role="assistant" content={'Revenue is 
**grouped** by `region`.'} />);
+    const answer = screen.getByRole('article', { name: 'Assistant answer' });
+    expect(answer.querySelector('strong')?.textContent).toBe('grouped');
+    expect(answer.querySelector('code')?.textContent).toBe('region');
+  });
+});
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..69ba1ccff4
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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',
+            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/AssistantMessageItem.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.tsx
new file mode 100644
index 0000000000..fa6203db25
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { ReactNode } from 'react';
+import type { AssistantMessage } from '../model/assistantContract';
+import type { AssistantContentRenderer } from './answerView';
+import * as styles from './AssistantMessageItem.css';
+import { MarkdownAnswer } from './MarkdownAnswer';
+
+const renderMarkdown: AssistantContentRenderer = content => <MarkdownAnswer 
content={content} />;
+
+export interface AssistantMessageItemProps {
+  role: AssistantMessage['role'];
+  content: string;
+  /** The answer is still arriving: marks it busy and shows a caret after the 
text. */
+  streaming?: boolean;
+  renderContent?: AssistantContentRenderer;
+  /** Shown under an assistant answer, such as the tools it used or an error. 
*/
+  children?: ReactNode;
+}
+
+export const AssistantMessageItem = ({
+  role,
+  content,
+  streaming = false,
+  renderContent = renderMarkdown,
+  children
+}: AssistantMessageItemProps) => {
+  if (role === 'user') {
+    return (
+      <article className={`${styles.message} ${styles.user}`} data-role="user" 
aria-label="Your question">
+        <div className={styles.label}>You</div>
+        <div className={`${styles.bubble} ${styles.plainText}`}>{content}</div>
+      </article>
+    );
+  }
+  return (
+    <article
+      className={`${styles.message} ${styles.assistant}`}
+      data-role="assistant"
+      aria-label="Assistant answer"
+      aria-busy={streaming || undefined}
+    >
+      <header className={styles.label}>
+        <span className={styles.badge} aria-hidden="true">
+          AI
+        </span>
+        <span>Assistant</span>
+      </header>
+      {content && (
+        <div className={styles.body} data-streaming={streaming || undefined}>
+          {renderContent(content)}
+        </div>
+      )}
+      {children}
+    </article>
+  );
+};
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
new file mode 100644
index 0000000000..aa1359733a
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { CheckOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import { Spin } from 'antd';
+import type { AssistantToolActivity } from './answerView';
+import * as styles from './AssistantAnswer.css';
+
+export const toolLabel = (tool: Pick<AssistantToolActivity, 'name'>) => 
tool.name || 'Tool';
+
+export interface AssistantToolLogProps {
+  tools: AssistantToolActivity[];
+}
+
+/** The tools behind an answer. Open while one is still running, collapsed 
once they have all finished. */
+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}` : 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}>
+            {tool.status === 'running' ? (
+              <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" />
+            )}
+            <span className={styles.toolName}>{toolLabel(tool)}</span>
+          </li>
+        ))}
+      </ul>
+    </details>
+  );
+};
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
new file mode 100644
index 0000000000..164a8ed8d5
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { ReactNode } from 'react';
+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' | 'unknown';
+}
+
+/**
+ * Why an answer stopped: the server reported `run.failed`, the stream ended 
early (`AssistantStreamError`),
+ * or the connection dropped and only a reload shows where the answer got to 
(`AssistantConnectionError`).
+ */
+export type AssistantAnswerErrorKind = 'failed' | 'stream' | 'connection';
+
+/** What one answer shows. The panel derives it from run events; the 
components only display it. */
+export type AssistantAnswerView =
+  | { status: 'loading' }
+  | { status: 'streaming'; content: string; tools?: AssistantToolActivity[] }
+  | { status: 'done'; message: AssistantAnswerMessage }
+  | {
+      status: 'error';
+      kind: AssistantAnswerErrorKind;
+      message: string;
+      /** The part of the answer that arrived before it stopped. */
+      content?: string;
+      tools?: AssistantToolActivity[];
+    };
+
+/** Renders an answer body. Defaults to `MarkdownAnswer`; a parent can pass 
one that resolves paragraph references. */
+export type AssistantContentRenderer = (content: string) => ReactNode;
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/shared/ui/assistant-theme/AssistantScope.stories.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/shared/ui/assistant-theme/AssistantScope.stories.tsx
deleted file mode 100644
index c39bb9d5a1..0000000000
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/shared/ui/assistant-theme/AssistantScope.stories.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 { Button, Space, Typography } from 'antd';
-import type { Meta, StoryObj } from '@storybook/react-vite';
-
-const ThemeSurface = () => (
-  <Space direction="vertical">
-    <Typography.Text>Assistant theme</Typography.Text>
-    <Button type="primary">Primary action</Button>
-    <Button>Secondary action</Button>
-    <Typography.Text type="secondary">Secondary text</Typography.Text>
-  </Space>
-);
-
-const meta = {
-  title: 'Assistant/Setup',
-  component: ThemeSurface,
-  parameters: {
-    docs: { description: { component: 'Temporary theme check. Remove after the 
Assistant UI stories land.' } }
-  }
-} satisfies Meta<typeof ThemeSurface>;
-
-export default meta;
-type Story = StoryObj<typeof meta>;
-export const TemporaryThemeCheck: Story = {};

Reply via email to