From 9b4155cf75143ca4727a83ee7551dbb740fed52c Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Tue, 5 Sep 2023 22:18:28 +0800 Subject: [PATCH] fix redux dispatch context error --- renio/src/assets/chat.less | 34 +++++++++ renio/src/assets/home.less | 29 ++++---- renio/src/components/Markdown.tsx | 6 +- renio/src/conf.ts | 2 +- renio/src/conversation/connection.ts | 2 +- renio/src/conversation/conversation.ts | 26 +++++-- renio/src/conversation/manager.ts | 42 ++++++++--- renio/src/routes/Home.tsx | 99 ++++++++++++++++++-------- renio/src/store/chat.ts | 10 ++- renio/src/utils.ts | 41 +++++++++++ 10 files changed, 224 insertions(+), 67 deletions(-) diff --git a/renio/src/assets/chat.less b/renio/src/assets/chat.less index 453e029..906585d 100644 --- a/renio/src/assets/chat.less +++ b/renio/src/assets/chat.less @@ -18,6 +18,15 @@ &:last-child { animation: FlexInAnimationFromBottom 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s 1 normal forwards running; + + .bing { + animation: fadein 0.2s ease-in-out; + + @keyframes fadein { + from { opacity: 0.5; } + to { opacity: 1; } + } + } } .message-content { @@ -99,6 +108,10 @@ } } + .code-inline { + margin: 0 2px; + } + pre, pre div { background: #1a1a1a !important; } @@ -113,3 +126,24 @@ word-break: break-word; } } + +.bing { + display: flex; + flex-direction: row; + align-items: center; + vertical-align: center; + gap: 6px; + color: #2f7eee; + background: #e8f2ff; + border-radius: 12px; + padding: 6px 12px; + font-size: 16px; + margin: 6px 0; + width: max-content; + user-select: none; + + svg { + width: 20px; + height: 20px; + } +} diff --git a/renio/src/assets/home.less b/renio/src/assets/home.less index 6e00b8c..75c0a3a 100644 --- a/renio/src/assets/home.less +++ b/renio/src/assets/home.less @@ -10,6 +10,7 @@ .sidebar { display: flex; + flex-shrink: 0; flex-direction: column; width: 0; height: 100%; @@ -129,20 +130,24 @@ } } - .refresh-action { - margin-left: auto; - margin-right: 6px; + .sidebar-action { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + padding: 0 6px; - &.active { - svg { - animation: RotateAnimation 0.5s linear infinite; + .refresh-action { + &.active { + svg { + animation: RotateAnimation 0.5s linear infinite; - @keyframes RotateAnimation { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); + @keyframes RotateAnimation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } } } diff --git a/renio/src/components/Markdown.tsx b/renio/src/components/Markdown.tsx index e431990..a2db033 100644 --- a/renio/src/components/Markdown.tsx +++ b/renio/src/components/Markdown.tsx @@ -29,11 +29,7 @@ function Markdown({ children, className }: MarkdownProps) { lang={match[1]} /> ) : ( -
-              
- {children} -
-
+ {children} ) }}} /> diff --git a/renio/src/conf.ts b/renio/src/conf.ts index 6140638..7568f5a 100644 --- a/renio/src/conf.ts +++ b/renio/src/conf.ts @@ -1,6 +1,6 @@ import axios from "axios"; -export const deploy: boolean = false; +export const deploy: boolean = true; export let rest_api: string = "http://localhost:8094"; export let ws_api: string = "ws://localhost:8094"; diff --git a/renio/src/conversation/connection.ts b/renio/src/conversation/connection.ts index 957d3d5..c528f96 100644 --- a/renio/src/conversation/connection.ts +++ b/renio/src/conversation/connection.ts @@ -54,7 +54,7 @@ export class Connection { public send(data: Record): boolean { if (!this.state || !this.connection) { - console.debug("[connection] connection not ready"); + console.debug("[connection] connection not ready, retrying in 500ms..."); return false; } this.connection.send(JSON.stringify(data)); diff --git a/renio/src/conversation/conversation.ts b/renio/src/conversation/conversation.ts index 31c05b8..8511024 100644 --- a/renio/src/conversation/conversation.ts +++ b/renio/src/conversation/conversation.ts @@ -25,6 +25,19 @@ export class Conversation { this.end = true; } + public setId(id: number): void { + this.id = id; + } + + public copyMessages(): Message[] { + // deep copy: cannot use return [...this.data]; + return this.data.map((item) => { + return { + ...item, + } + }); + } + public load(data: Message[]): void { this.data = data; this.idx = data.length - 1; @@ -51,7 +64,7 @@ export class Conversation { } public triggerCallback() { - this.callback && this.callback(this.id, this.data); + this.callback && this.callback(this.id, this.copyMessages()); } public addMessage(message: Message): number { @@ -67,7 +80,7 @@ export class Conversation { } public updateMessage(idx: number, message: string, keyword?: string) { - this.data[idx].content = message; + this.data[idx].content += message; if (keyword) this.data[idx].keyword = keyword; this.triggerCallback(); } @@ -105,17 +118,20 @@ export class Conversation { } this.end = false; this.connection.setCallback(this.useMessage()); // hook - this.connection.send(props); + this.connection.sendWithRetry(props); } - public sendMessage(auth: boolean, props: SendMessageProps): void { + public sendMessage(auth: boolean, props: SendMessageProps): boolean { + if (!this.end) return false; this.addMessage({ content: props.message, role: "user", }); - return auth ? + auth ? this.sendAuthenticated(props as AuthenticatedProps) : this.sendAnonymous(props as AnonymousProps); + + return true; } } diff --git a/renio/src/conversation/manager.ts b/renio/src/conversation/manager.ts index ba03644..cc2f100 100644 --- a/renio/src/conversation/manager.ts +++ b/renio/src/conversation/manager.ts @@ -1,13 +1,13 @@ import {Conversation, SendMessageProps} from "./conversation"; import {ConversationMapper, Message} from "./types.ts"; import {loadConversation} from "./history.ts"; -import {useSelector} from "react-redux"; -import {removeHistory, selectGPT4, selectWeb, setCurrent, setMessages} from "../store/chat.ts"; -import {selectAuthenticated} from "../store/auth.ts"; +import {addHistory, removeHistory, setCurrent, setMessages} from "../store/chat.ts"; +import {useShared} from "../utils.ts"; export class Manager { conversations: Record; current: number; + dispatch: any; public constructor() { this.conversations = {}; @@ -15,8 +15,13 @@ export class Manager { this.current = -1; } + public setDispatch(dispatch: any): void { + this.dispatch = dispatch; + } + public callback(idx: number, message: Message[]): void { - console.debug(`[manager] conversation migrated (id: ${idx}, length: ${message.length})`); + console.debug(`[manager] conversation receive message (id: ${idx}, length: ${message.length})`); + this.dispatch(setMessages(message)); } public getCurrent(): number { @@ -29,7 +34,6 @@ export class Manager { public createConversation(id: number): Conversation { console.debug(`[manager] create conversation instance (id: ${id})`); - if (this.conversations[id]) return this.conversations[id]; const _this = this; return new Conversation(id, function (idx: number, message: Message[]) { _this.callback(idx, message); @@ -48,7 +52,7 @@ export class Manager { if (!this.conversations[id]) await this.add(id); this.current = id; dispatch(setCurrent(id)); - dispatch(setMessages(this.get(id)!.data)); + dispatch(setMessages(this.get(id)!.copyMessages())); } public async delete(dispatch: any, id: number): Promise { @@ -57,13 +61,29 @@ export class Manager { if (this.conversations[id]) delete this.conversations[id]; } - public async send(auth: boolean, props: SendMessageProps): Promise { + public async send(auth: boolean, props: SendMessageProps): Promise { const id = this.getCurrent(); - if (!this.conversations[id]) return; - + if (!this.conversations[id]) return false; + console.debug(`[chat] trigger send event: ${props.message} (type: ${auth ? 'user' : 'anonymous'}, id: ${id})`); + if (id === -1 && auth) { + // check for raise conversation + console.debug(`[manager] raise new conversation (name: ${props.message})`); + const { hook, useHook } = useShared(); + this.dispatch(addHistory({ + message: props.message, + hook, + })); + const target = await useHook(); + this.conversations[target] = this.conversations[-1]; + this.get(target)!.setId(target); + delete this.conversations[-1]; // fix pointer + this.conversations[-1] = this.createConversation(-1); + this.current = target; + return this.get(target)!.sendMessage(auth, props); + } const conversation = this.get(id); - if (!conversation) return; - conversation.sendMessage(auth, props); + if (!conversation) return false; + return conversation.sendMessage(auth, props); } public get(id: number): Conversation | undefined { diff --git a/renio/src/routes/Home.tsx b/renio/src/routes/Home.tsx index d7551cd..314da0f 100644 --- a/renio/src/routes/Home.tsx +++ b/renio/src/routes/Home.tsx @@ -2,7 +2,7 @@ import "../assets/home.less"; import "../assets/chat.less"; import {Input} from "../components/ui/input.tsx"; import {Toggle} from "../components/ui/toggle.tsx"; -import {Globe, LogIn, MessageSquare, RotateCw, Trash2} from "lucide-react"; +import {Globe, Loader2, LogIn, MessageSquare, Plus, RotateCw, Trash2} from "lucide-react"; import {Button} from "../components/ui/button.tsx"; import {Switch} from "../components/ui/switch.tsx"; import {Label} from "../components/ui/label.tsx"; @@ -17,15 +17,14 @@ import type {RootState} from "../store"; import {selectAuthenticated} from "../store/auth.ts"; import {login} from "../conf.ts"; import {deleteConversation, toggleConversation, updateConversationList} from "../conversation/history.ts"; -import {useEffect, useRef} from "react"; +import React, {useEffect, useRef} from "react"; import {useAnimation, useEffectAsync} from "../utils.ts"; import {useToast} from "../components/ui/use-toast.ts"; +import {ConversationInstance, Message} from "../conversation/types.ts"; import { - ConversationInstance, - Message, - selectCurrent, + selectCurrent, selectGPT4, selectHistory, - selectMessages, + selectMessages, selectWeb, setGPT4, setWeb } from "../store/chat.ts"; @@ -41,6 +40,7 @@ import { AlertDialogTrigger, } from "../components/ui/alert-dialog.tsx"; import Markdown from "../components/Markdown.tsx"; +import {manager} from "../conversation/manager.ts"; function SideBar() { const dispatch = useDispatch(); @@ -59,19 +59,27 @@ function SideBar() { { auth ?
- +
+ +
+ +
{ history.map((conversation, i) => ( @@ -125,6 +133,16 @@ type ChatWrapperProps = { onSend?: (message: string) => void } +type BingProps = { keyword?: string }; +function BingComponent({ keyword }: BingProps) { + return ( keyword && keyword.length ? +
+ bing + {keyword} +
: null + ) +} + function ChatInterface() { const ref = useRef(null); const messages: Message[] = useSelector(selectMessages); @@ -142,7 +160,12 @@ function ChatInterface() { messages.map((message, i) => (
- + + { + message.content.length ? + + : + }
)) @@ -153,22 +176,30 @@ function ChatInterface() { function ChatWrapper({ onSend }: ChatWrapperProps) { const dispatch = useDispatch(); + const auth = useSelector(selectAuthenticated); + const gpt4 = useSelector(selectGPT4); + const web = useSelector(selectWeb); const target = useRef(null); + manager.setDispatch(dispatch); - function handleSend() { + async function handleSend(auth: boolean, gpt4: boolean, web: boolean) { + // because of the function wrapper, we need to update the selector state using props. if (!target.current) return; const el = target.current as HTMLInputElement; - const message = el.value.trim(); + const message: string = el.value.trim(); if (message.length > 0) { - onSend?.(message); - el.value = ""; + if (await manager.send(auth, { + message, web, gpt4, + })) { + onSend?.(message); + el.value = ""; + } } } - window.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - handleSend(); - } + window.addEventListener("load", () => { + const el = document.getElementById("input"); + if (el) el.focus(); }); return ( @@ -191,8 +222,14 @@ function ChatWrapper({ onSend }: ChatWrapperProps) { - -
@@ -212,7 +249,7 @@ function Home() { return (
- +
) } diff --git a/renio/src/store/chat.ts b/renio/src/store/chat.ts index b8d2cc2..faf89bd 100644 --- a/renio/src/store/chat.ts +++ b/renio/src/store/chat.ts @@ -1,6 +1,7 @@ import {createSlice} from "@reduxjs/toolkit"; import {ConversationInstance} from "../conversation/types.ts"; import {Message} from "postcss"; +import {insertStart} from "../utils.ts"; type initialStateType = { history: ConversationInstance[]; @@ -26,6 +27,13 @@ const chatSlice = createSlice({ removeHistory: (state, action) => { state.history = state.history.filter((item) => item.id !== (action.payload as number)); }, + addHistory: (state, action) => { + const name = action.payload.message as string; + const id = Math.max(...state.history.map((item) => item.id)) + 1; + state.history = insertStart(state.history, {id, name, message: []}); + state.current = id; + action.payload.hook(id); + }, setMessages: (state, action) => { state.messages = action.payload as Message[]; }, @@ -47,7 +55,7 @@ const chatSlice = createSlice({ } }); -export const {setHistory, removeHistory, setCurrent, setMessages, setGPT4, setWeb, addMessage, setMessage} = chatSlice.actions; +export const {setHistory, removeHistory, addHistory, setCurrent, setMessages, setGPT4, setWeb, addMessage, setMessage} = chatSlice.actions; export const selectHistory = (state: any) => state.chat.history; export const selectMessages = (state: any) => state.chat.messages; export const selectGPT4 = (state: any) => state.chat.gpt4; diff --git a/renio/src/utils.ts b/renio/src/utils.ts index 09fdf3c..68021a1 100644 --- a/renio/src/utils.ts +++ b/renio/src/utils.ts @@ -22,6 +22,47 @@ export function useAnimation(ref: React.MutableRefObject, cls: string, min? } } +export function useShared(): { hook: (v: T) => void, useHook: () => Promise } { + let value: T | undefined = undefined; + return { + hook: (v: T) => { + value = v; + }, + useHook: () => { + return new Promise((resolve) => { + if (value) return resolve(value); + const interval = setInterval(() => { + if (value) { + clearInterval(interval); + resolve(value); + } + }, 50); + }); + } + } +} + +export function insert(arr: T[], idx: number, value: T): T[] { + return [...arr.slice(0, idx), value, ...arr.slice(idx)]; +} + +export function insertStart(arr: T[], value: T): T[] { + return [value, ...arr]; +} + +export function remove(arr: T[], idx: number): T[] { + return [...arr.slice(0, idx), ...arr.slice(idx + 1)]; +} + +export function replace(arr: T[], idx: number, value: T): T[] { + return [...arr.slice(0, idx), value, ...arr.slice(idx + 1)]; +} + +export function move(arr: T[], from: number, to: number): T[] { + const value = arr[from]; + return insert(remove(arr, from), to, value); +} + window.addEventListener("resize", () => { mobile = (window.innerWidth <= 468 || window.innerHeight <= 468 || navigator.userAgent.includes("Mobile")); });