mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
update sharing
This commit is contained in:
parent
fd8c2d6eb7
commit
8e02dae8d3
@ -92,10 +92,13 @@
|
|||||||
transition: 0.2s ease-in-out;
|
transition: 0.2s ease-in-out;
|
||||||
background: var(--conversation-card);
|
background: var(--conversation-card);
|
||||||
|
|
||||||
.delete {
|
.more {
|
||||||
color: hsl(var(--text-secondary));
|
color: hsl(var(--text-secondary));
|
||||||
display: none;
|
display: none;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: hsl(var(--text));
|
color: hsl(var(--text));
|
||||||
@ -109,8 +112,9 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete {
|
.more {
|
||||||
display: block;
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,3 +336,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,12 +17,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.select-group-item {
|
.select-group-item {
|
||||||
padding: 0.35rem 0.5rem;
|
padding: 0.35rem 0.8rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: .2s;
|
transition: .2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: hsl(var(--accent-secondary));
|
background: hsl(var(--background));
|
||||||
|
color: hsl(var(--text));
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: hsl(var(--accent));
|
background: hsl(var(--accent));
|
||||||
@ -30,6 +32,7 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: hsl(var(--text));
|
background: hsl(var(--text));
|
||||||
|
border-color: hsl(var(--border-hover));
|
||||||
color: hsl(var(--background));
|
color: hsl(var(--background));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import { Textarea } from "./ui/textarea.tsx";
|
|||||||
import Markdown from "./Markdown.tsx";
|
import Markdown from "./Markdown.tsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Toggle } from "./ui/toggle.tsx";
|
import { Toggle } from "./ui/toggle.tsx";
|
||||||
import {mobile} from "../utils.ts";
|
import { mobile } from "../utils.ts";
|
||||||
|
|
||||||
type RichEditorProps = {
|
type RichEditorProps = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -103,9 +103,9 @@ function RichEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div className={`editor-wrapper`}>
|
<div className={`editor-wrapper`}>
|
||||||
<div
|
<div
|
||||||
className={`editor-object ${
|
className={`editor-object ${openInput ? "show-editor" : ""} ${
|
||||||
openInput ? "show-editor" : ""
|
openPreview ? "show-preview" : ""
|
||||||
} ${openPreview ? "show-preview" : ""}`}
|
}`}
|
||||||
>
|
>
|
||||||
{openInput && (
|
{openInput && (
|
||||||
<Textarea
|
<Textarea
|
||||||
@ -124,7 +124,7 @@ function RichEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorProvider(props: RichEditorProps) {
|
function EditorProvider(props: RichEditorProps) {
|
||||||
@ -134,9 +134,7 @@ function EditorProvider(props: RichEditorProps) {
|
|||||||
<>
|
<>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<div
|
<div className={`editor-action active ${props.className}`}>
|
||||||
className={`editor-action active ${props.className}`}
|
|
||||||
>
|
|
||||||
<Edit className={`h-3.5 w-3.5`} />
|
<Edit className={`h-3.5 w-3.5`} />
|
||||||
</div>
|
</div>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
@ -5,7 +5,9 @@ function ProjectLink() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => window.open("https://github.com/Deeptrain-Community/chatnio")}
|
onClick={() =>
|
||||||
|
window.open("https://github.com/Deeptrain-Community/chatnio")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 438.549 438.549"
|
viewBox="0 0 438.549 438.549"
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
import { useRegisterSW } from "virtual:pwa-register/react";
|
||||||
import {version} from "../conf.ts";
|
import { version } from "../conf.ts";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useToast} from "./ui/use-toast.ts";
|
import { useToast } from "./ui/use-toast.ts";
|
||||||
import {useEffect} from "react";
|
import { useEffect } from "react";
|
||||||
import {ToastAction} from "./ui/toast.tsx";
|
import { ToastAction } from "./ui/toast.tsx";
|
||||||
|
|
||||||
function ReloadPrompt() {
|
function ReloadPrompt() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -18,36 +18,42 @@ function ReloadPrompt() {
|
|||||||
console.debug(`[service] service worker registered (version ${version})`);
|
console.debug(`[service] service worker registered (version ${version})`);
|
||||||
},
|
},
|
||||||
onRegisterError(error) {
|
onRegisterError(error) {
|
||||||
console.log(`[service] service worker registration failed: ${error.message}`);
|
console.log(
|
||||||
|
`[service] service worker registration failed: ${error.message}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const before = localStorage.getItem('version') || '';
|
const before = localStorage.getItem("version") || "";
|
||||||
if (before.length > 0 && before !== version) {
|
if (before.length > 0 && before !== version) {
|
||||||
toast({
|
toast({
|
||||||
title: t('service.update-success'),
|
title: t("service.update-success"),
|
||||||
description: t('service.update-success-prompt'),
|
description: t("service.update-success-prompt"),
|
||||||
})
|
});
|
||||||
console.debug(`[service] service worker updated (from ${before} to ${version})`);
|
console.debug(
|
||||||
|
`[service] service worker updated (from ${before} to ${version})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
localStorage.setItem('version', version);
|
localStorage.setItem("version", version);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (offlineReady) {
|
if (offlineReady) {
|
||||||
toast({
|
toast({
|
||||||
title: t('service.offline-title'),
|
title: t("service.offline-title"),
|
||||||
description: t('service.offline'),
|
description: t("service.offline"),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needRefresh) {
|
if (needRefresh) {
|
||||||
toast({
|
toast({
|
||||||
title: t('service.title'),
|
title: t("service.title"),
|
||||||
description: t('service.description'),
|
description: t("service.description"),
|
||||||
action: (
|
action: (
|
||||||
<ToastAction altText={t('service.update')} onClick={() => updateServiceWorker(true)}>
|
<ToastAction
|
||||||
{t('service.update')}
|
altText={t("service.update")}
|
||||||
|
onClick={() => updateServiceWorker(true)}
|
||||||
|
>
|
||||||
|
{t("service.update")}
|
||||||
</ToastAction>
|
</ToastAction>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -61,4 +67,3 @@ function ReloadPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ReloadPrompt;
|
export default ReloadPrompt;
|
||||||
|
|
||||||
|
89
app/src/components/home/ConversationSegment.tsx
Normal file
89
app/src/components/home/ConversationSegment.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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 {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu.tsx";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ConversationInstance } from "../../conversation/types.ts";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type ConversationSegmentProps = {
|
||||||
|
conversation: ConversationInstance;
|
||||||
|
current: number;
|
||||||
|
operate: (conversation: { target: ConversationInstance, type: string }) => void;
|
||||||
|
};
|
||||||
|
function ConversationSegment({
|
||||||
|
conversation,
|
||||||
|
current,
|
||||||
|
operate,
|
||||||
|
}: ConversationSegmentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`conversation ${current === conversation.id ? "active" : ""}`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (
|
||||||
|
target.classList.contains("delete") ||
|
||||||
|
target.parentElement?.classList.contains("delete")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
await toggleConversation(dispatch, conversation.id);
|
||||||
|
if (mobile) dispatch(setMenu(false));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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());
|
||||||
|
}}>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<MoreHorizontal className={`more h-5 w-5 p-0.5`} />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
// prevent click event from opening the dropdown menu
|
||||||
|
if (offset + 500 > new Date().getTime()) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
operate({ target: conversation, type: "delete" });
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className={`more h-4 w-4 mx-1`} />
|
||||||
|
{t("conversation.delete-conversation")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
operate({ target: conversation, type: "share" });
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Share2 className={`more h-4 w-4 mx-1`} />
|
||||||
|
{t("share.share-conversation")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConversationSegment;
|
@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const version = "3.4.0";
|
export const version = "3.4.1";
|
||||||
export const deploy: boolean = true;
|
export const deploy: boolean = false;
|
||||||
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";
|
||||||
|
|
||||||
|
@ -4,6 +4,12 @@ import { setHistory } from "../store/chat.ts";
|
|||||||
import { manager } from "./manager.ts";
|
import { manager } from "./manager.ts";
|
||||||
import { AppDispatch } from "../store";
|
import { AppDispatch } from "../store";
|
||||||
|
|
||||||
|
type SharingForm = {
|
||||||
|
status: boolean;
|
||||||
|
message: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateConversationList(
|
export async function updateConversationList(
|
||||||
dispatch: AppDispatch,
|
dispatch: AppDispatch,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -36,6 +42,17 @@ export async function deleteConversation(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function shareConversation(
|
||||||
|
id: number, refs: number[] = [-1],
|
||||||
|
): Promise<SharingForm> {
|
||||||
|
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(
|
export async function toggleConversation(
|
||||||
dispatch: AppDispatch,
|
dispatch: AppDispatch,
|
||||||
id: number,
|
id: number,
|
||||||
|
@ -40,6 +40,7 @@ const resources = {
|
|||||||
"This action cannot be undone. This will permanently delete the conversation ",
|
"This action cannot be undone. This will permanently delete the conversation ",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
|
"delete-conversation": "Delete Conversation",
|
||||||
"delete-success": "Conversation deleted",
|
"delete-success": "Conversation deleted",
|
||||||
"delete-success-prompt": "Conversation has been deleted.",
|
"delete-success-prompt": "Conversation has been deleted.",
|
||||||
"delete-failed": "Delete failed",
|
"delete-failed": "Delete failed",
|
||||||
@ -164,13 +165,24 @@ const resources = {
|
|||||||
"learn-more": "Learn more",
|
"learn-more": "Learn more",
|
||||||
},
|
},
|
||||||
service: {
|
service: {
|
||||||
"title": "New Version Available",
|
title: "New Version Available",
|
||||||
"description": "A new version is available. Do you want to update now?",
|
description: "A new version is available. Do you want to update now?",
|
||||||
"update": "Update",
|
update: "Update",
|
||||||
"offline-title": "Offline Mode",
|
"offline-title": "Offline Mode",
|
||||||
"offline": "App is currently offline.",
|
offline: "App is currently offline.",
|
||||||
"update-success": "Update Success",
|
"update-success": "Update Success",
|
||||||
"update-success-prompt": "You have been updated to the latest version.",
|
"update-success-prompt": "You have been updated to the latest version.",
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
title: "Share",
|
||||||
|
"share-conversation": "Share Conversation",
|
||||||
|
description: "Share this conversation with others: ",
|
||||||
|
"copy-link": "Copy Link",
|
||||||
|
"view": "View",
|
||||||
|
success: "Share success",
|
||||||
|
failed: "Share failed",
|
||||||
|
copied: "Copied",
|
||||||
|
"copied-description": "Link has been copied to clipboard",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -196,18 +208,19 @@ const resources = {
|
|||||||
close: "关闭",
|
close: "关闭",
|
||||||
edit: "编辑",
|
edit: "编辑",
|
||||||
conversation: {
|
conversation: {
|
||||||
title: "会话",
|
title: "对话",
|
||||||
empty: "空空如也",
|
empty: "空空如也",
|
||||||
"refresh-failed": "刷新失败",
|
"refresh-failed": "刷新失败",
|
||||||
"refresh-failed-prompt": "请求出错,请重试。",
|
"refresh-failed-prompt": "请求出错,请重试。",
|
||||||
"remove-title": "是否确定?",
|
"remove-title": "是否确定?",
|
||||||
"remove-description": "此操作无法撤消。这将永久删除会话 ",
|
"remove-description": "此操作无法撤消。这将永久删除对话 ",
|
||||||
cancel: "取消",
|
cancel: "取消",
|
||||||
delete: "删除",
|
delete: "删除",
|
||||||
"delete-success": "会话已删除",
|
"delete-conversation": "删除对话",
|
||||||
"delete-success-prompt": "会话已删除。",
|
"delete-success": "对话已删除",
|
||||||
|
"delete-success-prompt": "对话已删除。",
|
||||||
"delete-failed": "删除失败",
|
"delete-failed": "删除失败",
|
||||||
"delete-failed-prompt": "删除会话失败,请检查您的网络并重试。",
|
"delete-failed-prompt": "删除对话失败,请检查您的网络并重试。",
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
web: "联网搜索功能",
|
web: "联网搜索功能",
|
||||||
@ -322,13 +335,24 @@ const resources = {
|
|||||||
"learn-more": "了解更多",
|
"learn-more": "了解更多",
|
||||||
},
|
},
|
||||||
service: {
|
service: {
|
||||||
"title": "发现新版本",
|
title: "发现新版本",
|
||||||
"description": "发现新版本,是否立即更新?",
|
description: "发现新版本,是否立即更新?",
|
||||||
"update": "更新",
|
update: "更新",
|
||||||
"offline-title": "离线模式",
|
"offline-title": "离线模式",
|
||||||
"offline": "应用当前处于离线状态。",
|
offline: "应用当前处于离线状态。",
|
||||||
"update-success": "更新成功",
|
"update-success": "更新成功",
|
||||||
"update-success-prompt": "您已更新至最新版本。",
|
"update-success-prompt": "您已更新至最新版本。",
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
title: "分享",
|
||||||
|
"share-conversation": "分享对话",
|
||||||
|
description: "将此对话与他人分享:",
|
||||||
|
"copy-link": "复制链接",
|
||||||
|
"view": "查看",
|
||||||
|
success: "分享成功",
|
||||||
|
failed: "分享失败",
|
||||||
|
copied: "复制成功",
|
||||||
|
"copied-description": "链接已复制到剪贴板",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -367,6 +391,7 @@ const resources = {
|
|||||||
"Это действие нельзя отменить. Это навсегда удалит разговор ",
|
"Это действие нельзя отменить. Это навсегда удалит разговор ",
|
||||||
cancel: "Отмена",
|
cancel: "Отмена",
|
||||||
delete: "Удалить",
|
delete: "Удалить",
|
||||||
|
"delete-conversation": "Удалить разговор",
|
||||||
"delete-success": "Разговор удален",
|
"delete-success": "Разговор удален",
|
||||||
"delete-success-prompt": "Разговор был удален.",
|
"delete-success-prompt": "Разговор был удален.",
|
||||||
"delete-failed": "Ошибка удаления",
|
"delete-failed": "Ошибка удаления",
|
||||||
@ -491,13 +516,24 @@ const resources = {
|
|||||||
"learn-more": "Узнать больше",
|
"learn-more": "Узнать больше",
|
||||||
},
|
},
|
||||||
service: {
|
service: {
|
||||||
"title": "Доступна новая версия",
|
title: "Доступна новая версия",
|
||||||
"description": "Доступна новая версия. Хотите обновить сейчас?",
|
description: "Доступна новая версия. Хотите обновить сейчас?",
|
||||||
"update": "Обновить",
|
update: "Обновить",
|
||||||
"offline-title": "Режим оффлайн",
|
"offline-title": "Режим оффлайн",
|
||||||
"offline": "Приложение в настоящее время находится в автономном режиме.",
|
offline: "Приложение в настоящее время находится в автономном режиме.",
|
||||||
"update-success": "Обновление успешно",
|
"update-success": "Обновление успешно",
|
||||||
"update-success-prompt": "Вы обновились до последней версии.",
|
"update-success-prompt": "Вы обновились до последней версии.",
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
title: "Поделиться",
|
||||||
|
"share-conversation": "Поделиться разговором",
|
||||||
|
description: "Поделитесь этим разговором с другими: ",
|
||||||
|
"copy-link": "Скопировать ссылку",
|
||||||
|
"view": "Посмотреть",
|
||||||
|
success: "Поделиться успешно",
|
||||||
|
failed: "Поделиться не удалось",
|
||||||
|
copied: "Скопировано",
|
||||||
|
"copied-description": "Ссылка скопирована в буфер обмена",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -4,14 +4,12 @@ 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,
|
ChevronRight, Copy,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
Globe,
|
Globe,
|
||||||
LogIn,
|
LogIn,
|
||||||
MessageSquare,
|
|
||||||
Plus,
|
Plus,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "../components/ui/button.tsx";
|
import { Button } from "../components/ui/button.tsx";
|
||||||
import {
|
import {
|
||||||
@ -25,7 +23,7 @@ import type { RootState } from "../store";
|
|||||||
import { selectAuthenticated, selectInit } from "../store/auth.ts";
|
import { selectAuthenticated, selectInit } from "../store/auth.ts";
|
||||||
import { login, supportModels } from "../conf.ts";
|
import { login, supportModels } from "../conf.ts";
|
||||||
import {
|
import {
|
||||||
deleteConversation,
|
deleteConversation, shareConversation,
|
||||||
toggleConversation,
|
toggleConversation,
|
||||||
updateConversationList,
|
updateConversationList,
|
||||||
} from "../conversation/history.ts";
|
} from "../conversation/history.ts";
|
||||||
@ -36,7 +34,7 @@ import {
|
|||||||
formatMessage,
|
formatMessage,
|
||||||
mobile,
|
mobile,
|
||||||
useAnimation,
|
useAnimation,
|
||||||
useEffectAsync,
|
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";
|
||||||
@ -67,6 +65,7 @@ import FileProvider, { FileObject } from "../components/FileProvider.tsx";
|
|||||||
import router from "../router.ts";
|
import router from "../router.ts";
|
||||||
import SelectGroup from "../components/SelectGroup.tsx";
|
import SelectGroup from "../components/SelectGroup.tsx";
|
||||||
import EditorProvider from "../components/EditorProvider.tsx";
|
import EditorProvider from "../components/EditorProvider.tsx";
|
||||||
|
import ConversationSegment from "../components/home/ConversationSegment.tsx";
|
||||||
|
|
||||||
function SideBar() {
|
function SideBar() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -74,15 +73,20 @@ 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 [removeConversation, setRemoveConversation] =
|
const [operateConversation, setOperateConversation] =
|
||||||
useState<ConversationInstance | null>(null);
|
useState<{
|
||||||
|
target: ConversationInstance | null;
|
||||||
|
type: string;
|
||||||
|
}>({ target: null, type: "" });
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const history: ConversationInstance[] = useSelector(selectHistory);
|
const history: ConversationInstance[] = useSelector(selectHistory);
|
||||||
const refresh = useRef(null);
|
const refresh = useRef(null);
|
||||||
|
const [shared, setShared] = useState<string>("");
|
||||||
useEffectAsync(async () => {
|
useEffectAsync(async () => {
|
||||||
await updateConversationList(dispatch);
|
await updateConversationList(dispatch);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
<div className={`sidebar ${open ? "open" : ""}`}>
|
<div className={`sidebar ${open ? "open" : ""}`}>
|
||||||
{auth ? (
|
{auth ? (
|
||||||
@ -123,45 +127,21 @@ function SideBar() {
|
|||||||
<div className={`conversation-list`}>
|
<div className={`conversation-list`}>
|
||||||
{history.length ? (
|
{history.length ? (
|
||||||
history.map((conversation, i) => (
|
history.map((conversation, i) => (
|
||||||
<div
|
<ConversationSegment
|
||||||
className={`conversation ${
|
operate={setOperateConversation}
|
||||||
current === conversation.id ? "active" : ""
|
conversation={conversation}
|
||||||
}`}
|
current={current}
|
||||||
key={i}
|
key={i}
|
||||||
onClick={async (e) => {
|
/>
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (
|
|
||||||
target.classList.contains("delete") ||
|
|
||||||
target.parentElement?.classList.contains("delete")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
await toggleConversation(dispatch, conversation.id);
|
|
||||||
if (mobile) dispatch(setMenu(false));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MessageSquare className={`h-4 w-4 mr-1`} />
|
|
||||||
<div className={`title`}>
|
|
||||||
{filterMessage(conversation.name)}
|
|
||||||
</div>
|
|
||||||
<div className={`id`}>{conversation.id}</div>
|
|
||||||
<Trash2
|
|
||||||
className={`delete h-4 w-4`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setRemoveConversation(conversation);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className={`empty`}>{t("conversation.empty")}</div>
|
<div className={`empty`}>{t("conversation.empty")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={removeConversation !== null}
|
open={operateConversation.type === "delete" && !!operateConversation.target}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) setRemoveConversation(null);
|
if (!open) setOperateConversation({ target: null, type: "" });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
@ -173,7 +153,7 @@ function SideBar() {
|
|||||||
{t("conversation.remove-description")}
|
{t("conversation.remove-description")}
|
||||||
<strong className={`conversation-name`}>
|
<strong className={`conversation-name`}>
|
||||||
{extractMessage(
|
{extractMessage(
|
||||||
filterMessage(removeConversation?.name || ""),
|
filterMessage(operateConversation?.target?.name || ""),
|
||||||
)}
|
)}
|
||||||
</strong>
|
</strong>
|
||||||
{t("end")}
|
{t("end")}
|
||||||
@ -191,7 +171,7 @@ function SideBar() {
|
|||||||
if (
|
if (
|
||||||
await deleteConversation(
|
await deleteConversation(
|
||||||
dispatch,
|
dispatch,
|
||||||
removeConversation?.id || -1,
|
operateConversation?.target?.id || -1,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
toast({
|
toast({
|
||||||
@ -203,7 +183,7 @@ function SideBar() {
|
|||||||
title: t("conversation.delete-failed"),
|
title: t("conversation.delete-failed"),
|
||||||
description: t("conversation.delete-failed-prompt"),
|
description: t("conversation.delete-failed-prompt"),
|
||||||
});
|
});
|
||||||
setRemoveConversation(null);
|
setOperateConversation({ target: null, type: "" });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("conversation.delete")}
|
{t("conversation.delete")}
|
||||||
@ -211,6 +191,96 @@ function SideBar() {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog
|
||||||
|
open={operateConversation.type === "share" && !!operateConversation.target}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setOperateConversation({ target: null, type: "" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("share.title")}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("share.description")}
|
||||||
|
<strong className={`conversation-name`}>
|
||||||
|
{extractMessage(
|
||||||
|
filterMessage(operateConversation?.target?.name || ""),
|
||||||
|
)}
|
||||||
|
</strong>
|
||||||
|
{t("end")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
{t("conversation.cancel")}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
setOperateConversation({ target: null, type: "" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("share.title")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog
|
||||||
|
open={shared.length > 0}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setShared("");
|
||||||
|
setOperateConversation({ target: null, type: "" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<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"),
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<Copy className={`h-4 w-4`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("close")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(shared, "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("share.view")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button className={`login-action`} variant={`default`} onClick={login}>
|
<Button className={`login-action`} variant={`default`} onClick={login}>
|
||||||
|
@ -71,9 +71,7 @@ function Package() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={`default`}
|
variant={`default`}
|
||||||
onClick={() =>
|
onClick={() => window.open("https://deeptrain.net/home/package")}
|
||||||
window.open("https://deeptrain.net/home/package")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t("pkg.go")}
|
{t("pkg.go")}
|
||||||
</Button>
|
</Button>
|
||||||
|
18
app/src/types/service.d.ts
vendored
18
app/src/types/service.d.ts
vendored
@ -1,15 +1,15 @@
|
|||||||
declare module 'virtual:pwa-register/react' {
|
declare module "virtual:pwa-register/react" {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
||||||
// @ts-expect-error ignore when React is not installed
|
// @ts-expect-error ignore when React is not installed
|
||||||
import type { Dispatch, SetStateAction } from 'react'
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import type { RegisterSWOptions } from 'vite-plugin-pwa/types'
|
import type { RegisterSWOptions } from "vite-plugin-pwa/types";
|
||||||
|
|
||||||
export type { RegisterSWOptions }
|
export type { RegisterSWOptions };
|
||||||
|
|
||||||
export function useRegisterSW(options?: RegisterSWOptions): {
|
export function useRegisterSW(options?: RegisterSWOptions): {
|
||||||
needRefresh: [boolean, Dispatch<SetStateAction<boolean>>]
|
needRefresh: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||||
offlineReady: [boolean, Dispatch<SetStateAction<boolean>>]
|
offlineReady: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||||
updateServiceWorker: (reloadPage?: boolean) => Promise<void>
|
updateServiceWorker: (reloadPage?: boolean) => Promise<void>;
|
||||||
onRegistered: (registration: ServiceWorkerRegistration) => void
|
onRegistered: (registration: ServiceWorkerRegistration) => void;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -111,8 +111,7 @@ func CreateSharingTable(db *sql.DB) {
|
|||||||
conversation_id INT,
|
conversation_id INT,
|
||||||
refs TEXT,
|
refs TEXT,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES auth(id),
|
FOREIGN KEY (user_id) REFERENCES auth(id)
|
||||||
FOREIGN KEY (conversation_id) REFERENCES conversation(id)
|
|
||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -10,8 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ShareForm struct {
|
type ShareForm struct {
|
||||||
ConversationId int64 `json:"conversation_id"`
|
Id int64 `json:"id"`
|
||||||
Refs []int `json:"refs"`
|
Refs []int `json:"refs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListAPI(c *gin.Context) {
|
func ListAPI(c *gin.Context) {
|
||||||
@ -121,18 +121,19 @@ func ShareAPI(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ShareConversation(db, user, form.ConversationId, form.Refs); err != nil {
|
if hash, err := ShareConversation(db, user, form.Id, form.Refs); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": false,
|
"status": false,
|
||||||
"message": err.Error(),
|
"message": err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": true,
|
||||||
|
"message": "",
|
||||||
|
"data": hash,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"status": true,
|
|
||||||
"message": "",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewAPI(c *gin.Context) {
|
func ViewAPI(c *gin.Context) {
|
||||||
|
@ -9,5 +9,6 @@ func Register(app *gin.Engine) {
|
|||||||
router.GET("/load", LoadAPI)
|
router.GET("/load", LoadAPI)
|
||||||
router.GET("/delete", DeleteAPI)
|
router.GET("/delete", DeleteAPI)
|
||||||
router.POST("/share", ShareAPI)
|
router.POST("/share", ShareAPI)
|
||||||
|
router.GET("/view", ViewAPI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,9 @@ func GetRef(refs []int) (result string) {
|
|||||||
return strings.TrimSuffix(result, ",")
|
return strings.TrimSuffix(result, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShareConversation(db *sql.DB, user *auth.User, id int64, refs []int) error {
|
func ShareConversation(db *sql.DB, user *auth.User, id int64, refs []int) (string, error) {
|
||||||
if id < 0 || user == nil {
|
if id < 0 || user == nil {
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ref := GetRef(refs)
|
ref := GetRef(refs)
|
||||||
@ -42,12 +42,14 @@ func ShareConversation(db *sql.DB, user *auth.User, id int64, refs []int) error
|
|||||||
Refs: refs,
|
Refs: refs,
|
||||||
})
|
})
|
||||||
|
|
||||||
_, err := db.Exec(`
|
if _, err := db.Exec(`
|
||||||
INSERT INTO sharing (hash, user_id, conversation_id, refs) VALUES (?, ?, ?, ?)
|
INSERT INTO sharing (hash, user_id, conversation_id, refs) VALUES (?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE refs = ?
|
ON DUPLICATE KEY UPDATE refs = ?
|
||||||
`, hash, user.GetID(db), id, ref, ref)
|
`, hash, user.GetID(db), id, ref, ref); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSharedMessages(db *sql.DB, userId int64, conversationId int64, refs []string) []globals.Message {
|
func GetSharedMessages(db *sql.DB, userId int64, conversationId int64, refs []string) []globals.Message {
|
||||||
@ -84,7 +86,7 @@ func GetSharedConversation(db *sql.DB, hash string) (*SharedForm, error) {
|
|||||||
sharing.user_id, sharing.conversation_id
|
sharing.user_id, sharing.conversation_id
|
||||||
FROM sharing
|
FROM sharing
|
||||||
INNER JOIN auth ON auth.id = sharing.user_id
|
INNER JOIN auth ON auth.id = sharing.user_id
|
||||||
INNER JOIN conversation ON conversation.id = sharing.conversation_id
|
INNER JOIN conversation ON conversation.conversation_id = sharing.conversation_id AND conversation.user_id = sharing.user_id
|
||||||
WHERE sharing.hash = ?
|
WHERE sharing.hash = ?
|
||||||
`, hash).Scan(&shared.Username, &ref, &updated, &shared.Name, &uid, &cid); err != nil {
|
`, hash).Scan(&shared.Username, &ref, &updated, &shared.Name, &uid, &cid); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
Loading…
Reference in New Issue
Block a user