Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for ai generated messages #2570

Merged
merged 34 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
26de263
feat: implement StreamingMessageView and add hook
isekovanic Dec 2, 2024
4d9d776
feat: add AITypingIndicatorView as well and include to Vite app
isekovanic Dec 2, 2024
077bb1e
chore: translations
isekovanic Dec 2, 2024
b971c5b
fix: remove edited stamps for ai generated messages
isekovanic Dec 2, 2024
5e01b57
feat: add stop generating functionality as well
isekovanic Dec 2, 2024
2b3c4ea
chore: extract inline css into separate repo
isekovanic Dec 2, 2024
0722e98
chore: remove display name
isekovanic Dec 2, 2024
01139bc
chore: update event names as per the changes
isekovanic Dec 3, 2024
e7468a0
fix: remove ts-ignore
isekovanic Dec 3, 2024
4c84723
fix: stop ai generation button PR remarks
isekovanic Dec 3, 2024
1f6df55
fix: rename typing indicator
isekovanic Dec 3, 2024
7fb1a27
fix: properly rename indicator fix hook type
isekovanic Dec 3, 2024
c6fe375
chore: rename streamed message comp
isekovanic Dec 3, 2024
2a53bb7
chore: rename
isekovanic Dec 3, 2024
3d25fb2
chore: prop renaming
isekovanic Dec 3, 2024
bb98f69
chore: renaming hook
isekovanic Dec 3, 2024
583df0c
fix: add StreamedMessageText to component ctx
isekovanic Dec 3, 2024
d8c941a
fix: add type in import
isekovanic Dec 3, 2024
929a91b
fix: revert yarn.lock changes
isekovanic Dec 3, 2024
5b8f016
fix: css class renaming
isekovanic Dec 3, 2024
4ee90b2
fix: move max-width to another class
isekovanic Dec 3, 2024
328797e
chore: move edited check upstream
isekovanic Dec 3, 2024
f8e2364
chore: move code blocks together and leave comment
isekovanic Dec 3, 2024
ebca56e
fix: use channel method for stopping
isekovanic Dec 3, 2024
aecd18d
fix: change aria text and add translations
isekovanic Dec 3, 2024
f2c65f5
fix: bump stream-chat-js to v8.46.0
isekovanic Dec 3, 2024
f58a780
chore: renaming again
isekovanic Dec 3, 2024
a023f17
fix: rename top level dir as well
isekovanic Dec 3, 2024
6527d72
chore: add JSDocs as well
isekovanic Dec 3, 2024
e52b6d1
fix: revert CSS local changes
isekovanic Dec 3, 2024
4479ef2
fix: remove optimistic code block capture as it is already there
isekovanic Dec 3, 2024
8377f27
chore: move table types to known ones
isekovanic Dec 3, 2024
0acf5fa
chore: rename css class
isekovanic Dec 4, 2024
9f9975a
chore: bump stream-chat-css version to latest
isekovanic Dec 4, 2024
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
2 changes: 2 additions & 0 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import {
AIStateIndicator,
Channel,
ChannelAvatar,
ChannelHeader,
Expand Down Expand Up @@ -88,6 +89,7 @@ const App = () => {
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<AIStateIndicator />
<MessageInput focus />
</Window>
<Thread virtualized />
Expand Down
1 change: 1 addition & 0 deletions examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ body,
width: 0;
flex-shrink: 0;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
max-width: 1000px;

&--open {
width: 30%;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"emoji-mart": "^5.4.0",
"react": "^18.0.0 || ^17.0.0 || ^16.8.0",
"react-dom": "^18.0.0 || ^17.0.0 || ^16.8.0",
"stream-chat": "^8.45.0"
"stream-chat": "^8.46.0"
},
"peerDependenciesMeta": {
"@breezystack/lamejs": {
Expand Down Expand Up @@ -186,7 +186,7 @@
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/git": "^10.0.1",
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
"@stream-io/stream-chat-css": "^5.4.0",
"@stream-io/stream-chat-css": "^5.5.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
Expand Down Expand Up @@ -255,7 +255,7 @@
"react-dom": "^18.1.0",
"react-test-renderer": "^18.1.0",
"semantic-release": "^19.0.5",
"stream-chat": "^8.45.0",
"stream-chat": "^8.46.0",
"ts-jest": "^29.1.4",
"typescript": "^5.4.5"
},
Expand Down
37 changes: 37 additions & 0 deletions src/components/AIStateIndicator/AIStateIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';

import { Channel } from 'stream-chat';

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

import { useChannelStateContext, useTranslationContext } from '../../context';
import type { DefaultStreamChatGenerics } from '../../types/types';

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

export const AIStateIndicator = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
channel: channelFromProps,
}: AIStateIndicatorProps<StreamChatGenerics>) => {
const { t } = useTranslationContext();
const { channel: channelFromContext } = useChannelStateContext<StreamChatGenerics>(

Check warning on line 22 in src/components/AIStateIndicator/AIStateIndicator.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/AIStateIndicator/AIStateIndicator.tsx#L21-L22

Added lines #L21 - L22 were not covered by tests
'AIStateIndicator',
);
const channel = channelFromProps || channelFromContext;
const { aiState } = useAIState(channel);
const allowedStates = {

Check warning on line 27 in src/components/AIStateIndicator/AIStateIndicator.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/AIStateIndicator/AIStateIndicator.tsx#L26-L27

Added lines #L26 - L27 were not covered by tests
[AIStates.Thinking]: t('Thinking...'),
[AIStates.Generating]: t('Generating...'),
};

return aiState in allowedStates ? (
<div className='str-chat__ai-state-indicator-container'>
<p className='str-chat__ai-state-indicator-text'>{allowedStates[aiState]}</p>
</div>
) : null;
};
57 changes: 57 additions & 0 deletions src/components/AIStateIndicator/hooks/useAIState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';

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

import type { DefaultStreamChatGenerics } from '../../../types/types';

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 [aiState, setAiState] = useState<AIState>(AIStates.Idle);

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

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

Check warning on line 36 in src/components/AIStateIndicator/hooks/useAIState.ts

View check run for this annotation

Codecov / codecov/patch

src/components/AIStateIndicator/hooks/useAIState.ts#L35-L36

Added lines #L35 - L36 were not covered by tests
if (channel.cid === cid) {
setAiState(state);

Check warning on line 38 in src/components/AIStateIndicator/hooks/useAIState.ts

View check run for this annotation

Codecov / codecov/patch

src/components/AIStateIndicator/hooks/useAIState.ts#L38

Added line #L38 was not covered by tests
}
},
);

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

