refactor(admin): unify model center and Memory V2 as asset plugin cards
Align Providers and Memory V2 pages with asset-gateway card layout; fix Memory V2 root layout and balanced grid on Asset Gateway. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -111,7 +111,7 @@ export function AssetGatewayPage() {
|
||||
<span>{config.enabled ? '已启用' : '已关闭'}</span>
|
||||
</label>
|
||||
</section>
|
||||
<div className="asset-plugin-grid">
|
||||
<div className="asset-plugin-grid asset-plugin-grid--balanced">
|
||||
{config.plugins.map((plugin) => {
|
||||
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
||||
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
|
||||
|
||||
+283
-183
@@ -124,6 +124,17 @@ const BACKENDS: Array<{
|
||||
];
|
||||
|
||||
const MODEL_BACKENDS: ModelBackendKey[] = ['mem0', 'letta', 'langgraph'];
|
||||
const BACKEND_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
|
||||
const BACKEND_ICONS: Record<BackendKey, string> = {
|
||||
pgvector: 'P',
|
||||
qdrant: 'Q',
|
||||
weaviate: 'W',
|
||||
mem0: 'M',
|
||||
letta: 'L',
|
||||
neo4j: 'N',
|
||||
redisStreams: 'R',
|
||||
langgraph: 'G',
|
||||
};
|
||||
const MODEL_API_TYPES: Array<{ value: MemoryV2ModelApiType; label: string }> = [
|
||||
{ value: 'chat', label: 'Chat API' },
|
||||
{ value: 'response', label: 'Response API' },
|
||||
@@ -231,6 +242,7 @@ export function MemoryV2Page() {
|
||||
const [busyScope, setBusyScope] = useState<'global' | 'chatIntentRouter' | BackendKey | 'reload' | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [expandedBackends, setExpandedBackends] = useState<Record<string, boolean>>({});
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -288,6 +300,9 @@ export function MemoryV2Page() {
|
||||
};
|
||||
|
||||
const updateBackendFlag = (backend: BackendKey) => (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target.checked) {
|
||||
setExpandedBackends((current) => ({ ...current, [backend]: false }));
|
||||
}
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[backend]: { ...current[backend], enabled: event.target.checked },
|
||||
@@ -398,9 +413,13 @@ export function MemoryV2Page() {
|
||||
const currentRouterSection = current.chatIntentRouter;
|
||||
const routerProviderKey = findProviderKey(providerKeys, String(routerSection.modelProviderKeyId ?? ''));
|
||||
const routerAvailableModels = modelOptionsForProvider(routerProviderKey, globalModelSettings);
|
||||
const enabledBackendCount = BACKENDS.filter((backend) => Boolean(current[backend.key]?.enabled)).length;
|
||||
const updatedAtText = payload?.updatedAt
|
||||
? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false })
|
||||
: '未保存过数据库配置';
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page memory-v2-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>Memory V2 配置</h2>
|
||||
<p className="muted">这里是 Memory V2 的统一后台控制面。`memind_adm` 负责改配置,主站会从共享数据库读取并自动切换。</p>
|
||||
@@ -409,20 +428,47 @@ export function MemoryV2Page() {
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
|
||||
<div className="admin-form">
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">MEMORY V2</div>
|
||||
<h3>统一记忆控制面</h3>
|
||||
<p>
|
||||
全局 {draft.global.enabled ? '已启用' : '未启用'}
|
||||
{' · '}
|
||||
默认 backend <strong className="mono">{String(draft.global.backend ?? 'legacy')}</strong>
|
||||
{' · '}
|
||||
已启用 backend {enabledBackendCount}/{BACKENDS.length}
|
||||
{' · '}
|
||||
更新时间 {updatedAtText}
|
||||
</p>
|
||||
</div>
|
||||
<div className="model-center-hero-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void load()}
|
||||
disabled={busyScope !== null && busyScope !== 'reload'}
|
||||
>
|
||||
{busyScope === 'reload' ? '加载中…' : '重新加载'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3 className="model-center-section-title">全局与路由</h3>
|
||||
<div className="asset-plugin-grid memory-v2-top-grid">
|
||||
<section className="asset-plugin-card asset-plugin-card--violet model-center-card memory-v2-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">M</div>
|
||||
<div>
|
||||
<h2>全局开关</h2>
|
||||
<p className="muted">更新时间:{payload?.updatedAt ? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false }) : '未保存过数据库配置'}</p>
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="send-btn" disabled={loading || busyScope !== null} onClick={() => void saveGlobalConfig()}>
|
||||
{busyScope === 'global' ? '保存中...' : '保存全局'}
|
||||
</button>
|
||||
<h3>全局开关</h3>
|
||||
<p>控制 Memory V2 总开关、Profile / Event Log / Vector 与 fail-open 行为。</p>
|
||||
</div>
|
||||
<span className={`asset-status ${draft.global.enabled ? 'is-on' : ''}`}>
|
||||
{draft.global.enabled ? '已启用' : '未启用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
|
||||
<div className="memory-v2-toggle-grid">
|
||||
<label className="inline-check">
|
||||
<input type="checkbox" checked={Boolean(draft.global.enabled)} onChange={updateGlobalFlag('enabled')} />
|
||||
启用 Memory V2
|
||||
@@ -443,8 +489,11 @@ export function MemoryV2Page() {
|
||||
<input type="checkbox" checked={Boolean(draft.global.failOpen)} onChange={updateGlobalFlag('failOpen')} />
|
||||
Memory 故障时 fail-open
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
<span>默认 Backend</span>
|
||||
默认 Backend
|
||||
<select value={String(draft.global.backend ?? 'legacy')} onChange={updateGlobalText('backend')}>
|
||||
<option value="legacy">legacy</option>
|
||||
{BACKENDS.map((backend) => (
|
||||
@@ -453,39 +502,41 @@ export function MemoryV2Page() {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!loading && (
|
||||
<p className="muted">
|
||||
<p className="model-center-card-note">
|
||||
当前数据库配置默认 backend:<strong>{String(current.global.backend ?? 'legacy')}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${draft.global.enabled ? 'is-on' : ''}`}>
|
||||
{draft.global.enabled ? 'Memory V2 运行中' : 'Memory V2 关闭'}
|
||||
</span>
|
||||
<button type="button" className="send-btn" disabled={loading || busyScope !== null} onClick={() => void saveGlobalConfig()}>
|
||||
{busyScope === 'global' ? '保存中...' : '保存全局'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<section className="asset-plugin-card asset-plugin-card--blue model-center-card memory-v2-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">R</div>
|
||||
<div>
|
||||
<h2>前置 LLM 意图路由</h2>
|
||||
<p className="muted">关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM,并可读取 Memory V2 的轻量上下文。</p>
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.enabled)}
|
||||
onChange={updateRouterFlag('enabled')}
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={loading || busyScope !== null}
|
||||
onClick={() => void saveRouterConfig()}
|
||||
>
|
||||
{busyScope === 'chatIntentRouter' ? '保存中...' : '保存路由'}
|
||||
</button>
|
||||
<h3>前置 LLM 意图路由</h3>
|
||||
<p>关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM。</p>
|
||||
</div>
|
||||
<label className="asset-mini-toggle" aria-label="启用前置 LLM 意图路由">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.enabled)}
|
||||
onChange={updateRouterFlag('enabled')}
|
||||
/>
|
||||
<span>{routerSection.enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
<label>
|
||||
<span>模型来源</span>
|
||||
<select
|
||||
@@ -523,69 +574,96 @@ export function MemoryV2Page() {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>最低置信度</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={String(routerSection.minConfidence ?? '0.65')}
|
||||
onChange={updateRouterValue('minConfidence')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>记忆条数</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={String(routerSection.memoryResolveLimit ?? '8')}
|
||||
onChange={updateRouterValue('memoryResolveLimit')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>超时 Ms</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="30000"
|
||||
value={String(routerSection.timeoutMs ?? '1500')}
|
||||
onChange={updateRouterValue('timeoutMs')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>低置信 / 失败回退</span>
|
||||
<select
|
||||
value={String(routerSection.fallbackRoute ?? 'agent_orchestration')}
|
||||
onChange={updateRouterValue('fallbackRoute')}
|
||||
>
|
||||
<option value="agent_orchestration">Agent 编排</option>
|
||||
<option value="direct_chat">Direct Chat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.memoryResolveEnabled)}
|
||||
onChange={updateRouterFlag('memoryResolveEnabled')}
|
||||
/>
|
||||
路由前读取 Memory V2
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">
|
||||
|
||||
<details className="memory-v2-advanced-details">
|
||||
<summary>高级参数</summary>
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
<label>
|
||||
<span>最低置信度</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={String(routerSection.minConfidence ?? '0.65')}
|
||||
onChange={updateRouterValue('minConfidence')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>记忆条数</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={String(routerSection.memoryResolveLimit ?? '8')}
|
||||
onChange={updateRouterValue('memoryResolveLimit')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>超时 Ms</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="30000"
|
||||
value={String(routerSection.timeoutMs ?? '1500')}
|
||||
onChange={updateRouterValue('timeoutMs')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>低置信 / 失败回退</span>
|
||||
<select
|
||||
value={String(routerSection.fallbackRoute ?? 'agent_orchestration')}
|
||||
onChange={updateRouterValue('fallbackRoute')}
|
||||
>
|
||||
<option value="agent_orchestration">Agent 编排</option>
|
||||
<option value="direct_chat">Direct Chat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.memoryResolveEnabled)}
|
||||
onChange={updateRouterFlag('memoryResolveEnabled')}
|
||||
/>
|
||||
路由前读取 Memory V2
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<p className="model-center-card-note">
|
||||
当前数据库配置:<strong>{currentRouterSection.enabled ? '已启用' : '未启用'}</strong>
|
||||
{' · '}
|
||||
生效模型链路:<strong>{describeEffectiveModelSource(routerSection, providerKeys, globalModelSettings)}</strong>
|
||||
</p>
|
||||
{!providerKeys.length && (
|
||||
<p className="muted">
|
||||
<p className="model-center-card-note">
|
||||
统一模型中心当前没有可用的激活 key;若启用路由,请先配置 Provider key 与模型列表。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{BACKENDS.map((backend) => {
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${routerSection.enabled ? 'is-on' : ''}`}>
|
||||
{routerSection.enabled ? '路由已启用' : '路由已关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={loading || busyScope !== null}
|
||||
onClick={() => void saveRouterConfig()}
|
||||
>
|
||||
{busyScope === 'chatIntentRouter' ? '保存中...' : '保存路由'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">Backend 配置</h3>
|
||||
<p className="model-center-card-note memory-v2-backend-intro">
|
||||
单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。未启用的 backend 默认折叠,点击标题可展开。
|
||||
</p>
|
||||
<div className="asset-plugin-grid memory-v2-backend-grid">
|
||||
{BACKENDS.map((backend, index) => {
|
||||
const section = draft[backend.key];
|
||||
const currentSection = current[backend.key];
|
||||
const providerKey = isModelBackend(backend.key)
|
||||
@@ -594,23 +672,123 @@ export function MemoryV2Page() {
|
||||
const availableModels = isModelBackend(backend.key)
|
||||
? modelOptionsForProvider(providerKey, globalModelSettings)
|
||||
: [];
|
||||
const accent = BACKEND_ACCENTS[index % BACKEND_ACCENTS.length];
|
||||
const isBackendOpen = Boolean(section.enabled) || Boolean(expandedBackends[backend.key]);
|
||||
|
||||
return (
|
||||
<section key={backend.key} className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<div>
|
||||
<h2>{backend.label}</h2>
|
||||
<p className="muted">单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。</p>
|
||||
<details
|
||||
key={backend.key}
|
||||
className={`asset-plugin-card asset-plugin-card--${accent} model-center-card memory-v2-card memory-v2-backend-details`}
|
||||
open={isBackendOpen}
|
||||
onToggle={(event) => {
|
||||
const nextOpen = event.currentTarget.open;
|
||||
setExpandedBackends((current) => ({
|
||||
...current,
|
||||
[backend.key]: nextOpen,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<summary className="memory-v2-backend-summary">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{BACKEND_ICONS[backend.key]}</div>
|
||||
<div className="memory-v2-backend-summary-main">
|
||||
<h3>{backend.label}</h3>
|
||||
<span className={`asset-status ${section.enabled ? 'is-on' : ''}`}>
|
||||
{section.enabled ? '已启用' : '未启用 · 点击展开'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(section.enabled)}
|
||||
onChange={updateBackendFlag(backend.key)}
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
<label
|
||||
className="asset-mini-toggle"
|
||||
aria-label={`启用 ${backend.label}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(section.enabled)}
|
||||
onChange={updateBackendFlag(backend.key)}
|
||||
/>
|
||||
<span>{section.enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</summary>
|
||||
|
||||
<div className="memory-v2-backend-body">
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
{backend.fields.map((field) => {
|
||||
const configured = Boolean(currentSection?.[`${field.key}Configured`]);
|
||||
const masked = String(currentSection?.[`${field.key}Masked`] ?? '');
|
||||
return (
|
||||
<label key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
value={String(section[field.key] ?? '')}
|
||||
placeholder={field.secret && configured ? `已配置:${masked || '******'};留空保持不变` : ''}
|
||||
onChange={updateBackendValue(backend.key, field.key)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isModelBackend(backend.key) && (
|
||||
<>
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
<label>
|
||||
<span>模型来源</span>
|
||||
<select
|
||||
value={String(section.modelProviderKeyId ?? '')}
|
||||
onChange={updateModelProvider(backend.key)}
|
||||
>
|
||||
<option value="">跟随统一模型中心默认</option>
|
||||
{providerKeys.map((item) => (
|
||||
<option key={item.id} value={item.id}>{providerLabel(item)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型</span>
|
||||
<select
|
||||
value={String(section.model ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'model')}
|
||||
>
|
||||
<option value="">
|
||||
{providerKey ? '使用当前 Provider 默认模型' : '跟随统一模型中心默认模型'}
|
||||
</option>
|
||||
{availableModels.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型类型</span>
|
||||
<select
|
||||
value={String(section.modelApiType ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'modelApiType')}
|
||||
>
|
||||
<option value="">未指定</option>
|
||||
{supportedApiTypes.map((apiType) => (
|
||||
<option key={apiType} value={apiType}>
|
||||
{MODEL_API_TYPES.find((item) => item.value === apiType)?.label ?? apiType}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="model-center-card-note">
|
||||
生效模型:<strong>{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}</strong>
|
||||
</p>
|
||||
{!providerKeys.length && (
|
||||
<p className="model-center-card-note">
|
||||
统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${section.enabled ? 'is-on' : ''}`}>
|
||||
{section.enabled ? `${backend.label} 已启用` : `${backend.label} 未启用`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
@@ -621,87 +799,9 @@ export function MemoryV2Page() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
{backend.fields.map((field) => {
|
||||
const configured = Boolean(currentSection?.[`${field.key}Configured`]);
|
||||
const masked = String(currentSection?.[`${field.key}Masked`] ?? '');
|
||||
return (
|
||||
<label key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
value={String(section[field.key] ?? '')}
|
||||
placeholder={field.secret && configured ? `已配置:${masked || '******'};留空保持不变` : ''}
|
||||
onChange={updateBackendValue(backend.key, field.key)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isModelBackend(backend.key) && (
|
||||
<>
|
||||
<div className="admin-form-grid" style={{ marginTop: 12 }}>
|
||||
<label>
|
||||
<span>模型来源</span>
|
||||
<select
|
||||
value={String(section.modelProviderKeyId ?? '')}
|
||||
onChange={updateModelProvider(backend.key)}
|
||||
>
|
||||
<option value="">跟随统一模型中心默认</option>
|
||||
{providerKeys.map((item) => (
|
||||
<option key={item.id} value={item.id}>{providerLabel(item)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型</span>
|
||||
<select
|
||||
value={String(section.model ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'model')}
|
||||
>
|
||||
<option value="">
|
||||
{providerKey ? '使用当前 Provider 默认模型' : '跟随统一模型中心默认模型'}
|
||||
</option>
|
||||
{availableModels.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型类型</span>
|
||||
<select
|
||||
value={String(section.modelApiType ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'modelApiType')}
|
||||
>
|
||||
<option value="">未指定</option>
|
||||
{supportedApiTypes.map((apiType) => (
|
||||
<option key={apiType} value={apiType}>
|
||||
{MODEL_API_TYPES.find((item) => item.value === apiType)?.label ?? apiType}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">
|
||||
当前生效模型链路:<strong>{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}</strong>
|
||||
</p>
|
||||
{!providerKeys.length && (
|
||||
<p className="muted">
|
||||
统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="ghost-btn" onClick={() => void load()} disabled={busyScope !== null && busyScope !== 'reload'}>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+215
-160
@@ -35,6 +35,14 @@ import type {
|
||||
|
||||
const CUSTOM_PROVIDER_ID = '__custom__';
|
||||
|
||||
const EXECUTOR_VISUALS: Record<string, { icon: string; accent: string }> = {
|
||||
goose: { icon: 'G', accent: 'blue' },
|
||||
aider: { icon: 'A', accent: 'amber' },
|
||||
openhands: { icon: 'O', accent: 'violet' },
|
||||
};
|
||||
|
||||
const PROVIDER_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
|
||||
|
||||
const defaultProviderForm = {
|
||||
providerId: CUSTOM_PROVIDER_ID,
|
||||
name: '',
|
||||
@@ -270,40 +278,41 @@ function ExecutorCard({
|
||||
}
|
||||
}, [model, selectedKey]);
|
||||
|
||||
const visual = EXECUTOR_VISUALS[binding.executor] ?? { icon: '✦', accent: 'blue' };
|
||||
|
||||
return (
|
||||
<article className="executor-card">
|
||||
<div className="admin-card-head">
|
||||
<section className={`asset-plugin-card asset-plugin-card--${visual.accent} model-center-card`}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
|
||||
<div>
|
||||
<h3>{binding.executorLabel}</h3>
|
||||
<p className="muted">{binding.executorDescription}</p>
|
||||
<p>{binding.executorDescription}</p>
|
||||
</div>
|
||||
<label className="inline-check">
|
||||
<label className="asset-mini-toggle" aria-label={`${binding.executorLabel} 启用状态`}>
|
||||
<input type="checkbox" checked={enabled} onChange={(event) => setEnabled(event.target.checked)} />
|
||||
启用
|
||||
<span>{enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="executor-form-grid">
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || (enabled && (!keyId || !model))}
|
||||
onClick={() => void onSave(binding, keyId || null, model, enabled)}
|
||||
>
|
||||
保存绑定
|
||||
</button>
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
Provider
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
</label>
|
||||
<label>
|
||||
模型
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="executor-footer">
|
||||
<div className="model-center-card-body">
|
||||
<div className="executor-meta">
|
||||
<span>{statusText(launchStatus)}</span>
|
||||
{runtime?.ok ? <span>{runtime.providerName} · {runtime.model}</span> : <span>{runtime?.message ?? '运行配置未就绪'}</span>}
|
||||
@@ -348,10 +357,24 @@ function ExecutorCard({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
|
||||
</div>
|
||||
|
||||
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
|
||||
</article>
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${enabled ? 'is-on' : ''}`}>
|
||||
{enabled ? '绑定启用中' : '绑定已关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || (enabled && (!keyId || !model))}
|
||||
onClick={() => void onSave(binding, keyId || null, model, enabled)}
|
||||
>
|
||||
保存绑定
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -387,45 +410,61 @@ function VisionSection({
|
||||
}, [model, selectedKey]);
|
||||
|
||||
return (
|
||||
<section className="admin-card providers-panel">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>图片任务模型</h3>
|
||||
<p className="muted">发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)</p>
|
||||
<div className="asset-plugin-grid model-center-vision-grid">
|
||||
<section className="asset-plugin-card asset-plugin-card--green model-center-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">◉</div>
|
||||
<div>
|
||||
<h3>图片任务模型</h3>
|
||||
<p>发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)</p>
|
||||
</div>
|
||||
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
|
||||
{settings?.keyId ? `${settings.keyName} · ${settings.visionModel}` : '未配置'}
|
||||
</span>
|
||||
</div>
|
||||
{settings?.keyId ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
|
||||
清除
|
||||
</button>
|
||||
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
Provider
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
</label>
|
||||
<label>
|
||||
视觉模型
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择视觉模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!settings?.keyId ? (
|
||||
<p className="model-center-card-note">未配置时,图片消息将由默认模型处理。</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{settings?.keyId ? (
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>
|
||||
当前:<strong>{settings.keyName}</strong> · <span className="mono">{settings.visionModel}</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>未配置,图片消息将由默认模型处理</p>
|
||||
)}
|
||||
|
||||
<div className="executor-form-grid">
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择视觉模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || !keyId || !model}
|
||||
onClick={() => void onSave(keyId, model)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
|
||||
{settings?.keyId ? '已绑定视觉模型' : '等待配置'}
|
||||
</span>
|
||||
<div className="model-center-card-actions">
|
||||
{settings?.keyId ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
|
||||
清除
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || !keyId || !model}
|
||||
onClick={() => void onSave(keyId, model)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -723,123 +762,139 @@ export function ProvidersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const activeProviderCount = keys.filter((item) => item.status === 'active').length;
|
||||
|
||||
return (
|
||||
<div className="admin-page providers-page">
|
||||
<div className="admin-page-head providers-hero">
|
||||
<div>
|
||||
<h2>统一模型中心</h2>
|
||||
<p className="muted">管理 Provider、执行器绑定和默认模型</p>
|
||||
</div>
|
||||
<div className="providers-hero-meta">
|
||||
<div className="providers-hero-stat">
|
||||
<span className="providers-hero-label">当前默认</span>
|
||||
<strong>{globalSettings?.globalModel ?? '未设置'}</strong>
|
||||
</div>
|
||||
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
|
||||
添加 Provider
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>统一模型中心</h2>
|
||||
<p className="muted">管理 Provider、执行器绑定和默认模型</p>
|
||||
</div>
|
||||
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
|
||||
<section className="admin-card providers-panel providers-panel-primary">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>执行器绑定</h3>
|
||||
<p className="muted">把 Provider 和模型绑定到 Goose、Aider、OpenHands</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
|
||||
同步 Goose 绑定
|
||||
</button>
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">MODEL CENTER</div>
|
||||
<h3>默认模型与 Provider 资源池</h3>
|
||||
<p>
|
||||
当前默认模型 <strong className="mono">{globalSettings?.globalModel ?? '未设置'}</strong>
|
||||
{' · '}
|
||||
Provider 资源池 {activeProviderCount}/{keys.length} 可用
|
||||
</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="muted">加载中...</p>
|
||||
) : (
|
||||
<div className="executor-grid">
|
||||
{bindings.map((binding) => (
|
||||
<ExecutorCard
|
||||
key={binding.executor}
|
||||
binding={binding}
|
||||
keys={activeKeys}
|
||||
runtime={runtimes.find((item) => item.executor === binding.executor)}
|
||||
plan={plans.find((item) => item.executor === binding.executor)}
|
||||
launchStatus={launchStatuses[binding.executor]}
|
||||
busy={busy}
|
||||
onSave={handleSaveBinding}
|
||||
onLaunch={handleLaunch}
|
||||
onStop={handleStop}
|
||||
onRestart={handleRestart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-card providers-panel providers-panel-secondary">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>Provider 资源池</h3>
|
||||
<p className="muted">按名称纵向查看,更接近 list</p>
|
||||
</div>
|
||||
<div className="model-center-hero-actions">
|
||||
<button type="button" className="ghost-btn" disabled={busy || loading} onClick={() => void load()}>
|
||||
刷新
|
||||
</button>
|
||||
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
|
||||
添加 Provider
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="providers-list">
|
||||
{keys.map((row) => (
|
||||
<article key={row.id} className="provider-list-item">
|
||||
<div className="provider-list-main">
|
||||
<div className="provider-list-top">
|
||||
<div className="providers-name">
|
||||
<span>{row.name}</span>
|
||||
{row.isSelected ? <span className="risk-pill risk-low">默认</span> : null}
|
||||
</div>
|
||||
<span className={`provider-state ${row.status === 'active' ? 'is-active' : 'is-disabled'}`}>
|
||||
{row.status === 'active' ? '可用' : '禁用'}
|
||||
</span>
|
||||
<div className="model-center-section-head">
|
||||
<h3 className="model-center-section-title">执行器绑定</h3>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
|
||||
同步 Goose 绑定
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="muted">加载中...</p>
|
||||
) : (
|
||||
<div className="asset-plugin-grid">
|
||||
{bindings.map((binding) => (
|
||||
<ExecutorCard
|
||||
key={binding.executor}
|
||||
binding={binding}
|
||||
keys={activeKeys}
|
||||
runtime={runtimes.find((item) => item.executor === binding.executor)}
|
||||
plan={plans.find((item) => item.executor === binding.executor)}
|
||||
launchStatus={launchStatuses[binding.executor]}
|
||||
busy={busy}
|
||||
onSave={handleSaveBinding}
|
||||
onLaunch={handleLaunch}
|
||||
onStop={handleStop}
|
||||
onRestart={handleRestart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="model-center-section-title">Provider 资源池</h3>
|
||||
<div className="asset-plugin-grid">
|
||||
{keys.map((row, index) => {
|
||||
const accent = PROVIDER_ACCENTS[index % PROVIDER_ACCENTS.length];
|
||||
return (
|
||||
<section key={row.id} className={`asset-plugin-card asset-plugin-card--${accent} model-center-card model-center-provider-card`}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">✦</div>
|
||||
<div>
|
||||
<h3>{row.name}</h3>
|
||||
<p>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</p>
|
||||
</div>
|
||||
<div className="provider-list-meta">
|
||||
<span>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</span>
|
||||
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
|
||||
{row.status === 'active' ? '可用' : '禁用'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="model-center-provider-badges">
|
||||
{row.isSelected ? <span className="risk-pill risk-low">默认</span> : null}
|
||||
{row.isVisionSelected ? <span className="risk-pill risk-medium">图片任务</span> : null}
|
||||
</div>
|
||||
|
||||
<div className="asset-plugin-fields model-center-provider-meta">
|
||||
<div className="model-center-meta-row">
|
||||
<span>默认模型</span>
|
||||
<span className="mono">{row.defaultModel}</span>
|
||||
</div>
|
||||
<div className="model-center-meta-row">
|
||||
<span>API Key</span>
|
||||
<span className="mono">{row.apiKeyMasked}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-actions providers-actions provider-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy}
|
||||
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
|
||||
测试
|
||||
</button>
|
||||
{!row.isSelected && row.status === 'active' ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
|
||||
设为默认
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
|
||||
{row.isSelected ? '当前默认 Provider' : '备用配置'}
|
||||
</span>
|
||||
<div className="model-center-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy}
|
||||
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || row.isSelected}
|
||||
onClick={() => void handleToggleProviderStatus(row)}
|
||||
>
|
||||
{row.status === 'active' ? '禁用' : '恢复'}
|
||||
</button>
|
||||
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
|
||||
删除
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
|
||||
测试
|
||||
</button>
|
||||
{!row.isSelected && row.status === 'active' ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
|
||||
设为默认
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || row.isSelected}
|
||||
onClick={() => void handleToggleProviderStatus(row)}
|
||||
>
|
||||
{row.status === 'active' ? '禁用' : '恢复'}
|
||||
</button>
|
||||
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">图片任务模型</h3>
|
||||
<VisionSection
|
||||
keys={activeKeys}
|
||||
settings={visionSettings}
|
||||
|
||||
+405
-44
@@ -835,50 +835,6 @@ 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);
|
||||
@@ -994,6 +950,411 @@ body,
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Asset gateway / model center ────────────────────── */
|
||||
|
||||
.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;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.asset-plugin-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
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-grid--balanced .asset-plugin-card {
|
||||
min-height: 326px;
|
||||
}
|
||||
|
||||
.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;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.model-center-hero-actions,
|
||||
.model-center-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-center-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.model-center-section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.model-center-section-head .model-center-section-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-center-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.model-center-provider-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.model-center-card-body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-center-card-note {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-center-provider-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-center-provider-meta {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-center-meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-center-meta-row .mono {
|
||||
color: var(--color-text-primary);
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.model-center-vision-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.model-center-card .executor-instruction,
|
||||
.model-center-card .executor-meta,
|
||||
.model-center-card .executor-note,
|
||||
.model-center-card .executor-log {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-center-card .executor-actions {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.memory-v2-page {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.memory-v2-page > .asset-gateway-hero,
|
||||
.memory-v2-page > .asset-plugin-grid,
|
||||
.memory-v2-page > .model-center-section-title,
|
||||
.memory-v2-page > .model-center-card-note,
|
||||
.memory-v2-page > .banner {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.memory-v2-page .memory-v2-fields input,
|
||||
.memory-v2-page .memory-v2-fields select,
|
||||
.memory-v2-page .asset-plugin-fields select {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border-input);
|
||||
background: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.memory-v2-top-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-grid {
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-card {
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.memory-v2-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.memory-v2-backend-intro {
|
||||
margin: -4px 0 10px;
|
||||
}
|
||||
|
||||
.memory-v2-backend-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary-main {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary-main h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.memory-v2-backend-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details > summary {
|
||||
padding: 8px 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details .memory-v2-fields {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.memory-v2-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.memory-v2-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px 12px;
|
||||
}
|
||||
|
||||
.memory-v2-fields label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.memory-v2-fields label > span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-card .inline-check {
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-page .model-center-card-note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.asset-gateway-hero,
|
||||
.model-center-section-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.asset-plugin-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.asset-plugin-footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Capability settings ─────────────────────────────── */
|
||||
|
||||
.capability-intro {
|
||||
|
||||
Reference in New Issue
Block a user