From 9eb021a52e40a429be49e4735ab477db5dd88c39 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Thu, 19 Oct 2023 21:41:41 +0800 Subject: [PATCH] update sharing using feature --- adapter/chatgpt/chat.go | 3 + .../components/home/ConversationSegment.tsx | 18 ++- app/src/conf.ts | 4 +- app/src/conversation/conversation.ts | 10 ++ app/src/conversation/manager.ts | 12 ++ app/src/conversation/sharing.ts | 15 ++- app/src/events/sharing.ts | 12 ++ app/src/events/struct.ts | 51 ++++++++ app/src/i18n.ts | 21 ++-- app/src/router.ts | 2 +- app/src/routes/Home.tsx | 61 ++++++---- app/src/routes/Sharing.tsx | 115 ++++++++++-------- app/src/store/api.ts | 6 +- main.go | 3 - 14 files changed, 227 insertions(+), 106 deletions(-) create mode 100644 app/src/events/sharing.ts create mode 100644 app/src/events/struct.ts diff --git a/adapter/chatgpt/chat.go b/adapter/chatgpt/chat.go index 7b31d70c..46ddcf9b 100644 --- a/adapter/chatgpt/chat.go +++ b/adapter/chatgpt/chat.go @@ -115,6 +115,9 @@ func (c *ChatInstance) Test() bool { Message: []globals.Message{{Role: "user", Content: "hi"}}, Token: 1, }) + if err != nil { + fmt.Println(fmt.Sprintf("%s: test failed (%s)", c.GetApiKey(), err.Error())) + } return err == nil && len(result) > 0 } diff --git a/app/src/components/home/ConversationSegment.tsx b/app/src/components/home/ConversationSegment.tsx index 9ee053d4..e93d65c8 100644 --- a/app/src/components/home/ConversationSegment.tsx +++ b/app/src/components/home/ConversationSegment.tsx @@ -1,7 +1,7 @@ import { toggleConversation } from "../../conversation/history.ts"; import { filterMessage, mobile } from "../../utils.ts"; import { setMenu } from "../../store/menu.ts"; -import {MessageSquare, MoreHorizontal, Share2, Trash2} from "lucide-react"; +import { MessageSquare, MoreHorizontal, Share2, Trash2 } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, @@ -16,7 +16,10 @@ import { useState } from "react"; type ConversationSegmentProps = { conversation: ConversationInstance; current: number; - operate: (conversation: { target: ConversationInstance, type: string }) => void; + operate: (conversation: { + target: ConversationInstance; + type: string; + }) => void; }; function ConversationSegment({ conversation, @@ -45,10 +48,13 @@ function ConversationSegment({
{filterMessage(conversation.name)}
{conversation.id}
- { - setOpen(state); - if (state) setOffset(new Date().getTime()); - }}> + { + setOpen(state); + if (state) setOffset(new Date().getTime()); + }} + > diff --git a/app/src/conf.ts b/app/src/conf.ts index cdf4fa3c..3c940cde 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -1,7 +1,7 @@ import axios from "axios"; -export const version = "3.4.2"; -export const deploy: boolean = false; +export const version = "3.4.3"; +export const deploy: boolean = true; export let rest_api: string = "http://localhost:8094"; export let ws_api: string = "ws://localhost:8094"; diff --git a/app/src/conversation/conversation.ts b/app/src/conversation/conversation.ts index a089726a..696e0ef3 100644 --- a/app/src/conversation/conversation.ts +++ b/app/src/conversation/conversation.ts @@ -1,5 +1,6 @@ import { ChatProps, Connection, StreamMessage } from "./connection.ts"; import { Message } from "./types.ts"; +import { event } from "../events/sharing.ts"; type ConversationCallback = (idx: number, message: Message[]) => void; @@ -18,6 +19,15 @@ export class Conversation { this.id = id; this.end = true; this.connection = new Connection(this.id); + + if (id === -1 && this.idx === -1) { + event.bind(({ refer, data }) => { + console.log( + `[conversation] load from sharing event (ref: ${refer}, length: ${data.length})`, + ); + this.load(data); + }); + } } public setId(id: number): void { diff --git a/app/src/conversation/manager.ts b/app/src/conversation/manager.ts index a7d9a51f..047271b8 100644 --- a/app/src/conversation/manager.ts +++ b/app/src/conversation/manager.ts @@ -11,6 +11,7 @@ import { useShared } from "../utils.ts"; import { ChatProps } from "./connection.ts"; import { supportModelConvertor } from "../conf.ts"; import { AppDispatch } from "../store"; +import { event } from "../events/sharing.ts"; export class Manager { conversations: Record; @@ -21,6 +22,17 @@ export class Manager { this.conversations = {}; this.conversations[-1] = this.createConversation(-1); this.current = -1; + + event.addEventListener(async (data) => { + console.debug(`[manager] accept sharing event (refer: ${data.refer})`); + + const interval = setInterval(() => { + if (this.dispatch) { + this.toggle(this.dispatch, -1); + clearInterval(interval); + } + }, 100); + }); } public setDispatch(dispatch: AppDispatch): void { diff --git a/app/src/conversation/sharing.ts b/app/src/conversation/sharing.ts index 559a27ea..d8e1d7a1 100644 --- a/app/src/conversation/sharing.ts +++ b/app/src/conversation/sharing.ts @@ -1,11 +1,11 @@ import axios from "axios"; -import {Message} from "./types.ts"; +import { Message } from "./types.ts"; export type SharingForm = { status: boolean; message: string; data: string; -} +}; export type ViewData = { name: string; @@ -18,10 +18,11 @@ export type ViewForm = { status: boolean; message: string; data: ViewData | null; -} +}; export async function shareConversation( - id: number, refs: number[] = [-1], + id: number, + refs: number[] = [-1], ): Promise { try { const resp = await axios.post("/conversation/share", { id, refs }); @@ -31,9 +32,7 @@ export async function shareConversation( } } -export async function viewConversation( - hash: string, -): Promise { +export async function viewConversation(hash: string): Promise { try { const resp = await axios.get(`/conversation/view?hash=${hash}`); return resp.data as ViewForm; @@ -42,6 +41,6 @@ export async function viewConversation( status: false, message: (e as Error).message, data: null, - } + }; } } diff --git a/app/src/events/sharing.ts b/app/src/events/sharing.ts new file mode 100644 index 00000000..3a7aca53 --- /dev/null +++ b/app/src/events/sharing.ts @@ -0,0 +1,12 @@ +import { EventCommitter } from "./struct.ts"; +import { Message } from "../conversation/types.ts"; + +export type SharingEvent = { + refer: string; + data: Message[]; +}; + +export const event = new EventCommitter({ + name: "sharing", + destroyedAfterTrigger: true, +}); diff --git a/app/src/events/struct.ts b/app/src/events/struct.ts new file mode 100644 index 00000000..81a0c836 --- /dev/null +++ b/app/src/events/struct.ts @@ -0,0 +1,51 @@ +export type EventCommitterProps = { + name: string; + destroyedAfterTrigger?: boolean; +}; + +export class EventCommitter { + name: string; + trigger: ((data: T) => void) | undefined; + listeners: ((data: T) => void)[] = []; + destroyedAfterTrigger: boolean; + + constructor({ name, destroyedAfterTrigger = false }: EventCommitterProps) { + this.name = name; + this.destroyedAfterTrigger = destroyedAfterTrigger; + } + + protected setTrigger(trigger: (data: T) => void) { + this.trigger = trigger; + } + + protected clearTrigger() { + this.trigger = undefined; + } + + protected triggerEvent(data: T) { + this.trigger && this.trigger(data); + if (this.destroyedAfterTrigger) this.clearTrigger(); + + this.listeners.forEach((listener) => listener(data)); + } + + public emit(data: T) { + this.triggerEvent(data); + } + + public bind(trigger: (data: T) => void) { + this.setTrigger(trigger); + } + + public addEventListener(listener: (data: T) => void) { + this.listeners.push(listener); + } + + public removeEventListener(listener: (data: T) => void) { + this.listeners = this.listeners.filter((item) => item !== listener); + } + + public clearEventListener() { + this.listeners = []; + } +} diff --git a/app/src/i18n.ts b/app/src/i18n.ts index a795ab63..511cf518 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -178,14 +178,15 @@ const resources = { "share-conversation": "Share Conversation", description: "Share this conversation with others: ", "copy-link": "Copy Link", - "view": "View", + view: "View", success: "Share success", failed: "Share failed", copied: "Copied", "copied-description": "Link has been copied to clipboard", "not-found": "Conversation not found", - "not-found-description": "Conversation not found, please check if the link is correct or the conversation has been deleted", - } + "not-found-description": + "Conversation not found, please check if the link is correct or the conversation has been deleted", + }, }, }, cn: { @@ -350,14 +351,15 @@ const resources = { "share-conversation": "分享对话", description: "将此对话与他人分享:", "copy-link": "复制链接", - "view": "查看", + view: "查看", success: "分享成功", failed: "分享失败", copied: "复制成功", "copied-description": "链接已复制到剪贴板", "not-found": "对话未找到", - "not-found-description": "对话未找到,请检查链接是否正确或对话是否已被删除", - } + "not-found-description": + "对话未找到,请检查链接是否正确或对话是否已被删除", + }, }, }, ru: { @@ -533,14 +535,15 @@ const resources = { "share-conversation": "Поделиться разговором", description: "Поделитесь этим разговором с другими: ", "copy-link": "Скопировать ссылку", - "view": "Посмотреть", + view: "Посмотреть", success: "Поделиться успешно", failed: "Поделиться не удалось", copied: "Скопировано", "copied-description": "Ссылка скопирована в буфер обмена", "not-found": "Разговор не найден", - "not-found-description": "Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален", - } + "not-found-description": + "Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален", + }, }, }, }; diff --git a/app/src/router.ts b/app/src/router.ts index d440c547..dd3a58bb 100644 --- a/app/src/router.ts +++ b/app/src/router.ts @@ -27,7 +27,7 @@ const router = createBrowserRouter([ id: "share", path: "/share/:hash", Component: Sharing, - } + }, ]); export default router; diff --git a/app/src/routes/Home.tsx b/app/src/routes/Home.tsx index 3d9df5bb..c2e23e17 100644 --- a/app/src/routes/Home.tsx +++ b/app/src/routes/Home.tsx @@ -4,7 +4,8 @@ import { Input } from "../components/ui/input.tsx"; import { Toggle } from "../components/ui/toggle.tsx"; import { ChevronDown, - ChevronRight, Copy, + ChevronRight, + Copy, FolderKanban, Globe, LogIn, @@ -35,7 +36,8 @@ import { formatMessage, mobile, useAnimation, - useEffectAsync, copyClipboard, + useEffectAsync, + copyClipboard, } from "../utils.ts"; import { toast, useToast } from "../components/ui/use-toast.ts"; import { ConversationInstance, Message } from "../conversation/types.ts"; @@ -74,11 +76,10 @@ function SideBar() { const open = useSelector((state: RootState) => state.menu.open); const auth = useSelector(selectAuthenticated); const current = useSelector(selectCurrent); - const [operateConversation, setOperateConversation] = - useState<{ - target: ConversationInstance | null; - type: string; - }>({ target: null, type: "" }); + const [operateConversation, setOperateConversation] = useState<{ + target: ConversationInstance | null; + type: string; + }>({ target: null, type: "" }); const { toast } = useToast(); const history: ConversationInstance[] = useSelector(selectHistory); const refresh = useRef(null); @@ -140,7 +141,10 @@ function SideBar() { )} { if (!open) setOperateConversation({ target: null, type: "" }); }} @@ -194,16 +198,17 @@ function SideBar() { { if (!open) setOperateConversation({ target: null, type: "" }); }} > - - {t("share.title")} - + {t("share.title")} {t("share.description")} @@ -223,9 +228,13 @@ function SideBar() { e.preventDefault(); e.stopPropagation(); - const resp = await shareConversation(operateConversation?.target?.id || -1); - if (resp.status) setShared(`${location.origin}/share/${resp.data}`); - else toast({ + const resp = await shareConversation( + operateConversation?.target?.id || -1, + ); + if (resp.status) + setShared(`${location.origin}/share/${resp.data}`); + else + toast({ title: t("share.failed"), description: resp.message, }); @@ -250,19 +259,21 @@ function SideBar() { > - - {t("share.success")} - + {t("share.success")}
-
diff --git a/app/src/routes/Sharing.tsx b/app/src/routes/Sharing.tsx index 1192babc..a666dd0a 100644 --- a/app/src/routes/Sharing.tsx +++ b/app/src/routes/Sharing.tsx @@ -1,25 +1,33 @@ import "../assets/sharing.less"; -import {useParams} from "react-router-dom"; -import {viewConversation, ViewData, ViewForm} from "../conversation/sharing.ts"; -import {copyClipboard, saveAsFile, useEffectAsync} from "../utils.ts"; -import {useState} from "react"; -import {Copy, File, HelpCircle, Loader2, MessagesSquare} from "lucide-react"; -import {useTranslation} from "react-i18next"; +import { useParams } from "react-router-dom"; +import { + viewConversation, + ViewData, + ViewForm, +} from "../conversation/sharing.ts"; +import { copyClipboard, saveAsFile, useEffectAsync } from "../utils.ts"; +import { useState } from "react"; +import { Copy, File, HelpCircle, Loader2, MessagesSquare } from "lucide-react"; +import { useTranslation } from "react-i18next"; import MessageSegment from "../components/Message.tsx"; -import {Button} from "../components/ui/button.tsx"; +import { Button } from "../components/ui/button.tsx"; import router from "../router.ts"; -import {useToast} from "../components/ui/use-toast.ts"; +import { useToast } from "../components/ui/use-toast.ts"; +import { event } from "../events/sharing.ts"; +import { Message } from "../conversation/types.ts"; type SharingFormProps = { refer?: string; data: ViewData | null; -} +}; function SharingForm({ refer, data }: SharingFormProps) { if (data === null) return null; const { t } = useTranslation(); const date = new Date(data.time); - const time = `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`; + const time = `${ + date.getMonth() + 1 + }-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`; const value = JSON.stringify(data, null, 2); const { toast } = useToast(); @@ -27,43 +35,56 @@ function SharingForm({ refer, data }: SharingFormProps) {
- + {data.username}
{data.name}
{time}
- { - data.messages.map((message, i) => ( - - )) - } + {data.messages.map((message, i) => ( + + ))}
- - -
- ) + ); } function Sharing() { @@ -84,25 +105,21 @@ function Sharing() { return (
- { - data === null ? ( -
- -
- ) : ( - data.status ? ( - - ) : ( -
- -

{t('share.not-found')}

-

{t('share.not-found-description')}

-
- ) - ) - } + {data === null ? ( +
+ +
+ ) : data.status ? ( + + ) : ( +
+ +

{t("share.not-found")}

+

{t("share.not-found-description")}

+
+ )}
- ) + ); } export default Sharing; diff --git a/app/src/store/api.ts b/app/src/store/api.ts index 2f1d2d45..5ad7fd82 100644 --- a/app/src/store/api.ts +++ b/app/src/store/api.ts @@ -1,6 +1,6 @@ import { createSlice } from "@reduxjs/toolkit"; import { getKey } from "../conversation/addition.ts"; -import { AppDispatch } from "./index.ts"; +import { AppDispatch, RootState } from "./index.ts"; export const apiSlice = createSlice({ name: "api", @@ -31,8 +31,8 @@ export const { toggleDialog, setDialog, openDialog, closeDialog, setKey } = apiSlice.actions; export default apiSlice.reducer; -export const dialogSelector = (state: any): boolean => state.api.dialog; -export const keySelector = (state: any): string => state.api.key; +export const dialogSelector = (state: RootState): boolean => state.api.dialog; +export const keySelector = (state: RootState): string => state.api.key; export const getApiKey = async (dispatch: AppDispatch) => { const response = await getKey(); diff --git a/main.go b/main.go index e6f315df..8aa75050 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "chat/adapter/chatgpt" "chat/addition" "chat/auth" "chat/manager" @@ -22,8 +21,6 @@ func main() { app := gin.Default() middleware.RegisterMiddleware(app) - fmt.Println(chatgpt.FilterKeys("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|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|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|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|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|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|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|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|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|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|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")) - return { auth.Register(app) manager.Register(app)