feat(frontend): CentralStatus + ModelActivityPanel components
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach } from "vitest";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { CentralStatus } from "./CentralStatus";
|
||||
|
||||
beforeEach(() => useSessionStore.getState().resetSession());
|
||||
|
||||
test("renders nothing when there is no user entry and no step messages", () => {
|
||||
const { container } = render(<CentralStatus />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test("echoes the last user entry and the step messages", () => {
|
||||
const st = useSessionStore.getState();
|
||||
st.setLastUserEntry({ kind: "input", text: "how many patients?" });
|
||||
st.applyEvent({ type: "info", level: "info", text: "Searching the schema…" });
|
||||
render(<CentralStatus />);
|
||||
expect(screen.getByText("how many patients?")).toBeInTheDocument();
|
||||
expect(screen.getByText("Searching the schema…")).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
|
||||
/** The minimal central view: the user's last input/choice + the gate's curated
|
||||
* messages for the current step. The verbose model stream lives in the left panel. */
|
||||
export function CentralStatus() {
|
||||
const lastUserEntry = useSessionStore((s) => s.lastUserEntry);
|
||||
const stepMessages = useSessionStore((s) => s.stepMessages);
|
||||
if (!lastUserEntry && stepMessages.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{lastUserEntry && (
|
||||
<div className="rounded-lg border border-border/70 bg-card px-3 py-2">
|
||||
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{lastUserEntry.kind === "input" ? "You asked" : "You chose"}
|
||||
</span>
|
||||
<p className="mt-0.5 text-sm text-foreground">{lastUserEntry.text}</p>
|
||||
</div>
|
||||
)}
|
||||
{stepMessages.map((m, i) => (
|
||||
<p
|
||||
key={i}
|
||||
className={[
|
||||
"text-sm",
|
||||
m.level === "error" ? "text-destructive"
|
||||
: m.level === "warning" ? "text-amber-600"
|
||||
: "text-foreground/90",
|
||||
].join(" ")}
|
||||
>
|
||||
{m.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, vi } from "vitest";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { ModelActivityPanel } from "./ModelActivityPanel";
|
||||
|
||||
beforeEach(() => useSessionStore.getState().resetSession());
|
||||
|
||||
test("renders the streamed model transcript", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "text_delta", text: "Promoting table dim_patient." });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
expect(screen.getByText(/Promoting table dim_patient/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("close button calls onClose", async () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ModelActivityPanel onClose={onClose} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /close/i }));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { X } from "lucide-react";
|
||||
import { Transcript } from "./Transcript";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
/** Left drawer showing the active session's streamed model text ("model activity"),
|
||||
* opened on demand from the work-in-progress icon. */
|
||||
export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
|
||||
return (
|
||||
<aside className="flex w-[380px] shrink-0 flex-col border-r border-border bg-sidebar">
|
||||
<div className="flex items-center justify-between border-b border-border/70 px-4 py-3">
|
||||
<h2 className="font-heading text-sm font-semibold text-foreground">Model activity</h2>
|
||||
<Button size="icon-sm" variant="ghost" aria-label="Close model activity" onClick={onClose}>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<Transcript />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user