feat: 新增聊天记录导出功能及密码缓存优化

新增消息导出组件,支持JSON/Markdown格式转换
优化密码修改逻辑,增加缓存清除机制
更新前端依赖库版本
添加多语言支持及导出按钮集成到聊天界面
This commit is contained in:
GreenDreamer 2025-02-23 17:57:51 +08:00
parent 4d841c4a33
commit f763b50084
7 changed files with 256 additions and 2 deletions

View File

@ -118,10 +118,20 @@ func passwordMigration(db *sql.DB, cache *redis.Client, id int64, password strin
if len(password) < 6 || len(password) > 36 {
return fmt.Errorf("password length must be between 6 and 36")
}
var username string
hash_passwd := utils.Sha2Encrypt(password)
err_u := db.QueryRow("SELECT username FROM auth WHERE id = ?", id).Scan(&username)
if err_u != nil {
return fmt.Errorf("failed to fetch username: %v", err_u)
}
cacheKey := fmt.Sprintf("nio:user:%s", username)
if err_u := cache.Del(context.Background(), cacheKey).Err(); err_u != nil {
return fmt.Errorf("failed to delete cache: %v", err_u)
}
_, err := globals.ExecDb(db, `
UPDATE auth SET password = ? WHERE id = ?
`, utils.Sha2Encrypt(password), id)
`, hash_passwd, id)
cache.Del(context.Background(), fmt.Sprint("nio:user:root"))

View File

@ -46,7 +46,7 @@
"html-to-image": "^1.11.11",
"i18next": "^23.4.6",
"localforage": "^1.10.0",
"lucide-react": "^0.309.0",
"lucide-react": "^0.320.0",
"match-sorter": "^6.3.1",
"mermaid": "^10.9.0",
"next-themes": "^0.2.1",

View File

@ -0,0 +1,178 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog.tsx";
import { Image, MenuSquare, PanelRight, ClipboardPaste } from "lucide-react";
import { useTranslation } from "react-i18next";
import "@/assets/common/editor.less";
import MarkdownExport from "./Markdown.tsx";
import React, { useMemo, useState } from "react";
import { Toggle } from "./ui/toggle.tsx";
import { mobile } from "@/utils/device.ts";
import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
import { Message } from "@/api/types.tsx";
import {
useMessages,
} from "@/store/chat.ts";
type ExportAllMsgButtonProps = {
maxLength?: number;
formatter?: (value: string) => string;
title?: string;
open?: boolean;
setOpen?: (open: boolean) => void;
children?: React.ReactNode;
submittable?: boolean;
onSubmit?: (value: string) => void;
closeOnSubmit?: boolean;
};
function ExportAllMsgButtonCall({
formatter,
}: ExportAllMsgButtonProps) {
const { t } = useTranslation();
const [openPreview, setOpenPreview] = useState(!mobile);
const [openInput, setOpenInput] = useState(true);
const messages: Message[] = useMessages();
const jsonArray = messages.map(message => ({
role: message.role,
content: message.content
}));
const jsonString = `\`\`\`json\n${JSON.stringify(jsonArray, null, 4)}\n\`\`\``;
function convertToMarkdown(jsonArray: { role: any; content: any; }[]) {
return jsonArray.map(({ role, content }) => {
const roleText = role === 'user' ? 'Óû§Ëµ' : 'AI˵';
return `## ${roleText}\n\n${content}`;
}).join('\n\n');
}
const markdownString = convertToMarkdown(jsonArray);
const markdownValue = useMemo(() => {
return formatter ? formatter(markdownString) : markdownString;
}, [markdownString, formatter]);
return (
<div className={`editor-container`}>
<div className={`editor-toolbar`}>
<div className={`grow`} />
<Toggle
variant={`outline`}
className={`h-8 w-8 p-0`}
pressed={openInput && !openPreview}
onClick={() => {
setOpenPreview(false);
setOpenInput(true);
}}
>
<MenuSquare className={`h-3.5 w-3.5`} />
</Toggle>
<Toggle
variant={`outline`}
className={`h-8 w-8 p-0`}
pressed={openInput && openPreview}
onClick={() => {
setOpenPreview(true);
setOpenInput(true);
}}
>
<PanelRight className={`h-3.5 w-3.5`} />
</Toggle>
<Toggle
variant={`outline`}
className={`h-8 w-8 p-0`}
pressed={!openInput && openPreview}
onClick={() => {
setOpenPreview(true);
setOpenInput(false);
}}
>
<Image className={`h-3.5 w-3.5`} />
</Toggle>
</div>
<div className={`editor-wrapper`}>
<div
className={cn(
"editor-object",
openInput && "show-editor",
openPreview && "show-preview",
)}
>
{openInput && (
<MarkdownExport
className={cn(
`transition-all`
)}
loading={true}
children={jsonString}
/>
)}
{openPreview && (
<MarkdownExport
loading={true}
children={markdownValue}
/>
)}
</div>
</div>
</div>
);
}
function ExportAllMsgButton(props: ExportAllMsgButtonProps) {
const { t } = useTranslation();
return (
<>
<Dialog open={props.open} onOpenChange={props.setOpen}>
{!props.setOpen && (
<DialogTrigger asChild>
{props.children ?? (
<ChatAction text={t("export.text")}>
<ClipboardPaste className={`h-4 w-4`} />
</ChatAction>
)}
</DialogTrigger>
)}
<DialogContent className={`editor-dialog flex-dialog`}>
<DialogHeader>
<DialogTitle>{props.title ?? t("export.user_used")}</DialogTitle>
<DialogDescription asChild>
<ExportAllMsgButtonCall {...props}>
</ExportAllMsgButtonCall>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
);
}
export default ExportAllMsgButton;
export function JSONTransMarkdownProvider({ ...props }: ExportAllMsgButtonProps) {
return (
<ExportAllMsgButton
{...props}
formatter={(value) => `\`\`\`markdown\n${value}\n\`\`\``}
/>
);
}

