feat: unify installation model catalog
This commit is contained in:
@@ -1,59 +1,18 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CheckCircle2, CircleAlert, ClipboardCheck, FlaskConical, LoaderCircle, ScrollText } from "lucide-react";
|
||||
import { CheckCircle2, CircleAlert, FlaskConical, LoaderCircle, ScrollText } from "lucide-react";
|
||||
import {
|
||||
asPiManagementApiError,
|
||||
getPiManagementLogs,
|
||||
getPiManagementOptions,
|
||||
getPiManagementStatus,
|
||||
runPiManagementTest,
|
||||
savePiManagementConfig,
|
||||
type PiInstallationConfig,
|
||||
type PiManagementOptions,
|
||||
} from "../api/pi-management";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { WorkAreaPanel } from "./WorkAreaPanel";
|
||||
|
||||
const fieldClass = "h-9 w-full rounded-md border border-input bg-background px-2.5 text-sm shadow-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-60";
|
||||
|
||||
type Feedback = { tone: "success" | "error"; message: string } | undefined;
|
||||
type SmokeState = "passed" | "failed" | undefined;
|
||||
|
||||
function suggestedConfig(status: { config: Partial<PiInstallationConfig> }, options: PiManagementOptions): PiInstallationConfig | undefined {
|
||||
const provider = status.config.provider && options.providers.includes(status.config.provider)
|
||||
? status.config.provider
|
||||
: options.providers[0];
|
||||
const model = status.config.model && options.models.some((item) => item.provider === provider && item.id === status.config.model)
|
||||
? status.config.model
|
||||
: options.models.find((item) => item.provider === provider)?.id;
|
||||
const reasoning = status.config.reasoning && options.reasoning.includes(status.config.reasoning)
|
||||
? status.config.reasoning
|
||||
: options.reasoning[0];
|
||||
return provider && model && reasoning ? { provider, model, reasoning } : undefined;
|
||||
}
|
||||
|
||||
function isSupportedConfig(
|
||||
config: Partial<PiInstallationConfig> | undefined,
|
||||
options: PiManagementOptions | undefined,
|
||||
): config is PiInstallationConfig {
|
||||
return Boolean(
|
||||
config?.provider
|
||||
&& config.model
|
||||
&& config.reasoning
|
||||
&& options?.providers.includes(config.provider)
|
||||
&& options.models.some((model) => model.provider === config.provider && model.id === config.model)
|
||||
&& options.reasoning.includes(config.reasoning),
|
||||
);
|
||||
}
|
||||
|
||||
function sameConfig(a: Partial<PiInstallationConfig> | undefined, b: PiInstallationConfig | undefined): boolean {
|
||||
return Boolean(
|
||||
a?.provider === b?.provider
|
||||
&& a?.model === b?.model
|
||||
&& a?.reasoning === b?.reasoning,
|
||||
);
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown, fallback: string): string {
|
||||
return asPiManagementApiError(error)?.message ?? fallback;
|
||||
}
|
||||
@@ -97,15 +56,10 @@ function RailItem({ label, value, state }: { label: string; value: string; state
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return <label className="grid gap-1.5 text-xs font-semibold text-foreground"><span>{label}</span>{children}</label>;
|
||||
}
|
||||
|
||||
type PiPlatform = "linux" | "macos" | "windows";
|
||||
|
||||
type PiPlatformDetails = {
|
||||
modelsPath: string;
|
||||
settingsPath: string;
|
||||
catalogPath: string;
|
||||
terminal: string;
|
||||
changeDirectoryCommand: string;
|
||||
credentialProtection: string;
|
||||
@@ -123,8 +77,7 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
||||
id: "linux",
|
||||
label: "Linux",
|
||||
details: {
|
||||
modelsPath: "deploy/pi/models.json",
|
||||
settingsPath: "deploy/pi/settings.json",
|
||||
catalogPath: "deploy/<installation-id>/thothii-installation.yaml",
|
||||
terminal: "a terminal",
|
||||
changeDirectoryCommand: "cd /absolute/path/to/ThothII",
|
||||
credentialProtection: "a protected host file with mode 0600",
|
||||
@@ -141,8 +94,7 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
||||
id: "macos",
|
||||
label: "macOS",
|
||||
details: {
|
||||
modelsPath: "deploy/pi/models.json",
|
||||
settingsPath: "deploy/pi/settings.json",
|
||||
catalogPath: "deploy/<installation-id>/thothii-installation.yaml",
|
||||
terminal: "Terminal",
|
||||
changeDirectoryCommand: "cd /absolute/path/to/ThothII",
|
||||
credentialProtection: "a protected host file with mode 0600",
|
||||
@@ -159,8 +111,7 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
||||
id: "windows",
|
||||
label: "Windows",
|
||||
details: {
|
||||
modelsPath: "deploy\\pi\\models.json",
|
||||
settingsPath: "deploy\\pi\\settings.json",
|
||||
catalogPath: "deploy\\<installation-id>\\thothii-installation.yaml",
|
||||
terminal: "PowerShell",
|
||||
changeDirectoryCommand: "Set-Location C:\\absolute\\path\\to\\ThothII",
|
||||
credentialProtection: "a protected host file with a user-only ACL",
|
||||
@@ -189,37 +140,37 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
<PiCodeBlock className="mt-1">project-root/
|
||||
├── compose.yaml
|
||||
├── deploy/
|
||||
│ └── pi/
|
||||
│ ├── models.json
|
||||
│ └── settings.json
|
||||
│ └── <installation-id>/
|
||||
│ └── thothii-installation.yaml
|
||||
└── docker/</PiCodeBlock>
|
||||
<p className="mt-2 text-muted-foreground">The <code>deploy/</code> directory contains the selected installation descriptor and profile files. You do not need to create or manage a <code>bin/</code> directory: <code>tht</code> is the installed host CLI. If discovery finds multiple descriptors, pass the intended one explicitly with <code>--installation <absolute-path>/thothii-installation.yaml</code>. If <code>tht</code> is not on your <code>PATH</code>, install it using the installation guide.</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Edit the provider catalog</h4>
|
||||
<p className="mt-1 text-muted-foreground">Edit <code>{details.modelsPath}</code> only when adding or correcting a provider/model definition. The provider catalog is an address book/map of the services Pi can call: each entry supplies the API endpoint and format, and lists the model identifiers offered there. It does not enable a model and never contains credentials. Provider integrations must remain declarative; do not add model-provider code under <code>harness/.pi/extensions/</code>.</p>
|
||||
<h4 className="font-semibold text-foreground">Edit the Installation Model Catalog</h4>
|
||||
<p className="mt-1 text-muted-foreground">Edit <code>{details.catalogPath}</code> when adding or correcting a provider or model. Its <code>modelCatalog</code> block is the only authored model source for sessions, metadata generation, and embedding. It declares endpoint references and authentication modes, but never secret values. Provider integrations remain declarative; do not add model-provider code under <code>harness/.pi/extensions/</code> or edit files under <code>generated/</code>.</p>
|
||||
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 gap-y-1 text-muted-foreground">
|
||||
<dt className="font-mono text-foreground">baseUrl </dt><dd>is the provider API endpoint.</dd>
|
||||
<dt className="font-mono text-foreground">api </dt><dd>selects the provider API format.</dd>
|
||||
<dt className="font-mono text-foreground">models </dt><dd>lists that provider's available models.</dd>
|
||||
<dt className="font-mono text-foreground">id </dt><dd>is the model identifier.</dd>
|
||||
<dt className="font-mono text-foreground">name </dt><dd>is the model name shown to operators.</dd>
|
||||
<dt className="font-mono text-foreground">providers </dt><dd>maps stable provider keys to endpoint, authentication, and runtime adapters.</dd>
|
||||
<dt className="font-mono text-foreground">models </dt><dd>maps upstream model keys; the canonical identity is provider/model.</dd>
|
||||
<dt className="font-mono text-foreground">session </dt><dd>makes a model selectable for interactive work.</dd>
|
||||
<dt className="font-mono text-foreground">metadataGeneration </dt><dd>makes a model available to metadata generation.</dd>
|
||||
<dt className="font-mono text-foreground">embedding </dt><dd>declares the one installation embedding identity and its dimensions.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Enable the model</h4>
|
||||
<p className="mt-1 text-muted-foreground">Enable a model after the provider setup is ready. For a custom provider, first define the provider and model ID in <code>{details.modelsPath}</code>; built-in Pi models may already be known without a local catalog entry. Then edit <code>{details.settingsPath}</code> in the project root, add the exact <code>provider/model</code> identifier to <code>enabledModels</code>, and reload Pi. Do not edit it merely to choose the default provider/model: use this page or <code>tht pi configure</code>.</p>
|
||||
<h4 className="font-semibold text-foreground">Choose eligibility and defaults</h4>
|
||||
<p className="mt-1 text-muted-foreground">Use <code>modelCatalog.defaults.session</code> as the single default for new sessions. Add a <code>session</code> or <code>metadataGeneration</code> block to make a model eligible for that use. When metadata generation is present, set <code>defaults.metadataGeneration</code>. Runtime files such as Pi <code>models.json</code> and <code>settings.json</code> are generated projections and must not be edited.</p>
|
||||
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 gap-y-1 text-muted-foreground">
|
||||
<dt className="font-mono text-foreground">enabledModels </dt><dd>uses provider/model identifiers to choose the models available for new Pi work.</dd>
|
||||
<dt className="font-mono text-foreground">defaults.session </dt><dd>contains exactly one canonical provider/model identity.</dd>
|
||||
<dt className="font-mono text-foreground">defaults.metadataGeneration </dt><dd>selects the metadata model when that use exists.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Complete the provider setup</h4>
|
||||
<p className="mt-1 text-muted-foreground">During the initial installation, run <code>tht setup</code> and complete the prompt named “Pi credentials file location”; it can create the empty protected template. On an existing installation with a missing credential, correct the protected credential file selected during setup by following the installation guide. Keep {details.credentialProtection}, then restart and run <code>tht pi doctor</code> and <code>tht pi test</code>. Never paste credentials into either JSON file, the page, a command, or a log.</p>
|
||||
<p className="mt-1 text-muted-foreground">During the initial installation, run <code>tht setup</code> and complete the prompt named “Pi credentials file location”; it can create the empty protected template. On an existing installation with a missing credential, correct the protected credential file selected during setup by following the installation guide. Keep {details.credentialProtection}, then restart and run <code>tht pi doctor</code> and <code>tht pi test</code>. Never paste credentials into the installation YAML, this page, a command, or a log: YAML may contain only approved environment-variable names or authentication modes.</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Reload Pi configuration</h4>
|
||||
<p className="mt-1 text-muted-foreground">After changing <code>models.json</code>, <code>settings.json</code>, or the provider credential, reload the running <code>core</code> service so it reads the new files. This is a configuration reload, not a Pi version update; it keeps the current image.</p>
|
||||
<p className="mt-1 text-muted-foreground">After changing the Installation Model Catalog or a provider credential, reload the running <code>core</code> service. The command validates the YAML and regenerates every runtime projection before recreation. This is a configuration reload, not a Pi version update; it keeps the current image.</p>
|
||||
<PiCodeBlock className="mt-2">{details.restartCommand}</PiCodeBlock>
|
||||
</li>
|
||||
<li>
|
||||
@@ -234,9 +185,9 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
<p className="mt-1 text-muted-foreground">Start with these diagnostics, in order:</p>
|
||||
<PiCodeBlock className="mt-2 text-[11px] text-muted-foreground">{details.diagnosticCommands}</PiCodeBlock>
|
||||
<dl className="mt-2 grid gap-2 text-muted-foreground">
|
||||
<dt className="font-semibold text-foreground">Configuration</dt><dd>Invalid JSON: fix the reported file and validate it. A provider/model ID mismatch or a model missing from <code>enabledModels</code>: correct the IDs or policy, then reload.</dd>
|
||||
<dt className="font-semibold text-foreground">Configuration</dt><dd>Invalid YAML, an unknown model default, or an incompatible use/adapter combination: fix the reported <code>modelCatalog</code> field, then reload. Do not repair generated JSON directly.</dd>
|
||||
<dt className="font-semibold text-foreground">Credentials</dt><dd>Missing or unreadable credential: correct the protected credential file selected during setup and its permissions, without printing the file.</dd>
|
||||
<dt className="font-semibold text-foreground">Provider connectivity</dt><dd>Wrong <code>baseUrl</code>, network, or provider error: correct the endpoint or network, then retry.</dd>
|
||||
<dt className="font-semibold text-foreground">Provider connectivity</dt><dd>Wrong <code>baseUrl</code>, network, or provider error: correct the catalog endpoint or network, then retry.</dd>
|
||||
<dt className="font-semibold text-foreground">Docker and disk</dt><dd>Unhealthy Docker or insufficient disk: restore Docker health or free space before retrying.</dd>
|
||||
</dl>
|
||||
<p className="mt-2 text-muted-foreground">If maintenance is inactive, no recovery is needed: correct the cause and retry the original command.</p>
|
||||
@@ -325,30 +276,19 @@ function PiPlatformInstructions() {
|
||||
|
||||
export function PiManagement({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [draft, setDraft] = useState<PiInstallationConfig>();
|
||||
const [feedback, setFeedback] = useState<Feedback>();
|
||||
const [smokeState, setSmokeState] = useState<SmokeState>();
|
||||
const [logsRequested, setLogsRequested] = useState(false);
|
||||
const statusQuery = useQuery({ queryKey: ["pi-management", "status"], queryFn: getPiManagementStatus, enabled: open });
|
||||
const optionsQuery = useQuery({ queryKey: ["pi-management", "options"], queryFn: getPiManagementOptions, enabled: open });
|
||||
const logsQuery = useQuery({ queryKey: ["pi-management", "logs"], queryFn: getPiManagementLogs, enabled: open && logsRequested });
|
||||
const models = useMemo(
|
||||
() => optionsQuery.data?.models.filter((model) => model.provider === draft?.provider) ?? [],
|
||||
[draft?.provider, optionsQuery.data?.models],
|
||||
const catalogReady = Boolean(
|
||||
statusQuery.data?.config.provider
|
||||
&& statusQuery.data.config.model
|
||||
&& statusQuery.data.config.reasoning,
|
||||
);
|
||||
const initialDraft = useMemo(
|
||||
() => statusQuery.data && optionsQuery.data
|
||||
? suggestedConfig(statusQuery.data, optionsQuery.data)
|
||||
: undefined,
|
||||
[optionsQuery.data, statusQuery.data],
|
||||
);
|
||||
const persistedReady = isSupportedConfig(statusQuery.data?.config, optionsQuery.data);
|
||||
const validDraft = isSupportedConfig(draft, optionsQuery.data);
|
||||
const dirty = Boolean(draft && !sameConfig(statusQuery.data?.config, draft));
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setDraft(undefined);
|
||||
setFeedback(undefined);
|
||||
setSmokeState(undefined);
|
||||
setLogsRequested(false);
|
||||
@@ -356,22 +296,6 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
|
||||
}
|
||||
}, [open, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (draft || !initialDraft) return;
|
||||
setDraft(initialDraft);
|
||||
}, [draft, initialDraft]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: savePiManagementConfig,
|
||||
onSuccess: async (saved) => {
|
||||
const config = { provider: saved.provider, model: saved.model, reasoning: saved.reasoning };
|
||||
setDraft(config);
|
||||
setSmokeState(undefined);
|
||||
await queryClient.invalidateQueries({ queryKey: ["pi-management", "status"] });
|
||||
setFeedback({ tone: "success", message: "Defaults saved." });
|
||||
},
|
||||
onError: (error) => setFeedback({ tone: "error", message: errorMessage(error, "Could not save Pi defaults.") }),
|
||||
});
|
||||
const smokeMutation = useMutation({
|
||||
mutationFn: runPiManagementTest,
|
||||
onSuccess: (result) => {
|
||||
@@ -379,41 +303,27 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
|
||||
setFeedback({
|
||||
tone: result.ready ? "success" : "error",
|
||||
message: result.ready
|
||||
? "Saved configuration test passed."
|
||||
: `Saved configuration test failed.${result.message ? ` ${result.message}` : ""}`,
|
||||
? "Catalog default test passed."
|
||||
: `Catalog default test failed.${result.message ? ` ${result.message}` : ""}`,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setSmokeState("failed");
|
||||
setFeedback({ tone: "error", message: errorMessage(error, "Could not test the saved Pi defaults.") });
|
||||
setFeedback({ tone: "error", message: errorMessage(error, "Could not test the catalog default.") });
|
||||
},
|
||||
});
|
||||
|
||||
function saveDefaults() {
|
||||
if (!draft || !validDraft) {
|
||||
setFeedback({ tone: "error", message: "Choose a supported provider, model, and reasoning level." });
|
||||
return;
|
||||
}
|
||||
saveMutation.mutate(draft);
|
||||
}
|
||||
|
||||
function updateDraft(update: (current: PiInstallationConfig) => PiInstallationConfig) {
|
||||
setDraft((current) => current ? update(current) : current);
|
||||
setSmokeState(undefined);
|
||||
setFeedback(undefined);
|
||||
}
|
||||
|
||||
function testSavedDefaults() {
|
||||
if (!persistedReady || dirty) {
|
||||
setFeedback({ tone: "error", message: "Save supported defaults before running the test." });
|
||||
function testCatalogDefault() {
|
||||
if (!catalogReady) {
|
||||
setFeedback({ tone: "error", message: "Fix the Installation Model Catalog before running the test." });
|
||||
return;
|
||||
}
|
||||
smokeMutation.mutate();
|
||||
}
|
||||
|
||||
const forbidden = asPiManagementApiError(statusQuery.error)?.code === "pi_management_forbidden";
|
||||
const loading = statusQuery.isLoading || optionsQuery.isLoading || Boolean(!draft && initialDraft);
|
||||
const unavailable = statusQuery.isError || optionsQuery.isError;
|
||||
const loading = statusQuery.isLoading;
|
||||
const unavailable = statusQuery.isError;
|
||||
|
||||
return (
|
||||
<WorkAreaPanel
|
||||
@@ -421,7 +331,7 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
|
||||
ariaLabel="Pi management"
|
||||
eyebrow="Installation controls"
|
||||
title="Pi management"
|
||||
description="Review the bundled runtime, set safe defaults, and test the saved provider configuration."
|
||||
description="Review the bundled runtime, inspect the catalog default, and test its provider configuration."
|
||||
onClose={onClose}
|
||||
closeLabel="Close Pi management"
|
||||
>
|
||||
@@ -430,14 +340,14 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
|
||||
{forbidden ? (
|
||||
<div role="alert" aria-label="Pi management unavailable" className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm">
|
||||
<p className="font-semibold">Pi management is not permitted</p>
|
||||
<p className="mt-1 text-muted-foreground">Ask an installation administrator to manage Pi defaults and diagnostics.</p>
|
||||
<p className="mt-1 text-muted-foreground">Ask an installation administrator to manage the model catalog and diagnostics.</p>
|
||||
</div>
|
||||
) : unavailable ? (
|
||||
<div role="alert" aria-label="Pi management unavailable" className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm">
|
||||
<p className="font-semibold">{errorMessage(statusQuery.error ?? optionsQuery.error, "Pi management is unavailable")}</p>
|
||||
<Button className="mt-3" size="sm" variant="outline" onClick={() => { void statusQuery.refetch(); void optionsQuery.refetch(); }}>Retry</Button>
|
||||
<p className="font-semibold">{errorMessage(statusQuery.error, "Pi management is unavailable")}</p>
|
||||
<Button className="mt-3" size="sm" variant="outline" onClick={() => { void statusQuery.refetch(); }}>Retry</Button>
|
||||
</div>
|
||||
) : loading ? <p className="text-sm text-muted-foreground">Loading Pi management…</p> : statusQuery.data && optionsQuery.data && (
|
||||
) : loading ? <p className="text-sm text-muted-foreground">Loading Pi management…</p> : statusQuery.data && (
|
||||
<div className="grid gap-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
@@ -451,7 +361,7 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
|
||||
|
||||
<ReadinessRail
|
||||
ready={statusQuery.data.ready}
|
||||
configured={persistedReady}
|
||||
configured={catalogReady}
|
||||
credentials={statusQuery.data.credentials ?? "missing"}
|
||||
smokeState={smokeState}
|
||||
/>
|
||||
@@ -462,56 +372,30 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
|
||||
{feedback.message}
|
||||
</p>}
|
||||
|
||||
<section aria-label="Installation defaults" className="border-t border-border/70 pt-5">
|
||||
<section aria-label="Installation catalog default" className="border-t border-border/70 pt-5">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<div><h3 className="font-heading text-sm font-semibold">Installation defaults</h3><p className="mt-1 text-sm text-muted-foreground">Select the provider, model, and reasoning used for new Pi work. Credentials stay in protected host files.</p></div>
|
||||
<div><h3 className="font-heading text-sm font-semibold">Installation catalog default</h3><p className="mt-1 text-sm text-muted-foreground">This value is read from <code>modelCatalog.defaults.session</code>. Change the installation YAML and reload Pi; this page never creates a second model default.</p></div>
|
||||
</div>
|
||||
{draft ? (
|
||||
{catalogReady ? (
|
||||
<>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<Field label="Provider">
|
||||
<select aria-label="Provider" className={fieldClass} value={draft.provider} onChange={(event) => {
|
||||
const provider = event.target.value;
|
||||
updateDraft((current) => ({
|
||||
...current,
|
||||
provider,
|
||||
model: optionsQuery.data.models.find((model) => model.provider === provider)?.id ?? "",
|
||||
}));
|
||||
}}>
|
||||
{optionsQuery.data.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Model">
|
||||
<select aria-label="Model" className={fieldClass} value={draft.model} onChange={(event) => updateDraft((current) => ({ ...current, model: event.target.value }))}>
|
||||
{models.map((model) => <option key={model.id} value={model.id}>{model.id === "glm-5.2" ? "GLM 5.2" : model.id}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Reasoning level">
|
||||
<select aria-label="Reasoning level" className={fieldClass} value={draft.reasoning} onChange={(event) => updateDraft((current) => ({ ...current, reasoning: event.target.value as PiInstallationConfig["reasoning"] }))}>
|
||||
{optionsQuery.data.reasoning.map((reasoning) => <option key={reasoning} value={reasoning}>{reasoning}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="mt-4 grid gap-3 text-sm sm:grid-cols-3">
|
||||
<div><p className="thot-label">Provider</p><p className="mt-1 font-mono font-semibold">{statusQuery.data.config.provider}</p></div>
|
||||
<div><p className="thot-label">Model</p><p className="mt-1 font-mono font-semibold">{statusQuery.data.config.model}</p></div>
|
||||
<div><p className="thot-label">Reasoning</p><p className="mt-1 font-mono font-semibold">{statusQuery.data.config.reasoning}</p></div>
|
||||
</div>
|
||||
{dirty && <p className="mt-3 text-sm text-amber-800 dark:text-amber-300">
|
||||
{persistedReady ? "Changes are not saved yet." : "Suggested choices are not saved yet."}
|
||||
</p>}
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" disabled={saveMutation.isPending || !validDraft || !dirty} onClick={saveDefaults}>{saveMutation.isPending ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <ClipboardCheck />}Save defaults</Button>
|
||||
<Button size="sm" variant="outline" disabled={smokeMutation.isPending || dirty || !persistedReady} onClick={testSavedDefaults}>{smokeMutation.isPending ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <FlaskConical />}Test saved defaults</Button>
|
||||
<Button size="sm" variant="outline" disabled={smokeMutation.isPending} onClick={testCatalogDefault}>{smokeMutation.isPending ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <FlaskConical />}Test catalog default</Button>
|
||||
</div>
|
||||
{dirty && <p className="mt-2 text-xs text-muted-foreground">Save these changes before testing. The test always uses saved defaults.</p>}
|
||||
{!dirty && !persistedReady && <p className="mt-2 text-xs text-muted-foreground">Save supported defaults before testing. The test always uses saved defaults.</p>}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<div role="alert" aria-label="Pi configuration incomplete" className="rounded-md border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-900 dark:text-amber-200">
|
||||
<p className="font-semibold">No provider or model options are available.</p>
|
||||
<p className="mt-1">Check the host-managed Pi model configuration, then retry this panel.</p>
|
||||
<p className="font-semibold">The catalog default is unavailable.</p>
|
||||
<p className="mt-1">Fix <code>modelCatalog.defaults.session</code> in the installation YAML, reload Pi, then retry this panel.</p>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" disabled><ClipboardCheck />Save defaults</Button>
|
||||
<Button size="sm" variant="outline" disabled><FlaskConical />Test saved defaults</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { void statusQuery.refetch(); void optionsQuery.refetch(); }}>Retry choices</Button>
|
||||
<Button size="sm" variant="outline" disabled><FlaskConical />Test catalog default</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { void statusQuery.refetch(); }}>Retry status</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user