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 4fcdd3ddbae5e9c4b20faab82f1b295acf28d930
Author: kimyenac <[email protected]>
AuthorDate: Mon Oct 12 12:53:29 2026 +0900

    [ZEPPELIN-6745] Add AI Assistant message item and answer view types
---
 .../assistant/ui/AssistantMessageItem.css.ts       | 85 ++++++++++++++++++++++
 .../assistant/ui/AssistantMessageItem.spec.tsx     | 35 +++++++++
 .../entities/assistant/ui/AssistantMessageItem.tsx | 67 +++++++++++++++++
 .../src/entities/assistant/ui/answerView.ts        | 44 +++++++++++
 4 files changed, 231 insertions(+)

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..3998cf6a3b
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.css.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { 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. */
+export const caret = style({
+  display: 'inline-block',
+  width: 2,
+  height: '1em',
+  marginLeft: 2,
+  verticalAlign: 'text-bottom',
+  background: vars.accent,
+  animation: `${blink} 1s steps(1) infinite`
+});
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..0b280b4277
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.spec.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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();
+  });
+});
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..e7c1e4913e
--- /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';
+
+const renderPlainText: AssistantContentRenderer = content => <div 
className={styles.plainText}>{content}</div>;
+
+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 = renderPlainText,
+  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}>
+          {renderContent(content)}
+          {streaming && <span className={styles.caret} aria-hidden="true" />}
+        </div>
+      )}
+      {children}
+    </article>
+  );
+};
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..b11e7ebcfc
--- /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 { AssistantMessage } 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';
+}
+
+/**
+ * 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: AssistantMessage }
+  | {
+      status: 'error';
+      kind: AssistantAnswerErrorKind;
+      message: string;
+      /** The part of the answer that arrived before it stopped. */
+      content?: string;
+      tools?: AssistantToolActivity[];
+    };
+
+/** Renders an answer body. Plain text until the Markdown answer component is 
wired in. */
+export type AssistantContentRenderer = (content: string) => ReactNode;

Reply via email to