Check warning on line 44 in src/components/AIStateIndicator/hooks/useAIState.ts

View check run for this annotation

Codecov / codecov/patch

src/components/AIStateIndicator/hooks/useAIState.ts#L44

Added line #L44 was not covered by tests
if (channel.cid === cid) {
setAiState(AIStates.Idle);

Check warning on line 46 in src/components/AIStateIndicator/hooks/useAIState.ts

View check run for this annotation

Codecov / codecov/patch

src/components/AIStateIndicator/hooks/useAIState.ts#L46

Added line #L46 was not covered by tests
}
});

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

return { aiState };
};
2 changes: 2 additions & 0 deletions src/components/AIStateIndicator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './AIStateIndicator';
export * from './hooks/useAIState';
6 changes: 6 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ type ChannelPropsForwardedToComponentContext<
| 'UnreadMessagesNotification'
| 'UnreadMessagesSeparator'
| 'VirtualMessage'
| 'StopAIGenerationButton'
| 'StreamedMessageText'
>;

const isUserResponseArray = <
Expand Down Expand Up @@ -1273,6 +1275,8 @@ const ChannelInner = <
ReactionsList: props.ReactionsList,
SendButton: props.SendButton,
StartRecordingAudioButton: props.StartRecordingAudioButton,
StopAIGenerationButton: props.StopAIGenerationButton,
StreamedMessageText: props.StreamedMessageText,
ThreadHead: props.ThreadHead,
ThreadHeader: props.ThreadHeader,
ThreadStart: props.ThreadStart,
Expand Down Expand Up @@ -1339,6 +1343,8 @@ const ChannelInner = <
props.UnreadMessagesNotification,
props.UnreadMessagesSeparator,
props.VirtualMessage,
props.StopAIGenerationButton,
props.StreamedMessageText,
props.emojiSearchIndex,
props.reactionOptions,
],
Expand Down
4 changes: 3 additions & 1 deletion src/components/ChannelPreview/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export const getLatestMessagePreview = <
}

if (previewTextToRender) {
return renderPreviewText(previewTextToRender);
return latestMessage.ai_generated
? previewTextToRender
: renderPreviewText(previewTextToRender);
}

if (latestMessage.command) {
Expand Down
8 changes: 7 additions & 1 deletion src/components/Message/MessageSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MessageEditedTimestamp } from './MessageEditedTimestamp';

import type { MessageUIComponentProps } from './types';
import type { DefaultStreamChatGenerics } from '../../types/types';
import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText';

type MessageSimpleWithContextProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down Expand Up @@ -81,6 +82,7 @@ const MessageSimpleWithContext = <
MessageStatus = DefaultMessageStatus,
MessageTimestamp = DefaultMessageTimestamp,
ReactionsList = DefaultReactionList,
StreamedMessageText = DefaultStreamedMessageText,
PinIndicator,
} = useComponentContext<StreamChatGenerics>('MessageSimple');

Expand Down Expand Up @@ -185,7 +187,11 @@ const MessageSimpleWithContext = <
{message.attachments?.length && !message.quoted_message ? (
<Attachment actionHandler={handleAction} attachments={message.attachments} />
) : null}
<MessageText message={message} renderText={renderText} />
{message.ai_generated ? (
<StreamedMessageText message={message} renderText={renderText} />
) : (
<MessageText message={message} renderText={renderText} />
)}
{message.mml && (
<MML
actionHandler={handleAction}
Expand Down
40 changes: 40 additions & 0 deletions src/components/Message/StreamedMessageText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

import { MessageText, MessageTextProps } from './MessageText';
import type { DefaultStreamChatGenerics } from '../../types/types';
import { useMessageContext } from '../../context';
import { useMessageTextStreaming } from './hooks';

export type StreamedMessageTextProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Pick<MessageTextProps<StreamChatGenerics>, 'message' | 'renderText'> & {
renderingLetterCount?: number;
streamingLetterIntervalMs?: number;
};

export const StreamedMessageText = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(
props: StreamedMessageTextProps<StreamChatGenerics>,
) => {
const {
message: messageFromProps,
renderingLetterCount,
renderText,
streamingLetterIntervalMs,
} = props;
const { message: messageFromContext } = useMessageContext<StreamChatGenerics>(

Check warning on line 26 in src/components/Message/StreamedMessageText.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Message/StreamedMessageText.tsx#L25-L26

Added lines #L25 - L26 were not covered by tests
'StreamedMessageText',
);
const message = messageFromProps || messageFromContext;
const { text = '' } = message;
const { streamedMessageText } = useMessageTextStreaming({

Check warning on line 31 in src/components/Message/StreamedMessageText.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Message/StreamedMessageText.tsx#L31

Added line #L31 was not covered by tests
renderingLetterCount,
streamingLetterIntervalMs,
text,
});

return (

Check warning on line 37 in src/components/Message/StreamedMessageText.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Message/StreamedMessageText.tsx#L37

Added line #L37 was not covered by tests
<MessageText message={{ ...message, text: streamedMessageText }} renderText={renderText} />
);
};
1 change: 1 addition & 0 deletions src/components/Message/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './useRetryHandler';
export * from './useUserHandler';
export * from './useUserRole';
export * from './useReactionsFetcher';
export * from './useMessageTextStreaming';
52 changes: 52 additions & 0 deletions src/components/Message/hooks/useMessageTextStreaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useRef, useState } from 'react';

import type { DefaultStreamChatGenerics } from '../../../types/types';
import type { StreamedMessageTextProps } from '../StreamedMessageText';

export type UseMessageTextStreamingProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Pick<
StreamedMessageTextProps<StreamChatGenerics>,
'streamingLetterIntervalMs' | 'renderingLetterCount'
> & { text: string };

const DEFAULT_LETTER_INTERVAL = 30;
const DEFAULT_RENDERING_LETTER_COUNT = 2;

/**
* A hook that returns text in a streamed, typewriter fashion. The speed of streaming is
* configurable.
* @param {number} [streamingLetterIntervalMs=30] - The timeout between each typing animation in milliseconds.
* @param {number} [renderingLetterCount=2] - The number of letters to be rendered each time we update.
* @param {string} text - The text that we want to render in a typewriter fashion.
* @returns {{ streamedMessageText: string }} - A substring of the text property, up until we've finished rendering the typewriter animation.
*/
export const useMessageTextStreaming = <
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
streamingLetterIntervalMs = DEFAULT_LETTER_INTERVAL,
renderingLetterCount = DEFAULT_RENDERING_LETTER_COUNT,
text,
}: UseMessageTextStreamingProps<StreamChatGenerics>): { streamedMessageText: string } => {
const [streamedMessageText, setStreamedMessageText] = useState<string>(text);
const textCursor = useRef<number>(text.length);

Check warning on line 32 in src/components/Message/hooks/useMessageTextStreaming.ts

View check run for this annotation

Codecov / codecov/patch

src/components/Message/hooks/useMessageTextStreaming.ts#L31-L32

Added lines #L31 - L32 were not covered by tests

useEffect(() => {
const textLength = text.length;
const interval = setInterval(() => {

Check warning on line 36 in src/components/Message/hooks/useMessageTextStreaming.ts

View check run for this annotation

Codecov / codecov/patch

src/components/Message/hooks/useMessageTextStreaming.ts#L34-L36

Added lines #L34 - L36 were not covered by tests
if (!text || textCursor.current >= textLength) {
clearInterval(interval);

Check warning on line 38 in src/components/Message/hooks/useMessageTextStreaming.ts

View check run for this annotation

Codecov / codecov/patch

src/components/Message/hooks/useMessageTextStreaming.ts#L38

Added line #L38 was not covered by tests
}
const newCursorValue = textCursor.current + renderingLetterCount;
const newText = text.substring(0, newCursorValue);
textCursor.current += newText.length - textCursor.current;
setStreamedMessageText(newText);

Check warning on line 43 in src/components/Message/hooks/useMessageTextStreaming.ts

View check run for this annotation

Codecov / codecov/patch

src/components/Message/hooks/useMessageTextStreaming.ts#L40-L43

Added lines #L40 - L43 were not covered by tests
}, streamingLetterIntervalMs);

return () => {
clearInterval(interval);

Check warning on line 47 in src/components/Message/hooks/useMessageTextStreaming.ts

View check run for this annotation

Codecov / codecov/patch

src/components/Message/hooks/useMessageTextStreaming.ts#L46-L47

Added lines #L46 - L47 were not covered by tests
};
}, [streamingLetterIntervalMs, renderingLetterCount, text]);

return { streamedMessageText };

Check warning on line 51 in src/components/Message/hooks/useMessageTextStreaming.ts

View check run for this annotation

Codecov / codecov/patch

src/components/Message/hooks/useMessageTextStreaming.ts#L51

Added line #L51 was not covered by tests
};
1 change: 1 addition & 0 deletions src/components/Message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export * from './QuotedMessage';
export * from './renderText';
export * from './types';
export * from './utils';
export * from './StreamedMessageText';
export type { TimestampProps } from './Timestamp';
7 changes: 7 additions & 0 deletions src/components/Message/renderText/renderText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export const defaultAllowedTagNames: Array<keyof JSX.IntrinsicElements | 'emoji'
'pre',
'blockquote',
'del',
'table',
'thead',
'tbody',
'th',
'tr',
'td',
'tfoot',
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
// custom types (tagNames)
'emoji',
'mention',
Expand Down
5 changes: 3 additions & 2 deletions src/components/Message/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -494,5 +494,6 @@ export const isMessageBounced = <
export const isMessageEdited = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(
message: Pick<StreamMessage<StreamChatGenerics>, 'message_text_updated_at'>,
) => !!message.message_text_updated_at;
message: Pick<StreamMessage<StreamChatGenerics>, 'message_text_updated_at'> &
Partial<Pick<StreamMessage<StreamChatGenerics>, 'ai_generated'>>,
) => !!message.message_text_updated_at && !message.ai_generated;
Loading
Loading