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"}},
Token: 1,
})
if err != nil {
fmt.Println(fmt.Sprintf("%s: test failed (%s)", c.GetApiKey(), err.Error()))
}
return err == nil && len(result) > 0
}

View File

@ -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({
<MessageSquare className={`h-4 w-4 mr-1`} />
<div className={`title`}>{filterMessage(conversation.name)}</div>
<div className={`id`}>{conversation.id}</div>
<DropdownMenu open={open} onOpenChange={(state: boolean) => {
setOpen(state);
if (state) setOffset(new Date().getTime());
}}>
<DropdownMenu
open={open}
onOpenChange={(state: boolean) => {
setOpen(state);
if (state) setOffset(new Date().getTime());
}}
>
<DropdownMenuTrigger>
<MoreHorizontal className={`more h-5 w-5 p-0.5`} />
</DropdownMenuTrigger>

View File

@ -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";

View File

@ -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 {

View File

@ -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<number, Conversation>;
@ -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 {

View File

@ -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<SharingForm> {
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<ViewForm> {
export async function viewConversation(hash: string): Promise<ViewForm> {
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,
}
};
}
}

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",
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":
"Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален",
},
},
},
};

View File

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

View File

@ -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() {
)}
</div>
<AlertDialog
open={operateConversation.type === "delete" && !!operateConversation.target}
open={
operateConversation.type === "delete" &&
!!operateConversation.target
}
onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" });
}}
@ -194,16 +198,17 @@ function SideBar() {
</AlertDialog>
<AlertDialog
open={operateConversation.type === "share" && !!operateConversation.target}
open={
operateConversation.type === "share" &&
!!operateConversation.target
}
onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" });
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("share.title")}
</AlertDialogTitle>
<AlertDialogTitle>{t("share.title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("share.description")}
<strong className={`conversation-name`}>
@ -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() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("share.success")}
</AlertDialogTitle>
<AlertDialogTitle>{t("share.success")}</AlertDialogTitle>
<AlertDialogDescription>
<div className={`share-wrapper mt-4 mb-2`}>
<Input value={shared} />
<Button variant={`default`} size={`icon`} onClick={async () => {
await copyClipboard(shared);
toast({
title: t("share.copied"),
description: t("share.copied-description"),
});
}}>
<Button
variant={`default`}
size={`icon`}
onClick={async () => {
await copyClipboard(shared);
toast({
title: t("share.copied"),
description: t("share.copied-description"),
});
}}
>
<Copy className={`h-4 w-4`} />
</Button>
</div>

View File

@ -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) {
<div className={`sharing-container`}>
<div className={`header`}>
<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>
</div>
<div className={`name`}>{data.name}</div>
<div className={`time`}>{time}</div>
</div>
<div className={`body`}>
{
data.messages.map((message, i) => (
<MessageSegment message={message} key={i} />
))
}
{data.messages.map((message, i) => (
<MessageSegment message={message} key={i} />
))}
</div>
<div className={`action`}>
<Button variant={`outline`} onClick={async () => {
await copyClipboard(value);
toast({
title: t('share.copied'),
});
}}>
<Button
variant={`outline`}
onClick={async () => {
await copyClipboard(value);
toast({
title: t("share.copied"),
});
}}
>
<Copy className={`h-4 w-4 mr-2`} />
{t('message.copy')}
{t("message.copy")}
</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`} />
{t('message.save')}
{t("message.save")}
</Button>
<Button variant={`outline`} onClick={async () => {
refer && sessionStorage.setItem('refer', refer);
await router.navigate('/');
}}>
<Button
variant={`outline`}
onClick={async () => {
event.emit({
refer: refer as string,
data: data?.messages as Message[],
});
await router.navigate("/");
}}
>
<MessagesSquare className={`h-4 w-4 mr-2`} />
{t('message.use')}
{t("message.use")}
</Button>
</div>
</div>
)
);
}
function Sharing() {
@ -84,25 +105,21 @@ function Sharing() {
return (
<div className={`sharing-page`}>
{
data === null ? (
<div className={`loading`}>
<Loader2 className={`loader w-12 h-12`} />
</div>
) : (
data.status ? (
<SharingForm refer={hash} data={data.data} />
) : (
<div className={`error-container`}>
<HelpCircle className={`w-12 h-12 mb-2.5`} />
<p className={`title`}>{t('share.not-found')}</p>
<p className={`description`}>{t('share.not-found-description')}</p>
</div>
)
)
}
{data === null ? (
<div className={`loading`}>
<Loader2 className={`loader w-12 h-12`} />
</div>
) : data.status ? (
<SharingForm refer={hash} data={data.data} />
) : (
<div className={`error-container`}>
<HelpCircle className={`w-12 h-12 mb-2.5`} />
<p className={`title`}>{t("share.not-found")}</p>
<p className={`description`}>{t("share.not-found-description")}</p>
</div>
)}
</div>
)
);
}
export default Sharing;

View File

@ -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();

View File

@ -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)