Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ root.render(
<UseAIProvider
serverUrl="ws://localhost:8081"
systemPrompt="You are a helpful AI assistant for a demo application. Be concise and friendly in your responses."
enabledFeatures={{ inputDisclaimer: true }}
forwardedPropsProvider={() => ({
mcpHeaders: {
'http://localhost:3002': {
Expand Down
72 changes: 72 additions & 0 deletions packages/client/src/components/UseAIChat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {}): ChatUIContextValue {
return {
Expand Down Expand Up @@ -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}>
<UseAIChat />
</__UseAIChatContext.Provider>
);

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}>
<UseAIChat />
</__UseAIChatContext.Provider>
);

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}>
<UseAIChat />
</__UseAIChatContext.Provider>
);

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(
<StringsContext.Provider value={strings}>
<__UseAIChatContext.Provider value={ctx}>
<UseAIChat />
</__UseAIChatContext.Provider>
</StringsContext.Provider>
);

expect(getByTestId('chat-input-disclaimer')).toHaveTextContent('Custom disclaimer');
});
});
});
17 changes: 17 additions & 0 deletions packages/client/src/components/UseAIChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,23 @@ export function UseAIChatPanel({
</div>
</div>
)}

{/* 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 && (
<div
data-testid="chat-input-disclaimer"
style={{
marginTop: '8px',
fontSize: '12px',
lineHeight: 1.3,
color: theme.disclaimerTextColor,
}}
>
{strings.input.disclaimer}
</div>
)}
</div>

<style>{`
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/theme/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const defaultStrings = {
thinking: 'Thinking',
/** File processing indicator text (shown during file transformation like OCR) */
processingFile: 'Processing file...',
/** Disclaimer shown under the input box once a conversation has started */
disclaimer: 'AI can make mistakes. Please double-check important information.',
},

// File upload
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const defaultTheme = {
secondaryTextColor: '#6b7280',
/** Placeholder text color */
placeholderTextColor: '#9ca3af',
/** Text color for the disclaimer under the input box */
disclaimerTextColor: 'rgba(0, 0, 0, 0.45)',

// Status colors
/** Online status indicator color */
Expand Down
17 changes: 13 additions & 4 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export interface UseAIConfig {
}

/**
* Toggles for optional chat UI features. Each feature defaults to enabled
* when its flag is omitted, so set a flag to false to opt out of that feature.
* Toggles for optional chat UI features. Opt-out features default to enabled
* when their flag is omitted; opt-in features default to disabled. The default
* is documented on each flag.
*/
export interface EnabledFeatures {
/**
Expand All @@ -17,14 +18,22 @@ export interface EnabledFeatures {
* @default true
*/
slashCommands?: boolean;
/**
* The disclaimer shown under the input box once a conversation has started,
* reminding the user that the AI can be wrong. Hidden on an empty chat.
* Override the text via `strings.input.disclaimer`.
* @default false
*/
inputDisclaimer?: boolean;
}

/**
* Default state for every opt-out feature. Merge user-supplied
* `enabledFeatures` over this to resolve effective flags. Extend this constant
* when adding a new feature toggle.
* when adding a new opt-out toggle; opt-in features are left out so they stay
* undefined until the consumer turns them on.
*/
export const DEFAULT_ENABLED_FEATURES: Required<EnabledFeatures> = {
export const DEFAULT_ENABLED_FEATURES: Pick<Required<EnabledFeatures>, 'slashCommands'> = {
slashCommands: true,
};

Expand Down
Loading