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
545edec89c
commit
f44235460a
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -146,6 +146,9 @@ export const queryKeys = {
|
||||||
agentSkills: (agentId: string) => ["skill-groups", "agent", agentId, "skills"] as const,
|
agentSkills: (agentId: string) => ["skill-groups", "agent", agentId, "skills"] as const,
|
||||||
agentEffective: (agentId: string) => ["skill-groups", "agent", agentId, "effective"] as const,
|
agentEffective: (agentId: string) => ["skill-groups", "agent", agentId, "effective"] as const,
|
||||||
},
|
},
|
||||||
|
hardware: {
|
||||||
|
info: ["hardware", "info"] as const,
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
all: ["plugins"] as const,
|
all: ["plugins"] as const,
|
||||||
examples: ["plugins", "examples"] as const,
|
examples: ["plugins", "examples"] as const,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue