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 9 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
5 changes: 4 additions & 1 deletion 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 {
AITypingIndicatorView,
Channel,
ChannelAvatar,
ChannelHeader,
Expand All @@ -13,7 +14,8 @@ import {
ThreadList,
ChatView,
} from 'stream-chat-react';
import 'stream-chat-react/css/v2/index.css';
// import 'stream-chat-react/css/v2/index.css';
import '@stream-io/stream-chat-css/dist/v2/css/index.css';
isekovanic marked this conversation as resolved.
Show resolved Hide resolved

const params = (new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, property) => searchParams.get(property as string),
Expand Down Expand Up @@ -88,6 +90,7 @@ const App = () => {
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<AITypingIndicatorView />
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
<MessageInput focus />
</Window>
<Thread virtualized />
Expand Down
4 changes: 4 additions & 0 deletions examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ body,
flex-shrink: 0;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);

.str-chat__channel-preview-messenger {
max-width: 1000px;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
}

&--open {
width: 30%;
position: fixed;
Expand Down
1,473 changes: 17 additions & 1,456 deletions examples/vite/yarn.lock

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/components/AITypingIndicatorView/AITypingIndicatorView.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 AITypingIndicatorViewProps<
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = {
channel?: Channel<StreamChatGenerics>;
};

export const AITypingIndicatorView = <
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
channel: channelFromProps,
}: AITypingIndicatorViewProps<StreamChatGenerics>) => {
const { t } = useTranslationContext();
const { channel: channelFromContext } = useChannelStateContext<StreamChatGenerics>(
'AITypingIndicatorView',
);
const channel = channelFromProps || channelFromContext;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
const { aiState } = useAIState(channel);
const allowedStates = {
[AIStates.Thinking]: t('Thinking...'),
MartinCupela marked this conversation as resolved.
Show resolved Hide resolved
[AIStates.Generating]: t('Generating...'),
};

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

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

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

export type AIStateType =
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
| 'AI_STATE_ERROR'
| 'AI_STATE_EXTERNAL_SOURCES'
| 'AI_STATE_GENERATING'
| 'AI_STATE_IDLE'
| 'AI_STATE_THINKING'
| string;

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

export const useAIState = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(
channel?: Channel<StreamChatGenerics>,
) => {
const [aiState, setAiState] = useState<AIStateType>(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 AIStateType;
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 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';
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
13 changes: 10 additions & 3 deletions 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 { StreamingMessageView } from './StreamingMessageView';

type MessageSimpleWithContextProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down Expand Up @@ -185,7 +186,11 @@ const MessageSimpleWithContext = <
{message.attachments?.length && !message.quoted_message ? (
<Attachment actionHandler={handleAction} attachments={message.attachments} />
) : null}
<MessageText message={message} renderText={renderText} />
{message.ai_generated ? (
<StreamingMessageView message={message} renderText={renderText} />
) : (
<MessageText message={message} renderText={renderText} />
)}
{message.mml && (
<MML
actionHandler={handleAction}
Expand All @@ -211,10 +216,12 @@ const MessageSimpleWithContext = <
</span>
)}
<MessageTimestamp customClass='str-chat__message-simple-timestamp' />
{isEdited && (
{isEdited && !message.ai_generated && (
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
<span className='str-chat__mesage-simple-edited'>{t<string>('Edited')}</span>
)}
{isEdited && <MessageEditedTimestamp calendar open={isEditedTimestampOpen} />}
{isEdited && !message.ai_generated && (
<MessageEditedTimestamp calendar open={isEditedTimestampOpen} />
)}
</div>
)}
</div>
Expand Down
33 changes: 33 additions & 0 deletions src/components/Message/StreamingMessageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

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

export type StreamingMessageViewProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Pick<MessageTextProps<StreamChatGenerics>, 'message' | 'renderText'> & {
letterInterval?: number;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
renderingLetterCount?: number;
};

export const StreamingMessageView = <
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(
props: StreamingMessageViewProps<StreamChatGenerics>,
) => {
const { letterInterval, message: messageFromProps, renderingLetterCount, renderText } = props;
const { message: messageFromContext } = useMessageContext<StreamChatGenerics>('MessageText');
const message = messageFromProps || messageFromContext;
const { text = '' } = message;
const { streamedMessageText } = useStreamingMessage({
letterInterval,
renderingLetterCount,
text,
});

return (
<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 './useStreamingMessage';
46 changes: 46 additions & 0 deletions src/components/Message/hooks/useStreamingMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useRef, useState } from 'react';

import type { DefaultStreamChatGenerics } from '../../../types/types';
import { StreamingMessageViewProps } from '../StreamingMessageView';
isekovanic marked this conversation as resolved.
Show resolved Hide resolved

export type UseStreamingMessageProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Pick<
StreamingMessageViewProps<StreamChatGenerics>,
'letterInterval' | 'renderingLetterCount'
> & { text: string };

const DEFAULT_LETTER_INTERVAL = 30;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
const DEFAULT_RENDERING_LETTER_COUNT = 2;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved

export const useStreamingMessage = <
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
letterInterval = DEFAULT_LETTER_INTERVAL,
renderingLetterCount = DEFAULT_RENDERING_LETTER_COUNT,
text,
}: UseStreamingMessageProps<StreamChatGenerics>) => {
const [streamedMessageText, setStreamedMessageText] = useState<string>(text);
const textCursor = useRef<number>(text.length);

useEffect(() => {
const textLength = text.length;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
const interval = setInterval(() => {
if (!text || textCursor.current >= textLength) {
clearInterval(interval);
}
const newCursorValue = textCursor.current + renderingLetterCount;
const newText = text.substring(0, newCursorValue);
textCursor.current += newText.length - textCursor.current;
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
const codeBlockCounts = (newText.match(/```/g) || []).length;
const shouldOptimisticallyCloseCodeBlock = codeBlockCounts > 0 && codeBlockCounts % 2 > 0;
setStreamedMessageText(shouldOptimisticallyCloseCodeBlock ? newText + '```' : newText);
}, letterInterval);

return () => {
clearInterval(interval);
};
}, [letterInterval, renderingLetterCount, text]);

return { streamedMessageText };
};
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 './StreamingMessageView';
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 @@ -35,6 +35,13 @@ export const defaultAllowedTagNames: Array<keyof JSX.IntrinsicElements | 'emoji'
// custom types (tagNames)
'emoji',
'mention',
'table',
'thead',
'tbody',
'th',
'tr',
'td',
'tfoot',
isekovanic marked this conversation as resolved.
Show resolved Hide resolved
];

function formatUrlForDisplay(url: string) {
Expand Down
Loading
Loading