feat: present asset plugins as control cards
This commit is contained in:
@@ -23,6 +23,13 @@ function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
|
||||
};
|
||||
}
|
||||
|
||||
const pluginVisuals: Record<string, { icon: string; accent: string }> = {
|
||||
'asset-search': { icon: '⌕', accent: 'blue' },
|
||||
'asset-generate': { icon: '✦', accent: 'violet' },
|
||||
'asset-transform': { icon: '✧', accent: 'amber' },
|
||||
'asset-analyze': { icon: '◉', accent: 'green' },
|
||||
};
|
||||
|
||||
export function AssetGatewayPage() {
|
||||
const [config, setConfig] = useState<AssetGatewayConfig | null>(null);
|
||||
const [keys, setKeys] = useState<LlmProviderKeyRow[]>([]);
|
||||
@@ -93,37 +100,51 @@ export function AssetGatewayPage() {
|
||||
</div>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-success">{message}</p>}
|
||||
<section className="settings-card">
|
||||
<h3>全局总开关</h3>
|
||||
<label><input type="checkbox" checked={config.enabled} disabled={busy === 'global'} onChange={(event) => void saveGlobal(event.target.checked)} /> 启用资产能力 Gateway</label>
|
||||
<p className="muted">关闭时,所有插件安全降级为原有流程;插件配置将被保留。</p>
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">OPTIONAL CAPABILITY</div>
|
||||
<h3>资产能力 Gateway</h3>
|
||||
<p>关闭时所有调用自动降级为现有流程;配置保留,现有聊天和页面生成不受影响。</p>
|
||||
</div>
|
||||
<label className="asset-toggle">
|
||||
<input type="checkbox" checked={config.enabled} disabled={busy === 'global'} onChange={(event) => void saveGlobal(event.target.checked)} />
|
||||
<span>{config.enabled ? '已启用' : '已关闭'}</span>
|
||||
</label>
|
||||
</section>
|
||||
<div className="asset-plugin-grid">
|
||||
{config.plugins.map((plugin) => {
|
||||
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
||||
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
|
||||
return <section className="settings-card" key={plugin.pluginId}>
|
||||
<h3>{plugin.label}</h3>
|
||||
<p className="muted">{plugin.description}</p>
|
||||
<div className="executor-form-grid">
|
||||
<label><input type="checkbox" checked={draft.enabled} onChange={(event) => updateDraft(plugin.pluginId, { enabled: event.target.checked })} /> 启用插件</label>
|
||||
<select value={draft.provider} onChange={(event) => updateDraft(plugin.pluginId, { provider: event.target.value })} disabled={!draft.enabled}>
|
||||
const visual = pluginVisuals[plugin.pluginId] ?? { icon: '✦', accent: 'blue' };
|
||||
return <section className={`asset-plugin-card asset-plugin-card--${visual.accent}`} key={plugin.pluginId}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
|
||||
<div><h3>{plugin.label}</h3><p>{plugin.description}</p></div>
|
||||
<label className="asset-mini-toggle" title={`启用${plugin.label}`}>
|
||||
<input type="checkbox" checked={draft.enabled} onChange={(event) => updateDraft(plugin.pluginId, { enabled: event.target.checked })} />
|
||||
<span>{draft.enabled ? '开启' : '关闭'}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="asset-plugin-fields">
|
||||
<label>运行 Provider<select value={draft.provider} onChange={(event) => updateDraft(plugin.pluginId, { provider: event.target.value })} disabled={!draft.enabled}>
|
||||
<option value="">选择 Provider</option>
|
||||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||
</select>
|
||||
</select></label>
|
||||
{plugin.supportsLlm ? <>
|
||||
<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
|
||||
<label>专属 LLM<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
|
||||
<option value="">不使用 LLM</option>
|
||||
{activeKeys.map((key) => <option key={key.id} value={key.id}>{key.name}</option>)}
|
||||
</select>
|
||||
<select value={draft.llmModel} onChange={(event) => updateDraft(plugin.pluginId, { llmModel: event.target.value })} disabled={!draft.enabled || !selectedKey}>
|
||||
</select></label>
|
||||
<label>模型<select value={draft.llmModel} onChange={(event) => updateDraft(plugin.pluginId, { llmModel: event.target.value })} disabled={!draft.enabled || !selectedKey}>
|
||||
<option value="">选择模型</option>
|
||||
{selectedKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||||
</select>
|
||||
</> : <p className="muted">此插件不调用 LLM。</p>}
|
||||
<button type="button" className="send-btn" disabled={busy === plugin.pluginId} onClick={() => void savePlugin(plugin)}>{busy === plugin.pluginId ? '保存中…' : '保存插件'}</button>
|
||||
</select></label>
|
||||
</> : <div className="asset-no-llm">确定性处理 · 不调用 LLM</div>}
|
||||
</div>
|
||||
<div className="asset-plugin-footer"><span className={`asset-status ${draft.enabled ? 'is-on' : ''}`}>{draft.enabled ? '待保存为启用' : '默认安全关闭'}</span><button type="button" className="send-btn" disabled={busy === plugin.pluginId} onClick={() => void savePlugin(plugin)}>{busy === plugin.pluginId ? '保存中…' : '保存配置'}</button></div>
|
||||
</section>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -835,6 +835,50 @@ body,
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ── Asset gateway ───────────────────────────────────── */
|
||||
|
||||
.asset-gateway-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 72%, #7c5cff);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(120deg, rgba(75, 55, 146, .28), rgba(21, 27, 38, .78));
|
||||
}
|
||||
|
||||
.asset-kicker { color: #aaa1ff; font-size: 11px; font-weight: 700; letter-spacing: .12em; }
|
||||
.asset-gateway-hero h3 { margin: 5px 0 6px; font-size: 18px; }
|
||||
.asset-gateway-hero p { max-width: 620px; margin: 0; color: var(--color-text-muted); font-size: 13px; }
|
||||
.asset-toggle, .asset-mini-toggle { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; font-size: 13px; }
|
||||
.asset-toggle { padding: 9px 12px; border-radius: 999px; background: rgba(255,255,255,.07); }
|
||||
|
||||
.asset-plugin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 14px; }
|
||||
.asset-plugin-card { display: flex; flex-direction: column; gap: 16px; min-height: 326px; padding: 16px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-bg-surface); box-shadow: 0 10px 28px rgba(0,0,0,.12); }
|
||||
.asset-plugin-card--violet { border-top: 3px solid #9374ff; }
|
||||
.asset-plugin-card--blue { border-top: 3px solid #54a8ff; }
|
||||
.asset-plugin-card--amber { border-top: 3px solid #e8af55; }
|
||||
.asset-plugin-card--green { border-top: 3px solid #55c79b; }
|
||||
.asset-plugin-card-head { display: flex; gap: 10px; align-items: flex-start; }
|
||||
.asset-plugin-icon { display: grid; flex: 0 0 38px; width: 38px; height: 38px; place-items: center; border-radius: 12px; background: rgba(132, 110, 255, .17); color: #c4b8ff; font-size: 22px; }
|
||||
.asset-plugin-card-head h3 { margin: 0 0 4px; font-size: 15px; }
|
||||
.asset-plugin-card-head p { margin: 0; color: var(--color-text-muted); font-size: 12px; line-height: 1.45; }
|
||||
.asset-mini-toggle { margin-left: auto; color: var(--color-text-muted); font-size: 12px; }
|
||||
.asset-plugin-fields { display: grid; gap: 9px; }
|
||||
.asset-plugin-fields label { display: grid; gap: 5px; color: var(--color-text-muted); font-size: 11px; }
|
||||
.asset-plugin-fields select { width: 100%; min-width: 0; }
|
||||
.asset-no-llm { padding: 10px; border-radius: 8px; background: rgba(255,255,255,.04); color: var(--color-text-muted); font-size: 12px; }
|
||||
.asset-plugin-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--color-border); }
|
||||
.asset-status { color: var(--color-text-muted); font-size: 11px; }
|
||||
.asset-status.is-on { color: #68d8a7; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.asset-gateway-hero { align-items: flex-start; flex-direction: column; }
|
||||
.asset-plugin-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.executor-note {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
Reference in New Issue
Block a user