diff --git a/plugins/example-plugin/web/AGENTS.md b/plugins/example-plugin/web/AGENTS.md index 841415e302..ed8f9874a5 100644 --- a/plugins/example-plugin/web/AGENTS.md +++ b/plugins/example-plugin/web/AGENTS.md @@ -74,7 +74,7 @@ instance Studio renders — same behavior, same styles, no second copy in the bundle. See `src/SharedUiPage.tsx`. ```ts -import { StudioDataView, useStudioDataViewState } from '@nemo/common'; +import { AssistantChat, StudioDataView, useStudioDataViewState } from '@nemo/common'; ``` - **Bare specifier only.** A deep `@nemo/common/src/...` import is not @@ -92,6 +92,11 @@ import { StudioDataView, useStudioDataViewState } from '@nemo/common'; fine (they erase). A shared component that needs data takes it as a prop or callback and the caller supplies it from `host.sdk`; `CreateSecretModal`'s `onCreate` and `fetchAllPages`' page fetcher are the pattern. +- **`AssistantChat` is shared.** A plugin can point it at an authenticated, + OpenAI-compatible `baseURL`; Studio supplies its current access token and + chat runtime. Use `messageContentProps.markdownLinkComponent` when a plugin + owns trusted, in-app citation targets. The plugin should still own the panel, + prompts, endpoint, and citation behavior specific to its feature. - **Types come from source**, via `paths` in `tsconfig.json`; `@nemo/common` is unpublished, so there is nothing to install. `src/env.d.ts` declares the `*.css` side-effect imports those sources carry. diff --git a/web/packages/common/src/components/AssistantChat/index.test.tsx b/web/packages/common/src/components/AssistantChat/index.test.tsx index e85d96ff58..831dcef6da 100644 --- a/web/packages/common/src/components/AssistantChat/index.test.tsx +++ b/web/packages/common/src/components/AssistantChat/index.test.tsx @@ -195,6 +195,29 @@ describe('AssistantChat', () => { interactionTimeoutMs ); + it('allows a caller to render trusted Markdown links in messages', async () => { + mocks.createChatCompletion.mockResolvedValueOnce( + createCompletion('[Trace source](#zoomer-node=summary-1)') + ); + renderAssistantChat( + {children}, + }} + /> + ); + + await userEvent.type(screen.getByRole('textbox', { name: /Task prompt/i }), 'Show evidence'); + await userEvent.click(screen.getByRole('button', { name: /Submit/i })); + + expect(await screen.findByRole('link', { name: 'Trace source' })).toHaveAttribute( + 'href', + '#zoomer-node=summary-1' + ); + }); + it('renders base64 images returned by an image model stream', async () => { const imageUrl = 'data:image/png;base64,iVBORw0KGgo='; const stream = { diff --git a/web/packages/common/src/components/AssistantChat/index.tsx b/web/packages/common/src/components/AssistantChat/index.tsx index e1c61fb311..54ccf45918 100644 --- a/web/packages/common/src/components/AssistantChat/index.tsx +++ b/web/packages/common/src/components/AssistantChat/index.tsx @@ -35,6 +35,7 @@ export const AssistantChat: FC = ({ stopCount, slotComposerStart, emptyState, + messageContentProps, composerOverride, enableImageAttachments = true, }) => { @@ -76,6 +77,7 @@ export const AssistantChat: FC = ({ composerMode={composerMode} slotComposerStart={slotComposerStart} emptyState={emptyState} + messageContentProps={messageContentProps} composerOverride={composerOverride} enableImageAttachments={imageAttachmentsEnabled} /> diff --git a/web/packages/common/src/components/AssistantChat/types.ts b/web/packages/common/src/components/AssistantChat/types.ts index b76e3c6def..ed205a223f 100644 --- a/web/packages/common/src/components/AssistantChat/types.ts +++ b/web/packages/common/src/components/AssistantChat/types.ts @@ -132,6 +132,8 @@ export interface AssistantChatProps { slotHeading?: string; slotSubheading?: string; }; + /** Overrides used when rendering Markdown inside chat messages. */ + messageContentProps?: AssistantChatMessageContentProps; composerOverride?: ReactNode; /** * @default true diff --git a/web/packages/common/src/plugin.ts b/web/packages/common/src/plugin.ts index 6afe3f0bf2..29ba1d1b1b 100644 --- a/web/packages/common/src/plugin.ts +++ b/web/packages/common/src/plugin.ts @@ -5,6 +5,11 @@ // Removals are breaking. Explicit exports, not `export *`. export { AccessibleTitle } from '@nemo/common/src/components/AccessibleTitle'; +export { AssistantChat } from '@nemo/common/src/components/AssistantChat'; +export type { + AssistantChatMessageContentProps, + AssistantChatProps, +} from '@nemo/common/src/components/AssistantChat/types'; export { AccordionSection } from '@nemo/common/src/components/AccordionSection'; // CancelJobButton is deliberately NOT exported export { ConfirmationModal } from '@nemo/common/src/components/ConfirmationModal';