256 lines
8.0 KiB
TypeScript
256 lines
8.0 KiB
TypeScript
import { CircleHelp, Plus, RefreshCcw, Save, Trash2 } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import { getAgcModelCatalog, saveAgcModelCatalog } from '../api/adminApiClient';
|
|
import type { AdminAgcModel, AdminAgcModelCatalog } from '../api/adminApiTypes';
|
|
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
|
import { handlePageError } from './pageUtils';
|
|
|
|
export function AdminAgcModelsPage({
|
|
token,
|
|
onUnauthorized,
|
|
}: {
|
|
token: string;
|
|
onUnauthorized: (message?: string) => void;
|
|
}) {
|
|
const [catalog, setCatalog] = useState<AdminAgcModelCatalog | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [saved, setSaved] = useState(false);
|
|
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
|
|
|
async function refresh() {
|
|
if (!token) return;
|
|
setBusy(true);
|
|
setError('');
|
|
setSaved(false);
|
|
try {
|
|
setCatalog(await getAgcModelCatalog(token));
|
|
} catch (error) {
|
|
handlePageError(error, onUnauthorized, setError);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void refresh();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [token]);
|
|
|
|
function update(id: string, patch: Partial<AdminAgcModel>) {
|
|
setSaved(false);
|
|
setCatalog(
|
|
(current) =>
|
|
current && {
|
|
...current,
|
|
models: current.models.map((model) =>
|
|
model.id === id ? { ...model, ...patch } : model,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
async function save() {
|
|
if (!catalog || busy) return;
|
|
if (
|
|
!(await confirmWrite({
|
|
action: '保存 AGC 模型目录',
|
|
target: `${catalog.models.length} 个模型`,
|
|
}))
|
|
)
|
|
return;
|
|
setBusy(true);
|
|
setError('');
|
|
setSaved(false);
|
|
try {
|
|
setCatalog(await saveAgcModelCatalog(token, catalog));
|
|
setSaved(true);
|
|
} catch (error) {
|
|
handlePageError(error, onUnauthorized, setError);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section className="admin-page admin-page-wide admin-agc-models">
|
|
<div className="admin-page-heading">
|
|
<div>
|
|
<h2>AGC 模型</h2>
|
|
<p>管理客户端可用模型与用户看到的名称</p>
|
|
</div>
|
|
<span className="admin-agc-models-revision">
|
|
版本 v{catalog?.revision ?? '-'}
|
|
</span>
|
|
</div>
|
|
<div className="admin-agc-models-summary">
|
|
<div>
|
|
<span>已启用</span>
|
|
<strong>
|
|
{catalog?.models.filter((model) => model.enabled).length ?? 0}
|
|
</strong>
|
|
</div>
|
|
<div>
|
|
<span>默认模型</span>
|
|
<strong>
|
|
{catalog
|
|
? (catalog.models.find(
|
|
(model) => model.id === catalog.defaultModelId,
|
|
)?.alias ?? '未设置')
|
|
: '未设置'}
|
|
</strong>
|
|
</div>
|
|
<div>
|
|
<span>发布状态</span>
|
|
<strong>{busy ? '处理中' : saved ? '已保存' : '待修改'}</strong>
|
|
</div>
|
|
</div>
|
|
<section className="admin-panel admin-agc-models-panel">
|
|
<div className="admin-panel-heading">
|
|
<div>
|
|
<h3>模型目录</h3>
|
|
<span>客户端仅显示别名,实际模型名仅在这里维护</span>
|
|
</div>
|
|
<CircleHelp size={17} aria-label="模型目录帮助" />
|
|
</div>
|
|
<div className="admin-agc-models-toolbar">
|
|
<button
|
|
type="button"
|
|
title="重新读取"
|
|
aria-label="重新读取模型"
|
|
disabled={busy}
|
|
onClick={() => void refresh()}
|
|
>
|
|
<RefreshCcw size={16} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
title="添加模型"
|
|
aria-label="添加模型"
|
|
disabled={busy || !catalog || catalog.models.length >= 32}
|
|
onClick={() => {
|
|
setSaved(false);
|
|
setCatalog(
|
|
(current) =>
|
|
current && {
|
|
...current,
|
|
models: [
|
|
...current.models,
|
|
{
|
|
id: crypto.randomUUID(),
|
|
alias: '',
|
|
modelId: '',
|
|
enabled: true,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
<Plus size={16} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={busy || !catalog}
|
|
onClick={() => void save()}
|
|
>
|
|
<Save size={16} />
|
|
保存
|
|
</button>
|
|
</div>
|
|
{error ? <p role="alert">{error}</p> : null}
|
|
{saved ? <p role="status">已保存</p> : null}
|
|
{busy ? <p role="status">正在处理</p> : null}
|
|
<div className="admin-table-wrap admin-agc-models-table">
|
|
<table className="admin-table admin-agc-models-table-grid">
|
|
<thead>
|
|
<tr>
|
|
<th>别名</th>
|
|
<th>实际模型名</th>
|
|
<th>启用</th>
|
|
<th>默认</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{catalog?.models.map((model, index) => (
|
|
<tr key={model.id}>
|
|
<td>
|
|
<input
|
|
aria-label={`模型 ${index + 1} 别名`}
|
|
maxLength={40}
|
|
required
|
|
value={model.alias}
|
|
disabled={busy}
|
|
onChange={(e) =>
|
|
update(model.id, { alias: e.target.value })
|
|
}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
aria-label={`模型 ${index + 1} 实际模型名`}
|
|
maxLength={200}
|
|
required
|
|
value={model.modelId}
|
|
disabled={busy}
|
|
onChange={(e) =>
|
|
update(model.id, { modelId: e.target.value })
|
|
}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
aria-label={`模型 ${index + 1} 启用`}
|
|
type="checkbox"
|
|
checked={model.enabled}
|
|
disabled={busy || model.id === catalog.defaultModelId}
|
|
onChange={(e) =>
|
|
update(model.id, { enabled: e.target.checked })
|
|
}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
aria-label={`模型 ${index + 1} 默认`}
|
|
name="agc-default-model"
|
|
type="radio"
|
|
checked={model.id === catalog.defaultModelId}
|
|
disabled={busy || !model.enabled}
|
|
onChange={() => {
|
|
setSaved(false);
|
|
setCatalog({ ...catalog, defaultModelId: model.id });
|
|
}}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
title="删除模型"
|
|
aria-label={`删除模型 ${index + 1}`}
|
|
disabled={busy || model.id === catalog.defaultModelId}
|
|
onClick={() => {
|
|
setSaved(false);
|
|
setCatalog({
|
|
...catalog,
|
|
models: catalog.models.filter(
|
|
(candidate) => candidate.id !== model.id,
|
|
),
|
|
});
|
|
}}
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{confirmDialog}
|
|
</section>
|
|
);
|
|
}
|