Skip to content

Commit 044b9d7

Browse files
committed
✨ integrate mvm page with backies api
1 parent 39fd278 commit 044b9d7

8 files changed

Lines changed: 333 additions & 232 deletions

File tree

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
11
import SimpleCard from "@site/src/components/ui/simple-card";
22
import { FC } from "react";
3+
import { HealthResponse } from "./mvm-client";
34

45
type Props = {
56
connected: boolean;
7+
health: HealthResponse | null;
68
};
79

8-
const ConnectionBanner: FC<Props> = ({ connected }) => {
9-
if (connected) return null;
10+
const ConnectionBanner: FC<Props> = ({ connected, health }) => {
11+
if (!connected) {
12+
return (
13+
<SimpleCard className="flex items-center gap-3 p-3 shadow-none!">
14+
<span className="text-sm text-(--reduced-emphasis-color)">
15+
<span className="font-medium text-[var(--ifm-font-color-base)]">
16+
Local API not running.
17+
</span>{" "}
18+
Start with{" "}
19+
<code className="rounded bg-[#191724] px-1.5 py-0.5 text-xs text-[#e0def4]">
20+
make run
21+
</code>{" "}
22+
in{" "}
23+
<code className="rounded bg-[#191724] px-1.5 py-0.5 text-xs text-[#e0def4]">
24+
backies/
25+
</code>
26+
</span>
27+
</SimpleCard>
28+
);
29+
}
1030

11-
return (
12-
<SimpleCard className="flex items-center gap-3 p-3 shadow-none!">
13-
<span className="text-sm text-(--reduced-emphasis-color)">
14-
<span className="font-medium text-[var(--ifm-font-color-base)]">
15-
Local API not running.
16-
</span>{" "}
17-
Start with{" "}
18-
<code className="rounded bg-[#191724] px-1.5 py-0.5 text-xs text-[#e0def4]">
19-
make run
20-
</code>{" "}
21-
in{" "}
22-
<code className="rounded bg-[#191724] px-1.5 py-0.5 text-xs text-[#e0def4]">
23-
backies/
24-
</code>
25-
</span>
26-
</SimpleCard>
27-
);
31+
if (health?.status === "degraded") {
32+
return (
33+
<SimpleCard className="flex items-center gap-3 p-3 shadow-none!">
34+
<span className="text-sm text-(--reduced-emphasis-color)">
35+
<span className="font-medium text-[var(--ifm-font-color-base)]">
36+
Server degraded.
37+
</span>{" "}
38+
{health.error}
39+
</span>
40+
</SimpleCard>
41+
);
42+
}
43+
44+
return null;
2845
};
2946

3047
export default ConnectionBanner;

www/src/components/mvm/mock-data.ts

Lines changed: 0 additions & 117 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { MVM_API_BASE } from "@site/src/constants/api";
2+
import { CompareRequest, ModelInfo, ModelUsage } from "@site/src/types/mvm";
3+
4+
export type HealthResponse = {
5+
status: "ok" | "degraded";
6+
cli_installed: boolean;
7+
cli_authenticated: boolean;
8+
auth_method?: string;
9+
email?: string;
10+
error?: string;
11+
};
12+
13+
export type CompareEvent =
14+
| { type: "model_start"; model: string }
15+
| { type: "token"; model: string; text: string }
16+
| {
17+
type: "model_done";
18+
model: string;
19+
duration_ms: number;
20+
total_cost_usd: number;
21+
usage: ModelUsage;
22+
}
23+
| { type: "model_error"; model: string; error: string }
24+
| { type: "done" };
25+
26+
export async function fetchModels(signal?: AbortSignal): Promise<ModelInfo[]> {
27+
const res = await fetch(`${MVM_API_BASE}/api/mvm/models`, { signal });
28+
if (!res.ok) throw new Error(`models request failed: HTTP ${res.status}`);
29+
return (await res.json()).models;
30+
}
31+
32+
export async function fetchHealth(signal?: AbortSignal): Promise<HealthResponse> {
33+
const res = await fetch(`${MVM_API_BASE}/api/mvm/health`, { signal });
34+
if (!res.ok) throw new Error(`health request failed: HTTP ${res.status}`);
35+
return res.json();
36+
}
37+
38+
export async function streamCompare(
39+
req: CompareRequest,
40+
onEvent: (ev: CompareEvent) => void,
41+
signal: AbortSignal
42+
): Promise<void> {
43+
const res = await fetch(`${MVM_API_BASE}/api/mvm/compare`, {
44+
method: "POST",
45+
headers: { "Content-Type": "application/json" },
46+
body: JSON.stringify(req),
47+
signal,
48+
});
49+
50+
// Validation failures arrive as plain 400 JSON {error, code}, not SSE.
51+
const contentType = res.headers.get("content-type") ?? "";
52+
if (!res.ok || !contentType.includes("text/event-stream")) {
53+
let message = `compare failed: HTTP ${res.status}`;
54+
try {
55+
const body = await res.json();
56+
if (body?.error) message = body.error;
57+
} catch {
58+
// non-JSON error body; keep generic message
59+
}
60+
throw new Error(message);
61+
}
62+
63+
const reader = res.body!.getReader();
64+
const decoder = new TextDecoder();
65+
let buffer = "";
66+
67+
const dispatchFrame = (frame: string) => {
68+
let name = "";
69+
const dataLines: string[] = [];
70+
for (const line of frame.split("\n")) {
71+
if (line.startsWith("event:")) name = line.slice("event:".length).trim();
72+
else if (line.startsWith("data:")) {
73+
dataLines.push(line.slice("data:".length).trimStart());
74+
}
75+
}
76+
if (!name) return;
77+
const data = dataLines.join("\n");
78+
onEvent({ type: name, ...(data ? JSON.parse(data) : {}) } as CompareEvent);
79+
};
80+
81+
// Frames can split across network chunks — buffer until "\n\n".
82+
for (;;) {
83+
const { done, value } = await reader.read();
84+
if (done) break;
85+
buffer += decoder.decode(value, { stream: true });
86+
let idx: number;
87+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
88+
const frame = buffer.slice(0, idx);
89+
buffer = buffer.slice(idx + 2);
90+
if (frame.trim()) dispatchFrame(frame);
91+
}
92+
}
93+
}

