refactor(mcp): improve form modal layout with adaptive height editor

Restructure MCP form modal for better space utilization.
- Split form into upper form fields and lower JSON editor sections
- Add full-height mode support for JsonEditor component
- Use flex layout for editor to fill available space
- Update PromptFormPanel to use full-height editor
- Fix locale text formatting
This commit is contained in:
YoVinchen
2025-11-22 16:11:06 +08:00
parent 99471f6706
commit 4ed3e3bf84
3 changed files with 231 additions and 217 deletions

View File

@@ -247,14 +247,23 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
} }
}; };
const isFullHeight = height === "100%";
return ( return (
<div style={{ width: "100%" }}> <div
<div ref={editorRef} style={{ width: "100%" }} /> style={{ width: "100%", height: isFullHeight ? "100%" : "auto" }}
className={isFullHeight ? "flex flex-col" : ""}
>
<div
ref={editorRef}
style={{ width: "100%", height: isFullHeight ? undefined : "auto" }}
className={isFullHeight ? "flex-1 min-h-0" : ""}
/>
{language === "json" && ( {language === "json" && (
<button <button
type="button" type="button"
onClick={handleFormat} onClick={handleFormat}
className="mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" className={`${isFullHeight ? "mt-2 flex-shrink-0" : "mt-2"} inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors`}
> >
<Wand2 className="w-3.5 h-3.5" /> <Wand2 className="w-3.5 h-3.5" />
{t("common.format", { defaultValue: "格式化" })} {t("common.format", { defaultValue: "格式化" })}

View File

@@ -429,209 +429,210 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</Button> </Button>
} }
> >
<div className="glass rounded-xl p-6 border border-white/10 space-y-6"> <div className="flex flex-col h-full gap-6">
{/* 预设选择(仅新增时展示) */} {/* 上半部分:表单字段 */}
{!isEditing && ( <div className="glass rounded-xl p-6 border border-white/10 space-y-6 flex-shrink-0">
{/* 预设选择(仅新增时展示) */}
{!isEditing && (
<div>
<label className="block text-sm font-medium text-foreground mb-3">
{t("mcp.presets.title")}
</label>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={applyCustom}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${selectedPreset === -1
? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-accent text-muted-foreground hover:bg-accent/80"
}`}
>
{t("presetSelector.custom")}
</button>
{mcpPresets.map((preset, idx) => {
const descriptionKey = `mcp.presets.${preset.id}.description`;
return (
<button
key={preset.id}
type="button"
onClick={() => applyPreset(idx)}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${selectedPreset === idx
? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-accent text-muted-foreground hover:bg-accent/80"
}`}
title={t(descriptionKey)}
>
{preset.id}
</button>
);
})}
</div>
</div>
)}
{/* ID (标题) */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-foreground">
{t("mcp.form.title")} <span className="text-red-500">*</span>
</label>
{!isEditing && idError && (
<span className="text-xs text-red-500 dark:text-red-400">
{idError}
</span>
)}
</div>
<Input
type="text"
placeholder={t("mcp.form.titlePlaceholder")}
value={formId}
onChange={(e) => handleIdChange(e.target.value)}
disabled={isEditing}
/>
</div>
{/* Name */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.name")}
</label>
<Input
type="text"
placeholder={t("mcp.form.namePlaceholder")}
value={formName}
onChange={(e) => setFormName(e.target.value)}
/>
</div>
{/* 启用到哪些应用 */}
<div> <div>
<label className="block text-sm font-medium text-foreground mb-3"> <label className="block text-sm font-medium text-foreground mb-3">
{t("mcp.presets.title")} {t("mcp.form.enabledApps")}
</label> </label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-4">
<button <div className="flex items-center gap-2">
type="button" <Checkbox
onClick={applyCustom} id="enable-claude"
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ checked={enabledApps.claude}
selectedPreset === -1 onCheckedChange={(checked: boolean) =>
? "bg-emerald-500 text-white dark:bg-emerald-600" setEnabledApps({ ...enabledApps, claude: checked })
: "bg-accent text-muted-foreground hover:bg-accent/80" }
}`} />
> <label
{t("presetSelector.custom")} htmlFor="enable-claude"
</button> className="text-sm text-foreground cursor-pointer select-none"
{mcpPresets.map((preset, idx) => { >
const descriptionKey = `mcp.presets.${preset.id}.description`; {t("mcp.unifiedPanel.apps.claude")}
return ( </label>
<button </div>
key={preset.id}
type="button" <div className="flex items-center gap-2">
onClick={() => applyPreset(idx)} <Checkbox
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ id="enable-codex"
selectedPreset === idx checked={enabledApps.codex}
? "bg-emerald-500 text-white dark:bg-emerald-600" onCheckedChange={(checked: boolean) =>
: "bg-accent text-muted-foreground hover:bg-accent/80" setEnabledApps({ ...enabledApps, codex: checked })
}`} }
title={t(descriptionKey)} />
> <label
{preset.id} htmlFor="enable-codex"
</button> className="text-sm text-foreground cursor-pointer select-none"
); >
})} {t("mcp.unifiedPanel.apps.codex")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="enable-gemini"
checked={enabledApps.gemini}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, gemini: checked })
}
/>
<label
htmlFor="enable-gemini"
className="text-sm text-foreground cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.gemini")}
</label>
</div>
</div> </div>
</div> </div>
)}
{/* ID (标题) */} {/* 可折叠的附加信息按钮 */}
<div> <div>
<div className="flex items-center justify-between mb-2"> <button
<label className="block text-sm font-medium text-foreground"> type="button"
{t("mcp.form.title")} <span className="text-red-500">*</span> onClick={() => setShowMetadata(!showMetadata)}
</label> className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
{!isEditing && idError && ( >
<span className="text-xs text-red-500 dark:text-red-400"> {showMetadata ? (
{idError} <ChevronUp size={16} />
</span> ) : (
)} <ChevronDown size={16} />
)}
{t("mcp.form.additionalInfo")}
</button>
</div> </div>
<Input
type="text" {/* 附加信息区域(可折叠) */}
placeholder={t("mcp.form.titlePlaceholder")} {showMetadata && (
value={formId} <>
onChange={(e) => handleIdChange(e.target.value)} <div>
disabled={isEditing} <label className="block text-sm font-medium text-foreground mb-2">
/> {t("mcp.form.description")}
</label>
<Input
type="text"
placeholder={t("mcp.form.descriptionPlaceholder")}
value={formDescription}
onChange={(e) => setFormDescription(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.tags")}
</label>
<Input
type="text"
placeholder={t("mcp.form.tagsPlaceholder")}
value={formTags}
onChange={(e) => setFormTags(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.homepage")}
</label>
<Input
type="text"
placeholder={t("mcp.form.homepagePlaceholder")}
value={formHomepage}
onChange={(e) => setFormHomepage(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.docs")}
</label>
<Input
type="text"
placeholder={t("mcp.form.docsPlaceholder")}
value={formDocs}
onChange={(e) => setFormDocs(e.target.value)}
/>
</div>
</>
)}
</div> </div>
{/* Name */} {/* 下半部分JSON 配置编辑器 - 自适应剩余高度 */}
<div> <div className="glass rounded-xl p-6 border border-white/10 flex flex-col flex-1 min-h-0">
<label className="block text-sm font-medium text-foreground mb-2"> <div className="flex items-center justify-between mb-4 flex-shrink-0">
{t("mcp.form.name")}
</label>
<Input
type="text"
placeholder={t("mcp.form.namePlaceholder")}
value={formName}
onChange={(e) => setFormName(e.target.value)}
/>
</div>
{/* 启用到哪些应用 */}
<div>
<label className="block text-sm font-medium text-foreground mb-3">
{t("mcp.form.enabledApps")}
</label>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="enable-claude"
checked={enabledApps.claude}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, claude: checked })
}
/>
<label
htmlFor="enable-claude"
className="text-sm text-foreground cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.claude")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="enable-codex"
checked={enabledApps.codex}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, codex: checked })
}
/>
<label
htmlFor="enable-codex"
className="text-sm text-foreground cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.codex")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="enable-gemini"
checked={enabledApps.gemini}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, gemini: checked })
}
/>
<label
htmlFor="enable-gemini"
className="text-sm text-foreground cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.gemini")}
</label>
</div>
</div>
</div>
{/* 可折叠的附加信息按钮 */}
<div>
<button
type="button"
onClick={() => setShowMetadata(!showMetadata)}
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{showMetadata ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
{t("mcp.form.additionalInfo")}
</button>
</div>
{/* 附加信息区域(可折叠) */}
{showMetadata && (
<>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.description")}
</label>
<Input
type="text"
placeholder={t("mcp.form.descriptionPlaceholder")}
value={formDescription}
onChange={(e) => setFormDescription(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.tags")}
</label>
<Input
type="text"
placeholder={t("mcp.form.tagsPlaceholder")}
value={formTags}
onChange={(e) => setFormTags(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.homepage")}
</label>
<Input
type="text"
placeholder={t("mcp.form.homepagePlaceholder")}
value={formHomepage}
onChange={(e) => setFormHomepage(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t("mcp.form.docs")}
</label>
<Input
type="text"
placeholder={t("mcp.form.docsPlaceholder")}
value={formDocs}
onChange={(e) => setFormDocs(e.target.value)}
/>
</div>
</>
)}
{/* 配置输入框 */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-foreground"> <label className="text-sm font-medium text-foreground">
{useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")} {useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")}
</label> </label>
@@ -645,26 +646,30 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</button> </button>
)} )}
</div> </div>
<JsonEditor <div className="flex-1 min-h-0 flex flex-col">
value={formConfig} <div className="flex-1 min-h-0">
onChange={handleConfigChange} <JsonEditor
placeholder={ value={formConfig}
useToml onChange={handleConfigChange}
? t("mcp.form.tomlPlaceholder") placeholder={
: t("mcp.form.jsonPlaceholder") useToml
} ? t("mcp.form.tomlPlaceholder")
darkMode={isDarkMode} : t("mcp.form.jsonPlaceholder")
rows={12} }
showValidation={!useToml} darkMode={isDarkMode}
language={useToml ? "javascript" : "json"} rows={12}
height="300px" showValidation={!useToml}
/> language={useToml ? "javascript" : "json"}
{configError && ( height="100%"
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm"> />
<AlertCircle size={16} />
<span>{configError}</span>
</div> </div>
)} {configError && (
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm flex-shrink-0">
<AlertCircle size={16} />
<span>{configError}</span>
</div>
)}
</div>
</div> </div>
</div> </div>
</FullScreenPanel> </FullScreenPanel>

View File

@@ -142,7 +142,7 @@ const PromptFormPanel: React.FC<PromptFormPanelProps> = ({
onChange={setContent} onChange={setContent}
placeholder={t("prompts.contentPlaceholder", { filename })} placeholder={t("prompts.contentPlaceholder", { filename })}
darkMode={isDarkMode} darkMode={isDarkMode}
minHeight="500px" minHeight="167px"
/> />
</div> </div>
</div> </div>