作者 202304001

增加思维导图联系上下文功能

@@ -52,7 +52,7 @@ import HeadphoneIcon from "../icons/headphone.svg"; @@ -52,7 +52,7 @@ import HeadphoneIcon from "../icons/headphone.svg";
52 //20250317新增word excel图标 52 //20250317新增word excel图标
53 import ExcelIcon from "../icons/excel.svg"; 53 import ExcelIcon from "../icons/excel.svg";
54 import WordIcon from "../icons/word.svg"; 54 import WordIcon from "../icons/word.svg";
55 -import MindIcon from "../icons/mind.svg" 55 +import MindIcon from "../icons/mind.svg";
56 56
57 import { 57 import {
58 BOT_HELLO, 58 BOT_HELLO,
@@ -66,6 +66,7 @@ import { @@ -66,6 +66,7 @@ import {
66 useAppConfig, 66 useAppConfig,
67 useChatStore, 67 useChatStore,
68 usePluginStore, 68 usePluginStore,
  69 + useMindMapStore, //新增消息仓库用于存储上下文至思维导图页面
69 } from "../store"; 70 } from "../store";
70 71
71 import { 72 import {
@@ -134,7 +135,8 @@ import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions"; @@ -134,7 +135,8 @@ import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
134 135
135 //20250317新增 136 //20250317新增
136 import { toExcel } from "../utils/excelAndWordUtils/export2Excel"; 137 import { toExcel } from "../utils/excelAndWordUtils/export2Excel";
137 -import {exportWord} from "../utils/excelAndWordUtils/word"; 138 +import { exportWord } from "../utils/excelAndWordUtils/word";
  139 +import { getMindPrompt } from "../utils/prompt";
138 const localStorage = safeLocalStorage(); 140 const localStorage = safeLocalStorage();
139 141
140 const ttsPlayer = createTTSPlayer(); 142 const ttsPlayer = createTTSPlayer();
@@ -181,7 +183,9 @@ function isTableContent(content: string): boolean { @@ -181,7 +183,9 @@ function isTableContent(content: string): boolean {
181 const hasTableSeparator = tablePattern.test(content); 183 const hasTableSeparator = tablePattern.test(content);
182 184
183 // 检查是否包含至少一行表格内容 185 // 检查是否包含至少一行表格内容
184 - const hasTableRows = content.split("\n").some((line) => rowPattern.test(line)); 186 + const hasTableRows = content
  187 + .split("\n")
  188 + .some((line) => rowPattern.test(line));
185 189
186 // 如果同时包含分隔符行和表格内容行,则认为是表格 190 // 如果同时包含分隔符行和表格内容行,则认为是表格
187 return hasTableSeparator && hasTableRows; 191 return hasTableSeparator && hasTableRows;
@@ -709,7 +713,8 @@ export function ChatActions(props: { @@ -709,7 +713,8 @@ export function ChatActions(props: {
709 <Selector 713 <Selector
710 defaultSelectedValue={`${currentModel}@${currentProviderName}`} 714 defaultSelectedValue={`${currentModel}@${currentProviderName}`}
711 items={models.map((m) => ({ 715 items={models.map((m) => ({
712 - title: `${m.displayName}${m?.provider?.providerName 716 + title: `${m.displayName}${
  717 + m?.provider?.providerName
713 ? " (" + m?.provider?.providerName + ")" 718 ? " (" + m?.provider?.providerName + ")"
714 : "" 719 : ""
715 }`, 720 }`,
@@ -1704,6 +1709,36 @@ function _Chat() { @@ -1704,6 +1709,36 @@ function _Chat() {
1704 1709
1705 const [showChatSidePanel, setShowChatSidePanel] = useState(false); 1710 const [showChatSidePanel, setShowChatSidePanel] = useState(false);
1706 1711
  1712 + // 20250327新增思维导图导出
  1713 + function toMind(messages: RenderMessage[], content: string) {
  1714 + const newMessages = [];
  1715 + const { setMindMapData } = useMindMapStore.getState();
  1716 + // 遍历 messages 数组
  1717 + for (const message of messages) {
  1718 + // 检查 content 类型并正确存储
  1719 + if (typeof message.content === "string") {
  1720 + newMessages.push({
  1721 + role: message.role,
  1722 + content: message.content,
  1723 + });
  1724 + } else {
  1725 + newMessages.push({
  1726 + role: message.role,
  1727 + content: JSON.stringify(message.content),
  1728 + });
  1729 + }
  1730 + if (message.content === content) {
  1731 + newMessages.push({
  1732 + role: "user",
  1733 + content: getMindPrompt(content, true),
  1734 + });
  1735 + break;
  1736 + }
  1737 + }
  1738 + setMindMapData(newMessages, content);
  1739 + navigate("/mind", { state: { msg: true } });
  1740 + }
  1741 +
1707 return ( 1742 return (
1708 <> 1743 <>
1709 <div className={styles.chat} key={session.id}> 1744 <div className={styles.chat} key={session.id}>
@@ -1939,7 +1974,7 @@ function _Chat() { @@ -1939,7 +1974,7 @@ function _Chat() {
1939 } 1974 }
1940 /> 1975 />
1941 {/* 以下 20250317 新增Word excel导出按钮 */} 1976 {/* 以下 20250317 新增Word excel导出按钮 */}
1942 - {isTableContent(getMessageTextContent(message)) && ( 1977 + {/* {isTableContent(getMessageTextContent(message)) && (
1943 <> 1978 <>
1944 <ChatAction 1979 <ChatAction
1945 text={Locale.Chat.Actions.Excel} 1980 text={Locale.Chat.Actions.Excel}
@@ -1949,23 +1984,38 @@ function _Chat() { @@ -1949,23 +1984,38 @@ function _Chat() {
1949 } 1984 }
1950 /> 1985 />
1951 </> 1986 </>
1952 - )}  
1953 -  
1954 - {!isTableContent(getMessageTextContent(message)) && ( 1987 + )} */}
  1988 + <ChatAction
  1989 + text={Locale.Chat.Actions.Excel}
  1990 + icon={<ExcelIcon />}
  1991 + onClick={() =>
  1992 + toExcel(
  1993 + getMessageTextContent(message),
  1994 + )
  1995 + }
  1996 + />
  1997 + {!isTableContent(
  1998 + getMessageTextContent(message),
  1999 + ) && (
1955 <> 2000 <>
1956 <ChatAction 2001 <ChatAction
1957 text={Locale.Chat.Actions.Word} 2002 text={Locale.Chat.Actions.Word}
1958 icon={<WordIcon />} 2003 icon={<WordIcon />}
1959 - onClick={() => exportWord( 2004 + onClick={() =>
  2005 + exportWord(
1960 getMessageTextContent(message), 2006 getMessageTextContent(message),
1961 - )} 2007 + )
  2008 + }
1962 /> 2009 />
1963 <ChatAction 2010 <ChatAction
1964 text={Locale.Chat.Actions.Mind} 2011 text={Locale.Chat.Actions.Mind}
1965 icon={<MindIcon />} 2012 icon={<MindIcon />}
1966 - onClick={() => exportWord( 2013 + onClick={() => {
  2014 + toMind(
  2015 + messages,
1967 getMessageTextContent(message), 2016 getMessageTextContent(message),
1968 - )} 2017 + );
  2018 + }}
1969 /> 2019 />
1970 </> 2020 </>
1971 )} 2021 )}
@@ -22,9 +22,15 @@ export function MindPanel(props: MindPanelProps) { @@ -22,9 +22,15 @@ export function MindPanel(props: MindPanelProps) {
22 if (!inputValue.trim()) return message.error("请输入提示词!"); 22 if (!inputValue.trim()) return message.error("请输入提示词!");
23 setIsLoading(true); 23 setIsLoading(true);
24 try { 24 try {
25 - const prompt = getMindPrompt(inputValue); 25 + const prompt = getMindPrompt(inputValue, false);
26 const response = await chatStore.directLlmInvoke(prompt, "gpt-4o-mini"); 26 const response = await chatStore.directLlmInvoke(prompt, "gpt-4o-mini");
27 console.log("原始响应:", response); 27 console.log("原始响应:", response);
  28 + let cleanedContent = response.startsWith("```json")
  29 + ? response.substring(8)
  30 + : response;
  31 + if (cleanedContent.endsWith("```")) {
  32 + cleanedContent = cleanedContent.substring(0, cleanedContent.length - 4);
  33 + }
28 const parsedData: MindElixirData = JSON.parse(response); 34 const parsedData: MindElixirData = JSON.parse(response);
29 console.log("解析后响应:", parsedData); 35 console.log("解析后响应:", parsedData);
30 // 增强校验逻辑 36 // 增强校验逻辑
@@ -8,7 +8,7 @@ import { useMobileScreen } from "@/app/utils"; @@ -8,7 +8,7 @@ import { useMobileScreen } from "@/app/utils";
8 import { IconButton } from "../button"; 8 import { IconButton } from "../button";
9 import Locale from "@/app/locales"; 9 import Locale from "@/app/locales";
10 import { Path } from "@/app/constant"; 10 import { Path } from "@/app/constant";
11 -import { useNavigate } from "react-router-dom"; 11 +import { useNavigate, useLocation } from "react-router-dom";
12 import clsx from "clsx"; 12 import clsx from "clsx";
13 import { getClientConfig } from "@/app/config/client"; 13 import { getClientConfig } from "@/app/config/client";
14 import React, { useEffect, useMemo, useRef, useState } from "react"; 14 import React, { useEffect, useMemo, useRef, useState } from "react";
@@ -18,6 +18,8 @@ import ReturnIcon from "@/app/icons/return.svg"; @@ -18,6 +18,8 @@ import ReturnIcon from "@/app/icons/return.svg";
18 import MinIcon from "@/app/icons/min.svg"; 18 import MinIcon from "@/app/icons/min.svg";
19 import MaxIcon from "@/app/icons/max.svg"; 19 import MaxIcon from "@/app/icons/max.svg";
20 import SDIcon from "@/app/icons/sd.svg"; 20 import SDIcon from "@/app/icons/sd.svg";
  21 +import { useChatStore, useMindMapStore } from "@/app/store";
  22 +import { message } from "antd";
21 23
22 export function MindPage() { 24 export function MindPage() {
23 const isMobileScreen = useMobileScreen(); 25 const isMobileScreen = useMobileScreen();
@@ -26,10 +28,14 @@ export function MindPage() { @@ -26,10 +28,14 @@ export function MindPage() {
26 const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; 28 const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
27 const config = useAppConfig(); 29 const config = useAppConfig();
28 const scrollRef = useRef<HTMLDivElement>(null); 30 const scrollRef = useRef<HTMLDivElement>(null);
29 - const isWriting = location.pathname === Path.Writing; 31 + const isMind = location.pathname === Path.Mind;
30 const [isLoading, setIsLoading] = useState(false); 32 const [isLoading, setIsLoading] = useState(false);
31 const containerRef = useRef<HTMLDivElement>(null); 33 const containerRef = useRef<HTMLDivElement>(null);
32 const mindInstance = useRef<InstanceType<typeof MindElixir> | null>(null); 34 const mindInstance = useRef<InstanceType<typeof MindElixir> | null>(null);
  35 + const chatStore = useChatStore();
  36 + const { newMessages, content } = useMindMapStore.getState();
  37 + const query = useLocation();
  38 + const { msg } = query.state || {};
33 const [data, setData] = useState<MindElixirData>({ 39 const [data, setData] = useState<MindElixirData>({
34 nodeData: { 40 nodeData: {
35 id: "root", 41 id: "root",
@@ -51,6 +57,46 @@ export function MindPage() { @@ -51,6 +57,46 @@ export function MindPage() {
51 mindInstance.current = new MindElixir(options); 57 mindInstance.current = new MindElixir(options);
52 mindInstance.current.init(data); 58 mindInstance.current.init(data);
53 59
  60 + const fetchData = async () => {
  61 + if (msg) {
  62 + if (content) {
  63 + setIsLoading(true);
  64 + try {
  65 + const response = await chatStore.getMindData(
  66 + newMessages,
  67 + "gpt-4o-mini",
  68 + );
  69 + console.log("原始响应:", response);
  70 + let cleanedContent = response.startsWith("```json")
  71 + ? response.substring(8)
  72 + : response;
  73 + if (cleanedContent.endsWith("```")) {
  74 + cleanedContent = cleanedContent.substring(
  75 + 0,
  76 + cleanedContent.length - 4,
  77 + );
  78 + }
  79 + const parsedData: MindElixirData = JSON.parse(cleanedContent);
  80 + console.log("解析后响应:", parsedData);
  81 + // 增强校验逻辑
  82 + if (
  83 + !parsedData?.nodeData?.id ||
  84 + !Array.isArray(parsedData.nodeData.children)
  85 + ) {
  86 + throw new Error("数据结构不完整");
  87 + }
  88 + setData(parsedData);
  89 + } catch (error) {
  90 + console.log(error);
  91 + message.error("请求失败,请重试");
  92 + } finally {
  93 + setIsLoading(false); // 确保关闭加载状态
  94 + }
  95 + }
  96 + }
  97 + };
  98 + fetchData();
  99 +
54 return () => { 100 return () => {
55 if (mindInstance.current) { 101 if (mindInstance.current) {
56 mindInstance.current.destroy(); 102 mindInstance.current.destroy();
@@ -73,6 +119,13 @@ export function MindPage() { @@ -73,6 +119,13 @@ export function MindPage() {
73 topic: "生成中....", 119 topic: "生成中....",
74 }, 120 },
75 }); 121 });
  122 + } else {
  123 + mindInstance.current?.refresh({
  124 + nodeData: {
  125 + id: "root",
  126 + topic: "中心主题",
  127 + },
  128 + });
76 } 129 }
77 }, [isLoading]); 130 }, [isLoading]);
78 131
@@ -133,7 +186,7 @@ export function MindPage() { @@ -133,7 +186,7 @@ export function MindPage() {
133 return ( 186 return (
134 <> 187 <>
135 <MindSiderBar 188 <MindSiderBar
136 - className={clsx({ [homeStyles["sidebar-show"]]: isWriting })} 189 + className={clsx({ [homeStyles["sidebar-show"]]: isMind })}
137 setData={setData} 190 setData={setData}
138 isLoading={isLoading} 191 isLoading={isLoading}
139 setIsLoading={setIsLoading} 192 setIsLoading={setIsLoading}
@@ -855,7 +855,7 @@ export const useChatStore = createPersistStore( @@ -855,7 +855,7 @@ export const useChatStore = createPersistStore(
855 } 855 }
856 }, 856 },
857 857
858 - async directLlmInvoke(content: string,model:string): Promise<string> { 858 + async directLlmInvoke(content: string, model: string): Promise<string> {
859 return new Promise((resolve, reject) => { 859 return new Promise((resolve, reject) => {
860 const config = useAppConfig.getState(); 860 const config = useAppConfig.getState();
861 const accessStore = useAccessStore.getState(); 861 const accessStore = useAccessStore.getState();
@@ -864,7 +864,7 @@ export const useChatStore = createPersistStore( @@ -864,7 +864,7 @@ export const useChatStore = createPersistStore(
864 const modelConfig = { 864 const modelConfig = {
865 ...config.modelConfig, 865 ...config.modelConfig,
866 model: model, 866 model: model,
867 - providerName: accessStore.provider 867 + providerName: accessStore.provider,
868 }; 868 };
869 869
870 // 直接构造消息 870 // 直接构造消息
@@ -872,7 +872,7 @@ export const useChatStore = createPersistStore( @@ -872,7 +872,7 @@ export const useChatStore = createPersistStore(
872 createMessage({ 872 createMessage({
873 role: "user", 873 role: "user",
874 content: fillTemplateWith(content, modelConfig), 874 content: fillTemplateWith(content, modelConfig),
875 - }) 875 + }),
876 ]; 876 ];
877 877
878 const api: ClientApi = getClientApi(modelConfig.providerName); 878 const api: ClientApi = getClientApi(modelConfig.providerName);
@@ -887,13 +887,94 @@ export const useChatStore = createPersistStore( @@ -887,13 +887,94 @@ export const useChatStore = createPersistStore(
887 resolve(message); 887 resolve(message);
888 }, 888 },
889 onError(error) { 889 onError(error) {
890 - reject(error instanceof Error ? error : new Error(prettyObject(error)));  
891 - } 890 + reject(
  891 + error instanceof Error ? error : new Error(prettyObject(error)),
  892 + );
  893 + },
  894 + });
892 }); 895 });
  896 + },
  897 +
  898 + async getMindData(
  899 + messages: Array<{ role: string; content: string }>,
  900 + model: string,
  901 + ): Promise<string> {
  902 + return new Promise(async (resolve, reject) => {
  903 + try {
  904 + const config = useAppConfig.getState();
  905 + const accessStore = useAccessStore.getState();
  906 +
  907 + // 1. 构建模型配置
  908 + const modelConfig = {
  909 + ...config.modelConfig,
  910 + model: model,
  911 + providerName: accessStore.provider,
  912 + enableInjectSystemPrompts:
  913 + config.modelConfig.enableInjectSystemPrompts,
  914 + };
  915 +
  916 + // 2. 处理系统提示
  917 + let systemPrompts: ChatMessage[] = [];
  918 + const shouldInjectSystem =
  919 + modelConfig.enableInjectSystemPrompts &&
  920 + (model.startsWith("gpt-") || model.startsWith("chatgpt-"));
  921 +
  922 + const mcpEnabled = await isMcpEnabled();
  923 + let systemContent = "";
  924 +
  925 + // 3. 添加基础系统提示
  926 + if (shouldInjectSystem) {
  927 + systemContent += fillTemplateWith("", {
  928 + ...modelConfig,
  929 + template: DEFAULT_SYSTEM_TEMPLATE,
893 }); 930 });
894 } 931 }
895 932
  933 + // 4. 添加MCP系统提示
  934 + if (mcpEnabled) {
  935 + systemContent += await getMcpSystemPrompt();
  936 + }
  937 +
  938 + if (systemContent) {
  939 + systemPrompts.push(
  940 + createMessage({
  941 + role: "system",
  942 + content: systemContent,
  943 + }),
  944 + );
  945 + }
896 946
  947 + // 5. 处理消息模板
  948 + const processedMessages = messages.map((msg) => {
  949 + if (msg.role === "user") {
  950 + return {
  951 + ...msg,
  952 + content: fillTemplateWith(msg.content, modelConfig),
  953 + } as ChatMessage;
  954 + }
  955 + return msg as ChatMessage;
  956 + });
  957 +
  958 + // 6. 组合最终消息(系统提示 + 处理后的消息)
  959 + const finalMessages = [...systemPrompts, ...processedMessages];
  960 +
  961 + // 7. 创建API客户端并调用
  962 + const api = getClientApi(modelConfig.providerName);
  963 + api.llm.chat({
  964 + messages: finalMessages,
  965 + config: {
  966 + ...modelConfig,
  967 + stream: false,
  968 + },
  969 + onFinish: resolve,
  970 + onError: (err) =>
  971 + reject(err instanceof Error ? err : new Error(err)),
  972 + });
  973 + } catch (err) {
  974 + reject(err instanceof Error ? err : new Error(String(err)));
  975 + }
  976 + });
  977 + },
897 }; 978 };
898 979
899 return methods; 980 return methods;
@@ -3,3 +3,4 @@ export * from "./update"; @@ -3,3 +3,4 @@ export * from "./update";
3 export * from "./access"; 3 export * from "./access";
4 export * from "./config"; 4 export * from "./config";
5 export * from "./plugin"; 5 export * from "./plugin";
  6 +export * from "./message";
  1 +import { createPersistStore } from "../utils/store";
  2 +
  3 +export const useMindMapStore = createPersistStore<
  4 + { newMessages: { role: string; content: string }[]; content: string },
  5 + {
  6 + setMindMapData: (
  7 + newMessages: { role: string; content: string }[],
  8 + content: string,
  9 + ) => void;
  10 + clearMindMapData: () => void;
  11 + }
  12 +>(
  13 + {
  14 + newMessages: [],
  15 + content: "",
  16 + },
  17 + (set, get) => ({
  18 + setMindMapData: (newMessages, content) => {
  19 + set(() => ({
  20 + newMessages,
  21 + content,
  22 + }));
  23 + },
  24 +
  25 + clearMindMapData: () => {
  26 + set(() => ({
  27 + newMessages: [],
  28 + content: "",
  29 + }));
  30 + },
  31 + }),
  32 + {
  33 + name: "mind-map-store",
  34 + version: 1,
  35 + },
  36 +);
  1 +import type { ChatMessage } from "../store";
  2 +
  3 +export type RenderMessage = ChatMessage & { preview?: boolean };
  4 +
  5 +export interface MindMapStore {
  6 + newMessages: { role: string; content: string }[];
  7 + content: string;
  8 + setMindMapData: (
  9 + newMessages: { role: string; content: string }[],
  10 + content: string,
  11 + ) => void;
  12 + clearMindMapData: () => void;
  13 +}
1 import type { writePromptParam } from "@/app/types/prompt"; 1 import type { writePromptParam } from "@/app/types/prompt";
2 -  
3 export function getWrtingPrompt(param: writePromptParam) { 2 export function getWrtingPrompt(param: writePromptParam) {
4 const isImg = `文案要配上图片,实现图文混排,要美观,要符合${param.writingPurposeName}的排版标准和写作风格,写作风格要${param.writingStyleName}, 3 const isImg = `文案要配上图片,实现图文混排,要美观,要符合${param.writingPurposeName}的排版标准和写作风格,写作风格要${param.writingStyleName},
5 你没有图片没关系,把图文混排的效果实现,并在你认为要插入图片的地方将图片的Prompt用英文输出给:![description](https://image.pollinations.ai/prompt/description?nologo=true),记得图片地址后面的?nologo=true一定不能去掉了, 4 你没有图片没关系,把图文混排的效果实现,并在你认为要插入图片的地方将图片的Prompt用英文输出给:![description](https://image.pollinations.ai/prompt/description?nologo=true),记得图片地址后面的?nologo=true一定不能去掉了,
@@ -19,8 +18,9 @@ export function getBgPrompt(content: string) { @@ -19,8 +18,9 @@ export function getBgPrompt(content: string) {
19 return input; 18 return input;
20 } 19 }
21 20
22 -export function getMindPrompt(content: string) {  
23 - return `请你帮我生成一份以"${content}"为主题的思维导图数据,请严格遵循以下要求生成思维导图数据: 21 +export function getMindPrompt(content: string, isContext: boolean) {
  22 + const context = `联系上下文`;
  23 + let prompt = `请你帮我生成一份以"${content}"为主题的思维导图数据,请严格遵循以下要求生成思维导图数据:
24 1. 所有键名必须使用双引号 24 1. 所有键名必须使用双引号
25 2. 所有字符串值必须使用双引号 25 2. 所有字符串值必须使用双引号
26 3. 确保没有尾随逗号 26 3. 确保没有尾随逗号
@@ -42,4 +42,8 @@ export function getMindPrompt(content: string) { @@ -42,4 +42,8 @@ export function getMindPrompt(content: string) {
42 },} 42 },}
43 只需要返回数据,不要做任何解释 43 只需要返回数据,不要做任何解释
44 `; 44 `;
  45 + if (isContext) {
  46 + prompt = context + prompt;
  47 + }
  48 + return prompt;
45 } 49 }