mirror of
https://github.com/coaidev/coai.git
synced 2025-05-28 09:20:18 +09:00
add sharing manage feature
This commit is contained in:
parent
248e2043a6
commit
3bc8b892fb
@ -10,6 +10,7 @@ import {
|
||||
Boxes,
|
||||
CalendarPlus,
|
||||
Cloud,
|
||||
ListStart,
|
||||
Menu,
|
||||
Plug,
|
||||
} 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 openSub } from "./store/subscription.ts";
|
||||
import { openDialog as openApiDialog } from "./store/api.ts";
|
||||
import { openDialog as openSharingDialog } from "./store/sharing.ts";
|
||||
import Package from "./routes/Package.tsx";
|
||||
import Subscription from "./routes/Subscription.tsx";
|
||||
import ApiKey from "./routes/ApiKey.tsx";
|
||||
import ShareManagement from "./routes/ShareManagement.tsx";
|
||||
|
||||
function Settings() {
|
||||
const { t } = useTranslation();
|
||||
@ -78,6 +81,10 @@ function Settings() {
|
||||
<Boxes className={`h-4 w-4 mr-1`} />
|
||||
{t("pkg.title")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => dispatch(openSharingDialog())}>
|
||||
<ListStart className={`h-4 w-4 mr-1`} />
|
||||
{t("share.manage")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => dispatch(openApiDialog())}>
|
||||
<Plug className={`h-4 w-4 mr-1`} />
|
||||
{t("api.title")}
|
||||
@ -149,6 +156,7 @@ function App() {
|
||||
<ApiKey />
|
||||
<Package />
|
||||
<Subscription />
|
||||
<ShareManagement />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
--secondary: 210 40% 96.1%;
|
||||
--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%;
|
||||
|
||||
--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 {Message} from "../../conversation/types.ts";
|
||||
import {useSelector} from "react-redux";
|
||||
import {selectCurrent, selectMessages} from "../../store/chat.ts";
|
||||
import {Button} from "../ui/button.tsx";
|
||||
import {ChevronDown} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Message } from "../../conversation/types.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectCurrent, selectMessages } from "../../store/chat.ts";
|
||||
import { Button } from "../ui/button.tsx";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import MessageSegment from "../Message.tsx";
|
||||
import {connectionEvent} from "../../events/connection.ts";
|
||||
import { connectionEvent } from "../../events/connection.ts";
|
||||
|
||||
function ChatInterface() {
|
||||
const ref = useRef(null);
|
||||
|
@ -1,18 +1,28 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import FileProvider, {FileObject} from "../FileProvider.tsx";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {selectAuthenticated, selectInit} from "../../store/auth.ts";
|
||||
import {selectMessages, selectModel, selectWeb, setWeb} from "../../store/chat.ts";
|
||||
import {manager} from "../../conversation/manager.ts";
|
||||
import {formatMessage} from "../../utils.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import FileProvider, { FileObject } from "../FileProvider.tsx";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectAuthenticated, selectInit } from "../../store/auth.ts";
|
||||
import {
|
||||
selectMessages,
|
||||
selectModel,
|
||||
selectWeb,
|
||||
setWeb,
|
||||
} from "../../store/chat.ts";
|
||||
import { manager } from "../../conversation/manager.ts";
|
||||
import { formatMessage } from "../../utils.ts";
|
||||
import ChatInterface from "./ChatInterface.tsx";
|
||||
import {Button} from "../ui/button.tsx";
|
||||
import { Button } from "../ui/button.tsx";
|
||||
import router from "../../router.ts";
|
||||
import {BookMarked, ChevronRight, FolderKanban, Globe} from "lucide-react";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "../ui/tooltip.tsx";
|
||||
import {Toggle} from "../ui/toggle.tsx";
|
||||
import {Input} from "../ui/input.tsx";
|
||||
import { BookMarked, ChevronRight, FolderKanban, Globe } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
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 ModelSelector from "./ModelSelector.tsx";
|
||||
|
||||
|
@ -1,27 +1,41 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "../../store";
|
||||
import {selectAuthenticated} from "../../store/auth.ts";
|
||||
import {selectCurrent, selectHistory} from "../../store/chat.ts";
|
||||
import {useRef, useState} from "react";
|
||||
import {ConversationInstance} from "../../conversation/types.ts";
|
||||
import {useToast} from "../ui/use-toast.ts";
|
||||
import {copyClipboard, extractMessage, filterMessage, 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 { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "../../store";
|
||||
import { selectAuthenticated } from "../../store/auth.ts";
|
||||
import { selectCurrent, selectHistory } from "../../store/chat.ts";
|
||||
import { useRef, useState } from "react";
|
||||
import { ConversationInstance } from "../../conversation/types.ts";
|
||||
import { useToast } from "../ui/use-toast.ts";
|
||||
import {
|
||||
copyClipboard,
|
||||
extractMessage,
|
||||
filterMessage,
|
||||
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 {
|
||||
AlertDialog, AlertDialogAction, AlertDialogCancel,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription, AlertDialogFooter,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
AlertDialogTitle,
|
||||
} from "../ui/alert-dialog.tsx";
|
||||
import {shareConversation} from "../../conversation/sharing.ts";
|
||||
import {Input} from "../ui/input.tsx";
|
||||
import {login} from "../../conf.ts";
|
||||
import {getSharedLink, shareConversation} from "../../conversation/sharing.ts";
|
||||
import { Input } from "../ui/input.tsx";
|
||||
import { login } from "../../conf.ts";
|
||||
|
||||
function SideBar() {
|
||||
const { t } = useTranslation();
|
||||
@ -185,7 +199,7 @@ function SideBar() {
|
||||
operateConversation?.target?.id || -1,
|
||||
);
|
||||
if (resp.status)
|
||||
setShared(`${location.origin}/share/${resp.data}`);
|
||||
setShared(getSharedLink(resp.data));
|
||||
else
|
||||
toast({
|
||||
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 { Model } from "./conversation/types.ts";
|
||||
|
||||
export const version = "3.4.6";
|
||||
export const version = "3.5.0";
|
||||
export const deploy: boolean = true;
|
||||
export let rest_api: string = "http://localhost:8094";
|
||||
export let ws_api: string = "ws://localhost:8094";
|
||||
|
@ -7,6 +7,13 @@ export type SharingForm = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type SharingPreviewForm = {
|
||||
name: string;
|
||||
conversation_id: number;
|
||||
hash: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
export type ViewData = {
|
||||
name: string;
|
||||
username: string;
|
||||
@ -20,6 +27,17 @@ export type ViewForm = {
|
||||
data: ViewData | null;
|
||||
};
|
||||
|
||||
export type ListSharingResponse = {
|
||||
status: boolean;
|
||||
message: string;
|
||||
data?: SharingPreviewForm[];
|
||||
};
|
||||
|
||||
export type DeleteSharingResponse = {
|
||||
status: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export async function shareConversation(
|
||||
id: number,
|
||||
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-description":
|
||||
"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: {
|
||||
title: "Open Docs",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
cn: {
|
||||
@ -362,10 +367,15 @@ const resources = {
|
||||
"not-found": "对话未找到",
|
||||
"not-found-description":
|
||||
"对话未找到,请检查链接是否正确或对话是否已被删除",
|
||||
manage: "分享管理",
|
||||
"sync-error": "同步失败",
|
||||
name: "对话标题",
|
||||
time: "时间",
|
||||
action: "操作",
|
||||
},
|
||||
docs: {
|
||||
title: "开放文档",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
ru: {
|
||||
@ -549,10 +559,15 @@ const resources = {
|
||||
"not-found": "Разговор не найден",
|
||||
"not-found-description":
|
||||
"Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален",
|
||||
manage: "Управление обменом",
|
||||
"sync-error": "Ошибка синхронизации",
|
||||
name: "Название разговора",
|
||||
time: "Время",
|
||||
action: "Действие",
|
||||
},
|
||||
docs: {
|
||||
title: "Открыть документы",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -173,7 +173,7 @@ function Generation() {
|
||||
console.debug(
|
||||
`[generation] create generation request (prompt: ${prompt}, model: ${model})`,
|
||||
);
|
||||
return manager.generateWithBlock(prompt, model,);
|
||||
return manager.generateWithBlock(prompt, model);
|
||||
}}
|
||||
/>
|
||||
</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`} />
|
||||
<p className={`title`}>{t("share.not-found")}</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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { insertStart } from "../utils.ts";
|
||||
import { RootState } from "./index.ts";
|
||||
@ -14,8 +14,10 @@ type initialStateType = {
|
||||
};
|
||||
|
||||
function GetModel(model: string | undefined | null): string {
|
||||
return model && supportModels.filter((item: Model) => item.id === model).length
|
||||
? model : supportModels[0].id;
|
||||
return model &&
|
||||
supportModels.filter((item: Model) => item.id === model).length
|
||||
? model
|
||||
: supportModels[0].id;
|
||||
}
|
||||
|
||||
const chatSlice = createSlice({
|
||||
|
@ -6,6 +6,7 @@ import quotaReducer from "./quota";
|
||||
import packageReducer from "./package";
|
||||
import subscriptionReducer from "./subscription";
|
||||
import apiReducer from "./api";
|
||||
import sharingReducer from "./sharing";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
@ -16,6 +17,7 @@ const store = configureStore({
|
||||
package: packageReducer,
|
||||
subscription: subscriptionReducer,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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("/load", LoadAPI)
|
||||
router.GET("/delete", DeleteAPI)
|
||||
|
||||
// share
|
||||
router.POST("/share", ShareAPI)
|
||||
router.GET("/view", ViewAPI)
|
||||
router.GET("/share/list", ListSharingAPI)
|
||||
router.GET("/share/delete", DeleteSharingAPI)
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,13 @@ import (
|
||||
"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 {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
@ -73,6 +80,54 @@ func GetSharedMessages(db *sql.DB, userId int64, conversationId int64, refs []st
|
||||
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) {
|
||||
var shared SharedForm
|
||||
var (
|
||||
|
Loading…
Reference in New Issue
Block a user