作者 202304001

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

... ... @@ -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}
... ...
... ... @@ -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);
// 增强校验逻辑
... ...
... ... @@ -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}
... ...
... ... @@ -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;
... ...
... ... @@ -3,3 +3,4 @@ export * from "./update";
export * from "./access";
export * from "./config";
export * from "./plugin";
export * from "./message";
... ...
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,
},
);
... ...
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;
}
... ...
import type { writePromptParam } from "@/app/types/prompt";
export function getWrtingPrompt(param: writePromptParam) {
const isImg = `文案要配上图片,实现图文混排,要美观,要符合${param.writingPurposeName}的排版标准和写作风格,写作风格要${param.writingStyleName},
你没有图片没关系,把图文混排的效果实现,并在你认为要插入图片的地方将图片的Prompt用英文输出给:![description](https://image.pollinations.ai/prompt/description?nologo=true),记得图片地址后面的?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;
}
... ...