From d15ed0b785d737d340720c78dcd3c0c44b3e203e Mon Sep 17 00:00:00 2001 From: masudahiroto <96814344+masudahiroto@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:02:39 +0900 Subject: [PATCH] feat(client): add an opt-in AI accuracy disclaimer under the chat input The text lives in `defaultStrings.input.disclaimer` and the color in `defaultTheme.disclaimerTextColor`, so consumers override them the same way as the rest of the chat UI. It is off unless the consumer sets `enabledFeatures.inputDisclaimer`, and stays hidden until the first message is sent. DEFAULT_ENABLED_FEATURES keeps holding only opt-out defaults; opt-in flags stay undefined there. Co-Authored-By: Claude Opus 5 (1M context) --- apps/example/src/index.tsx | 1 + .../client/src/components/UseAIChat.test.tsx | 72 +++++++++++++++++++ .../client/src/components/UseAIChatPanel.tsx | 17 +++++ packages/client/src/theme/strings.ts | 2 + packages/client/src/theme/theme.ts | 2 + packages/client/src/types.ts | 17 +++-- 6 files changed, 107 insertions(+), 4 deletions(-) diff --git a/apps/example/src/index.tsx b/apps/example/src/index.tsx index 77d0d32b..eefd5ee8 100644 --- a/apps/example/src/index.tsx +++ b/apps/example/src/index.tsx @@ -12,6 +12,7 @@ root.render( ({ mcpHeaders: { 'http://localhost:3002': { diff --git a/packages/client/src/components/UseAIChat.test.tsx b/packages/client/src/components/UseAIChat.test.tsx index 9b42cb8a..c21dab48 100644 --- a/packages/client/src/components/UseAIChat.test.tsx +++ b/packages/client/src/components/UseAIChat.test.tsx @@ -3,6 +3,7 @@ import { describe, test, expect, mock } from 'bun:test'; import { render, fireEvent } from '@testing-library/react'; import { UseAIChat, __UseAIChatContext, type ChatUIContextValue } from './UseAIChat'; import type { PersistedMessage } from '../providers/chatRepository/types'; +import { StringsContext, defaultStrings } from '../theme'; function createContextValue(overrides: Partial = {}): ChatUIContextValue { return { @@ -116,4 +117,75 @@ describe('UseAIChat', () => { expect(queryByTestId('inline-save-command')).toBeNull(); }); }); + + describe('input disclaimer', () => { + const userMessage: PersistedMessage = { + id: 'msg-1', + role: 'user', + content: 'hello world', + createdAt: new Date(0), + }; + + test('stays hidden when the inputDisclaimer feature is not enabled', () => { + const ctx = createContextValue({ messages: [userMessage] }); + + const { queryByTestId } = render( + <__UseAIChatContext.Provider value={ctx}> + + + ); + + expect(queryByTestId('chat-input-disclaimer')).toBeNull(); + }); + + test('stays hidden while the chat is empty', () => { + const ctx = createContextValue({ enabledFeatures: { inputDisclaimer: true } }); + + const { queryByTestId } = render( + <__UseAIChatContext.Provider value={ctx}> + + + ); + + expect(queryByTestId('chat-input-disclaimer')).toBeNull(); + }); + + test('appears once the conversation has started', () => { + const ctx = createContextValue({ + messages: [userMessage], + enabledFeatures: { inputDisclaimer: true }, + }); + + const { getByTestId } = render( + <__UseAIChatContext.Provider value={ctx}> + + + ); + + expect(getByTestId('chat-input-disclaimer')).toHaveTextContent( + defaultStrings.input.disclaimer + ); + }); + + test('uses the overridden text from strings', () => { + const ctx = createContextValue({ + messages: [userMessage], + enabledFeatures: { inputDisclaimer: true }, + }); + const strings = { + ...defaultStrings, + input: { ...defaultStrings.input, disclaimer: 'Custom disclaimer' }, + }; + + const { getByTestId } = render( + + <__UseAIChatContext.Provider value={ctx}> + + + + ); + + expect(getByTestId('chat-input-disclaimer')).toHaveTextContent('Custom disclaimer'); + }); + }); }); diff --git a/packages/client/src/components/UseAIChatPanel.tsx b/packages/client/src/components/UseAIChatPanel.tsx index 944fd64a..751b7505 100644 --- a/packages/client/src/components/UseAIChatPanel.tsx +++ b/packages/client/src/components/UseAIChatPanel.tsx @@ -1381,6 +1381,23 @@ export function UseAIChatPanel({ )} + + {/* Accuracy disclaimer. Kept out of the empty chat so the first + impression stays clean; once the user has sent a message it stays + visible for the rest of the conversation. */} + {features.inputDisclaimer && displayMessages.length > 0 && ( +
+ {strings.input.disclaimer} +
+ )}