Skip to content

Commit

Permalink
feat: support multiple conversations (#3)
Browse files Browse the repository at this point in the history
feat: support multiple conversations
  • Loading branch information
hahahumble committed Apr 17, 2023
2 parents 3309770 + 7cac83c commit e81cfd9
Show file tree
Hide file tree
Showing 33 changed files with 1,161 additions and 67 deletions.
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -19,6 +19,7 @@
"@tippyjs/react": "^4.2.6",
"@types/i18next": "^13.0.0",
"@types/react-speech-recognition": "^3.9.1",
"@types/uuid": "^9.0.1",
"aws-sdk": "^2.1348.0",
"axios": "^1.3.4",
"classnames": "^2.3.2",
Expand All @@ -38,7 +39,8 @@
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"react-scroll": "^1.8.9",
"react-textarea-autosize": "^8.4.1"
"react-textarea-autosize": "^8.4.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/react": "^18.0.31",
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
@@ -1,12 +1,13 @@
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import React from 'react';
import Home from './pages/Home';
import { initialGlobalState } from './store/module';
import { initialGlobalState, initialSessionState } from './store/module';
import NotFound from './pages/NotFound';
import 'highlight.js/styles/github.css';

function App() {
initialGlobalState();
initialSessionState();
return (
<Router>
<Routes>
Expand Down
9 changes: 9 additions & 0 deletions src/components/ButtonGroup.tsx
Expand Up @@ -10,9 +10,11 @@ import SpeakerIcon from './Icons/SpeakerIcon';
import MuteSpeakerIcon from './Icons/MuteSpeakerIcon';
import PlayIcon from './Icons/PlayIcon';
import { useTranslation } from 'react-i18next';
import ConversationIcon from './Icons/ConversationIcon';

interface ButtonGroupProps {
setOpenSetting: (open: boolean) => void;
setOpenConversations: (open: boolean) => void;
disableMicrophone: boolean;
onClickDisableMicrophone: () => void;
disableSpeaker: boolean;
Expand All @@ -27,6 +29,7 @@ interface ButtonGroupProps {

function ButtonGroup({
setOpenSetting,
setOpenConversations,
disableMicrophone,
onClickDisableMicrophone,
disableSpeaker,
Expand Down Expand Up @@ -95,6 +98,12 @@ function ButtonGroup({
/>
<MicrophoneButton />
<SpeakerButton />
<TippyButton
tooltip={i18n.t('common.conversations-list') as string}
onClick={() => setOpenConversations(true)}
icon={<ConversationIcon className="w-6 h-6 text-gray-500" />}
style="hover:bg-gray-200 active:bg-gray-300"
/>
</div>
<div className="flex flex-row space-x-1">
{status === 'speaking' && !finished && (
Expand Down
69 changes: 57 additions & 12 deletions src/components/Content.tsx
Expand Up @@ -16,11 +16,12 @@ import {
resumeSpeechSynthesis,
} from '../utils/speechSynthesis';

import { db } from '../db';
import { chatDB } from '../db';
import { useLiveQuery } from 'dexie-react-hooks';
import { useGlobalStore } from '../store/module';
import { useGlobalStore, useSessionStore } from '../store/module';
import { existEnvironmentVariable, getEnvironmentVariable } from '../helpers/utils';
import { isMobile } from 'react-device-detect';
import ConversationDialog from './Conversations/ConversationDialog';

type baseStatus = 'idle' | 'waiting' | 'speaking' | 'recording' | 'connecting';

Expand All @@ -38,17 +39,29 @@ const useIsMount = () => {

const Content: React.FC<ContentProps> = ({ notify }) => {
const { key, chat, speech, voice } = useGlobalStore();
const { currentSessionId, sessions, addSession, setCurrentSessionId, setMessageCount } =
useSessionStore();

const [sendMessages, setSendMessages] = useState<boolean>(false);
const list = useLiveQuery(() => db.chat.toArray(), []);

const chatList = useLiveQuery(() => chatDB.chat.toArray(), []);

const conversations = useMemo(() => {
return list?.map(l => ({ role: l.role, content: l.content, id: l.id })) || [];
}, [list]);
return (
chatList?.map(l => ({
role: l.role,
content: l.content,
id: l.id,
sessionId: l.sessionId,
})) || []
);
}, [chatList]);

const [input, setInput] = useState<string>('');
const [response, setResponse] = useState<string>(''); // openai response

const [openSetting, setOpenSetting] = useState<boolean>(false);
const [openConversations, setOpenConversations] = useState<boolean>(false);

const [status, setStatus] = useState<baseStatus>('idle');
const prevStatusRef = useRef(status);
Expand All @@ -74,6 +87,24 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
}
}, [conversations]);

useEffect(() => {
if (currentSessionId) {
const count = conversations.filter(c => c.sessionId === currentSessionId).length;
setMessageCount({
id: currentSessionId,
messageCount: count,
});
}
}, [conversations.length]);

const calculateMessageCount = () => {
const count = conversations.filter(c => c.sessionId === currentSessionId).length;
setMessageCount({
id: currentSessionId,
messageCount: count,
});
};

const generateSpeech = async (text: string) => {
if (disableSpeaker) {
return;
Expand Down Expand Up @@ -159,7 +190,7 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
useEffect(() => {
if (response.length !== 0 && response !== 'undefined') {
setSendMessages(false);
db.chat.add({ role: 'assistant', content: response });
chatDB.chat.add({ role: 'assistant', content: response, sessionId: currentSessionId });
generateSpeech(response).then();
}
}, [response]);
Expand Down Expand Up @@ -225,9 +256,13 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
if (input.length === 0 || status === 'waiting' || status === 'speaking') {
return;
}
const input_json = { role: 'user', content: input };
const input_json = {
role: 'user',
content: input,
sessionId: currentSessionId,
};
setSendMessages(true);
db.chat.add(input_json);
chatDB.chat.add(input_json);
setInput('');
if (!isMobile) {
focusInput();
Expand All @@ -242,9 +277,13 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
if (input.length === 0) {
return;
} else {
const input_json = { role: 'user', content: input };
const input_json = {
role: 'user',
content: input,
sessionId: currentSessionId,
};
setSendMessages(true);
db.chat.add(input_json);
chatDB.chat.add(input_json);

setInput('');
if (!isMobile) {
Expand All @@ -267,7 +306,7 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
};

const clearConversation = () => {
db.chat.clear();
chatDB.chat.clear();
setInput(chat.defaultPrompt);
setStatus('idle');
stopSpeechSynthesis();
Expand All @@ -286,7 +325,7 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
}

function deleteContent(key: number) {
db.chat.delete(key);
chatDB.chat.delete(key);
notify.deletedNotify();
}

Expand Down Expand Up @@ -394,6 +433,11 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
return (
<div className="w-160 flex flex-col h-full justify-between pb-3 dark:bg-gray-900">
<SettingDialog open={openSetting} onClose={() => setOpenSetting(false)} />
<ConversationDialog
open={openConversations}
onClose={() => setOpenConversations(false)}
notify={notify}
/>
{voice.service == 'System' && (
<BrowserSpeechToText
isListening={isListening}
Expand Down Expand Up @@ -435,6 +479,7 @@ const Content: React.FC<ContentProps> = ({ notify }) => {
<div className="">
<ButtonGroup
setOpenSetting={setOpenSetting}
setOpenConversations={setOpenConversations}
disableMicrophone={disableMicrophone}
disableSpeaker={disableSpeaker}
onClickDisableMicrophone={onClickDisableMicrophone}
Expand Down
98 changes: 55 additions & 43 deletions src/components/ConversationPanel.tsx
Expand Up @@ -8,6 +8,7 @@ import React, { useState } from 'react';
import { marked } from '../helpers/markdown';
import { Chat } from '../db/chat';
import { useTranslation } from 'react-i18next';
import { useSessionStore } from '../store/module';

interface ConversationPanelProps {
conversations: Chat[];
Expand All @@ -31,6 +32,8 @@ function ConversationPanel({
setIsHidden(true);
};

const { currentSessionId } = useSessionStore();

function ChatIcon({ role }: { role: 'user' | 'assistant' | 'system' }) {
if (role === 'user') {
return (
Expand All @@ -43,58 +46,67 @@ function ConversationPanel({
}
}

function isConversationEmpty() {
return (
conversations.length === 0 ||
conversations.filter(conversation => conversation.sessionId === currentSessionId).length === 0
);
}

return (
<Element name="messages" className="flex-grow border border-gray-300 rounded-lg p-4 mb-4">
{conversations.length === 0 && <Tips />}
{conversations.map((conversation, index) => (
<div
key={conversation.id}
className="group relative rounded-lg hover:bg-gray-200 p-2 flex flex-row space-x-3 transition-colors duration-100"
>
<ChatIcon role={conversation.role} />
{isConversationEmpty() && <Tips />}
{conversations
.filter(conversation => conversation.sessionId === currentSessionId)
.map((conversation, index) => (
<div
className="py-1 text-gray-800 markdown-content"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onTouchStart={handleMouseDown}
onTouchEnd={handleMouseUp}
key={conversation.id}
className="group relative rounded-lg hover:bg-gray-200 p-2 flex flex-row space-x-3 transition-colors duration-100"
>
{marked(conversation.content ?? '')}
</div>
<div
<ChatIcon role={conversation.role} />
<div
className="py-1 text-gray-800 markdown-content"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onTouchStart={handleMouseDown}
onTouchEnd={handleMouseUp}
>
{marked(conversation.content ?? '')}
</div>
<div
className={`absolute right-2 top-2 group-hover:opacity-100 opacity-0 transition-colors duration-100 flex-row space-x-0.5 ${
isHidden ? 'hidden' : 'flex'
}`}
>
<TippyButton
onClick={() => {
generateSpeech(conversation.content);
}}
tooltip={i18n.t('common.replay') as string}
icon={<SpeakerIcon className="w-4 h-4 text-gray-500" />}
style="bg-gray-100 active:bg-gray-300 rounded-sm"
/>
<TippyButton
onClick={() => {
deleteContent(conversation.id);
}}
tooltip={i18n.t('common.delete') as string}
icon={<TrashIcon className="w-4 h-4 text-gray-500" />}
style="bg-gray-100 active:bg-gray-300 rounded-sm"
/>
<TippyButton
onClick={() => {
copyContentToClipboard(conversation.content);
}}
tooltip={i18n.t('common.copy') as string}
icon={<CopyIcon className="w-4 h-4 text-gray-500" />}
style="bg-gray-100 active:bg-gray-300 rounded-sm"
/>
>
<TippyButton
onClick={() => {
generateSpeech(conversation.content);
}}
tooltip={i18n.t('common.replay') as string}
icon={<SpeakerIcon className="w-4 h-4 text-gray-500" />}
style="bg-gray-100 active:bg-gray-300 rounded-sm"
/>
<TippyButton
onClick={() => {
deleteContent(conversation.id);
}}
tooltip={i18n.t('common.delete') as string}
icon={<TrashIcon className="w-4 h-4 text-gray-500" />}
style="bg-gray-100 active:bg-gray-300 rounded-sm"
/>
<TippyButton
onClick={() => {
copyContentToClipboard(conversation.content);
}}
tooltip={i18n.t('common.copy') as string}
icon={<CopyIcon className="w-4 h-4 text-gray-500" />}
style="bg-gray-100 active:bg-gray-300 rounded-sm"
/>
</div>
</div>
</div>
))}
))}
</Element>
);
}

export default ConversationPanel;
export default ConversationPanel;

1 comment on commit e81cfd9

@vercel
Copy link

@vercel vercel bot commented on e81cfd9 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

speechgpt – ./

speechgpt-hahahumble.vercel.app
speechgpt-git-main-hahahumble.vercel.app
speechgpt-alpha.vercel.app

Please sign in to comment.