View File

@ -96,6 +96,52 @@ function Markdown({
[processedContent, acceptHtml, codeStyle, className, loading],
);
}
type MarkdownExportProps = {
children: string;
className?: string;
acceptHtml?: boolean;
codeStyle?: string;
loading?: boolean;
};
function MarkdownExport({
children,
acceptHtml,
codeStyle,
className,
loading,
}: MarkdownExportProps) {
const processedContent = useMemo(() => {
let content = children;
// Inline math: replace \(...\) with $ ... $
content = content.replace(/\\\((.*?)\\\)/g, (_, equation) => `$ ${equation.trim()} $`);
// Block math: replace \[...\] with $$...$$ on separate lines
content = content.replace(
/\s*\\\[\s*([\s\S]*?)\s*\\\]\s*/g,
(_, equation) => `\n$$\n${equation.trim()}\n$$\n`
);
return content;
}, [children]);
return useMemo(
() => (
<MarkdownContent
children={processedContent}
acceptHtml={acceptHtml}
codeStyle={codeStyle}
className={className}
loading={loading}
/>
),
[processedContent, acceptHtml, codeStyle, className, loading],
);
}
export { MarkdownExport };
type CodeMarkdownProps = MarkdownProps & {
filename: string;
};

View File

@ -35,6 +35,7 @@ import ScrollAction from "@/components/home/assemblies/ScrollAction.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
import { goAuth } from "@/utils/app.ts";
import { getModelFromId } from "@/conf/model.ts";
import { JSONTransMarkdownProvider } from "@/components/ExportAllMsgButton.tsx";
type InterfaceProps = {
scrollable: boolean;
@ -180,6 +181,7 @@ function ChatWrapper() {
<MaskAction />
<MarketAction />
<SettingsAction />
<JSONTransMarkdownProvider />
</div>
<div className={`input-wrapper`}>
<div className={`chat-box no-scrollbar`}>

View File

@ -108,6 +108,19 @@ export function SettingsAction() {
</ChatAction>
);
}
export function ExportButtonAction() {
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<ChatAction
text={t("export.markdown")}
onClick={() => dispatch(openDialog())}
>
<Settings className={`h-4 w-4`} />
</ChatAction>
);
}
export function MarketAction() {
const { t } = useTranslation();

View File

@ -328,6 +328,11 @@
"update-success": "更新成功",
"update-success-prompt": "您已更新至最新版本。"
},
"export":{
"text": "导出文本",
"user_used": "用户自己导出",
"pdf": "导出成pdf"
},
"share": {
"title": "分享",
"share-conversation": "分享对话",