Skip to content

Commit

Permalink
feat: ai-bot integration poc (#2819)
Browse files Browse the repository at this point in the history
* feat: add StreamingMessageView to kick off ai feature

* fix: issues with message view

* feat: add AITypingIndicatorView

* feat: make send message button react to ai state

* fix: improve typewriter animation

* fix: improvements in ui and typewriter

* chore: add customizations to StreamingMessageView

* fix: hook deps

* chore: extract logic in hook

* fix: custom events

* fix: revert the type change in favor of changes in the LLC

* feat: codeblock scrollable view

* feat: table initial reimpl

* feat: finish table impl

* fix: horizontal scroll list performance issues

* feat: add markdown parsing fixes, optimistic code capture and various improvements

* fix: theme prop and theming in general

* fix: remove edited lalbel for ai messages

* fix: bug with stop streaming button and types

* fix: colors in md rendering

* fix: rename custom scrollview

* chore: translations

* fix: remove TODO

* chore: extract indicator styles in theme

* fix: safeguard if channel does not exist

* fix: get rid of enum and introduce proper type

* fix: allow only message overrides

* chore: update event names as per the changes

* fix: bump stream-chat-js version to v8.46.0

* fix: use channel method for sending events

* fix: cover background mode case

* fix: use type from LLC

* chore: add jsdocs

* fix: move check to checker fn

* fix: add overrides for StreamingMessageView

* chore: add override for stop streaming button
  • Loading branch information
isekovanic committed Dec 3, 2024
1 parent 6d5a51b commit 770bb11
Show file tree
Hide file tree
Showing 38 changed files with 608 additions and 22 deletions.
1 change: 1 addition & 0 deletions examples/SampleApp/src/hooks/useStreamChatTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const getChatStyle = (colorScheme: ColorSchemeName): DeepPartial<Theme> => ({
border: '#141924',
button_background: '#FFFFFF',
button_text: '#005FFF',
code_block: '#222222',
grey: '#7A7A7A',
grey_gainsboro: '#2D2F2F',
grey_whisper: '#1C1E22',
Expand Down
2 changes: 2 additions & 0 deletions examples/SampleApp/src/screens/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useChatContext,
useTheme,
useTypingString,
AITypingIndicatorView,
} from 'stream-chat-react-native';
import { Platform, StyleSheet, View } from 'react-native';
import type { StackNavigationProp } from '@react-navigation/stack';
Expand Down Expand Up @@ -168,6 +169,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
});
}}
/>
<AITypingIndicatorView channel={channel} />
<MessageInput />
</Channel>
</View>
Expand Down
15 changes: 15 additions & 0 deletions examples/SampleApp/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6878,6 +6878,21 @@ [email protected]:
jsonwebtoken "~9.0.0"
ws "^7.5.10"

[email protected]:
version "8.46.0"
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.46.0.tgz#416b325e05b144d0937a3527d1e622463113d605"
integrity sha512-HQVCRVldrfQFAvsBOHiHR0TKYf+wpsg/cAzRojeZY+buy1vG6eoqk09h6Fl4k2eG3zFLoA0G9W6o7o45jyFE1g==
dependencies:
"@babel/runtime" "^7.16.3"
"@types/jsonwebtoken" "~9.0.0"
"@types/ws" "^7.4.0"
axios "^1.6.0"
base64-js "^1.5.1"
form-data "^4.0.0"
isomorphic-ws "^4.0.1"
jsonwebtoken "~9.0.0"
ws "^7.5.10"

strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^1.3.0",
"stream-chat": "8.45.1"
"stream-chat": "8.46.0"
},
"peerDependencies": {
"react-native-quick-sqlite": ">=5.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';

import { StyleSheet, Text, View } from 'react-native';

import { Channel } from 'stream-chat';

import { AIStates, useAIState } from './hooks/useAIState';

import { useChannelContext, useTheme, useTranslationContext } from '../../contexts';
import type { DefaultStreamChatGenerics } from '../../types/types';

export type AITypingIndicatorViewProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = {
channel?: Channel<StreamChatGenerics>;
};

export const AITypingIndicatorView = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>({
channel: channelFromProps,
}: AITypingIndicatorViewProps<StreamChatGenerics>) => {
const { t } = useTranslationContext();
const { channel: channelFromContext } = useChannelContext<StreamChatGenerics>();
const channel = channelFromProps || channelFromContext;
const { aiState } = useAIState(channel);
const allowedStates = {
[AIStates.Thinking]: t('Thinking...'),
[AIStates.Generating]: t('Generating...'),
};

const {
theme: {
aiTypingIndicatorView: { container, text },
colors: { black, grey_gainsboro },
},
} = useTheme();

return aiState in allowedStates ? (
<View style={[styles.container, { backgroundColor: grey_gainsboro }, container]}>
<Text style={[{ color: black }, text]}>{allowedStates[aiState]}</Text>
</View>
) : null;
};

AITypingIndicatorView.displayName = 'AITypingIndicatorView{messageSimple{content}}';

const styles = StyleSheet.create({
container: { paddingHorizontal: 16, paddingVertical: 18 },
});
68 changes: 68 additions & 0 deletions package/src/components/AITypingIndicatorView/hooks/useAIState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';

import { AIState, Channel, Event } from 'stream-chat';

import { useChatContext } from '../../../contexts';
import type { DefaultStreamChatGenerics } from '../../../types/types';
import { useIsOnline } from '../../Chat/hooks/useIsOnline';

export const AIStates = {
Error: 'AI_STATE_ERROR',
ExternalSources: 'AI_STATE_EXTERNAL_SOURCES',
Generating: 'AI_STATE_GENERATING',
Idle: 'AI_STATE_IDLE',
Thinking: 'AI_STATE_THINKING',
};

/**
* A hook that returns the current state of the AI.
* @param {Channel} channel - The channel for which we want to know the AI state.
* @returns {{ aiState: AIState }} The current AI state for the given channel.
*/
export const useAIState = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>(
channel?: Channel<StreamChatGenerics>,
): { aiState: AIState } => {
const { client } = useChatContext<StreamChatGenerics>();
const { isOnline } = useIsOnline<StreamChatGenerics>(client);

const [aiState, setAiState] = useState<AIState>(AIStates.Idle);

useEffect(() => {
if (!isOnline) {
setAiState(AIStates.Idle);
}
}, [isOnline]);

useEffect(() => {
if (!channel) {
return;
}

const indicatorChangedListener = channel.on(
'ai_indicator.update',
(event: Event<StreamChatGenerics>) => {
const { cid } = event;
const state = event.ai_state as AIState;
if (channel.cid === cid) {
setAiState(state);
}
},
);

const indicatorClearedListener = channel.on('ai_indicator.clear', (event) => {
const { cid } = event;
if (channel.cid === cid) {
setAiState(AIStates.Idle);
}
});

return () => {
indicatorChangedListener.unsubscribe();
indicatorClearedListener.unsubscribe();
};
}, [channel]);

return { aiState };
};
2 changes: 2 additions & 0 deletions package/src/components/AITypingIndicatorView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './AITypingIndicatorView';
export * from './hooks/useAIState';
28 changes: 26 additions & 2 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import { MessageSimple as MessageSimpleDefault } from '../Message/MessageSimple/
import { MessageStatus as MessageStatusDefault } from '../Message/MessageSimple/MessageStatus';
import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageSimple/MessageTimestamp';
import { ReactionList as ReactionListDefault } from '../Message/MessageSimple/ReactionList';
import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageSimple/StreamingMessageView';
import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
import { AudioRecorder as AudioRecorderDefault } from '../MessageInput/components/AudioRecorder/AudioRecorder';
Expand All @@ -154,6 +155,7 @@ import { MoreOptionsButton as MoreOptionsButtonDefault } from '../MessageInput/M
import { SendButton as SendButtonDefault } from '../MessageInput/SendButton';
import { SendMessageDisallowedIndicator as SendMessageDisallowedIndicatorDefault } from '../MessageInput/SendMessageDisallowedIndicator';
import { ShowThreadMessageInChannelButton as ShowThreadMessageInChannelButtonDefault } from '../MessageInput/ShowThreadMessageInChannelButton';
import { StopMessageStreamingButton as DefaultStopMessageStreamingButton } from '../MessageInput/StopMessageStreamingButton';
import { UploadProgressIndicator as UploadProgressIndicatorDefault } from '../MessageInput/UploadProgressIndicator';
import { DateHeader as DateHeaderDefault } from '../MessageList/DateHeader';
import type { MessageType } from '../MessageList/hooks/useMessageList';
Expand Down Expand Up @@ -333,6 +335,7 @@ export type ChannelPropsWithContext<
| 'VideoThumbnail'
| 'PollContent'
| 'hasCreatePoll'
| 'StreamingMessageView'
>
> &
Partial<Pick<ThreadContextValue<StreamChatGenerics>, 'allowThreadMessagesInChannel'>> & {
Expand Down Expand Up @@ -420,7 +423,12 @@ export type ChannelPropsWithContext<
* Tells if channel is rendering a thread list
*/
threadList?: boolean;
} & Partial<Pick<InputMessageInputContextValue, 'openPollCreationDialog' | 'CreatePollContent'>>;
} & Partial<
Pick<
InputMessageInputContextValue,
'openPollCreationDialog' | 'CreatePollContent' | 'StopMessageStreamingButton'
>
>;

