* enhance ai menu

* remove api prefix from mcp
This commit is contained in:
Philipinho
2026-03-02 03:31:52 +00:00
parent cf43e2b4fe
commit ee6b98edaa
7 changed files with 21 additions and 14 deletions
@@ -614,6 +614,7 @@
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"AI & MCP": "AI & MCP", "AI & MCP": "AI & MCP",
"AI": "AI", "AI": "AI",
"AI settings": "AI settings",
"MCP": "MCP", "MCP": "MCP",
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)", "Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
@@ -113,7 +113,7 @@ const groupedData: DataGroup[] = [
showDisabledInNonEE: true, showDisabledInNonEE: true,
}, },
{ {
label: "AI", label: "AI settings",
icon: IconSparkles, icon: IconSparkles,
path: "/settings/ai", path: "/settings/ai",
isAdmin: true, isAdmin: true,
@@ -1,5 +1,5 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { ActionIcon, TextInput, Tooltip } from "@mantine/core"; import { ActionIcon, TextInput } from "@mantine/core";
import { useDebouncedCallback, useMediaQuery } from "@mantine/hooks"; import { useDebouncedCallback, useMediaQuery } from "@mantine/hooks";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
@@ -14,7 +14,7 @@ import { ResultPreview } from "./result-preview.tsx";
import classes from "./ai-menu.module.css"; import classes from "./ai-menu.module.css";
import { marked } from "marked"; import { marked } from "marked";
import { DOMSerializer } from "@tiptap/pm/model"; import { DOMSerializer } from "@tiptap/pm/model";
import { htmlToMarkdown } from "@docmost/editor-ext"; import { copyToClipboard, htmlToMarkdown } from "@docmost/editor-ext";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
interface EditorAiMenuProps { interface EditorAiMenuProps {
@@ -110,6 +110,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
setOutput((output) => output + chunk.content); setOutput((output) => output + chunk.content);
}, },
onComplete: () => { onComplete: () => {
setPrompt("");
setIsLoading(false); setIsLoading(false);
setActiveCommandSet("result"); setActiveCommandSet("result");
}, },
@@ -146,13 +147,18 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
} }
const html = (marked.parse(output) as string).trim(); const html = (marked.parse(output) as string).trim();
// Strip <p> wrapper for single-paragraph output to preserve inline context const isSingleParagraph =
const content =
html.startsWith("<p>") && html.startsWith("<p>") &&
html.endsWith("</p>") && html.endsWith("</p>") &&
html.lastIndexOf("<p>") === 0 html.lastIndexOf("<p>") === 0;
? html.slice(3, -4)
: html; // Strip <p> wrapper for single-paragraph output to preserve inline context,
// then decode HTML entities via DOMParser since TipTap would otherwise
// treat the tagless string as plain text and insert entities literally.
const content = isSingleParagraph
? new DOMParser().parseFromString(html.slice(3, -4), "text/html")
.body.innerHTML
: html;
chain.insertContent(content).run(); chain.insertContent(content).run();
@@ -169,7 +175,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
return setShowAiMenu(false); return setShowAiMenu(false);
} }
if (item.id === "result-copy") { if (item.id === "result-copy") {
navigator.clipboard.writeText(output); copyToClipboard(output);
return setShowAiMenu(false); return setShowAiMenu(false);
} }
@@ -27,7 +27,7 @@ export default function McpSettings() {
const [checked, setChecked] = useState(workspace?.settings?.ai?.mcp); const [checked, setChecked] = useState(workspace?.settings?.ai?.mcp);
const hasAccess = useIsCloudEE(); const hasAccess = useIsCloudEE();
const mcpUrl = `${getAppUrl()}/api/mcp`; const mcpUrl = `${getAppUrl()}/mcp`;
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked; const value = event.currentTarget.checked;
+2 -2
View File
@@ -37,9 +37,9 @@ export default function AiSettings() {
return ( return (
<> <>
<Helmet> <Helmet>
<title>AI - {getAppName()}</title> <title>AI settings - {getAppName()}</title>
</Helmet> </Helmet>
<SettingsTitle title={t("AI")} /> <SettingsTitle title={t("AI settings")} />
<Tabs color="dark" value={activeTab} onChange={handleTabChange}> <Tabs color="dark" value={activeTab} onChange={handleTabChange}>
<Tabs.List> <Tabs.List>
@@ -77,7 +77,7 @@ export default function UserApiKeys() {
<Text size="sm" mt={4}> <Text size="sm" mt={4}>
{t("MCP server URL:")}{" "} {t("MCP server URL:")}{" "}
<Text size="sm" fw={500} span ff="monospace"> <Text size="sm" fw={500} span ff="monospace">
{`${getAppUrl()}/api/mcp`} {`${getAppUrl()}/mcp`}
</Text> </Text>
</Text> </Text>
</Alert> </Alert>
+1 -1
View File
@@ -36,7 +36,7 @@ async function bootstrap() {
app.useLogger(app.get(PinoLogger)); app.useLogger(app.get(PinoLogger));
app.setGlobalPrefix('api', { app.setGlobalPrefix('api', {
exclude: ['robots.txt', 'share/:shareId/p/:pageSlug'], exclude: ['robots.txt', 'share/:shareId/p/:pageSlug', 'mcp'],
}); });
const reflector = app.get(Reflector); const reflector = app.get(Reflector);