mirror of
https://github.com/coaidev/coai.git
synced 2025-05-29 09:50:16 +09:00
add sharing manage feature
This commit is contained in:
parent
248e2043a6
commit
3bc8b892fb
@ -10,6 +10,7 @@ import {
|
|||||||
Boxes,
|
Boxes,
|
||||||
CalendarPlus,
|
CalendarPlus,
|
||||||
Cloud,
|
Cloud,
|
||||||
|
ListStart,
|
||||||
Menu,
|
Menu,
|
||||||
Plug,
|
Plug,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@ -39,9 +40,11 @@ import { openDialog as openQuotaDialog, quotaSelector } from "./store/quota.ts";
|
|||||||
import { openDialog as openPackageDialog } from "./store/package.ts";
|
import { openDialog as openPackageDialog } from "./store/package.ts";
|
||||||
import { openDialog as openSub } from "./store/subscription.ts";
|
import { openDialog as openSub } from "./store/subscription.ts";
|
||||||
import { openDialog as openApiDialog } from "./store/api.ts";
|
import { openDialog as openApiDialog } from "./store/api.ts";
|
||||||
|
import { openDialog as openSharingDialog } from "./store/sharing.ts";
|
||||||
import Package from "./routes/Package.tsx";
|
import Package from "./routes/Package.tsx";
|
||||||
import Subscription from "./routes/Subscription.tsx";
|
import Subscription from "./routes/Subscription.tsx";
|
||||||
import ApiKey from "./routes/ApiKey.tsx";
|
import ApiKey from "./routes/ApiKey.tsx";
|
||||||
|
import ShareManagement from "./routes/ShareManagement.tsx";
|
||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -78,6 +81,10 @@ function Settings() {
|
|||||||
<Boxes className={`h-4 w-4 mr-1`} />
|
<Boxes className={`h-4 w-4 mr-1`} />
|
||||||
{t("pkg.title")}
|
{t("pkg.title")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => dispatch(openSharingDialog())}>
|
||||||
|
<ListStart className={`h-4 w-4 mr-1`} />
|
||||||
|
{t("share.manage")}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => dispatch(openApiDialog())}>
|
<DropdownMenuItem onClick={() => dispatch(openApiDialog())}>
|
||||||
<Plug className={`h-4 w-4 mr-1`} />
|
<Plug className={`h-4 w-4 mr-1`} />
|
||||||
{t("api.title")}
|
{t("api.title")}
|
||||||
@ -149,6 +156,7 @@ function App() {
|
|||||||
<ApiKey />
|
<ApiKey />
|
||||||
<Package />
|
<Package />
|
||||||
<Subscription />
|
<Subscription />
|
||||||
|
<ShareManagement />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--muted: 240 4.8% 95.9%;
|
--muted: 60 26% 92%;
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
--accent: 37 26% 90%;
|
--accent: 37 26% 90%;
|
||||||
|
11
app/src/assets/share-manager.less
Normal file
11
app/src/assets/share-manager.less
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.share-table {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import {useEffect, useRef, useState} from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {Message} from "../../conversation/types.ts";
|
import { Message } from "../../conversation/types.ts";
|
||||||
import {useSelector} from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import {selectCurrent, selectMessages} from "../../store/chat.ts";
|
import { selectCurrent, selectMessages } from "../../store/chat.ts";
|
||||||
import {Button} from "../ui/button.tsx";
|
import { Button } from "../ui/button.tsx";
|
||||||
import {ChevronDown} from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import MessageSegment from "../Message.tsx";
|
import MessageSegment from "../Message.tsx";
|
||||||
import {connectionEvent} from "../../events/connection.ts";
|
import { connectionEvent } from "../../events/connection.ts";
|
||||||
|
|
||||||
function ChatInterface() {
|
function ChatInterface() {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import FileProvider, {FileObject} from "../FileProvider.tsx";
|
import FileProvider, { FileObject } from "../FileProvider.tsx";
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {selectAuthenticated, selectInit} from "../../store/auth.ts";
|
import { selectAuthenticated, selectInit } from "../../store/auth.ts";
|
||||||
import {selectMessages, selectModel, selectWeb, setWeb} from "../../store/chat.ts";
|
import {
|
||||||
import {manager} from "../../conversation/manager.ts";
|
selectMessages,
|
||||||
import {formatMessage} from "../../utils.ts";
|
selectModel,
|
||||||
|
selectWeb,
|
||||||
|
setWeb,
|
||||||
|
} from "../../store/chat.ts";
|
||||||
|
import { manager } from "../../conversation/manager.ts";
|
||||||
|
import { formatMessage } from "../../utils.ts";
|
||||||
import ChatInterface from "./ChatInterface.tsx";
|
import ChatInterface from "./ChatInterface.tsx";
|
||||||
import {Button} from "../ui/button.tsx";
|
import { Button } from "../ui/button.tsx";
|
||||||
import router from "../../router.ts";
|
import router from "../../router.ts";
|
||||||
import {BookMarked, ChevronRight, FolderKanban, Globe} from "lucide-react";
|
import { BookMarked, ChevronRight, FolderKanban, Globe } from "lucide-react";
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "../ui/tooltip.tsx";
|
import {
|
||||||
import {Toggle} from "../ui/toggle.tsx";
|
Tooltip,
|
||||||
import {Input} from "../ui/input.tsx";
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../ui/tooltip.tsx";
|
||||||
|
import { Toggle } from "../ui/toggle.tsx";
|
||||||
|
import { Input } from "../ui/input.tsx";
|
||||||
import EditorProvider from "../EditorProvider.tsx";
|
import EditorProvider from "../EditorProvider.tsx";
|
||||||
import ModelSelector from "./ModelSelector.tsx";
|
import ModelSelector from "./ModelSelector.tsx";
|
||||||
|
|
||||||
|
@ -1,27 +1,41 @@
|
|||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {RootState} from "../../store";
|
import { RootState } from "../../store";
|
||||||
import {selectAuthenticated} from "../../store/auth.ts";
|
import { selectAuthenticated } from "../../store/auth.ts";
|
||||||
import {selectCurrent, selectHistory} from "../../store/chat.ts";
|
import { selectCurrent, selectHistory } from "../../store/chat.ts";
|
||||||
import {useRef, useState} from "react";
|
import { useRef, useState } from "react";
|
||||||
import {ConversationInstance} from "../../conversation/types.ts";
|
import { ConversationInstance } from "../../conversation/types.ts";
|
||||||
import {useToast} from "../ui/use-toast.ts";
|
import { useToast } from "../ui/use-toast.ts";
|
||||||
import {copyClipboard, extractMessage, filterMessage, mobile, useAnimation, useEffectAsync} from "../../utils.ts";
|
import {
|
||||||
import {deleteConversation, toggleConversation, updateConversationList} from "../../conversation/history.ts";
|
copyClipboard,
|
||||||
import {Button} from "../ui/button.tsx";
|
extractMessage,
|
||||||
import {setMenu} from "../../store/menu.ts";
|
filterMessage,
|
||||||
import {Copy, LogIn, Plus, RotateCw} from "lucide-react";
|
mobile,
|
||||||
|
useAnimation,
|
||||||
|
useEffectAsync,
|
||||||
|
} from "../../utils.ts";
|
||||||
|
import {
|
||||||
|
deleteConversation,
|
||||||
|
toggleConversation,
|
||||||
|
updateConversationList,
|
||||||
|
} from "../../conversation/history.ts";
|
||||||
|
import { Button } from "../ui/button.tsx";
|
||||||
|
import { setMenu } from "../../store/menu.ts";
|
||||||
|
import { Copy, LogIn, Plus, RotateCw } from "lucide-react";
|
||||||
import ConversationSegment from "./ConversationSegment.tsx";
|
import ConversationSegment from "./ConversationSegment.tsx";
|
||||||
import {
|
import {
|
||||||
AlertDialog, AlertDialogAction, AlertDialogCancel,
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription, AlertDialogFooter,
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle,
|
||||||
} from "../ui/alert-dialog.tsx";
|
} from "../ui/alert-dialog.tsx";
|
||||||
import {shareConversation} from "../../conversation/sharing.ts";
|
import {getSharedLink, shareConversation} from "../../conversation/sharing.ts";
|
||||||
import {Input} from "../ui/input.tsx";
|
import { Input } from "../ui/input.tsx";
|
||||||
import {login} from "../../conf.ts";
|
import { login } from "../../conf.ts";
|
||||||
|
|
||||||
function SideBar() {
|
function SideBar() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -185,7 +199,7 @@ function SideBar() {
|
|||||||
operateConversation?.target?.id || -1,
|
operateConversation?.target?.id || -1,
|
||||||
);
|
);
|
||||||
if (resp.status)
|
if (resp.status)
|
||||||
setShared(`${location.origin}/share/${resp.data}`);
|
setShared(getSharedLink(resp.data));
|
||||||
else
|
else
|
||||||
toast({
|
toast({
|
||||||
title: t("share.failed"),
|
title: t("share.failed"),
|
||||||
|
114
app/src/components/ui/table.tsx
Normal file
114
app/src/components/ui/table.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "./lib/utils.ts"
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
Table.displayName = "Table"
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = "TableHeader"
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableBody.displayName = "TableBody"
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn("bg-primary font-medium text-primary-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = "TableFooter"
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = "TableHead"
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = "TableCell"
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCaption.displayName = "TableCaption"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Model } from "./conversation/types.ts";
|
import { Model } from "./conversation/types.ts";
|
||||||
|
|
||||||
export const version = "3.4.6";
|
export const version = "3.5.0";
|
||||||
export const deploy: boolean = true;
|
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";
|
||||||
|
@ -7,6 +7,13 @@ export type SharingForm = {
|
|||||||
data: string;
|
data: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SharingPreviewForm = {
|
||||||
|
name: string;
|
||||||
|
conversation_id: number;
|
||||||
|
hash: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ViewData = {
|
export type ViewData = {
|
||||||
name: string;
|
name: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -20,6 +27,17 @@ export type ViewForm = {
|
|||||||
data: ViewData | null;
|
data: ViewData | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ListSharingResponse = {
|
||||||
|
status: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: SharingPreviewForm[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSharingResponse = {
|
||||||
|
status: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
export async function shareConversation(
|
export async function shareConversation(
|
||||||
id: number,
|
id: number,
|
||||||
refs: number[] = [-1],
|
refs: number[] = [-1],
|
||||||
@ -44,3 +62,33 @@ export async function viewConversation(hash: string): Promise<ViewForm> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listSharing(): Promise<ListSharingResponse> {
|
||||||
|
try {
|
||||||
|
const resp = await axios.get("/conversation/share/list");
|
||||||
|
return resp.data as ListSharingResponse;
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
message: (e as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSharing(
|
||||||
|
hash: string,
|
||||||
|
): Promise<DeleteSharingResponse> {
|
||||||
|
try {
|
||||||
|
const resp = await axios.get(`/conversation/share/delete?hash=${hash}`);
|
||||||
|
return resp.data as DeleteSharingResponse;
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
message: (e as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSharedLink(hash: string): string {
|
||||||
|
return `${location.origin}/share/${hash}`;
|
||||||
|
}
|
||||||
|
@ -186,10 +186,15 @@ const resources = {
|
|||||||
"not-found": "Conversation not found",
|
"not-found": "Conversation not found",
|
||||||
"not-found-description":
|
"not-found-description":
|
||||||
"Conversation not found, please check if the link is correct or the conversation has been deleted",
|
"Conversation not found, please check if the link is correct or the conversation has been deleted",
|
||||||
|
manage: "Manage Share",
|
||||||
|
"sync-error": "Sync Error",
|
||||||
|
name: "Conversation Title",
|
||||||
|
time: "Time",
|
||||||
|
action: "Action",
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
title: "Open Docs",
|
title: "Open Docs",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cn: {
|
cn: {
|
||||||
@ -362,10 +367,15 @@ const resources = {
|
|||||||
"not-found": "对话未找到",
|
"not-found": "对话未找到",
|
||||||
"not-found-description":
|
"not-found-description":
|
||||||
"对话未找到,请检查链接是否正确或对话是否已被删除",
|
"对话未找到,请检查链接是否正确或对话是否已被删除",
|
||||||
|
manage: "分享管理",
|
||||||
|
"sync-error": "同步失败",
|
||||||
|
name: "对话标题",
|
||||||
|
time: "时间",
|
||||||
|
action: "操作",
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
title: "开放文档",
|
title: "开放文档",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
@ -549,10 +559,15 @@ const resources = {
|
|||||||
"not-found": "Разговор не найден",
|
"not-found": "Разговор не найден",
|
||||||
"not-found-description":
|
"not-found-description":
|
||||||
"Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален",
|
"Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален",
|
||||||
|
manage: "Управление обменом",
|
||||||
|
"sync-error": "Ошибка синхронизации",
|
||||||
|
name: "Название разговора",
|
||||||
|
time: "Время",
|
||||||
|
action: "Действие",
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
title: "Открыть документы",
|
title: "Открыть документы",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -173,7 +173,7 @@ function Generation() {
|
|||||||
console.debug(
|
console.debug(
|
||||||
`[generation] create generation request (prompt: ${prompt}, model: ${model})`,
|
`[generation] create generation request (prompt: ${prompt}, model: ${model})`,
|
||||||
);
|
);
|
||||||
return manager.generateWithBlock(prompt, model,);
|
return manager.generateWithBlock(prompt, model);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
137
app/src/routes/ShareManagement.tsx
Normal file
137
app/src/routes/ShareManagement.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import "../assets/share-manager.less";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {dialogSelector, dataSelector, syncData, deleteData} from "../store/sharing.ts";
|
||||||
|
import { useToast } from "../components/ui/use-toast.ts";
|
||||||
|
import { selectInit } from "../store/auth.ts";
|
||||||
|
import {useEffectAsync} from "../utils.ts";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from "../components/ui/dialog.tsx";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "../components/ui/table.tsx";
|
||||||
|
import {closeDialog, setDialog} from "../store/sharing.ts";
|
||||||
|
import {Button} from "../components/ui/button.tsx";
|
||||||
|
import {useMemo} from "react";
|
||||||
|
import {Eye, MoreHorizontal, Trash2} from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "../components/ui/dropdown-menu.tsx";
|
||||||
|
import {getSharedLink, SharingPreviewForm} from "../conversation/sharing.ts";
|
||||||
|
|
||||||
|
|
||||||
|
type ShareTableProps = {
|
||||||
|
data: SharingPreviewForm[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShareTable({ data }: ShareTableProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const time = useMemo(() => {
|
||||||
|
return data.map((row) => {
|
||||||
|
const date = new Date(row.time);
|
||||||
|
return `${date.getMonth()}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table className={`mt-5`}>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>{t('share.name')}</TableHead>
|
||||||
|
<TableHead>{t('share.time')}</TableHead>
|
||||||
|
<TableHead>{t('share.action')}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((row, idx) => (
|
||||||
|
<TableRow key={idx}>
|
||||||
|
<TableCell>{row.conversation_id}</TableCell>
|
||||||
|
<TableCell>{row.name}</TableCell>
|
||||||
|
<TableCell className={`whitespace-nowrap`}>{time[idx]}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant={`outline`} size={`icon`}>
|
||||||
|
<MoreHorizontal className={`h-4 w-4`} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align={`center`}>
|
||||||
|
<DropdownMenuItem onClick={() => {
|
||||||
|
window.open(getSharedLink(row.hash), '_blank');
|
||||||
|
}}>
|
||||||
|
<Eye className={`h-4 w-4 mr-1`} />
|
||||||
|
{t("share.view")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={async () => {
|
||||||
|
await deleteData(dispatch, row.hash);
|
||||||
|
}}>
|
||||||
|
<Trash2 className={`h-4 w-4 mr-1`} />
|
||||||
|
{t("conversation.delete")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShareManagement() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const open = useSelector(dialogSelector);
|
||||||
|
const data = useSelector(dataSelector);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const init = useSelector(selectInit);
|
||||||
|
|
||||||
|
useEffectAsync(async () => {
|
||||||
|
if (init) {
|
||||||
|
const resp = await syncData(dispatch);
|
||||||
|
if (resp) {
|
||||||
|
toast({
|
||||||
|
title: t("share.sync-error"),
|
||||||
|
description: resp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [init]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(open) => dispatch(setDialog(open))}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("share.manage")}</DialogTitle>
|
||||||
|
<DialogDescription className={`share-table`}>
|
||||||
|
<ShareTable data={data} />
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant={`outline`} onClick={() => dispatch(closeDialog())}>
|
||||||
|
{t("close")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShareManagement;
|
@ -116,7 +116,9 @@ function Sharing() {
|
|||||||
<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>
|
||||||
<Button className={`mt-4`} onClick={() => router.navigate("/")}>{t("home")}</Button>
|
<Button className={`mt-4`} onClick={() => router.navigate("/")}>
|
||||||
|
{t("home")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import {ConversationInstance, Model} from "../conversation/types.ts";
|
import { ConversationInstance, Model } from "../conversation/types.ts";
|
||||||
import { Message } from "../conversation/types.ts";
|
import { Message } from "../conversation/types.ts";
|
||||||
import { insertStart } from "../utils.ts";
|
import { insertStart } from "../utils.ts";
|
||||||
import { RootState } from "./index.ts";
|
import { RootState } from "./index.ts";
|
||||||
@ -14,8 +14,10 @@ type initialStateType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function GetModel(model: string | undefined | null): string {
|
function GetModel(model: string | undefined | null): string {
|
||||||
return model && supportModels.filter((item: Model) => item.id === model).length
|
return model &&
|
||||||
? model : supportModels[0].id;
|
supportModels.filter((item: Model) => item.id === model).length
|
||||||
|
? model
|
||||||
|
: supportModels[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatSlice = createSlice({
|
const chatSlice = createSlice({
|
||||||
|
@ -6,6 +6,7 @@ import quotaReducer from "./quota";
|
|||||||
import packageReducer from "./package";
|
import packageReducer from "./package";
|
||||||
import subscriptionReducer from "./subscription";
|
import subscriptionReducer from "./subscription";
|
||||||
import apiReducer from "./api";
|
import apiReducer from "./api";
|
||||||
|
import sharingReducer from "./sharing";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -16,6 +17,7 @@ const store = configureStore({
|
|||||||
package: packageReducer,
|
package: packageReducer,
|
||||||
subscription: subscriptionReducer,
|
subscription: subscriptionReducer,
|
||||||
api: apiReducer,
|
api: apiReducer,
|
||||||
|
sharing: sharingReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
69
app/src/store/sharing.ts
Normal file
69
app/src/store/sharing.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { AppDispatch, RootState } from "./index.ts";
|
||||||
|
import {
|
||||||
|
deleteSharing,
|
||||||
|
listSharing,
|
||||||
|
SharingPreviewForm,
|
||||||
|
} from "../conversation/sharing.ts";
|
||||||
|
|
||||||
|
export const sharingSlice = createSlice({
|
||||||
|
name: "sharing",
|
||||||
|
initialState: {
|
||||||
|
dialog: false,
|
||||||
|
data: [] as SharingPreviewForm[],
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
toggleDialog: (state) => {
|
||||||
|
state.dialog = !state.dialog;
|
||||||
|
},
|
||||||
|
setDialog: (state, action) => {
|
||||||
|
state.dialog = action.payload as boolean;
|
||||||
|
},
|
||||||
|
openDialog: (state) => {
|
||||||
|
state.dialog = true;
|
||||||
|
},
|
||||||
|
closeDialog: (state) => {
|
||||||
|
state.dialog = false;
|
||||||
|
},
|
||||||
|
setData: (state, action) => {
|
||||||
|
state.data = action.payload as SharingPreviewForm[];
|
||||||
|
},
|
||||||
|
removeData: (state, action) => {
|
||||||
|
const hash = action.payload as string;
|
||||||
|
state.data = state.data.filter((item) => item.hash !== hash);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
toggleDialog,
|
||||||
|
setDialog,
|
||||||
|
openDialog,
|
||||||
|
closeDialog,
|
||||||
|
setData,
|
||||||
|
removeData,
|
||||||
|
} = sharingSlice.actions;
|
||||||
|
export default sharingSlice.reducer;
|
||||||
|
|
||||||
|
export const dialogSelector = (state: RootState): boolean =>
|
||||||
|
state.sharing.dialog;
|
||||||
|
export const dataSelector = (state: RootState): SharingPreviewForm[] =>
|
||||||
|
state.sharing.data;
|
||||||
|
|
||||||
|
export const syncData = async (dispatch: AppDispatch): Promise<string> => {
|
||||||
|
const response = await listSharing();
|
||||||
|
|
||||||
|
if (response.status) dispatch(setData(response.data));
|
||||||
|
|
||||||
|
return response.status ? "" : response.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteData = async (
|
||||||
|
dispatch: AppDispatch,
|
||||||
|
hash: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
const response = await deleteSharing(hash);
|
||||||
|
if (response.status) dispatch(removeData(hash));
|
||||||
|
|
||||||
|
return response.status ? "" : response.message;
|
||||||
|
};
|
@ -162,3 +162,56 @@ func ViewAPI(c *gin.Context) {
|
|||||||
"data": shared,
|
"data": shared,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListSharingAPI(c *gin.Context) {
|
||||||
|
user := auth.GetUser(c)
|
||||||
|
if user == nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": false,
|
||||||
|
"message": "user not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := utils.GetDBFromContext(c)
|
||||||
|
data := ListSharedConversation(db, user)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": true,
|
||||||
|
"message": "",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSharingAPI(c *gin.Context) {
|
||||||
|
user := auth.GetUser(c)
|
||||||
|
if user == nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": false,
|
||||||
|
"message": "user not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := utils.GetDBFromContext(c)
|
||||||
|
hash := strings.TrimSpace(c.Query("hash"))
|
||||||
|
if hash == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": false,
|
||||||
|
"message": "invalid hash",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DeleteSharedConversation(db, user, hash); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -8,7 +8,11 @@ func Register(app *gin.Engine) {
|
|||||||
router.GET("/list", ListAPI)
|
router.GET("/list", ListAPI)
|
||||||
router.GET("/load", LoadAPI)
|
router.GET("/load", LoadAPI)
|
||||||
router.GET("/delete", DeleteAPI)
|
router.GET("/delete", DeleteAPI)
|
||||||
|
|
||||||
|
// share
|
||||||
router.POST("/share", ShareAPI)
|
router.POST("/share", ShareAPI)
|
||||||
router.GET("/view", ViewAPI)
|
router.GET("/view", ViewAPI)
|
||||||
|
router.GET("/share/list", ListSharingAPI)
|
||||||
|
router.GET("/share/delete", DeleteSharingAPI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SharedPreviewForm struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ConversationId int64 `json:"conversation_id"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
type SharedForm struct {
|
type SharedForm struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -73,6 +80,54 @@ func GetSharedMessages(db *sql.DB, userId int64, conversationId int64, refs []st
|
|||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListSharedConversation(db *sql.DB, user *auth.User) []SharedPreviewForm {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id := user.GetID(db)
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT conversation.conversation_name, conversation.conversation_id, sharing.updated_at, sharing.hash
|
||||||
|
FROM sharing
|
||||||
|
INNER JOIN conversation
|
||||||
|
ON conversation.conversation_id = sharing.conversation_id
|
||||||
|
AND conversation.user_id = sharing.user_id
|
||||||
|
WHERE sharing.user_id = ?
|
||||||
|
ORDER BY sharing.updated_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]SharedPreviewForm, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var updated []uint8
|
||||||
|
var form SharedPreviewForm
|
||||||
|
if err := rows.Scan(&form.Name, &form.ConversationId, &updated, &form.Hash); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Time = *utils.ConvertTime(updated)
|
||||||
|
result = append(result, form)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSharedConversation(db *sql.DB, user *auth.User, hash string) error {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id := user.GetID(db)
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
DELETE FROM sharing WHERE user_id = ? AND hash = ?
|
||||||
|
`, id, hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetSharedConversation(db *sql.DB, hash string) (*SharedForm, error) {
|
func GetSharedConversation(db *sql.DB, hash string) (*SharedForm, error) {
|
||||||
var shared SharedForm
|
var shared SharedForm
|
||||||
var (
|
var (
|
||||||
|
Loading…
Reference in New Issue
Block a user