diff --git a/app/src/assets/sharing.less b/app/src/assets/sharing.less new file mode 100644 index 00000000..38402312 --- /dev/null +++ b/app/src/assets/sharing.less @@ -0,0 +1,133 @@ +.sharing-page { + position: relative; + display: flex; + width: 100%; + height: calc(100vh - 56px); + padding: 0 2rem; +} + +.loading { + margin: auto; + transform: translateY(-28px); + user-select: none; + + .loader { + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + animation: spin 1.25s cubic-bezier(0.5, 0, 0.5, 1) infinite; + } +} + +.error-container { + display: flex; + flex-direction: column; + margin: auto; + transform: translateY(-28px); + color: hsl(var(--text)); + text-align: center; + align-items: center; + user-select: none; + + .title { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 0.75rem; + } + + .message { + font-size: 1rem; + } +} + +.sharing-container { + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + margin: auto; + padding: 0; + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + width: 80vw; + height: 70vh; + + .header { + display: flex; + flex-direction: row; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--background-container)); + color: hsl(var(--text)); + padding: 0.85rem 1rem 0.75rem; + align-items: center; + + .user { + display: flex; + flex-direction: row; + align-items: center; + user-select: none; + flex-shrink: 0; + + img { + width: 2rem; + height: 2rem; + border-radius: 4px; + margin-right: 0.75rem; + flex-shrink: 0; + } + + span { + font-size: 1rem; + transform: translateY(-2px); + white-space: nowrap; + + @media (max-width: 768px) { + display: none; + } + } + } + + .name { + margin: 0 auto; + padding: 0 1rem; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .time { + user-select: none; + white-space: nowrap; + flex-shrink: 0; + } + } + + .body { + display: flex; + flex-direction: column; + flex-grow: 1; + width: 100%; + overflow-x: hidden; + overflow-y: auto; + touch-action: pan-y; + padding: 1rem; + scrollbar-width: thin; + gap: 8px; + } + + .action { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-end; + gap: 6px; + padding: 0.75rem 1.5rem 1rem; + + button { + white-space: nowrap; + } + } +} diff --git a/app/src/conf.ts b/app/src/conf.ts index b501186f..cdf4fa3c 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -1,6 +1,6 @@ import axios from "axios"; -export const version = "3.4.1"; +export const version = "3.4.2"; export const deploy: boolean = false; export let rest_api: string = "http://localhost:8094"; export let ws_api: string = "ws://localhost:8094"; diff --git a/app/src/conversation/history.ts b/app/src/conversation/history.ts index be23c77a..8aaeaf50 100644 --- a/app/src/conversation/history.ts +++ b/app/src/conversation/history.ts @@ -4,12 +4,6 @@ import { setHistory } from "../store/chat.ts"; import { manager } from "./manager.ts"; import { AppDispatch } from "../store"; -type SharingForm = { - status: boolean; - message: string; - data: string; -} - export async function updateConversationList( dispatch: AppDispatch, ): Promise { @@ -42,17 +36,6 @@ export async function deleteConversation( return true; } -export async function shareConversation( - id: number, refs: number[] = [-1], -): Promise { - try { - const resp = await axios.post("/conversation/share", { id, refs }); - return resp.data; - } catch (e) { - return { status: false, message: (e as Error).message, data: "" }; - } -} - export async function toggleConversation( dispatch: AppDispatch, id: number, diff --git a/app/src/conversation/sharing.ts b/app/src/conversation/sharing.ts new file mode 100644 index 00000000..559a27ea --- /dev/null +++ b/app/src/conversation/sharing.ts @@ -0,0 +1,47 @@ +import axios from "axios"; +import {Message} from "./types.ts"; + +export type SharingForm = { + status: boolean; + message: string; + data: string; +} + +export type ViewData = { + name: string; + username: string; + time: string; + messages: Message[]; +}; + +export type ViewForm = { + status: boolean; + message: string; + data: ViewData | null; +} + +export async function shareConversation( + id: number, refs: number[] = [-1], +): Promise { + try { + const resp = await axios.post("/conversation/share", { id, refs }); + return resp.data; + } catch (e) { + return { status: false, message: (e as Error).message, data: "" }; + } +} + +export async function viewConversation( + hash: string, +): Promise { + try { + const resp = await axios.get(`/conversation/view?hash=${hash}`); + return resp.data as ViewForm; + } catch (e) { + return { + status: false, + message: (e as Error).message, + data: null, + } + } +} diff --git a/app/src/conversation/types.ts b/app/src/conversation/types.ts index e8b76b6e..fec02f94 100644 --- a/app/src/conversation/types.ts +++ b/app/src/conversation/types.ts @@ -1,10 +1,10 @@ import { Conversation } from "./conversation.ts"; export type Message = { + role: string; content: string; keyword?: string; quota?: number; - role: string; }; export type Id = number; diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 8520ed8c..a795ab63 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -183,6 +183,8 @@ const resources = { 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", } }, }, @@ -353,6 +355,8 @@ const resources = { failed: "分享失败", copied: "复制成功", "copied-description": "链接已复制到剪贴板", + "not-found": "对话未找到", + "not-found-description": "对话未找到,请检查链接是否正确或对话是否已被删除", } }, }, @@ -534,6 +538,8 @@ const resources = { failed: "Поделиться не удалось", copied: "Скопировано", "copied-description": "Ссылка скопирована в буфер обмена", + "not-found": "Разговор не найден", + "not-found-description": "Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален", } }, }, diff --git a/app/src/router.ts b/app/src/router.ts index 716efa94..d440c547 100644 --- a/app/src/router.ts +++ b/app/src/router.ts @@ -3,6 +3,7 @@ import Home from "./routes/Home.tsx"; import NotFound from "./routes/NotFound.tsx"; import Auth from "./routes/Auth.tsx"; import Generation from "./routes/Generation.tsx"; +import Sharing from "./routes/Sharing.tsx"; const router = createBrowserRouter([ { @@ -22,6 +23,11 @@ const router = createBrowserRouter([ path: "/generate", Component: Generation, }, + { + 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 bcca0404..3d9df5bb 100644 --- a/app/src/routes/Home.tsx +++ b/app/src/routes/Home.tsx @@ -23,10 +23,11 @@ import type { RootState } from "../store"; import { selectAuthenticated, selectInit } from "../store/auth.ts"; import { login, supportModels } from "../conf.ts"; import { - deleteConversation, shareConversation, + deleteConversation, toggleConversation, updateConversationList, } from "../conversation/history.ts"; +import { shareConversation } from "../conversation/sharing.ts"; import React, { useEffect, useRef, useState } from "react"; import { filterMessage, diff --git a/app/src/routes/Sharing.tsx b/app/src/routes/Sharing.tsx new file mode 100644 index 00000000..1192babc --- /dev/null +++ b/app/src/routes/Sharing.tsx @@ -0,0 +1,108 @@ +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 MessageSegment from "../components/Message.tsx"; +import {Button} from "../components/ui/button.tsx"; +import router from "../router.ts"; +import {useToast} from "../components/ui/use-toast.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 value = JSON.stringify(data, null, 2); + const { toast } = useToast(); + + return ( +
+
+
+ + {data.username} +
+
{data.name}
+
{time}
+
+
+ { + data.messages.map((message, i) => ( + + )) + } +
+
+ + + +
+
+ ) +} + +function Sharing() { + const { t } = useTranslation(); + const { hash } = useParams(); + const [setup, setSetup] = useState(false); + const [data, setData] = useState(null); + + useEffectAsync(async () => { + if (!hash || setup) return; + + setSetup(true); + + const resp = await viewConversation(hash as string); + setData(resp); + if (!resp.status) console.debug(`[sharing] error: ${resp.message}`); + }, []); + + return ( +
+ { + data === null ? ( +
+ +
+ ) : ( + data.status ? ( + + ) : ( +
+ +

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

+

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

+
+ ) + ) + } +
+ ) +} + +export default Sharing; diff --git a/main.go b/main.go index 8aa75050..e6f315df 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "chat/adapter/chatgpt" "chat/addition" "chat/auth" "chat/manager" @@ -21,6 +22,8 @@ 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)