Skip to content

Commit 82464bc

Browse files
committed
feat: add AITypingIndicatorView
1 parent 0ec98f9 commit 82464bc

File tree

7 files changed

+96
-1
lines changed

7 files changed

+96
-1
lines changed

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useChatContext,
1313
useTheme,
1414
useTypingString,
15+
AITypingIndicatorView,
1516
} from 'stream-chat-react-native';
1617
import { Platform, StyleSheet, View } from 'react-native';
1718
import type { StackNavigationProp } from '@react-navigation/stack';
@@ -168,6 +169,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
168169
});
169170
}}
170171
/>
172+
<AITypingIndicatorView channel={channel} />
171173
<MessageInput />
172174
</Channel>
173175
</View>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
3+
import { Text, View } from 'react-native';
4+
5+
import { Channel } from 'stream-chat';
6+
7+
import { AIStates, useAIState } from './hooks/useAIState';
8+
9+
import { useChannelContext } from '../../contexts';
10+
import type { DefaultStreamChatGenerics } from '../../types/types';
11+
12+
export const AITypingIndicatorView = <
13+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
14+
>({
15+
channel: channelFromProps,
16+
}: {
17+
channel: Channel<StreamChatGenerics>;
18+
}) => {
19+
const { channel: channelFromContext } = useChannelContext();
20+
const channel = channelFromProps || channelFromContext;
21+
const { aiState } = useAIState(channel);
22+
const allowedStates = {
23+
[AIStates.Thinking]: 'Thinking...',
24+
[AIStates.Generating]: 'Generating...',
25+
};
26+
return aiState in allowedStates ? (
27+
<View style={{ paddingHorizontal: 16, paddingVertical: 18 }}>
28+
<Text>{allowedStates[aiState]}</Text>
29+
</View>
30+
) : null;
31+
};
32+
33+
AITypingIndicatorView.displayName = 'AITypingIndicatorView{messageSimple{content}}';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import { Channel } from 'stream-chat';
4+
5+
import type { DefaultStreamChatGenerics } from '../../../types/types';
6+
7+
export enum AIStatesEnum {
8+
Error = 'AI_STATE_ERROR',
9+
ExternalSources = 'AI_STATE_EXTERNAL_SOURCES',
10+
Generating = 'AI_STATE_GENERATING',
11+
Idle = 'AI_STATE_IDLE',
12+
Thinking = 'AI_STATE_THINKING',
13+
}
14+
15+
export const AIStates = {
16+
Error: 'AI_STATE_ERROR',
17+
ExternalSources: 'AI_STATE_EXTERNAL_SOURCES',
18+
Generating: 'AI_STATE_GENERATING',
19+
Idle: 'AI_STATE_IDLE',
20+
Thinking: 'AI_STATE_THINKING',
21+
};
22+
23+
export const useAIState = <
24+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
25+
>(
26+
channel: Channel<StreamChatGenerics>,
27+
) => {
28+
const [aiState, setAiState] = useState<AIStatesEnum>(AIStatesEnum.Idle);
29+
30+
useEffect(() => {
31+
const indicatorChangedListener = channel.on('ai_indicator_changed', (event) => {
32+
const { cid } = event;
33+
const state = event.state as AIStatesEnum;
34+
if (channel.cid === cid) {
35+
setAiState(state);
36+
}
37+
});
38+
39+
const indicatorClearedListener = channel.on('ai_indicator_clear', (event) => {
40+
const { cid } = event;
41+
if (channel.cid === cid) {
42+
setAiState(AIStatesEnum.Idle);
43+
}
44+
});
45+
46+
return () => {
47+
indicatorChangedListener.unsubscribe();
48+
indicatorClearedListener.unsubscribe();
49+
};
50+
}, [channel]);
51+
52+
return { aiState };
53+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './AITypingIndicatorView';
2+
export * from './hooks/useAIState';

package/src/components/Message/Message.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,8 @@ const MessageWithContext = <
418418
return !!attachments.images.length || !!attachments.videos.length;
419419
case 'poll':
420420
return !!message.poll_id;
421+
case 'ai_text':
422+
return !!message.ai_generated;
421423
case 'text':
422424
default:
423425
return !!message.text;

package/src/components/Message/MessageSimple/StreamingMessageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ export const StreamingMessageView = <
3232
return <MessageTextContainer message={{ ...message, text: streamedMessageText }} {...props} />;
3333
};
3434

35-
StreamingMessageView.displayName = 'MessageTextContainer{messageSimple{content}}';
35+
StreamingMessageView.displayName = 'StreamingMessageView{messageSimple{content}}';

package/src/components/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,6 @@ export * from './UIComponents/Spinner';
173173
export * from './Thread/Thread';
174174
export * from './Thread/components/ThreadFooterComponent';
175175
export * from './ThreadList/ThreadList';
176+
177+
export * from './Message/MessageSimple/StreamingMessageView';
178+
export * from './AITypingIndicatorView';

0 commit comments

Comments
 (0)