www/src/components/mvm/mvm-sidebar.tsx

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,72 @@ import {ModelInfo} from "@site/src/types/mvm";
44
import {IconManufacture, IconSwitchOff,} from "nucleo-isometric";
55
import {FC} from "react";
66
import ModelSelector from "./model-selector";
7+
import {HealthResponse} from "./mvm-client";
8+
79
type Props = {
810
connected: boolean;
11+
health: HealthResponse | null;
912
models: ModelInfo[];
1013
selectedModels: string[];
1114
onToggleModel: (alias: string) => void;
1215
};
1316

1417
const MUTED_ICON_STYLE = { color: "var(--reduced-emphasis-color)", flexShrink: 0 } as const;
1518

19+
const statusTag = (connected: boolean, health: HealthResponse | null) => {
20+
if (!connected) return { color: "muted" as const, label: "DISCONNECTED" };
21+
if (health?.status === "degraded") return { color: "rose" as const, label: "DEGRADED" };
22+
return { color: "foam" as const, label: "CONNECTED" };
23+
};
24+
1625
const MvmSidebar: FC<Props> = ({
1726
connected,
27+
health,
1828
models,
1929
selectedModels,
2030
onToggleModel,
21-
}) => (
22-
<div className="flex flex-col gap-6">
23-
{/* Server status */}
24-
{/* TODO: wire up actual healthcheck once server port/path is finalized */}
25-
<div className="flex flex-col gap-2">
26-
<SecondaryHeader className="flex items-center gap-1.5">
27-
<IconSwitchOff size="18px" style={MUTED_ICON_STYLE} />
28-
Status
29-
</SecondaryHeader>
30-
<div className="flex items-center gap-2">
31-
<CustomTag color={connected ? "foam" : "muted"} className="text-sm!">
32-
{connected ? "CONNECTED" : "DISCONNECTED"}
33-
</CustomTag>
31+
}) => {
32+
const tag = statusTag(connected, health);
33+
34+
return (
35+
<div className="flex flex-col gap-6">
36+
{/* Server status */}
37+
<div className="flex flex-col gap-2">
38+
<SecondaryHeader className="flex items-center gap-1.5">
39+
<IconSwitchOff size="18px" style={MUTED_ICON_STYLE} />
40+
Status
41+
</SecondaryHeader>
42+
<div className="flex items-center gap-2">
43+
<CustomTag color={tag.color} className="text-sm!">
44+
{tag.label}
45+
</CustomTag>
46+
</div>
3447
</div>
35-
</div>
3648

37-
{/* Controls */}
38-
<div className="flex flex-col gap-4">
39-
<ModelSelector
40-
models={models}
41-
selectedModels={selectedModels}
42-
onToggleModel={onToggleModel}
43-
/>
44-
</div>
49+
{/* Controls */}
50+
<div className="flex flex-col gap-4">
51+
<ModelSelector
52+
models={models}
53+
selectedModels={selectedModels}
54+
onToggleModel={onToggleModel}
55+
/>
56+
</div>
4557

46-
{/* Components */}
47-
<div className="flex flex-col gap-3">
48-
<SecondaryHeader className="flex items-center gap-1.5">
49-
<IconManufacture size="18px" />
50-
Components
51-
</SecondaryHeader>
52-
<div className="flex items-center gap-2">
53-
<span className="text-sm">Custom server</span>
54-
<CustomTag color="locked" className="text-xs!">LOCKED</CustomTag>
58+
{/* Components */}
59+
<div className="flex flex-col gap-3">
60+
<SecondaryHeader className="flex items-center gap-1.5">
61+
<IconManufacture size="18px" />
62+
Components
63+
</SecondaryHeader>
64+
<div className="flex items-center gap-2">
65+
<span className="text-sm">Custom server</span>
66+
<CustomTag color="locked" className="text-xs!">LOCKED</CustomTag>
67+
</div>
68+
<span className="text-sm">Streamdown</span>
69+
<span className="text-sm">Claude Code</span>
5570
</div>
56-
<span className="text-sm">Streamdown</span>
57-
<span className="text-sm">Claude Code</span>
5871
</div>
59-
</div>
60-
);
72+
);
73+
};
6174

6275
export default MvmSidebar;

0 commit comments

Comments
 (0)