diff --git a/frontend/src/components/ai/AIMCPHelpBlock.tsx b/frontend/src/components/ai/AIMCPHelpBlock.tsx new file mode 100644 index 0000000..ccfa2d8 --- /dev/null +++ b/frontend/src/components/ai/AIMCPHelpBlock.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; +import type { MCPFieldState } from '../../utils/mcpServerGuidance'; + +export const mcpLabelStyle: React.CSSProperties = { + fontSize: 12, + fontWeight: 700, +}; + +export const buildMCPHintStyle = (mutedText: string): React.CSSProperties => ({ + fontSize: 12, + color: mutedText, + lineHeight: 1.6, +}); + +export const buildMCPFieldTone = (kind: MCPFieldState, darkMode: boolean) => { + switch (kind) { + case 'required': + return { + label: '必填', + color: '#b45309', + bg: darkMode ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.12)', + }; + case 'fixed': + return { + label: '固定', + color: '#2563eb', + bg: darkMode ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.12)', + }; + default: + return { + label: '可选', + color: '#475569', + bg: darkMode ? 'rgba(148,163,184,0.18)' : 'rgba(148,163,184,0.12)', + }; + } +}; + +interface AIMCPHelpBlockProps { + title: string; + description: string; + overlayTheme: OverlayWorkbenchTheme; + darkMode: boolean; + fieldState: MCPFieldState; + example?: string; + children: React.ReactNode; +} + +const AIMCPHelpBlock: React.FC = ({ + title, + description, + overlayTheme, + darkMode, + fieldState, + example, + children, +}) => { + const tone = buildMCPFieldTone(fieldState, darkMode); + + return ( +
+
+
{title}
+ + {tone.label} + +
+
+ {description} + {example ? ( + <> + {' '}例如:{example} + + ) : null} +
+ {children} +
+ ); +}; + +export default AIMCPHelpBlock; diff --git a/frontend/src/components/ai/AIMCPServerCard.tsx b/frontend/src/components/ai/AIMCPServerCard.tsx index edac7fb..b813eaf 100644 --- a/frontend/src/components/ai/AIMCPServerCard.tsx +++ b/frontend/src/components/ai/AIMCPServerCard.tsx @@ -1,20 +1,12 @@ import React from 'react'; -import { Button, Input, Popconfirm, Select } from 'antd'; -import { DeleteOutlined } from '@ant-design/icons'; import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; import type { AIMCPServerConfig, AIMCPToolDescriptor } from '../../types'; import { parseMCPCommandDraft } from '../../utils/mcpCommandDraft'; import { formatMCPEnvDraft, parseMCPEnvDraft } from '../../utils/mcpEnvDraft'; -import { - MCP_COMMAND_EXAMPLES, - MCP_COMMAND_PARSE_EXAMPLE, - MCP_FIELD_GUIDES, - MCP_SERVER_FILL_STEPS, - buildMCPLaunchPreview, - type MCPFieldState, -} from '../../utils/mcpServerGuidance'; -import AIMCPCommandDraftPreview from './AIMCPCommandDraftPreview'; +import { buildMCPLaunchPreview } from '../../utils/mcpServerGuidance'; +import AIMCPServerFormPanel from './AIMCPServerFormPanel'; +import AIMCPServerGuidePanel from './AIMCPServerGuidePanel'; interface AIMCPServerCardProps { server: AIMCPServerConfig; @@ -31,80 +23,6 @@ interface AIMCPServerCardProps { onDelete: () => void; } -const labelStyle: React.CSSProperties = { - fontSize: 12, - fontWeight: 700, -}; - -const hintStyle = (mutedText: string): React.CSSProperties => ({ - fontSize: 12, - color: mutedText, - lineHeight: 1.6, -}); - -const buildFieldTone = (kind: MCPFieldState, darkMode: boolean) => { - switch (kind) { - case 'required': - return { - label: '必填', - color: '#b45309', - bg: darkMode ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.12)', - }; - case 'fixed': - return { - label: '固定', - color: '#2563eb', - bg: darkMode ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.12)', - }; - default: - return { - label: '可选', - color: '#475569', - bg: darkMode ? 'rgba(148,163,184,0.18)' : 'rgba(148,163,184,0.12)', - }; - } -}; - -const MCPHelpBlock: React.FC<{ - title: string; - description: string; - overlayTheme: OverlayWorkbenchTheme; - darkMode: boolean; - fieldState: MCPFieldState; - example?: string; - children: React.ReactNode; -}> = ({ title, description, overlayTheme, darkMode, fieldState, example, children }) => { - const tone = buildFieldTone(fieldState, darkMode); - return ( -
-
-
{title}
- - {tone.label} - -
-
- {description} - {example ? ( - <> - {' '}例如:{example} - - ) : null} -
- {children} -
- ); -}; - export const AIMCPServerCard: React.FC = ({ server, serverTools, @@ -141,272 +59,40 @@ export const AIMCPServerCard: React.FC = ({ }); }; + const handleEnvDraftChange = (nextValue: string) => { + setEnvDraft(nextValue); + onChange({ env: parseMCPEnvDraft(nextValue).env }); + }; + return (
-
-
填写示例
-
- 启动命令只填可执行程序本身,不要把参数混在一起。常见形式: - {' '} - {MCP_COMMAND_EXAMPLES.join(' / ')} -
-
- -
-
推荐填写顺序
-
- 小白用户可以按这个顺序填:先选上面的模板或粘整行命令,再确认下面的必填项,最后只在需要时补参数、环境变量和超时。 -
-
- {MCP_SERVER_FILL_STEPS.map((item) => ( - - {item.step}. {item.title} - - ))} -
-
- -
-
字段速查
-
- 如果看到某个参数名不知道该填什么,先看这一块;下面每个字段也都有更具体的示例和注意事项。 -
-
- {MCP_FIELD_GUIDES.map((item) => { - const tone = buildFieldTone(item.fieldState, darkMode); - return ( -
-
-
{item.title}
- - {tone.label} - -
-
{item.summary}
-
{item.detail}
- {item.example ? ( -
- 示例值: - {' '} - {item.example} -
- ) : null} -
- ); - })} -
-
- -
-
只有一条完整命令?
-
- 直接粘贴完整命令,GoNavi 会自动拆成“启动命令 / 命令参数 / 环境变量”三块,适合你只拿到 README 里的一整行示例时快速录入。 -
- setRawCommandDraft(event.target.value)} - placeholder={`直接粘贴完整命令,例如:\n${MCP_COMMAND_PARSE_EXAMPLE}`} - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}`, fontFamily: 'var(--gn-font-mono)' }} - /> -
-
- {rawCommandDraft.trim() - ? parsedCommandDraft.ok && parsedCommandDraft.draft - ? `将解析为:命令 ${parsedCommandDraft.draft.command},参数 ${parsedCommandDraft.draft.args.length} 个,环境变量 ${Object.keys(parsedCommandDraft.draft.env).length} 个。` - : parsedCommandDraft.error - : '支持带引号路径、带空格参数,以及命令前缀的 KEY=VALUE 环境变量。'} -
- -
- {parsedCommandDraft.ok && parsedCommandDraft.draft && rawCommandDraft.trim() && ( - - )} -
- -
- - onChange({ name: event.target.value })} - placeholder="服务名称,例如:Filesystem / Browser / GitHub" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - - - onChange({ transport: value as AIMCPServerConfig['transport'] })} - options={[{ label: 'stdio', value: 'stdio' }]} - /> - - - onChange({ command: event.target.value })} - placeholder="启动命令,例如:node / uvx / python" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - - - onChange({ timeoutSeconds: Number(event.target.value) || 20 })} - placeholder="超时(秒)" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> -
- {[ - { label: '默认 20 秒', value: 20 }, - { label: '稍宽松 45 秒', value: 45 }, - { label: '慢启动 60 秒', value: 60 }, - ].map((option) => ( - - ))} -
-
-
- - - onChange({ name: event.target.value })} + placeholder="服务名称,例如:Filesystem / Browser / GitHub" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + + + onChange({ transport: value as AIMCPServerConfig['transport'] })} + options={[{ label: 'stdio', value: 'stdio' }]} + /> + + + onChange({ command: event.target.value })} + placeholder="启动命令,例如:node / uvx / python" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + + + onChange({ timeoutSeconds: Number(event.target.value) || 20 })} + placeholder="超时(秒)" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> +
+ {[ + { label: '默认 20 秒', value: 20 }, + { label: '稍宽松 45 秒', value: 45 }, + { label: '慢启动 60 秒', value: 60 }, + ].map((option) => ( + + ))} +
+
+
+ + +