const ChannelWithContext = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
Expand Down Expand Up @@ -544,7 +552,15 @@ const ChannelWithContext = <
MessageAvatar = MessageAvatarDefault,
MessageBounce = MessageBounceDefault,
MessageContent = MessageContentDefault,
messageContentOrder = ['quoted_reply', 'gallery', 'files', 'poll', 'text', 'attachments'],
messageContentOrder = [
'quoted_reply',
'gallery',
'files',
'poll',
'ai_text',
'text',
'attachments',
],
MessageDeleted = MessageDeletedDefault,
MessageEditedTimestamp = MessageEditedTimestampDefault,
MessageError = MessageErrorDefault,
Expand Down Expand Up @@ -596,6 +612,8 @@ const ChannelWithContext = <
StartAudioRecordingButton = AudioRecordingButtonDefault,
stateUpdateThrottleInterval = defaultThrottleInterval,
StickyHeader = StickyHeaderDefault,
StopMessageStreamingButton: StopMessageStreamingButtonOverride,
StreamingMessageView = DefaultStreamingMessageView,
supportedReactions = reactionData,
t,
thread: threadFromProps,
Expand All @@ -612,6 +630,10 @@ const ChannelWithContext = <
} = props;

const { thread: threadProps, threadInstance } = threadFromProps;
const StopMessageStreamingButton =
StopMessageStreamingButtonOverride === undefined
? DefaultStopMessageStreamingButton
: StopMessageStreamingButtonOverride;

const {
theme: {
Expand Down Expand Up @@ -2338,6 +2360,7 @@ const ChannelWithContext = <
setQuotedMessageState,
ShowThreadMessageInChannelButton,
StartAudioRecordingButton,
StopMessageStreamingButton,
UploadProgressIndicator,
});

