Files
AgentHub/client/src/components/run/RouterPicker.tsx
T

103 lines
3.2 KiB
TypeScript

/**
* @file RouterPicker.tsx
* @description The AI Router a NEW session starts on — a session's choice, not
* its workspace's (`router` on `POST /api/lanes/:id/start`, `server/lib/router-apply.js`):
* Native or one of the saved profiles (`useAiRouters`, which the caller also
* reads to list the router's models). A tool that cannot use a router (only
* Claude and Codex can) shows Native, disabled. The last pick is remembered
* for the next session (`agenthub.session.router`).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { api, type AiRouter } from "../../lib/api";
import { useAiTools } from "../../hooks/useAiTools";
import { Select } from "../Select";
const KEY = "agenthub.session.router";
/** The remembered pick, `"native"` by default. */
export function useSessionRouter(): [string, (v: string) => void] {
const [value, setValue] = useState(() => {
try {
return localStorage.getItem(KEY) || "native";
} catch {
return "native";
}
});
const set = (v: string) => {
setValue(v);
try {
localStorage.setItem(KEY, v);
} catch {
/* storage off — the pick still holds for this page */
}
};
return [value, set];
}
/** The saved profiles (Native included), null until they arrive. */
export function useAiRouters(): AiRouter[] | null {
const [routers, setRouters] = useState<AiRouter[] | null>(null);
useEffect(() => {
let alive = true;
api.aiRouters
.list()
.then((r) => alive && setRouters(r.items))
.catch(() => alive && setRouters([]));
return () => {
alive = false;
};
}, []);
return routers;
}
/** What to send as `router` for `tool`: nothing for native or a tool that has none. */
export function routerArg(routerCapable: boolean | undefined, router: string): { router?: string } {
return routerCapable && router && router !== "native" ? { router } : {};
}
export function RouterPicker({
tool,
routers,
value,
onChange,
variant,
}: {
variant?: "field" | "ghost";
tool: string;
/** From `useAiRouters`. */
routers: AiRouter[] | null;
value: string;
onChange: (v: string) => void;
}) {
const { t } = useTranslation(["lanes"]);
const aiTools = useAiTools();
const capable = aiTools.byId(tool)?.routerCapable ?? (tool === "claude" || tool === "codex");
const native = t("router.native", { tool: aiTools.label(tool) });
// A remembered profile that has since been deleted falls back to native,
// or the start would be refused for a router the user cannot see.
useEffect(() => {
if (routers && value !== "native" && !routers.some((r) => r.name === value)) onChange("native");
}, [routers, value, onChange]);
const options =
capable && routers?.length
? routers.map((r) => ({ value: r.name, label: r.builtin ? native : r.label }))
: [{ value: "native", label: native }];
return (
<div data-testid="router-picker">
<Select<string>
value={capable ? value : "native"}
onChange={onChange}
options={options}
disabled={!capable}
variant={variant}
label={t("router.label")}
/>
</div>
);
}