feat: present asset plugins as control cards

This commit is contained in:
john
2026-07-12 10:00:30 +08:00
parent 90cbcbba7c
commit 0358484942
2 changed files with 82 additions and 17 deletions
+38 -17
View File
@@ -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>
);
}
+44
View File
@@ -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);