Expand Down Expand Up @@ -2439,6 +2462,7 @@ const ChannelWithContext = <
setEditingState,
setQuotedMessageState,
shouldShowUnreadUnderlay,
StreamingMessageView,
supportedReactions,
targetedMessage,
TypingIndicator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const useCreateInputMessageInputContext = <
showPollCreationDialog,
ShowThreadMessageInChannelButton,
StartAudioRecordingButton,
StopMessageStreamingButton,
UploadProgressIndicator,
}: InputMessageInputContextValue<StreamChatGenerics> & {
/**
Expand Down Expand Up @@ -137,6 +138,7 @@ export const useCreateInputMessageInputContext = <
showPollCreationDialog,
ShowThreadMessageInChannelButton,
StartAudioRecordingButton,
StopMessageStreamingButton,
UploadProgressIndicator,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const useCreateMessagesContext = <
setEditingState,
setQuotedMessageState,
shouldShowUnreadUnderlay,
StreamingMessageView,
supportedReactions,
targetedMessage,
TypingIndicator,
Expand Down Expand Up @@ -189,6 +190,7 @@ export const useCreateMessagesContext = <
setEditingState,
setQuotedMessageState,
shouldShowUnreadUnderlay,
StreamingMessageView,
supportedReactions,
targetedMessage,
TypingIndicator,
Expand Down
5 changes: 4 additions & 1 deletion package/src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ const MessageWithContext = <
return !!attachments.images.length || !!attachments.videos.length;
case 'poll':
return !!message.poll_id;
case 'ai_text':
return !!message.ai_generated;
case 'text':
default:
return !!message.text;
Expand Down Expand Up @@ -863,7 +865,8 @@ const areEqual = <StreamChatGenerics extends DefaultStreamChatGenerics = Default
prevMessage.text === nextMessage.text &&
prevMessage.pinned === nextMessage.pinned &&
`${prevMessage?.updated_at}` === `${nextMessage?.updated_at}` &&
prevMessage.i18n === nextMessage.i18n;
prevMessage.i18n === nextMessage.i18n &&
prevMessage.ai_generated === nextMessage.ai_generated;

if (!messageEqual) return false;

Expand Down
16 changes: 14 additions & 2 deletions package/src/components/Message/MessageSimple/MessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export type MessageContentPropsWithContext<
| 'myMessageTheme'
| 'onPressInMessage'
| 'Reply'
| 'StreamingMessageView'
> &
Pick<TranslationContextValue, 't'> & {
setMessageContentWidth: React.Dispatch<React.SetStateAction<number>>;
Expand Down Expand Up @@ -142,6 +143,7 @@ const MessageContentWithContext = <
Reply,
setMessageContentWidth,
showMessageStatus,
StreamingMessageView,
threadList,
} = props;
const { client } = useChatContext();
Expand Down Expand Up @@ -393,9 +395,16 @@ const MessageContentWithContext = <
/>
) : null;
}
case 'ai_text':
return message.ai_generated ? (
<StreamingMessageView
key={`ai_message_text_container_${messageContentOrderIndex}`}
/>
) : null;
case 'text':
default:
return otherAttachments.length && otherAttachments[0].actions ? null : (
return (otherAttachments.length && otherAttachments[0].actions) ||
message.ai_generated ? null : (
<MessageTextContainer<StreamChatGenerics>
key={`message_text_container_${messageContentOrderIndex}`}
/>
Expand Down Expand Up @@ -484,7 +493,8 @@ const areEqual = <StreamChatGenerics extends DefaultStreamChatGenerics = Default
prevMessage.type === nextMessage.type &&
prevMessage.text === nextMessage.text &&
prevMessage.pinned === nextMessage.pinned &&
prevMessage.i18n === nextMessage.i18n;
prevMessage.i18n === nextMessage.i18n &&
prevMessage.ai_generated === nextMessage.ai_generated;
if (!messageEqual) return false;

const isPrevQuotedMessageTypeDeleted = prevMessage.quoted_message?.type === 'deleted';
Expand Down Expand Up @@ -597,6 +607,7 @@ export const MessageContent = <
MessageStatus,
myMessageTheme,
Reply,
StreamingMessageView,
} = useMessagesContext<StreamChatGenerics>();
const { t } = useTranslationContext();

Expand Down Expand Up @@ -635,6 +646,7 @@ export const MessageContent = <
preventPress,
Reply,
showMessageStatus,
StreamingMessageView,
t,
threadList,
}}
Expand Down
Loading

0 comments on commit 770bb11

Please sign in to comment.