2023-05-21 05:48:11 +00:00
import React , { FC , useEffect , useRef , useState } from 'react' ;
import { useTranslation } from 'react-i18next' ;
import { RunButton } from '../components/RunButton' ;
import { Avatar , Divider , PresenceBadge , Text , Textarea } from '@fluentui/react-components' ;
import commonStore , { ModelStatus } from '../stores/commonStore' ;
import { observer } from 'mobx-react-lite' ;
import { PresenceBadgeStatus } from '@fluentui/react-badge' ;
import { ConfigSelector } from '../components/ConfigSelector' ;
import { v4 as uuid } from 'uuid' ;
2023-05-19 06:22:37 +00:00
import classnames from 'classnames' ;
2023-05-21 05:48:11 +00:00
import { fetchEventSource } from '@microsoft/fetch-event-source' ;
import { ConversationPair , getConversationPairs , Record } from '../utils/get-conversation-pairs' ;
2023-05-19 06:22:37 +00:00
import logo from '../../../build/appicon.png' ;
2023-05-19 07:40:17 +00:00
import MarkdownRender from '../components/MarkdownRender' ;
2023-05-21 05:48:11 +00:00
import { ToolTipButton } from '../components/ToolTipButton' ;
import { ArrowCircleUp28Regular , Delete28Regular , RecordStop28Regular } from '@fluentui/react-icons' ;
import { CopyButton } from '../components/CopyButton' ;
import { ReadButton } from '../components/ReadButton' ;
import { toast } from 'react-toastify' ;
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export const userName = 'M E' ;
export const botName = 'A I' ;
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export enum MessageType {
2023-05-19 06:22:37 +00:00
Normal ,
Error
}
2023-05-20 08:07:08 +00:00
export type Side = 'left' | 'right'
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export type Color = 'neutral' | 'brand' | 'colorful'
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export type MessageItem = {
2023-05-19 06:22:37 +00:00
sender : string ,
type : MessageType ,
color : Color ,
avatarImg? : string ,
time : string ,
content : string ,
side : Side ,
done : boolean
}
2023-05-20 08:07:08 +00:00
export type Conversations = {
2023-05-19 06:22:37 +00:00
[ uuid : string ] : MessageItem
}
const ChatPanel : FC = observer ( ( ) = > {
2023-05-21 05:48:11 +00:00
const { t } = useTranslation ( ) ;
2023-05-19 06:22:37 +00:00
const [ message , setMessage ] = useState ( '' ) ;
const bodyRef = useRef < HTMLDivElement > ( null ) ;
2023-05-19 07:40:17 +00:00
const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
2023-05-19 06:22:37 +00:00
const port = commonStore . getCurrentModelConfig ( ) . apiParameters . apiPort ;
2023-05-19 12:10:30 +00:00
const sseControllerRef = useRef < AbortController | null > ( null ) ;
let lastMessageId : string ;
let generating : boolean = false ;
2023-05-20 08:07:08 +00:00
if ( commonStore . conversationsOrder . length > 0 ) {
lastMessageId = commonStore . conversationsOrder [ commonStore . conversationsOrder . length - 1 ] ;
const lastMessage = commonStore . conversations [ lastMessageId ] ;
2023-05-19 12:10:30 +00:00
if ( lastMessage . sender === botName )
generating = ! lastMessage . done ;
}
2023-05-19 06:22:37 +00:00
2023-05-19 07:40:17 +00:00
useEffect ( ( ) = > {
if ( inputRef . current )
inputRef . current . style . maxHeight = '16rem' ;
} , [ ] ) ;
2023-05-20 08:07:08 +00:00
useEffect ( ( ) = > {
if ( commonStore . conversationsOrder . length === 0 ) {
commonStore . setConversationsOrder ( [ 'welcome' ] ) ;
commonStore . setConversations ( {
'welcome' : {
sender : botName ,
type : MessageType . Normal ,
color : 'colorful' ,
avatarImg : logo ,
time : new Date ( ) . toISOString ( ) ,
content : t ( 'Hello! I\'m RWKV, an open-source and commercially available large language model.' ) ,
side : 'left' ,
done : true
}
} ) ;
}
} , [ ] ) ;
2023-05-19 06:22:37 +00:00
const scrollToBottom = ( ) = > {
if ( bodyRef . current )
bodyRef . current . scrollTop = bodyRef . current . scrollHeight ;
} ;
2023-05-19 12:10:30 +00:00
const handleKeyDownOrClick = ( e : any ) = > {
e . stopPropagation ( ) ;
if ( e . type === 'click' || ( e . keyCode === 13 && ! e . shiftKey ) ) {
e . preventDefault ( ) ;
2023-05-20 02:38:35 +00:00
if ( commonStore . modelStatus === ModelStatus . Offline ) {
2023-05-21 05:48:11 +00:00
toast ( t ( 'Please click the button in the top right corner to start the model' ) , { type : 'warning' } ) ;
2023-05-20 02:38:35 +00:00
return ;
}
2023-05-19 12:10:30 +00:00
if ( ! message ) return ;
onSubmit ( message ) ;
2023-05-19 06:22:37 +00:00
setMessage ( '' ) ;
2023-05-19 12:10:30 +00:00
}
} ;
const onSubmit = ( message : string ) = > {
const newId = uuid ( ) ;
2023-05-20 08:07:08 +00:00
commonStore . conversations [ newId ] = {
2023-05-19 12:10:30 +00:00
sender : userName ,
type : MessageType . Normal ,
color : 'brand' ,
time : new Date ( ) . toISOString ( ) ,
content : message ,
side : 'right' ,
done : true
} ;
2023-05-20 08:07:08 +00:00
commonStore . setConversations ( commonStore . conversations ) ;
commonStore . conversationsOrder . push ( newId ) ;
commonStore . setConversationsOrder ( commonStore . conversationsOrder ) ;
2023-05-19 12:10:30 +00:00
const records : Record [ ] = [ ] ;
2023-05-20 08:07:08 +00:00
commonStore . conversationsOrder . forEach ( ( uuid , index ) = > {
const conversation = commonStore . conversations [ uuid ] ;
2023-05-19 12:10:30 +00:00
if ( conversation . done && conversation . type === MessageType . Normal && conversation . sender === botName ) {
if ( index > 0 ) {
2023-05-20 08:07:08 +00:00
const questionId = commonStore . conversationsOrder [ index - 1 ] ;
const question = commonStore . conversations [ questionId ] ;
2023-05-19 12:10:30 +00:00
if ( question . done && question . type === MessageType . Normal && question . sender === userName ) {
2023-05-21 05:48:11 +00:00
records . push ( { question : question.content , answer : conversation.content } ) ;
2023-05-19 06:22:37 +00:00
}
}
2023-05-19 12:10:30 +00:00
}
} ) ;
const messages = getConversationPairs ( records , false ) ;
2023-05-21 05:48:11 +00:00
( messages as ConversationPair [ ] ) . push ( { role : 'user' , content : message } ) ;
2023-05-19 12:10:30 +00:00
const answerId = uuid ( ) ;
2023-05-20 08:07:08 +00:00
commonStore . conversations [ answerId ] = {
2023-05-19 12:10:30 +00:00
sender : botName ,
type : MessageType . Normal ,
color : 'colorful' ,
avatarImg : logo ,
time : new Date ( ) . toISOString ( ) ,
content : '' ,
side : 'left' ,
done : false
} ;
2023-05-20 08:07:08 +00:00
commonStore . setConversations ( commonStore . conversations ) ;
commonStore . conversationsOrder . push ( answerId ) ;
commonStore . setConversationsOrder ( commonStore . conversationsOrder ) ;
2023-05-19 12:10:30 +00:00
setTimeout ( scrollToBottom ) ;
let answer = '' ;
sseControllerRef . current = new AbortController ( ) ;
fetchEventSource ( ` http://127.0.0.1: ${ port } /chat/completions ` , // https://api.openai.com/v1/chat/completions || http://127.0.0.1:${port}/chat/completions
{
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
Authorization : ` Bearer sk- `
} ,
body : JSON.stringify ( {
messages ,
stream : true ,
model : 'gpt-3.5-turbo'
} ) ,
signal : sseControllerRef.current?.signal ,
onmessage ( e ) {
console . log ( 'sse message' , e ) ;
scrollToBottom ( ) ;
if ( e . data === '[DONE]' ) {
2023-05-20 08:07:08 +00:00
commonStore . conversations [ answerId ] . done = true ;
2023-05-21 15:44:56 +00:00
commonStore . conversations [ answerId ] . content = commonStore . conversations [ answerId ] . content . trim ( ) ;
2023-05-20 08:07:08 +00:00
commonStore . setConversations ( commonStore . conversations ) ;
commonStore . setConversationsOrder ( [ . . . commonStore . conversationsOrder ] ) ;
2023-05-19 12:10:30 +00:00
return ;
2023-05-19 06:22:37 +00:00
}
2023-05-19 12:10:30 +00:00
let data ;
try {
data = JSON . parse ( e . data ) ;
} catch ( error ) {
console . debug ( 'json error' , error ) ;
return ;
}
if ( data . choices && Array . isArray ( data . choices ) && data . choices . length > 0 ) {
answer += data . choices [ 0 ] ? . delta ? . content || '' ;
2023-05-20 08:07:08 +00:00
commonStore . conversations [ answerId ] . content = answer ;
commonStore . setConversations ( commonStore . conversations ) ;
commonStore . setConversationsOrder ( [ . . . commonStore . conversationsOrder ] ) ;
2023-05-19 12:10:30 +00:00
}
} ,
onclose() {
console . log ( 'Connection closed' ) ;
} ,
onerror ( err ) {
2023-05-20 08:07:08 +00:00
commonStore . conversations [ answerId ] . type = MessageType . Error ;
commonStore . conversations [ answerId ] . done = true ;
commonStore . setConversations ( commonStore . conversations ) ;
commonStore . setConversationsOrder ( [ . . . commonStore . conversationsOrder ] ) ;
2023-05-19 12:10:30 +00:00
throw err ;
}
} ) ;
2023-05-19 06:22:37 +00:00
} ;
2023-05-05 15:23:34 +00:00
2023-05-19 02:08:28 +00:00
return (
2023-05-19 06:22:37 +00:00
< div className = "flex flex-col w-full grow gap-4 pt-4 overflow-hidden" >
< div ref = { bodyRef } className = "grow overflow-y-scroll overflow-x-hidden pr-2" >
2023-05-20 08:07:08 +00:00
{ commonStore . conversationsOrder . map ( ( uuid , index ) = > {
const conversation = commonStore . conversations [ uuid ] ;
2023-05-19 06:22:37 +00:00
return < div
key = { uuid }
className = { classnames (
2023-05-19 07:40:17 +00:00
'flex gap-2 mb-2 overflow-hidden' ,
2023-05-19 06:22:37 +00:00
conversation . side === 'left' ? 'flex-row' : 'flex-row-reverse'
) }
2023-05-19 12:55:12 +00:00
onMouseEnter = { ( ) = > {
const utils = document . getElementById ( 'utils-' + uuid ) ;
if ( utils ) utils . classList . remove ( 'invisible' ) ;
} }
onMouseLeave = { ( ) = > {
const utils = document . getElementById ( 'utils-' + uuid ) ;
if ( utils ) utils . classList . add ( 'invisible' ) ;
} }
2023-05-19 06:22:37 +00:00
>
< Avatar
color = { conversation . color }
name = { conversation . sender }
2023-05-21 05:48:11 +00:00
image = { conversation . avatarImg ? { src : conversation.avatarImg } : undefined }
2023-05-19 06:22:37 +00:00
/ >
< div
className = { classnames (
2023-05-19 07:40:17 +00:00
'p-2 rounded-lg overflow-hidden' ,
2023-05-19 06:22:37 +00:00
conversation . side === 'left' ? 'bg-gray-200' : 'bg-blue-500' ,
conversation . side === 'left' ? 'text-gray-600' : 'text-white'
) }
>
2023-05-19 07:40:17 +00:00
< MarkdownRender > { conversation . content } < / MarkdownRender >
2023-05-19 06:22:37 +00:00
< / div >
2023-05-19 12:55:12 +00:00
< div className = "flex flex-col gap-1 items-start" >
2023-05-21 05:48:11 +00:00
< div className = "grow" / >
2023-05-19 12:55:12 +00:00
{ ( conversation . type === MessageType . Error || ! conversation . done ) &&
< PresenceBadge size = "extra-small" status = {
conversation . type === MessageType . Error ? 'busy' : 'away'
2023-05-21 05:48:11 +00:00
} / >
2023-05-19 12:55:12 +00:00
}
< div className = "flex invisible" id = { 'utils-' + uuid } >
2023-05-21 05:48:11 +00:00
< ReadButton content = { conversation . content } / >
< CopyButton content = { conversation . content } / >
2023-05-19 12:55:12 +00:00
< / div >
< / div >
2023-05-19 06:22:37 +00:00
< / div > ;
} ) }
< / div >
2023-05-19 12:10:30 +00:00
< div className = "flex items-end gap-2" >
< ToolTipButton desc = { t ( 'Clear' ) }
2023-05-21 05:48:11 +00:00
icon = { < Delete28Regular / > }
size = "large" shape = "circular" appearance = "subtle"
onClick = { ( e ) = > {
if ( generating )
sseControllerRef . current ? . abort ( ) ;
commonStore . setConversations ( { } ) ;
commonStore . setConversationsOrder ( [ ] ) ;
} }
2023-05-19 12:10:30 +00:00
/ >
< Textarea
ref = { inputRef }
className = "grow"
resize = "vertical"
placeholder = { t ( 'Type your message here' ) ! }
value = { message }
onChange = { ( e ) = > setMessage ( e . target . value ) }
onKeyDown = { handleKeyDownOrClick }
/ >
< ToolTipButton desc = { generating ? t ( 'Stop' ) : t ( 'Send' ) }
2023-05-21 05:48:11 +00:00
icon = { generating ? < RecordStop28Regular / > : < ArrowCircleUp28Regular / > }
size = "large" shape = "circular" appearance = "subtle"
onClick = { ( e ) = > {
if ( generating ) {
sseControllerRef . current ? . abort ( ) ;
if ( lastMessageId ) {
commonStore . conversations [ lastMessageId ] . type = MessageType . Error ;
commonStore . conversations [ lastMessageId ] . done = true ;
commonStore . setConversations ( commonStore . conversations ) ;
commonStore . setConversationsOrder ( [ . . . commonStore . conversationsOrder ] ) ;
}
} else {
handleKeyDownOrClick ( e ) ;
}
} } / >
2023-05-19 06:22:37 +00:00
< / div >
< / div >
2023-05-19 02:08:28 +00:00
) ;
2023-05-19 06:22:37 +00:00
} ) ;
2023-05-19 02:08:28 +00:00
const statusText = {
[ ModelStatus . Offline ] : 'Offline' ,
[ ModelStatus . Starting ] : 'Starting' ,
[ ModelStatus . Loading ] : 'Loading' ,
[ ModelStatus . Working ] : 'Working'
} ;
const badgeStatus : { [ modelStatus : number ] : PresenceBadgeStatus } = {
[ ModelStatus . Offline ] : 'unknown' ,
[ ModelStatus . Starting ] : 'away' ,
[ ModelStatus . Loading ] : 'away' ,
[ ModelStatus . Working ] : 'available'
} ;
export const Chat : FC = observer ( ( ) = > {
2023-05-21 05:48:11 +00:00
const { t } = useTranslation ( ) ;
2023-05-19 13:24:09 +00:00
const port = commonStore . getCurrentModelConfig ( ) . apiParameters . apiPort ;
2023-05-18 12:48:53 +00:00
2023-05-05 15:23:34 +00:00
return (
2023-05-19 02:08:28 +00:00
< div className = "flex flex-col gap-1 p-2 h-full overflow-hidden" >
< div className = "flex justify-between items-center" >
< div className = "flex items-center gap-2" >
2023-05-21 05:48:11 +00:00
< PresenceBadge status = { badgeStatus [ commonStore . modelStatus ] } / >
2023-05-19 02:08:28 +00:00
< Text size = { 100 } > { t ( 'Model Status' ) + ': ' + t ( statusText [ commonStore . modelStatus ] ) } < / Text >
< / div >
< div className = "flex items-center gap-2" >
2023-05-21 05:48:11 +00:00
< ConfigSelector size = "small" / >
< RunButton iconMode / >
2023-05-19 02:08:28 +00:00
< / div >
2023-05-17 15:27:52 +00:00
< / div >
2023-05-19 13:24:09 +00:00
< Text size = { 100 } >
2023-05-22 02:08:38 +00:00
{ t ( 'This tool\'s API is compatible with OpenAI API. It can be used with any ChatGPT tool you like. Go to the settings of some ChatGPT tool, replace the \'https://api.openai.com\' part in the API address with \'' ) + ` http://127.0.0.1: ${ port } ` + '\'.' }
2023-05-19 13:24:09 +00:00
< / Text >
2023-05-21 05:48:11 +00:00
< Divider style = { { flexGrow : 0 } } / >
< ChatPanel / >
2023-05-19 02:08:28 +00:00
< / div >
2023-05-05 15:23:34 +00:00
) ;
2023-05-19 02:08:28 +00:00
} ) ;