feat: collapsible nav sidebar and chat param panel, rename to service management
This commit is contained in:
+120
-83
@@ -10,6 +10,7 @@ import {
|
||||
onEvent,
|
||||
} from "../api";
|
||||
import { useStore } from "../store";
|
||||
import Icon from "../components/Icon";
|
||||
|
||||
interface LocalMessage {
|
||||
id: string;
|
||||
@@ -38,8 +39,19 @@ export default function ChatPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [params, setParams] = useState<ChatParams>(defaultParams);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||
const [rightOpen, setRightOpen] = useState(
|
||||
() => localStorage.getItem("xianren-right-open") !== "0",
|
||||
);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function toggleRight() {
|
||||
setRightOpen((v) => {
|
||||
const next = !v;
|
||||
localStorage.setItem("xianren-right-open", next ? "1" : "0");
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (models.length > 0 && !selectedModelId) {
|
||||
setSelectedModelId(models[0].id);
|
||||
@@ -275,96 +287,121 @@ export default function ChatPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧边栏:当前模型 + 部署/请求参数 */}
|
||||
<div className="flex w-72 flex-col overflow-y-auto border-l border-border bg-panel p-3">
|
||||
<div className="mb-1 text-xs text-slate-400">当前模型</div>
|
||||
<select
|
||||
className="input w-full"
|
||||
value={selectedModelId}
|
||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||
>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.file_name}
|
||||
{m.kind === "remote" ? "(API)" : ""}
|
||||
{engineRunning && engineModel === m.file_name ? " ●已部署" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIsRemote ? (
|
||||
<div className="mt-2 rounded-lg border border-sky-500/30 bg-sky-500/10 px-3 py-1.5 text-xs text-sky-300">
|
||||
在线 API 模型,无需本地引擎
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 flex gap-2">
|
||||
{/* 右侧边栏:当前模型 + 部署/请求参数(可收起) */}
|
||||
{rightOpen ? (
|
||||
<div className="flex w-72 flex-col overflow-y-auto border-l border-border bg-panel p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs uppercase text-slate-400">参数面板</span>
|
||||
<button
|
||||
className="flex-1 rounded-lg border border-border bg-panel-2 py-1.5 text-xs hover:bg-panel-2/60"
|
||||
onClick={() =>
|
||||
api.engineStart(selectedModelId, params).catch((e) => alert(String(e)))
|
||||
}
|
||||
disabled={engineRunning}
|
||||
className="rounded p-1 text-slate-400 hover:bg-panel-2/60 hover:text-slate-200"
|
||||
onClick={toggleRight}
|
||||
title="收起参数面板"
|
||||
>
|
||||
启动引擎
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 rounded-lg border border-border bg-panel-2 py-1.5 text-xs hover:bg-panel-2/60"
|
||||
onClick={() => api.engineStop().catch((e) => alert(String(e)))}
|
||||
disabled={!engineRunning}
|
||||
>
|
||||
停止引擎
|
||||
<Icon name="chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 truncate text-xs text-slate-500">{engineLabel}</div>
|
||||
<div className="mb-1 text-xs text-slate-400">当前模型</div>
|
||||
<select
|
||||
className="input w-full"
|
||||
value={selectedModelId}
|
||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||
>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.file_name}
|
||||
{m.kind === "remote" ? "(API)" : ""}
|
||||
{engineRunning && engineModel === m.file_name ? " ●已部署" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIsRemote ? (
|
||||
<div className="mt-2 rounded-lg border border-sky-500/30 bg-sky-500/10 px-3 py-1.5 text-xs text-sky-300">
|
||||
在线 API 模型,无需本地引擎
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
className="flex-1 rounded-lg border border-border bg-panel-2 py-1.5 text-xs hover:bg-panel-2/60"
|
||||
onClick={() =>
|
||||
api.engineStart(selectedModelId, params).catch((e) => alert(String(e)))
|
||||
}
|
||||
disabled={engineRunning}
|
||||
>
|
||||
启动引擎
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 rounded-lg border border-border bg-panel-2 py-1.5 text-xs hover:bg-panel-2/60"
|
||||
onClick={() => api.engineStop().catch((e) => alert(String(e)))}
|
||||
disabled={!engineRunning}
|
||||
>
|
||||
停止引擎
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 truncate text-xs text-slate-500">{engineLabel}</div>
|
||||
|
||||
<div className="mt-4 border-t border-border pt-3">
|
||||
<div className="mb-3 text-xs uppercase text-slate-400">部署参数</div>
|
||||
<ParamSlider
|
||||
label="上下文长度 ctx_size"
|
||||
value={params.ctx_size}
|
||||
min={512}
|
||||
max={32768}
|
||||
step={512}
|
||||
onChange={(v) => setParams({ ...params, ctx_size: v })}
|
||||
/>
|
||||
<ParamSlider
|
||||
label="GPU 层数 ngl(99=全部)"
|
||||
value={params.ngl}
|
||||
min={0}
|
||||
max={99}
|
||||
step={1}
|
||||
onChange={(v) => setParams({ ...params, ngl: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-border pt-3">
|
||||
<div className="mb-3 text-xs uppercase text-slate-400">部署参数</div>
|
||||
<ParamSlider
|
||||
label="上下文长度 ctx_size"
|
||||
value={params.ctx_size}
|
||||
min={512}
|
||||
max={32768}
|
||||
step={512}
|
||||
onChange={(v) => setParams({ ...params, ctx_size: v })}
|
||||
/>
|
||||
<ParamSlider
|
||||
label="GPU 层数 ngl(99=全部)"
|
||||
value={params.ngl}
|
||||
min={0}
|
||||
max={99}
|
||||
step={1}
|
||||
onChange={(v) => setParams({ ...params, ngl: v })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border-t border-border pt-3">
|
||||
<div className="mb-3 text-xs uppercase text-slate-400">请求参数</div>
|
||||
<ParamSlider
|
||||
label="temperature"
|
||||
value={params.temperature}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
onChange={(v) => setParams({ ...params, temperature: v })}
|
||||
/>
|
||||
<ParamSlider
|
||||
label="top_p"
|
||||
value={params.top_p}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
onChange={(v) => setParams({ ...params, top_p: v })}
|
||||
/>
|
||||
<ParamSlider
|
||||
label="max_tokens"
|
||||
value={params.max_tokens}
|
||||
min={64}
|
||||
max={8192}
|
||||
step={64}
|
||||
onChange={(v) => setParams({ ...params, max_tokens: v })}
|
||||
/>
|
||||
<div className="mt-4 border-t border-border pt-3">
|
||||
<div className="mb-3 text-xs uppercase text-slate-400">请求参数</div>
|
||||
<ParamSlider
|
||||
label="temperature"
|
||||
value={params.temperature}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
onChange={(v) => setParams({ ...params, temperature: v })}
|
||||
/>
|
||||
<ParamSlider
|
||||
label="top_p"
|
||||
value={params.top_p}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
onChange={(v) => setParams({ ...params, top_p: v })}
|
||||
/>
|
||||
<ParamSlider
|
||||
label="max_tokens"
|
||||
value={params.max_tokens}
|
||||
min={64}
|
||||
max={8192}
|
||||
step={64}
|
||||
onChange={(v) => setParams({ ...params, max_tokens: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-9 flex-col items-center border-l border-border bg-panel py-2">
|
||||
<button
|
||||
className="rounded p-1.5 text-slate-400 hover:bg-panel-2/60 hover:text-slate-200"
|
||||
onClick={toggleRight}
|
||||
title="展开参数面板"
|
||||
>
|
||||
<Icon name="chevron-left" />
|
||||
</button>
|
||||
<span className="mt-3 whitespace-nowrap text-[10px] text-slate-500 [writing-mode:vertical-rl]">
|
||||
参数
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ServerPage() {
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<h1 className="text-xl font-semibold">本地 API 服务</h1>
|
||||
<h1 className="text-xl font-semibold">服务管理</h1>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
启动后即可用 OpenAI SDK / 任意 HTTP 客户端调用本地模型
|
||||
</p>
|
||||
@@ -91,4 +91,3 @@ export default function ServerPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user