feat(30-02): API client, hook, ModeSelector, and HardwareSummaryStep
- Add ui/src/api/hardware.ts with fetchHardwareInfo, fetchNexusSettings, updateNexusSettings - Add ui/src/hooks/useHardwareInfo.ts with useQuery wrapper - Add queryKeys.hardware.info to ui/src/lib/queryKeys.ts - Add ModeSelector with three-card layout and selected state styling - Add HardwareSummaryStep with skeleton loading, tier-appropriate labels, privacy frame
This commit is contained in:
parent
949f09ac54
commit
2a47c60057
5 changed files with 195 additions and 0 deletions
34
ui/src/api/hardware.ts
Normal file
34
ui/src/api/hardware.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// [nexus] Hardware detection and nexus settings API client
|
||||
import { api } from "./client";
|
||||
|
||||
export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";
|
||||
|
||||
export interface HardwareInfo {
|
||||
totalGb: number;
|
||||
freeGb: number;
|
||||
usableGb: number;
|
||||
platform: string;
|
||||
gpuName: string | null;
|
||||
gpuVramGb: number | null;
|
||||
unifiedMemory: boolean;
|
||||
hardwareTier: HardwareTier;
|
||||
cpuModel: string | null;
|
||||
}
|
||||
|
||||
export type NexusMode = "personal_ai" | "project_builder" | "both";
|
||||
|
||||
export interface NexusSettings {
|
||||
mode: NexusMode;
|
||||
}
|
||||
|
||||
export function fetchHardwareInfo(): Promise<HardwareInfo> {
|
||||
return api.get<HardwareInfo>("/system/providers");
|
||||
}
|
||||
|
||||
export function fetchNexusSettings(): Promise<NexusSettings> {
|
||||
return api.get<NexusSettings>("/nexus/settings");
|
||||
}
|
||||
|
||||
export function updateNexusSettings(settings: Partial<NexusSettings>): Promise<NexusSettings> {
|
||||
return api.patch<NexusSettings>("/nexus/settings", settings);
|
||||
}
|
||||
95
ui/src/components/onboarding/HardwareSummaryStep.tsx
Normal file
95
ui/src/components/onboarding/HardwareSummaryStep.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// [nexus] Hardware summary display for onboarding wizard step 1
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { HardwareInfo } from "@/api/hardware";
|
||||
|
||||
interface HardwareSummaryStepProps {
|
||||
hardwareInfo: HardwareInfo | undefined;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
interface StatRowProps {
|
||||
label: string;
|
||||
value: string | number | null | undefined;
|
||||
}
|
||||
|
||||
function StatRow({ label, value }: StatRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<span className="text-sm font-medium">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: HardwareSummaryStepProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full rounded" />
|
||||
<Skeleton className="h-4 w-full rounded" />
|
||||
<Skeleton className="h-4 w-full rounded" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Could not detect hardware. You can still continue.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hardwareInfo) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Could not detect hardware. You can still continue.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const { hardwareTier } = hardwareInfo;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{hardwareTier === "apple_silicon" && (
|
||||
<>
|
||||
<StatRow label="Unified memory" value={`${hardwareInfo.totalGb} GB`} />
|
||||
<StatRow label="Available" value={`${hardwareInfo.usableGb} GB`} />
|
||||
<StatRow label="CPU" value={hardwareInfo.cpuModel} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hardwareTier === "gpu" && (
|
||||
<>
|
||||
<StatRow label="GPU" value={hardwareInfo.gpuName} />
|
||||
<StatRow label="GPU VRAM" value={`${hardwareInfo.gpuVramGb} GB`} />
|
||||
<StatRow label="System RAM" value={`${hardwareInfo.totalGb} GB`} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hardwareTier === "cpu_only" && (
|
||||
<>
|
||||
<StatRow label="System RAM" value={`${hardwareInfo.totalGb} GB`} />
|
||||
<StatRow label="CPU" value={hardwareInfo.cpuModel} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Slower than GPU-accelerated models -- cloud AI recommended
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hardwareTier !== "cpu_only" && (
|
||||
<div className="flex flex-col gap-1 pt-2">
|
||||
<span className="text-sm font-medium">Local AI (recommended for privacy)</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Runs entirely on your machine.{"\n"}
|
||||
No accounts. No tracking. Works offline.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
ui/src/components/onboarding/ModeSelector.tsx
Normal file
49
ui/src/components/onboarding/ModeSelector.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// [nexus] Three-card mode selector for onboarding wizard step 2
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { NexusMode } from "@/api/hardware";
|
||||
|
||||
interface ModeSelectorProps {
|
||||
value: NexusMode;
|
||||
onChange: (mode: NexusMode) => void;
|
||||
}
|
||||
|
||||
const MODES: { id: NexusMode; label: string; description: string }[] = [
|
||||
{
|
||||
id: "personal_ai",
|
||||
label: "Personal AI Assistant",
|
||||
description: "Always available, persistent memory, private.",
|
||||
},
|
||||
{
|
||||
id: "project_builder",
|
||||
label: "Project Builder",
|
||||
description: "Brainstorm -> PM -> Engineer -> shipped product.",
|
||||
},
|
||||
{
|
||||
id: "both",
|
||||
label: "Both (recommended)",
|
||||
description: "A conversation becomes a project with one click.",
|
||||
},
|
||||
];
|
||||
|
||||
export function ModeSelector({ value, onChange }: ModeSelectorProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
{MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
onClick={() => onChange(mode.id)}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
|
||||
value === mode.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-sm">{mode.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{mode.description}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
ui/src/hooks/useHardwareInfo.ts
Normal file
14
ui/src/hooks/useHardwareInfo.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// [nexus] React Query hook for hardware detection data
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchHardwareInfo, type HardwareInfo } from "../api/hardware";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
|
||||
export function useHardwareInfo(enabled = true) {
|
||||
return useQuery<HardwareInfo>({
|
||||
queryKey: queryKeys.hardware.info,
|
||||
queryFn: fetchHardwareInfo,
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes — matches server cache TTL
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
|
@ -147,6 +147,9 @@ export const queryKeys = {
|
|||
agentSkills: (agentId: string) => ["skill-groups", "agent", agentId, "skills"] as const,
|
||||
agentEffective: (agentId: string) => ["skill-groups", "agent", agentId, "effective"] as const,
|
||||
},
|
||||
hardware: {
|
||||
info: ["hardware", "info"] as const,
|
||||
},
|
||||
plugins: {
|
||||
all: ["plugins"] as const,
|
||||
examples: ["plugins", "examples"] as const,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue