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 loading bubble on chat screen #85

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: make changes according to suggestions made by @a-ghorbani
  • Loading branch information
EXTREMOPHILARUM committed Nov 7, 2024
commit 0b2f11a7a9bcfe40c49695b2ec9ad81ce4771bd6
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
root: true,
extends: '@react-native',
ignorePatterns: ['coverage/'],
};
19 changes: 15 additions & 4 deletions src/components/ChatView/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {oneOf} from '@flyerhq/react-native-link-preview';
import {useSafeAreaInsets} from 'react-native-safe-area-context';

import {useComponentSize} from '../KeyboardAccessoryView/hooks';
import {LoadingBubble} from '../LoadingBubble';

import {usePrevious, useTheme} from '../../hooks';

Expand Down Expand Up @@ -81,6 +82,10 @@ export interface ChatProps extends ChatTopLevelProps {
* When true, indicates that there are no more pages to load and
* pagination will not be triggered. */
isLastPage?: boolean;
/** Indicates if the AI is currently streaming tokens */
isStreaming?: boolean;
/** Indicates if the AI is currently thinking (processing but not yet streaming) */
isThinking?: boolean;
/** Override the default localized copy. */
l10nOverride?: Partial<
Record<keyof (typeof l10n)[keyof typeof l10n], string>
Expand Down Expand Up @@ -116,6 +121,8 @@ export const ChatView = ({
isAttachmentUploading,
isLastPage,
isStopVisible,
isStreaming = false,
isThinking = false,
l10nOverride,
locale = 'en',
messages,
Expand Down Expand Up @@ -149,7 +156,6 @@ export const ChatView = ({
flatListContentContainer,
footer,
footerLoadingPage,
header,
keyboardAccessoryView,
} = styles({theme});

Expand Down Expand Up @@ -359,6 +365,11 @@ export const ChatView = ({
[footer, footerLoadingPage, isNextPageLoading, theme.colors.primary],
);

const renderListHeaderComponent = React.useCallback(
() => (isThinking ? <LoadingBubble /> : null),
[isThinking],
);

const renderScrollable = React.useCallback(
(panHandlers: GestureResponderHandlers) => (
<FlatList
Expand All @@ -374,8 +385,7 @@ export const ChatView = ({
initialNumToRender={10}
ListEmptyComponent={renderListEmptyComponent}
ListFooterComponent={renderListFooterComponent}
ListHeaderComponent={<View />}
ListHeaderComponentStyle={header}
ListHeaderComponent={renderListHeaderComponent}
maxToRenderPerBatch={6}
onEndReachedThreshold={0.75}
style={flatList}
Expand All @@ -397,12 +407,12 @@ export const ChatView = ({
flatListContentContainer,
flatListProps,
handleEndReached,
header,
insets.bottom,
keyExtractor,
renderItem,
renderListEmptyComponent,
renderListFooterComponent,
renderListHeaderComponent,
],
);

Expand All @@ -428,6 +438,7 @@ export const ChatView = ({
{...{
...unwrap(inputProps),
isAttachmentUploading,
isStreaming,
onAttachmentPress,
onSendPress,
onStopPress,
Expand Down
18 changes: 14 additions & 4 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface InputTopLevelProps {
* managing media in dependencies we have no way of knowing if
* something is uploading so you need to set this manually. */
isAttachmentUploading?: boolean;
/** Whether the AI is currently streaming tokens */
isStreaming?: boolean;
/** @see {@link AttachmentButtonProps.onPress} */
onAttachmentPress?: () => void;
/** Will be called on {@link SendButton} tap. Has {@link MessageType.PartialText} which can
Expand All @@ -49,6 +51,7 @@ export const Input = ({
attachmentButtonProps,
attachmentCircularActivityIndicatorProps,
isAttachmentUploading,
isStreaming = false,
onAttachmentPress,
onSendPress,
onStopPress,
Expand Down Expand Up @@ -84,6 +87,16 @@ export const Input = ({
}
};

const shouldShowSendButton = () => {
if (isStreaming || isStopVisible) {
return false;
}
if (sendButtonVisibilityMode === 'always') {
return true;
}
return sendButtonVisibilityMode === 'editing' && user && value.trim();
};

return (
<View style={container}>
{user &&
Expand Down Expand Up @@ -114,10 +127,7 @@ export const Input = ({
onChangeText={handleChangeText}
value={value}
/>
{sendButtonVisibilityMode === 'always' ||
(sendButtonVisibilityMode === 'editing' && user && value.trim()) ? (
<SendButton onPress={handleSend} />
) : null}
{shouldShowSendButton() ? <SendButton onPress={handleSend} /> : null}
{isStopVisible && <StopButton onPress={onStopPress} />}
</View>
);
Expand Down
29 changes: 9 additions & 20 deletions src/components/LoadingBubble/LoadingBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React, {useEffect, useRef} from 'react';
import {View, Animated, StyleSheet} from 'react-native';
import {View, Animated} from 'react-native';
import {useTheme} from '../../hooks';
import {Theme} from '../../utils/types';
import {styles} from './styles';

const LoadingDot = ({delay, theme}: {delay: number; theme: Theme}) => {
interface LoadingDotProps {
delay: number;
theme: Theme;
}

const LoadingDot: React.FC<LoadingDotProps> = ({delay, theme}) => {
const opacity = useRef(new Animated.Value(0.3)).current;

useEffect(() => {
Expand Down Expand Up @@ -37,7 +43,7 @@ const LoadingDot = ({delay, theme}: {delay: number; theme: Theme}) => {
);
};

export const LoadingBubble = () => {
export const LoadingBubble: React.FC = () => {
const theme = useTheme();

return (
Expand All @@ -52,20 +58,3 @@ export const LoadingBubble = () => {
</View>
);
};

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 12,
borderRadius: 16,
alignSelf: 'flex-start',
marginVertical: 4,
marginHorizontal: 8,
gap: 4,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
},
});
18 changes: 18 additions & 0 deletions src/components/LoadingBubble/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {StyleSheet} from 'react-native';

export const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 8,
borderRadius: 16,
alignSelf: 'flex-start',
marginVertical: 8,
marginHorizontal: 12,
gap: 4,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
},
});
33 changes: 8 additions & 25 deletions src/screens/ChatScreen/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {observer} from 'mobx-react';
import {SafeAreaProvider} from 'react-native-safe-area-context';

import {Bubble, ChatView} from '../../components';
import {LoadingBubble} from '../../components/LoadingBubble';

import {useChatSession} from '../../hooks';

Expand All @@ -25,10 +24,6 @@ const renderBubble = ({
message: MessageType.Any;
nextMessageInGroup: boolean;
}) => {
// Check for our special loading message
if (message.id === 'loading-indicator') {
return <LoadingBubble />;
}
return (
<Bubble
child={child}
Expand All @@ -44,27 +39,13 @@ export const ChatScreen: React.FC = observer(() => {
null,
);
const l10n = React.useContext(L10nContext);
const baseMessages: MessageType.Any[] =
chatSessionStore.currentSessionMessages;
const messages = chatSessionStore.currentSessionMessages;

const {handleSendPress, handleStopPress, inferencing, isStreaming} =
useChatSession(context, currentMessageInfo, baseMessages, user, assistant);
useChatSession(context, currentMessageInfo, messages, user, assistant);

// Add loading message if inferencing but not yet streaming
const messages = React.useMemo(() => {
if (!inferencing || isStreaming) {
return baseMessages;
}
return [
{
id: 'loading-indicator',
type: 'text',
text: '',
author: assistant,
} as MessageType.Text,
...baseMessages,
];
}, [baseMessages, inferencing, isStreaming]);
// Show loading bubble only during the thinking phase (inferencing but not streaming)
const isThinking = inferencing && !isStreaming;

return (
<SafeAreaProvider>
Expand All @@ -80,9 +61,11 @@ export const ChatScreen: React.FC = observer(() => {
onStopPress={handleStopPress}
user={user}
isStopVisible={inferencing}
isThinking={isThinking}
isStreaming={isStreaming}
sendButtonVisibilityMode="editing"
textInputProps={{
editable: !!context && !inferencing,
value: inferencing ? '' : undefined,
editable: !!context,
placeholder: !context
? modelStore.isContextLoading
? l10n.loadingModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
expect(mockProps.onChange).toHaveBeenCalledWith('addBosToken', false);
});

it('opens and closes the template dialog', async () => {

Check warning on line 88 in src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

Disabled test
const {getByText, queryByText} = render(<ModelSettings {...mockProps} />);

// Open dialog
Expand All @@ -94,19 +94,27 @@
fireEvent.press(editButton);
});

// Check if dialog is visible
expect(getByText('Save')).toBeTruthy();
expect(getByText('Cancel')).toBeTruthy();
// Wait for dialog to be visible
await waitFor(() => {
expect(getByText('Save')).toBeTruthy();
expect(getByText('Cancel')).toBeTruthy();
});

const cancelButton = getByText('Cancel');
await act(async () => {
fireEvent.press(cancelButton);
});

await waitFor(() => {
expect(queryByText('Save')).toBeNull();
});
});
// Wait for dialog to be hidden
await waitFor(
() => {
expect(queryByText('Save')).toBeNull();
},
{
timeout: 10000,
},
);
}, 15000);

Check warning on line 117 in src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

Trailing spaces not allowed

Check failure on line 117 in src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

Delete `·`

it('saves template changes', async () => {
const {getByText, getByPlaceholderText} = render(
Expand Down
21 changes: 17 additions & 4 deletions src/screens/ModelsScreen/__tests__/ModelsScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,36 @@
expect(modelStore.resetModels).toHaveBeenCalled();
});

it('hides reset dialog on cancel', async () => {

Check warning on line 215 in src/screens/ModelsScreen/__tests__/ModelsScreen.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

Disabled test
const {getByTestId, queryByTestId} = render(<ModelsScreen />, {
withNavigation: true,
});

// Open dialog
const resetFab = getByTestId('reset-models-fab');
await act(async () => {
fireEvent.press(resetFab);
});

// Wait for dialog to be visible
await waitFor(() => {
expect(getByTestId('reset-dialog')).toBeTruthy();
});

// Press cancel button
const cancelButton = getByTestId('cancel-reset-button');
await act(async () => {
fireEvent.press(cancelButton);
});

await waitFor(() => {
expect(queryByTestId('reset-dialog')).toBeNull();
});
});
// Wait for dialog to be hidden
await waitFor(
() => {
expect(queryByTestId('reset-dialog')).toBeNull();
},
{
timeout: 10000,
},
);
}, 15000);
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"jsx": "react",
"noImplicitAny": false,
}
}
Loading