diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 99d7981..2a33ac4 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -52,7 +52,7 @@ import HeadphoneIcon from "../icons/headphone.svg"; //20250317新增word excel图标 import ExcelIcon from "../icons/excel.svg"; import WordIcon from "../icons/word.svg"; -import MindIcon from "../icons/mind.svg" +import MindIcon from "../icons/mind.svg"; import { BOT_HELLO, @@ -66,6 +66,7 @@ import { useAppConfig, useChatStore, usePluginStore, + useMindMapStore, //新增消息仓库用于存储上下文至思维导图页面 } from "../store"; import { @@ -134,7 +135,8 @@ import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions"; //20250317新增 import { toExcel } from "../utils/excelAndWordUtils/export2Excel"; -import {exportWord} from "../utils/excelAndWordUtils/word"; +import { exportWord } from "../utils/excelAndWordUtils/word"; +import { getMindPrompt } from "../utils/prompt"; const localStorage = safeLocalStorage(); const ttsPlayer = createTTSPlayer(); @@ -181,7 +183,9 @@ function isTableContent(content: string): boolean { const hasTableSeparator = tablePattern.test(content); // 检查是否包含至少一行表格内容 - const hasTableRows = content.split("\n").some((line) => rowPattern.test(line)); + const hasTableRows = content + .split("\n") + .some((line) => rowPattern.test(line)); // 如果同时包含分隔符行和表格内容行,则认为是表格 return hasTableSeparator && hasTableRows; @@ -709,10 +713,11 @@ export function ChatActions(props: { <Selector defaultSelectedValue={`${currentModel}@${currentProviderName}`} items={models.map((m) => ({ - title: `${m.displayName}${m?.provider?.providerName - ? " (" + m?.provider?.providerName + ")" - : "" - }`, + title: `${m.displayName}${ + m?.provider?.providerName + ? " (" + m?.provider?.providerName + ")" + : "" + }`, value: `${m.name}@${m?.provider?.providerName}`, }))} onClose={() => setShowModelSelector(false)} @@ -1029,9 +1034,9 @@ function _Chat() { const scrollRef = useRef<HTMLDivElement>(null); const isScrolledToBottom = scrollRef?.current ? Math.abs( - scrollRef.current.scrollHeight - - (scrollRef.current.scrollTop + scrollRef.current.clientHeight), - ) <= 1 + scrollRef.current.scrollHeight - + (scrollRef.current.scrollTop + scrollRef.current.clientHeight), + ) <= 1 : false; const isAttachWithTop = useMemo(() => { const lastMessage = scrollRef.current?.lastElementChild as HTMLElement; @@ -1377,27 +1382,27 @@ function _Chat() { .concat( isLoading ? [ - { - ...createMessage({ - role: "assistant", - content: "……", - }), - preview: true, - }, - ] + { + ...createMessage({ + role: "assistant", + content: "……", + }), + preview: true, + }, + ] : [], ) .concat( userInput.length > 0 && config.sendPreviewBubble ? [ - { - ...createMessage({ - role: "user", - content: userInput, - }), - preview: true, - }, - ] + { + ...createMessage({ + role: "user", + content: userInput, + }), + preview: true, + }, + ] : [], ); }, [ @@ -1494,7 +1499,7 @@ function _Chat() { if (payload.key || payload.url) { showConfirm( Locale.URLCommand.Settings + - `\n${JSON.stringify(payload, null, 4)}`, + `\n${JSON.stringify(payload, null, 4)}`, ).then((res) => { if (!res) return; if (payload.key) { @@ -1704,6 +1709,36 @@ function _Chat() { const [showChatSidePanel, setShowChatSidePanel] = useState(false); + // 20250327新增思维导图导出 + function toMind(messages: RenderMessage[], content: string) { + const newMessages = []; + const { setMindMapData } = useMindMapStore.getState(); + // 遍历 messages 数组 + for (const message of messages) { + // 检查 content 类型并正确存储 + if (typeof message.content === "string") { + newMessages.push({ + role: message.role, + content: message.content, + }); + } else { + newMessages.push({ + role: message.role, + content: JSON.stringify(message.content), + }); + } + if (message.content === content) { + newMessages.push({ + role: "user", + content: getMindPrompt(content, true), + }); + break; + } + } + setMindMapData(newMessages, content); + navigate("/mind", { state: { msg: true } }); + } + return ( <> <div className={styles.chat} key={session.id}> @@ -1939,7 +1974,7 @@ function _Chat() { } /> {/* 以下 20250317 新增Word excel导出按钮 */} - {isTableContent(getMessageTextContent(message)) && ( + {/* {isTableContent(getMessageTextContent(message)) && ( <> <ChatAction text={Locale.Chat.Actions.Excel} @@ -1949,23 +1984,38 @@ function _Chat() { } /> </> - )} - - {!isTableContent(getMessageTextContent(message)) && ( + )} */} + <ChatAction + text={Locale.Chat.Actions.Excel} + icon={<ExcelIcon />} + onClick={() => + toExcel( + getMessageTextContent(message), + ) + } + /> + {!isTableContent( + getMessageTextContent(message), + ) && ( <> <ChatAction text={Locale.Chat.Actions.Word} icon={<WordIcon />} - onClick={() => exportWord( - getMessageTextContent(message), - )} + onClick={() => + exportWord( + getMessageTextContent(message), + ) + } /> <ChatAction text={Locale.Chat.Actions.Mind} icon={<MindIcon />} - onClick={() => exportWord( - getMessageTextContent(message), - )} + onClick={() => { + toMind( + messages, + getMessageTextContent(message), + ); + }} /> </> )} @@ -2065,7 +2115,7 @@ function _Chat() { <img className={ styles[ - "chat-message-item-image-multi" + "chat-message-item-image-multi" ] } key={index} diff --git a/app/components/mind/mind-panel.tsx b/app/components/mind/mind-panel.tsx index 3ab8445..ce638c3 100644 --- a/app/components/mind/mind-panel.tsx +++ b/app/components/mind/mind-panel.tsx @@ -22,9 +22,15 @@ export function MindPanel(props: MindPanelProps) { if (!inputValue.trim()) return message.error("请输入提示词!"); setIsLoading(true); try { - const prompt = getMindPrompt(inputValue); + const prompt = getMindPrompt(inputValue, false); const response = await chatStore.directLlmInvoke(prompt, "gpt-4o-mini"); console.log("原始响应:", response); + let cleanedContent = response.startsWith("```json") + ? response.substring(8) + : response; + if (cleanedContent.endsWith("```")) { + cleanedContent = cleanedContent.substring(0, cleanedContent.length - 4); + } const parsedData: MindElixirData = JSON.parse(response); console.log("解析后响应:", parsedData); // 增强校验逻辑 diff --git a/app/components/mind/mind.tsx b/app/components/mind/mind.tsx index 33c820e..2917530 100644 --- a/app/components/mind/mind.tsx +++ b/app/components/mind/mind.tsx @@ -8,7 +8,7 @@ import { useMobileScreen } from "@/app/utils"; import { IconButton } from "../button"; import Locale from "@/app/locales"; import { Path } from "@/app/constant"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import clsx from "clsx"; import { getClientConfig } from "@/app/config/client"; import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -18,6 +18,8 @@ import ReturnIcon from "@/app/icons/return.svg"; import MinIcon from "@/app/icons/min.svg"; import MaxIcon from "@/app/icons/max.svg"; import SDIcon from "@/app/icons/sd.svg"; +import { useChatStore, useMindMapStore } from "@/app/store"; +import { message } from "antd"; export function MindPage() { const isMobileScreen = useMobileScreen(); @@ -26,10 +28,14 @@ export function MindPage() { const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; const config = useAppConfig(); const scrollRef = useRef<HTMLDivElement>(null); - const isWriting = location.pathname === Path.Writing; + const isMind = location.pathname === Path.Mind; const [isLoading, setIsLoading] = useState(false); const containerRef = useRef<HTMLDivElement>(null); const mindInstance = useRef<InstanceType<typeof MindElixir> | null>(null); + const chatStore = useChatStore(); + const { newMessages, content } = useMindMapStore.getState(); + const query = useLocation(); + const { msg } = query.state || {}; const [data, setData] = useState<MindElixirData>({ nodeData: { id: "root", @@ -51,6 +57,46 @@ export function MindPage() { mindInstance.current = new MindElixir(options); mindInstance.current.init(data); + const fetchData = async () => { + if (msg) { + if (content) { + setIsLoading(true); + try { + const response = await chatStore.getMindData( + newMessages, + "gpt-4o-mini", + ); + console.log("原始响应:", response); + let cleanedContent = response.startsWith("```json") + ? response.substring(8) + : response; + if (cleanedContent.endsWith("```")) { + cleanedContent = cleanedContent.substring( + 0, + cleanedContent.length - 4, + ); + } + const parsedData: MindElixirData = JSON.parse(cleanedContent); + console.log("解析后响应:", parsedData); + // 增强校验逻辑 + if ( + !parsedData?.nodeData?.id || + !Array.isArray(parsedData.nodeData.children) + ) { + throw new Error("数据结构不完整"); + } + setData(parsedData); + } catch (error) { + console.log(error); + message.error("请求失败,请重试"); + } finally { + setIsLoading(false); // 确保关闭加载状态 + } + } + } + }; + fetchData(); + return () => { if (mindInstance.current) { mindInstance.current.destroy(); @@ -73,6 +119,13 @@ export function MindPage() { topic: "生成中....", }, }); + } else { + mindInstance.current?.refresh({ + nodeData: { + id: "root", + topic: "中心主题", + }, + }); } }, [isLoading]); @@ -133,7 +186,7 @@ export function MindPage() { return ( <> <MindSiderBar - className={clsx({ [homeStyles["sidebar-show"]]: isWriting })} + className={clsx({ [homeStyles["sidebar-show"]]: isMind })} setData={setData} isLoading={isLoading} setIsLoading={setIsLoading} diff --git a/app/store/chat.ts b/app/store/chat.ts index c58fbea..c96eab2 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -855,31 +855,31 @@ export const useChatStore = createPersistStore( } }, - async directLlmInvoke(content: string,model:string): Promise<string> { + async directLlmInvoke(content: string, model: string): Promise<string> { return new Promise((resolve, reject) => { const config = useAppConfig.getState(); const accessStore = useAccessStore.getState(); - + // 使用默认模型配置 const modelConfig = { ...config.modelConfig, - model: model, - providerName: accessStore.provider + model: model, + providerName: accessStore.provider, }; - + // 直接构造消息 const messages: ChatMessage[] = [ createMessage({ role: "user", content: fillTemplateWith(content, modelConfig), - }) + }), ]; - + const api: ClientApi = getClientApi(modelConfig.providerName); - + api.llm.chat({ messages, - config: { + config: { ...modelConfig, stream: false, // 关闭流式响应 }, @@ -887,13 +887,94 @@ export const useChatStore = createPersistStore( resolve(message); }, onError(error) { - reject(error instanceof Error ? error : new Error(prettyObject(error))); - } + reject( + error instanceof Error ? error : new Error(prettyObject(error)), + ); + }, }); }); - } - + }, + + async getMindData( + messages: Array<{ role: string; content: string }>, + model: string, + ): Promise<string> { + return new Promise(async (resolve, reject) => { + try { + const config = useAppConfig.getState(); + const accessStore = useAccessStore.getState(); + + // 1. 构建模型配置 + const modelConfig = { + ...config.modelConfig, + model: model, + providerName: accessStore.provider, + enableInjectSystemPrompts: + config.modelConfig.enableInjectSystemPrompts, + }; + + // 2. 处理系统提示 + let systemPrompts: ChatMessage[] = []; + const shouldInjectSystem = + modelConfig.enableInjectSystemPrompts && + (model.startsWith("gpt-") || model.startsWith("chatgpt-")); + + const mcpEnabled = await isMcpEnabled(); + let systemContent = ""; + + // 3. 添加基础系统提示 + if (shouldInjectSystem) { + systemContent += fillTemplateWith("", { + ...modelConfig, + template: DEFAULT_SYSTEM_TEMPLATE, + }); + } + + // 4. 添加MCP系统提示 + if (mcpEnabled) { + systemContent += await getMcpSystemPrompt(); + } + if (systemContent) { + systemPrompts.push( + createMessage({ + role: "system", + content: systemContent, + }), + ); + } + + // 5. 处理消息模板 + const processedMessages = messages.map((msg) => { + if (msg.role === "user") { + return { + ...msg, + content: fillTemplateWith(msg.content, modelConfig), + } as ChatMessage; + } + return msg as ChatMessage; + }); + + // 6. 组合最终消息(系统提示 + 处理后的消息) + const finalMessages = [...systemPrompts, ...processedMessages]; + + // 7. 创建API客户端并调用 + const api = getClientApi(modelConfig.providerName); + api.llm.chat({ + messages: finalMessages, + config: { + ...modelConfig, + stream: false, + }, + onFinish: resolve, + onError: (err) => + reject(err instanceof Error ? err : new Error(err)), + }); + } catch (err) { + reject(err instanceof Error ? err : new Error(String(err))); + } + }); + }, }; return methods; diff --git a/app/store/index.ts b/app/store/index.ts index 122afd5..fb82fc1 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -3,3 +3,4 @@ export * from "./update"; export * from "./access"; export * from "./config"; export * from "./plugin"; +export * from "./message"; diff --git a/app/store/message.ts b/app/store/message.ts new file mode 100644 index 0000000..23f2a02 --- /dev/null +++ b/app/store/message.ts @@ -0,0 +1,36 @@ +import { createPersistStore } from "../utils/store"; + +export const useMindMapStore = createPersistStore< + { newMessages: { role: string; content: string }[]; content: string }, + { + setMindMapData: ( + newMessages: { role: string; content: string }[], + content: string, + ) => void; + clearMindMapData: () => void; + } +>( + { + newMessages: [], + content: "", + }, + (set, get) => ({ + setMindMapData: (newMessages, content) => { + set(() => ({ + newMessages, + content, + })); + }, + + clearMindMapData: () => { + set(() => ({ + newMessages: [], + content: "", + })); + }, + }), + { + name: "mind-map-store", + version: 1, + }, +); diff --git a/app/types/message.d.ts b/app/types/message.d.ts new file mode 100644 index 0000000..9e84100 --- /dev/null +++ b/app/types/message.d.ts @@ -0,0 +1,13 @@ +import type { ChatMessage } from "../store"; + +export type RenderMessage = ChatMessage & { preview?: boolean }; + +export interface MindMapStore { + newMessages: { role: string; content: string }[]; + content: string; + setMindMapData: ( + newMessages: { role: string; content: string }[], + content: string, + ) => void; + clearMindMapData: () => void; +} diff --git a/app/utils/prompt.ts b/app/utils/prompt.ts index e6f7152..68d22b9 100644 --- a/app/utils/prompt.ts +++ b/app/utils/prompt.ts @@ -1,5 +1,4 @@ import type { writePromptParam } from "@/app/types/prompt"; - export function getWrtingPrompt(param: writePromptParam) { const isImg = `文案要配上图片,实现图文混排,要美观,要符合${param.writingPurposeName}的排版标准和写作风格,写作风格要${param.writingStyleName}, 你没有图片没关系,把图文混排的效果实现,并在你认为要插入图片的地方将图片的Prompt用英文输出给:,记得图片地址后面的?nologo=true一定不能去掉了, @@ -19,8 +18,9 @@ export function getBgPrompt(content: string) { return input; } -export function getMindPrompt(content: string) { - return `请你帮我生成一份以"${content}"为主题的思维导图数据,请严格遵循以下要求生成思维导图数据: +export function getMindPrompt(content: string, isContext: boolean) { + const context = `联系上下文`; + let prompt = `请你帮我生成一份以"${content}"为主题的思维导图数据,请严格遵循以下要求生成思维导图数据: 1. 所有键名必须使用双引号 2. 所有字符串值必须使用双引号 3. 确保没有尾随逗号 @@ -42,4 +42,8 @@ export function getMindPrompt(content: string) { },} 只需要返回数据,不要做任何解释 `; + if (isContext) { + prompt = context + prompt; + } + return prompt; }