463 lines
29 KiB
TypeScript
463 lines
29 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { CheckCircle2, CircleAlert, FlaskConical, LoaderCircle, ScrollText } from "lucide-react";
|
|
import {
|
|
asPiManagementApiError,
|
|
getPiManagementLogs,
|
|
getPiManagementStatus,
|
|
runPiManagementTest,
|
|
} from "../api/pi-management";
|
|
import { listModels } from "../api/models";
|
|
import { Button } from "../components/ui/button";
|
|
import { nextTabIndex } from "../components/ui/tab-navigation";
|
|
import { WorkAreaPanel } from "./WorkAreaPanel";
|
|
|
|
type Feedback = { tone: "success" | "error"; message: string } | undefined;
|
|
type SmokeState = "passed" | "failed" | undefined;
|
|
|
|
function errorMessage(error: unknown, fallback: string): string {
|
|
return asPiManagementApiError(error)?.message ?? fallback;
|
|
}
|
|
|
|
function ReadinessRail({ ready, configured, credentials, smokeState }: {
|
|
ready: boolean;
|
|
configured: boolean;
|
|
credentials: "present" | "missing";
|
|
smokeState: SmokeState;
|
|
}) {
|
|
return (
|
|
<section
|
|
data-testid="pi-readiness-rail"
|
|
aria-label="Pi readiness sequence"
|
|
className="grid gap-3 border-y border-border py-4 sm:grid-cols-2"
|
|
>
|
|
<RailItem label="Runtime" value={ready ? "Runtime ready" : "Runtime needs attention"} state={ready ? "ready" : "attention"} />
|
|
<RailItem label="Defaults" value={configured ? "Defaults ready" : "Defaults incomplete"} state={configured ? "ready" : "attention"} />
|
|
<RailItem
|
|
label="Credentials"
|
|
value={`Credentials ${credentials}`}
|
|
state={credentials === "present" ? "ready" : "attention"}
|
|
/>
|
|
<RailItem
|
|
label="Configuration test"
|
|
value={smokeState === "passed" ? "Saved configuration test passed" : smokeState === "failed" ? "Saved configuration test failed" : "Saved configuration test not run"}
|
|
state={smokeState === "passed" ? "ready" : smokeState === "failed" ? "attention" : "idle"}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function RailItem({ label, value, state }: { label: string; value: string; state: "ready" | "attention" | "idle" }) {
|
|
const iconClass = state === "ready" ? "text-[oklch(var(--success))]" : state === "attention" ? "text-amber-700 dark:text-amber-400" : "text-muted-foreground";
|
|
return <div className="min-w-0">
|
|
<p className="thot-label">{label}</p>
|
|
<p className={`mt-1 flex items-start gap-1.5 text-sm font-semibold ${iconClass}`}>
|
|
{state === "ready" ? <CheckCircle2 className="mt-0.5 size-3.5 shrink-0" /> : state === "attention" ? <CircleAlert className="mt-0.5 size-3.5 shrink-0" /> : <span className="mt-1.5 size-1.5 shrink-0 rounded-full bg-current" />}
|
|
<span>{value}</span>
|
|
</p>
|
|
</div>;
|
|
}
|
|
|
|
type PiPlatform = "linux" | "macos" | "windows";
|
|
|
|
type PiPlatformDetails = {
|
|
catalogPath: string;
|
|
terminal: string;
|
|
changeDirectoryCommand: string;
|
|
credentialProtection: string;
|
|
restartCommand: string;
|
|
updateCommand: string;
|
|
pullCommand: string;
|
|
diagnosticCommands: string;
|
|
rollbackCommand: string;
|
|
recoverCommand: string;
|
|
verificationCommands: string;
|
|
};
|
|
|
|
const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDetails }> = [
|
|
{
|
|
id: "linux",
|
|
label: "Linux",
|
|
details: {
|
|
catalogPath: "deploy/<installation-id>/thothii-installation.yaml",
|
|
terminal: "a terminal",
|
|
changeDirectoryCommand: "cd /absolute/path/to/ThothII",
|
|
credentialProtection: "a protected host file with mode 0600",
|
|
restartCommand: "tht pi restart --yes --drain",
|
|
updateCommand: "tht pi update",
|
|
pullCommand: "tht pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
|
diagnosticCommands: "tht pi maintenance status\ntht pi status\ntht pi doctor\ntht pi logs",
|
|
rollbackCommand: "tht pi rollback --yes",
|
|
recoverCommand: "tht pi maintenance recover --yes",
|
|
verificationCommands: "tht pi maintenance status\ntht pi doctor\ntht pi test",
|
|
},
|
|
},
|
|
{
|
|
id: "macos",
|
|
label: "macOS",
|
|
details: {
|
|
catalogPath: "deploy/<installation-id>/thothii-installation.yaml",
|
|
terminal: "Terminal",
|
|
changeDirectoryCommand: "cd /absolute/path/to/ThothII",
|
|
credentialProtection: "a protected host file with mode 0600",
|
|
restartCommand: "tht pi restart --yes --drain",
|
|
updateCommand: "tht pi update",
|
|
pullCommand: "tht pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
|
diagnosticCommands: "tht pi maintenance status\ntht pi status\ntht pi doctor\ntht pi logs",
|
|
rollbackCommand: "tht pi rollback --yes",
|
|
recoverCommand: "tht pi maintenance recover --yes",
|
|
verificationCommands: "tht pi maintenance status\ntht pi doctor\ntht pi test",
|
|
},
|
|
},
|
|
{
|
|
id: "windows",
|
|
label: "Windows",
|
|
details: {
|
|
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",
|
|
restartCommand: "tht pi restart --yes --drain",
|
|
updateCommand: "tht pi update",
|
|
pullCommand: "tht pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
|
diagnosticCommands: "tht pi maintenance status\ntht pi status\ntht pi doctor\ntht pi logs",
|
|
rollbackCommand: "tht pi rollback --yes",
|
|
recoverCommand: "tht pi maintenance recover --yes",
|
|
verificationCommands: "tht pi maintenance status\ntht pi doctor\ntht pi test",
|
|
},
|
|
},
|
|
];
|
|
|
|
function PiCodeBlock({ children, className = "" }: { children: string; className?: string }) {
|
|
return <pre className={`overflow-x-auto rounded bg-muted p-2 ${className}`}><code>{children}</code></pre>;
|
|
}
|
|
|
|
function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
|
return <ol className="grid gap-4 pl-5 marker:font-semibold marker:text-muted-foreground">
|
|
<li>
|
|
<h4 className="font-semibold text-foreground">Open the project root</h4>
|
|
<p className="mt-1 text-muted-foreground">Using {details.terminal}, before editing files or running any lifecycle command, enter the exact checkout or worktree root. Run the platform command below, then run every command in this list from that directory: <code>tht</code> resolves the installation descriptor and configuration from the selected project. A worktree is an independent checkout, so this prevents changing a different copy by mistake.</p>
|
|
<PiCodeBlock className="mt-2">{details.changeDirectoryCommand}</PiCodeBlock>
|
|
<p className="mt-2 text-muted-foreground">The relevant layout is:</p>
|
|
<PiCodeBlock className="mt-1">project-root/
|
|
├── compose.yaml
|
|
├── deploy/
|
|
│ └── <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 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">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">Choose eligibility and defaults</h4>
|
|
<p className="mt-1 text-muted-foreground">Use <code>modelCatalog.defaults.interaction</code> as the single installation-wide LLM default for Core and Administration. When Admin AI is configured, a selectable model needs both <code>session</code> and <code>metadataGeneration</code> blocks. The user's last model choice takes precedence and does not depend on workspace. 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">defaults.interaction </dt><dd>contains exactly one canonical provider/model identity for every workspace.</dd>
|
|
</dl>
|
|
<p className="mt-2 text-muted-foreground">Before making each model available, verify an actual Core tool-call/review cycle through Pi and a small test description-generation run through LiteLLM. Configuration validation and Pi smoke tests alone do not certify both paths. Use approved test data; generation may incur provider charges.</p>
|
|
</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 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 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>
|
|
<h4 className="font-semibold text-foreground">Update the Pi version</h4>
|
|
<p className="mt-1 text-muted-foreground">Use <code>tht pi update</code> for the routine build: it resolves the latest stable Pi release from the registry, builds it locally, drains active sessions, and recreates only <code>core</code>. Add <code>--version <VERSION></code> when an explicitly reviewed version is required. The digest-pinned <code>--source pull</code> form is for an already-built, reviewed registry image; its declared version must match the digest-pinned image. Configuration changes use Reload above, not update.</p>
|
|
<PiCodeBlock className="mt-2">{details.updateCommand}</PiCodeBlock>
|
|
<p className="mt-2 text-[11px] text-muted-foreground">Advanced: pull an immutable, digest-pinned image.</p>
|
|
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.pullCommand}</PiCodeBlock>
|
|
</li>
|
|
<li>
|
|
<h4 className="font-semibold text-foreground">Diagnose and recover a failed operation</h4>
|
|
<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 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 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>
|
|
<p className="mt-2 text-muted-foreground">If maintenance is active after an update, rollback the previous image:</p>
|
|
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.rollbackCommand}</PiCodeBlock>
|
|
<p className="mt-2 text-muted-foreground">If maintenance is active after a restart, fix the cause first, then recover the captured lifecycle state:</p>
|
|
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.recoverCommand}</PiCodeBlock>
|
|
<p className="mt-2 text-muted-foreground">After the selected remedy, verify the installation:</p>
|
|
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.verificationCommands}</PiCodeBlock>
|
|
</li>
|
|
</ol>;
|
|
}
|
|
|
|
function PiPlatformInstructions({ hostPlatform }: { hostPlatform?: PiPlatform }) {
|
|
const [chosenPlatform, setActivePlatform] = useState<PiPlatform>();
|
|
const activePlatform = chosenPlatform ?? hostPlatform ?? "linux";
|
|
const tabRefs = useRef<Record<PiPlatform, HTMLButtonElement | null>>({ linux: null, macos: null, windows: null });
|
|
|
|
function activateAndFocus(platform: PiPlatform) {
|
|
setActivePlatform(platform);
|
|
tabRefs.current[platform]?.focus();
|
|
}
|
|
|
|
function handleTabKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, platform: PiPlatform) {
|
|
const index = piPlatforms.findIndex((item) => item.id === platform);
|
|
const next = nextTabIndex(event.key, index, piPlatforms.length);
|
|
if (next === undefined) return;
|
|
event.preventDefault();
|
|
activateAndFocus(piPlatforms[next].id);
|
|
}
|
|
|
|
return (
|
|
<section aria-label="Pi operator workflow" className="mt-2 grid gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-xs">
|
|
<p className="text-muted-foreground">Using the host terminal:</p>
|
|
<div role="tablist" aria-label="Pi host platform" className="grid grid-cols-3 gap-1 rounded-md bg-muted p-1">
|
|
{piPlatforms.map((platform) => (
|
|
<button
|
|
key={platform.id}
|
|
id={`pi-${platform.id}-tab`}
|
|
ref={(element) => { tabRefs.current[platform.id] = element; }}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={activePlatform === platform.id}
|
|
aria-controls={`pi-${platform.id}-panel`}
|
|
tabIndex={activePlatform === platform.id ? 0 : -1}
|
|
className={activePlatform === platform.id ? "rounded bg-background px-2 py-1.5 font-semibold text-foreground shadow-sm" : "rounded px-2 py-1.5 font-semibold text-muted-foreground hover:bg-background/70 hover:text-foreground"}
|
|
onKeyDown={(event) => handleTabKeyDown(event, platform.id)}
|
|
onClick={() => {
|
|
setActivePlatform(platform.id);
|
|
}}
|
|
>
|
|
{platform.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div data-testid="pi-platform-instructions-scroll" className="min-h-0 max-h-[min(34vh,22rem)] overflow-y-scroll overscroll-contain pr-2">
|
|
{piPlatforms.map((platform) => (
|
|
<div
|
|
key={platform.id}
|
|
id={`pi-${platform.id}-panel`}
|
|
role="tabpanel"
|
|
aria-label={platform.label}
|
|
aria-labelledby={`pi-${platform.id}-tab`}
|
|
tabIndex={activePlatform === platform.id ? 0 : -1}
|
|
hidden={activePlatform !== platform.id}
|
|
className="outline-none"
|
|
>
|
|
<PiInstructionSteps details={platform.details} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function PiManagement({ open, onClose, page = false }: { open: boolean; onClose: () => void; page?: boolean }) {
|
|
const [section, setSection] = useState<"runtime" | "instructions">("runtime");
|
|
const queryClient = useQueryClient();
|
|
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 modelsQuery = useQuery({ queryKey: ["models"], queryFn: listModels, enabled: open });
|
|
const logsQuery = useQuery({ queryKey: ["pi-management", "logs"], queryFn: getPiManagementLogs, enabled: open && logsRequested });
|
|
const catalogReady = Boolean(
|
|
statusQuery.data?.config.provider
|
|
&& statusQuery.data.config.model
|
|
&& statusQuery.data.config.reasoning,
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setFeedback(undefined);
|
|
setSmokeState(undefined);
|
|
setLogsRequested(false);
|
|
queryClient.removeQueries({ queryKey: ["pi-management"] });
|
|
}
|
|
}, [open, queryClient]);
|
|
|
|
const smokeMutation = useMutation({
|
|
mutationFn: runPiManagementTest,
|
|
onSuccess: (result) => {
|
|
setSmokeState(result.ready ? "passed" : "failed");
|
|
setFeedback({
|
|
tone: result.ready ? "success" : "error",
|
|
message: result.ready
|
|
? "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 catalog default.") });
|
|
},
|
|
});
|
|
|
|
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;
|
|
const unavailable = statusQuery.isError;
|
|
|
|
return (
|
|
<WorkAreaPanel
|
|
open={open}
|
|
mode={page ? "page" : "panel"}
|
|
ariaLabel="Pi management"
|
|
width="expanded"
|
|
eyebrow="Installation controls"
|
|
title="Pi management"
|
|
description="Review the bundled runtime, inspect the catalog default, and test its provider configuration."
|
|
onClose={onClose}
|
|
closeLabel="Close Pi management"
|
|
bodyClassName={page ? "thot-work-area-page__body--pi" : undefined}
|
|
>
|
|
<div className={page ? "thot-pi-workbench" : undefined}>
|
|
{page && <nav aria-label="Pi sections" className="thot-pi-index">
|
|
<p className="thot-administration-eyebrow mb-3">Installation</p>
|
|
<Button variant={section === "runtime" ? "navigationActive" : "ghost"} className="w-full justify-start mb-2" aria-current={section === "runtime" ? "page" : undefined} onClick={() => setSection("runtime")}>Runtime and models</Button>
|
|
<Button variant={section === "instructions" ? "navigationActive" : "ghost"} className="w-full justify-start whitespace-normal" aria-current={section === "instructions" ? "page" : undefined} onClick={() => setSection("instructions")}>Host maintenance</Button>
|
|
</nav>}
|
|
<div className={page ? "thot-pi-detail" : undefined}>
|
|
<div hidden={page && section !== "instructions"}>
|
|
<PiPlatformInstructions hostPlatform={statusQuery.data?.hostPlatform} />
|
|
</div>
|
|
<div className={page ? undefined : "mt-5"} hidden={page && section !== "runtime"}>
|
|
{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 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, "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 && (
|
|
<div className="grid gap-6">
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<p className="thot-label">Bundled runtime</p>
|
|
<p className="mt-1 font-mono text-sm font-semibold">Pi {statusQuery.data.version ?? "version unavailable"}</p>
|
|
</div>
|
|
<p role="status" aria-label="Pi readiness" className={`rounded-md border px-2.5 py-1 text-xs font-semibold ${statusQuery.data.ready ? "border-[oklch(var(--success)/0.4)] bg-[oklch(var(--success)/0.1)] text-foreground" : "border-amber-500/35 bg-amber-500/10 text-amber-800 dark:text-amber-300"}`}>
|
|
{statusQuery.data.ready ? "Ready" : "Needs attention"}
|
|
</p>
|
|
</div>
|
|
|
|
<ReadinessRail
|
|
ready={statusQuery.data.ready}
|
|
configured={catalogReady}
|
|
credentials={statusQuery.data.credentials ?? "missing"}
|
|
smokeState={smokeState}
|
|
/>
|
|
|
|
{statusQuery.data.message && <p role="status" aria-live="polite" className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">{statusQuery.data.message}</p>}
|
|
{feedback && <p role="status" aria-label="Pi management feedback" aria-live="polite" className={`flex items-start gap-2 rounded-md border px-3 py-2 text-sm ${feedback.tone === "error" ? "border-destructive/30 bg-destructive/5" : "border-border bg-muted/50"}`}>
|
|
{feedback.tone === "error" ? <CircleAlert className="mt-0.5 size-4 shrink-0 text-destructive" /> : <CheckCircle2 className="mt-0.5 size-4 shrink-0 text-[oklch(var(--success))]" />}
|
|
{feedback.message}
|
|
</p>}
|
|
|
|
<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 catalog default</h3><p className="mt-1 text-sm text-muted-foreground">This value is read from <code>modelCatalog.defaults.interaction</code>. Change the installation YAML and reload Pi; this page never creates a second model default.</p></div>
|
|
</div>
|
|
{catalogReady ? (
|
|
<>
|
|
<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>
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
<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>
|
|
</>
|
|
) : (
|
|
<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">The catalog default is unavailable.</p>
|
|
<p className="mt-1">Fix <code>modelCatalog.defaults.interaction</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><FlaskConical />Test catalog default</Button>
|
|
<Button size="sm" variant="ghost" onClick={() => { void statusQuery.refetch(); }}>Retry status</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section aria-label="Session models" 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">Session models</h3>
|
|
<p className="mt-1 text-sm text-muted-foreground">Models currently available for interactive sessions in this installation.</p>
|
|
</div>
|
|
{modelsQuery.data && (
|
|
<span className="font-mono text-xs tabular-nums text-muted-foreground">{modelsQuery.data.models.length} available</span>
|
|
)}
|
|
</div>
|
|
{modelsQuery.isLoading ? (
|
|
<p className="mt-4 text-sm text-muted-foreground">Loading session models…</p>
|
|
) : modelsQuery.isError ? (
|
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3" role="alert">
|
|
<p className="text-sm text-destructive">Could not load the session model catalog.</p>
|
|
<Button size="sm" variant="ghost" onClick={() => { void modelsQuery.refetch(); }}>Retry models</Button>
|
|
</div>
|
|
) : modelsQuery.data?.models.length ? (
|
|
<ul className="mt-4 divide-y divide-border/70 border-y border-border/70">
|
|
{modelsQuery.data.models.map((model) => {
|
|
const isDefault = statusQuery.data.config.provider === model.provider
|
|
&& statusQuery.data.config.model === model.id;
|
|
return (
|
|
<li key={model.provider + "/" + model.id} className="flex min-w-0 items-center justify-between gap-4 py-3">
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-semibold" title={model.name}>{model.name}</p>
|
|
<p className="mt-0.5 truncate font-mono text-xs text-muted-foreground" title={model.provider + "/" + model.id}>{model.provider}/{model.id}</p>
|
|
</div>
|
|
{isDefault && <span className="shrink-0 rounded-md bg-muted px-2 py-1 text-xs font-semibold text-foreground">Default</span>}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
) : (
|
|
<p className="mt-4 text-sm text-muted-foreground">No session models are configured for this installation.</p>
|
|
)}
|
|
</section>
|
|
|
|
<section aria-label="Sanitized diagnostics" className="border-t border-border/70 pt-5">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div><h3 className="font-heading text-sm font-semibold">Sanitized diagnostics</h3><p className="mt-1 text-sm text-muted-foreground">Shows at most 200 recent lines with declared secret values removed.</p></div>
|
|
<Button size="sm" variant="outline" onClick={() => setLogsRequested(true)} disabled={logsQuery.isFetching}>{logsQuery.isFetching ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <ScrollText />}Show sanitized logs</Button>
|
|
</div>
|
|
{logsQuery.isError && <p role="alert" className="mt-3 text-sm text-destructive">Could not load sanitized Pi diagnostics.</p>}
|
|
{logsQuery.data && <pre aria-label="Sanitized Pi diagnostics" className="mt-3 max-h-48 overflow-auto rounded-md border border-border bg-muted/50 p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap">{logsQuery.data.lines.join("\n") || "No diagnostic lines are available."}</pre>}
|
|
</section>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</WorkAreaPanel>
|
|
);
|
|
}
|