update sharing using feature

This commit is contained in:
Zhang Minghan 2023-10-19 21:41:41 +08:00
parent 0f345fbfa4
commit 9eb021a52e
14 changed files with 227 additions and 106 deletions

View File

@ -115,6 +115,9 @@ func (c *ChatInstance) Test() bool {
Message: []globals.Message{{Role: "user", Content: "hi"}}, Message: []globals.Message{{Role: "user", Content: "hi"}},
Token: 1, Token: 1,
}) })
if err != nil {
fmt.Println(fmt.Sprintf("%s: test failed (%s)", c.GetApiKey(), err.Error()))
}
return err == nil && len(result) > 0 return err == nil && len(result) > 0
} }

View File

@ -1,7 +1,7 @@
import { toggleConversation } from "../../conversation/history.ts"; import { toggleConversation } from "../../conversation/history.ts";
import { filterMessage, mobile } from "../../utils.ts"; import { filterMessage, mobile } from "../../utils.ts";
import { setMenu } from "../../store/menu.ts"; import { setMenu } from "../../store/menu.ts";
import {MessageSquare, MoreHorizontal, Share2, Trash2} from "lucide-react"; import { MessageSquare, MoreHorizontal, Share2, Trash2 } from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -16,7 +16,10 @@ import { useState } from "react";
type ConversationSegmentProps = { type ConversationSegmentProps = {
conversation: ConversationInstance; conversation: ConversationInstance;
current: number; current: number;
operate: (conversation: { target: ConversationInstance, type: string }) => void; operate: (conversation: {
target: ConversationInstance;
type: string;
}) => void;
}; };
function ConversationSegment({ function ConversationSegment({
conversation, conversation,
@ -45,10 +48,13 @@ function ConversationSegment({
<MessageSquare className={`h-4 w-4 mr-1`} /> <MessageSquare className={`h-4 w-4 mr-1`} />
<div className={`title`}>{filterMessage(conversation.name)}</div> <div className={`title`}>{filterMessage(conversation.name)}</div>
<div className={`id`}>{conversation.id}</div> <div className={`id`}>{conversation.id}</div>
<DropdownMenu open={open} onOpenChange={(state: boolean) => { <DropdownMenu
open={open}
onOpenChange={(state: boolean) => {
setOpen(state); setOpen(state);
if (state) setOffset(new Date().getTime()); if (state) setOffset(new Date().getTime());
}}> }}
>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<MoreHorizontal className={`more h-5 w-5 p-0.5`} /> <MoreHorizontal className={`more h-5 w-5 p-0.5`} />
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
export const version = "3.4.2"; export const version = "3.4.3";
export const deploy: boolean = false; export const deploy: boolean = true;
export let rest_api: string = "http://localhost:8094"; export let rest_api: string = "http://localhost:8094";
export let ws_api: string = "ws://localhost:8094"; export let ws_api: string = "ws://localhost:8094";

View File

@ -1,5 +1,6 @@
import { ChatProps, Connection, StreamMessage } from "./connection.ts"; import { ChatProps, Connection, StreamMessage } from "./connection.ts";
import { Message } from "./types.ts"; import { Message } from "./types.ts";
import { event } from "../events/sharing.ts";
type ConversationCallback = (idx: number, message: Message[]) => void; type ConversationCallback = (idx: number, message: Message[]) => void;
@ -18,6 +19,15 @@ export class Conversation {
this.id = id; this.id = id;
this.end = true; this.end = true;
this.connection = new Connection(this.id); 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 { public setId(id: number): void {

View File

@ -11,6 +11,7 @@ import { useShared } from "../utils.ts";
import { ChatProps } from "./connection.ts"; import { ChatProps } from "./connection.ts";
import { supportModelConvertor } from "../conf.ts"; import { supportModelConvertor } from "../conf.ts";
import { AppDispatch } from "../store"; import { AppDispatch } from "../store";
import { event } from "../events/sharing.ts";
export class Manager { export class Manager {
conversations: Record<number, Conversation>; conversations: Record<number, Conversation>;
@ -21,6 +22,17 @@ export class Manager {
this.conversations = {}; this.conversations = {};
this.conversations[-1] = this.createConversation(-1); this.conversations[-1] = this.createConversation(-1);
this.current = -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 { public setDispatch(dispatch: AppDispatch): void {

View File

@ -1,11 +1,11 @@
import axios from "axios"; import axios from "axios";
import {Message} from "./types.ts"; import { Message } from "./types.ts";
export type SharingForm = { export type SharingForm = {
status: boolean; status: boolean;
message: string; message: string;
data: string; data: string;
} };
export type ViewData = { export type ViewData = {
name: string; name: string;
@ -18,10 +18,11 @@ export type ViewForm = {
status: boolean; status: boolean;
message: string; message: string;
data: ViewData | null; data: ViewData | null;
} };
export async function shareConversation( export async function shareConversation(
id: number, refs: number[] = [-1], id: number,
refs: number[] = [-1],
): Promise<SharingForm> { ): Promise<SharingForm> {
try { try {
const resp = await axios.post("/conversation/share", { id, refs }); const resp = await axios.post("/conversation/share", { id, refs });
@ -31,9 +32,7 @@ export async function shareConversation(
} }
} }
export async function viewConversation( export async function viewConversation(hash: string): Promise<ViewForm> {
hash: string,
): Promise<ViewForm> {
try { try {
const resp = await axios.get(`/conversation/view?hash=${hash}`); const resp = await axios.get(`/conversation/view?hash=${hash}`);
return resp.data as ViewForm; return resp.data as ViewForm;
@ -42,6 +41,6 @@ export async function viewConversation(
status: false, status: false,
message: (e as Error).message, message: (e as Error).message,
data: null, data: null,
} };
} }
} }

12
app/src/events/sharing.ts Normal file
View File

@ -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<SharingEvent>({
name: "sharing",
destroyedAfterTrigger: true,
});

51
app/src/events/struct.ts Normal file
View File

@ -0,0 +1,51 @@
export type EventCommitterProps = {
name: string;
destroyedAfterTrigger?: boolean;
};
export class EventCommitter<T> {
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 = [];
}
}

View File

@ -178,14 +178,15 @@ const resources = {
"share-conversation": "Share Conversation", "share-conversation": "Share Conversation",
description: "Share this conversation with others: ", description: "Share this conversation with others: ",
"copy-link": "Copy Link", "copy-link": "Copy Link",
"view": "View", view: "View",
success: "Share success", success: "Share success",
failed: "Share failed", failed: "Share failed",
copied: "Copied", copied: "Copied",
"copied-description": "Link has been copied to clipboard", "copied-description": "Link has been copied to clipboard",
"not-found": "Conversation not found", "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: { cn: {
@ -350,14 +351,15 @@ const resources = {
"share-conversation": "分享对话", "share-conversation": "分享对话",
description: "将此对话与他人分享:", description: "将此对话与他人分享:",
"copy-link": "复制链接", "copy-link": "复制链接",
"view": "查看", view: "查看",
success: "分享成功", success: "分享成功",
failed: "分享失败", failed: "分享失败",
copied: "复制成功", copied: "复制成功",
"copied-description": "链接已复制到剪贴板", "copied-description": "链接已复制到剪贴板",
"not-found": "对话未找到", "not-found": "对话未找到",
"not-found-description": "对话未找到,请检查链接是否正确或对话是否已被删除", "not-found-description":
} "对话未找到,请检查链接是否正确或对话是否已被删除",
},
}, },
}, },
ru: { ru: {
@ -533,14 +535,15 @@ const resources = {
"share-conversation": "Поделиться разговором", "share-conversation": "Поделиться разговором",
description: "Поделитесь этим разговором с другими: ", description: "Поделитесь этим разговором с другими: ",
"copy-link": "Скопировать ссылку", "copy-link": "Скопировать ссылку",
"view": "Посмотреть", view: "Посмотреть",
success: "Поделиться успешно", success: "Поделиться успешно",
failed: "Поделиться не удалось", failed: "Поделиться не удалось",
copied: "Скопировано", copied: "Скопировано",
"copied-description": "Ссылка скопирована в буфер обмена", "copied-description": "Ссылка скопирована в буфер обмена",
"not-found": "Разговор не найден", "not-found": "Разговор не найден",
"not-found-description": "Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален", "not-found-description":
} "Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален",
},
}, },
}, },
}; };

View File

@ -27,7 +27,7 @@ const router = createBrowserRouter([
id: "share", id: "share",
path: "/share/:hash", path: "/share/:hash",
Component: Sharing, Component: Sharing,
} },
]); ]);
export default router; export default router;

View File

@ -4,7 +4,8 @@ import { Input } from "../components/ui/input.tsx";
import { Toggle } from "../components/ui/toggle.tsx"; import { Toggle } from "../components/ui/toggle.tsx";
import { import {
ChevronDown, ChevronDown,
ChevronRight, Copy, ChevronRight,
Copy,
FolderKanban, FolderKanban,
Globe, Globe,
LogIn, LogIn,
@ -35,7 +36,8 @@ import {
formatMessage, formatMessage,
mobile, mobile,
useAnimation, useAnimation,
useEffectAsync, copyClipboard, useEffectAsync,
copyClipboard,
} from "../utils.ts"; } from "../utils.ts";
import { toast, useToast } from "../components/ui/use-toast.ts"; import { toast, useToast } from "../components/ui/use-toast.ts";
import { ConversationInstance, Message } from "../conversation/types.ts"; import { ConversationInstance, Message } from "../conversation/types.ts";
@ -74,8 +76,7 @@ function SideBar() {
const open = useSelector((state: RootState) => state.menu.open); const open = useSelector((state: RootState) => state.menu.open);
const auth = useSelector(selectAuthenticated); const auth = useSelector(selectAuthenticated);
const current = useSelector(selectCurrent); const current = useSelector(selectCurrent);
const [operateConversation, setOperateConversation] = const [operateConversation, setOperateConversation] = useState<{
useState<{
target: ConversationInstance | null; target: ConversationInstance | null;
type: string; type: string;
}>({ target: null, type: "" }); }>({ target: null, type: "" });
@ -140,7 +141,10 @@ function SideBar() {
)} )}
</div> </div>
<AlertDialog <AlertDialog
open={operateConversation.type === "delete" && !!operateConversation.target} open={
operateConversation.type === "delete" &&
!!operateConversation.target
}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" }); if (!open) setOperateConversation({ target: null, type: "" });
}} }}
@ -194,16 +198,17 @@ function SideBar() {
</AlertDialog> </AlertDialog>
<AlertDialog <AlertDialog
open={operateConversation.type === "share" && !!operateConversation.target} open={
operateConversation.type === "share" &&
!!operateConversation.target
}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" }); if (!open) setOperateConversation({ target: null, type: "" });
}} }}
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>{t("share.title")}</AlertDialogTitle>
{t("share.title")}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t("share.description")} {t("share.description")}
<strong className={`conversation-name`}> <strong className={`conversation-name`}>
@ -223,9 +228,13 @@ function SideBar() {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const resp = await shareConversation(operateConversation?.target?.id || -1); const resp = await shareConversation(
if (resp.status) setShared(`${location.origin}/share/${resp.data}`); operateConversation?.target?.id || -1,
else toast({ );
if (resp.status)
setShared(`${location.origin}/share/${resp.data}`);
else
toast({
title: t("share.failed"), title: t("share.failed"),
description: resp.message, description: resp.message,
}); });
@ -250,19 +259,21 @@ function SideBar() {
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>{t("share.success")}</AlertDialogTitle>
{t("share.success")}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<div className={`share-wrapper mt-4 mb-2`}> <div className={`share-wrapper mt-4 mb-2`}>
<Input value={shared} /> <Input value={shared} />
<Button variant={`default`} size={`icon`} onClick={async () => { <Button
variant={`default`}
size={`icon`}
onClick={async () => {
await copyClipboard(shared); await copyClipboard(shared);
toast({ toast({
title: t("share.copied"), title: t("share.copied"),
description: t("share.copied-description"), description: t("share.copied-description"),
}); });
}}> }}
>
<Copy className={`h-4 w-4`} /> <Copy className={`h-4 w-4`} />
</Button> </Button>
</div> </div>

View File

@ -1,25 +1,33 @@
import "../assets/sharing.less"; import "../assets/sharing.less";
import {useParams} from "react-router-dom"; import { useParams } from "react-router-dom";
import {viewConversation, ViewData, ViewForm} from "../conversation/sharing.ts"; import {
import {copyClipboard, saveAsFile, useEffectAsync} from "../utils.ts"; viewConversation,
import {useState} from "react"; ViewData,
import {Copy, File, HelpCircle, Loader2, MessagesSquare} from "lucide-react"; ViewForm,
import {useTranslation} from "react-i18next"; } 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 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 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 = { type SharingFormProps = {
refer?: string; refer?: string;
data: ViewData | null; data: ViewData | null;
} };
function SharingForm({ refer, data }: SharingFormProps) { function SharingForm({ refer, data }: SharingFormProps) {
if (data === null) return null; if (data === null) return null;
const { t } = useTranslation(); const { t } = useTranslation();
const date = new Date(data.time); 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 value = JSON.stringify(data, null, 2);
const { toast } = useToast(); const { toast } = useToast();
@ -27,43 +35,56 @@ function SharingForm({ refer, data }: SharingFormProps) {
<div className={`sharing-container`}> <div className={`sharing-container`}>
<div className={`header`}> <div className={`header`}>
<div className={`user`}> <div className={`user`}>
<img src={`https://api.deeptrain.net/avatar/${data.username}`} alt="" /> <img
src={`https://api.deeptrain.net/avatar/${data.username}`}
alt=""
/>
<span>{data.username}</span> <span>{data.username}</span>
</div> </div>
<div className={`name`}>{data.name}</div> <div className={`name`}>{data.name}</div>
<div className={`time`}>{time}</div> <div className={`time`}>{time}</div>
</div> </div>
<div className={`body`}> <div className={`body`}>
{ {data.messages.map((message, i) => (
data.messages.map((message, i) => (
<MessageSegment message={message} key={i} /> <MessageSegment message={message} key={i} />
)) ))}
}
</div> </div>
<div className={`action`}> <div className={`action`}>
<Button variant={`outline`} onClick={async () => { <Button
variant={`outline`}
onClick={async () => {
await copyClipboard(value); await copyClipboard(value);
toast({ toast({
title: t('share.copied'), title: t("share.copied"),
}); });
}}> }}
>
<Copy className={`h-4 w-4 mr-2`} /> <Copy className={`h-4 w-4 mr-2`} />
{t('message.copy')} {t("message.copy")}
</Button> </Button>
<Button variant={`outline`} onClick={() => saveAsFile("conversation.json", value)}> <Button
variant={`outline`}
onClick={() => saveAsFile("conversation.json", value)}
>
<File className={`h-4 w-4 mr-2`} /> <File className={`h-4 w-4 mr-2`} />
{t('message.save')} {t("message.save")}
</Button> </Button>
<Button variant={`outline`} onClick={async () => { <Button
refer && sessionStorage.setItem('refer', refer); variant={`outline`}
await router.navigate('/'); onClick={async () => {
}}> event.emit({
refer: refer as string,
data: data?.messages as Message[],
});
await router.navigate("/");
}}
>
<MessagesSquare className={`h-4 w-4 mr-2`} /> <MessagesSquare className={`h-4 w-4 mr-2`} />
{t('message.use')} {t("message.use")}
</Button> </Button>
</div> </div>
</div> </div>
) );
} }
function Sharing() { function Sharing() {
@ -84,25 +105,21 @@ function Sharing() {
return ( return (
<div className={`sharing-page`}> <div className={`sharing-page`}>
{ {data === null ? (
data === null ? (
<div className={`loading`}> <div className={`loading`}>
<Loader2 className={`loader w-12 h-12`} /> <Loader2 className={`loader w-12 h-12`} />
</div> </div>
) : ( ) : data.status ? (
data.status ? (
<SharingForm refer={hash} data={data.data} /> <SharingForm refer={hash} data={data.data} />
) : ( ) : (
<div className={`error-container`}> <div className={`error-container`}>
<HelpCircle className={`w-12 h-12 mb-2.5`} /> <HelpCircle className={`w-12 h-12 mb-2.5`} />
<p className={`title`}>{t('share.not-found')}</p> <p className={`title`}>{t("share.not-found")}</p>
<p className={`description`}>{t('share.not-found-description')}</p> <p className={`description`}>{t("share.not-found-description")}</p>
</div> </div>
) )}
)
}
</div> </div>
) );
} }
export default Sharing; export default Sharing;

View File

@ -1,6 +1,6 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { getKey } from "../conversation/addition.ts"; import { getKey } from "../conversation/addition.ts";
import { AppDispatch } from "./index.ts"; import { AppDispatch, RootState } from "./index.ts";
export const apiSlice = createSlice({ export const apiSlice = createSlice({
name: "api", name: "api",
@ -31,8 +31,8 @@ export const { toggleDialog, setDialog, openDialog, closeDialog, setKey } =
apiSlice.actions; apiSlice.actions;
export default apiSlice.reducer; export default apiSlice.reducer;
export const dialogSelector = (state: any): boolean => state.api.dialog; export const dialogSelector = (state: RootState): boolean => state.api.dialog;
export const keySelector = (state: any): string => state.api.key; export const keySelector = (state: RootState): string => state.api.key;
export const getApiKey = async (dispatch: AppDispatch) => { export const getApiKey = async (dispatch: AppDispatch) => {
const response = await getKey(); const response = await getKey();

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"chat/adapter/chatgpt"
"chat/addition" "chat/addition"
"chat/auth" "chat/auth"
"chat/manager" "chat/manager"
@ -22,8 +21,6 @@ func main() {
app := gin.Default() app := gin.Default()
middleware.RegisterMiddleware(app) 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) auth.Register(app)
manager.Register(app) manager.Register(app)