New-session page reads like a pre-session: no phase chip, outlined bar, opening-question placeholder

Three owner fixes for /chat/new (refinement round 4, 2026-09-24) —
the page must stop looking like a session that's already running:

1. NO PHASE CHIP: the header's <PhaseChip phase="probe" /> dies —
   no session has started, so no phase exists yet. Active sessions
   keep their chip (chat.tsx's derives from the live timeline).

2. TOP BAR STRUCTURE MATCHS SESSIONS: the header gains the same
   border-b border-border as the chat header (h-15, lh-drag,
   60px). The floating-bar look is gone; /chat/new now reads as
   the same shell with empty content.

3. COMPOSER PLACEHOLDER = THE OPENING QUESTION: active sessions
   keep "Ask, answer, or steer the lesson…" (now the DEFAULT of a
   new Composer `placeholder` prop), but the fresh page asks what
   the graph view's new-session input asks — "What do you want to
   learn?" No lesson is in flight, so the composer must not read
   like one is.

The ConversationEmptyState intro card ("The teacher will open by
probing…") stays, per owner confirmation.

Live-verified on :3001 /chat/new: no phase-chip node, computed
1px header border rgb(67,67,67) at h-15/60px, placeholder reads
"What do you want to learn?"; the empty-state card intact. Unit
test pins the default session placeholder AND the override.
903/903.
This commit is contained in:
amosblomqvist
2026-09-24 19:21:03 +03:00
parent 4936b776ab
commit 8c50b50749
3 changed files with 21 additions and 6 deletions
+6 -1
View File
@@ -35,6 +35,7 @@ export function Composer({
autoFocus,
verdict,
onEscape,
placeholder = "Ask, answer, or steer the lesson…",
}: {
busy: boolean;
value: string;
@@ -50,6 +51,10 @@ export function Composer({
/** Escape while a dictation runs stops it (STT wave, 2026-09-17). No-op
* when absent — Esc otherwise means nothing in the composer. */
onEscape?: () => void;
/** The textarea's placeholder (owner refinement round, 2026-09-24):
* active sessions read "Ask, answer, or steer the lesson…"; the fresh
* new-session page asks the graph view's opening question instead. */
placeholder?: string;
}) {
return (
<form
@@ -90,7 +95,7 @@ export function Composer({
else if (action === "send") onSend();
}
}}
placeholder="Ask, answer, or steer the lesson…"
placeholder={placeholder}
autoFocus={autoFocus}
rows={1}
className="field-sizing-content lh-input max-h-48 min-h-10 flex-1 resize-none bg-transparent px-2.5 py-2 outline-none placeholder:text-muted-foreground"
+6 -3
View File
@@ -6,7 +6,6 @@ import { Conversation, ConversationContent, ConversationEmptyState } from "@/com
import { Composer } from "@/components/composer";
import { DictationBar, useDictation } from "@/components/dictation";
import { useDefaultSettings } from "@/components/default-settings";
import { PhaseChip } from "@/components/phase-divider";
import { SidebarToggle } from "@/components/sidebar";
import { SetupDialog } from "@/components/setup-dialog";
import { useSettingsDialog } from "@/components/settings/settings-dialog.tsx";
@@ -92,10 +91,9 @@ export function DraftShell({
<div className="flex min-w-0 flex-1 flex-col">
{/* h-15 (60px) matches the chat header and the sidebar's LH wordmark
band (owner, round 6). */}
<header className="lh-drag relative flex h-15 items-center gap-3 px-3">
<header className="lh-drag flex h-15 items-center gap-3 border-b border-border px-3">
<SidebarToggle />
<div className="min-w-0 flex-1 truncate font-title text-3xl tracking-tight">New session</div>
<PhaseChip phase="probe" />
</header>
<div className="relative flex min-h-0 flex-1">
@@ -125,6 +123,11 @@ export function DraftShell({
canSend={canSend}
autoFocus
onEscape={dictation.active ? dictation.cancel : undefined}
// The opening question, same as the graph view's new-session
// input (owner refinement round, 2026-09-24) — no lesson has
// started yet, so the composer must not read like one is in
// flight.
placeholder="What do you want to learn?"
/>
</div>
<StatsBar
+9 -2
View File
@@ -13,7 +13,7 @@ import { describe, it } from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { Composer, enterAction } from "../components/composer.tsx";
function render(opts: { busy?: boolean; canSend?: boolean } = {}) {
function render(opts: { busy?: boolean; canSend?: boolean; placeholder?: string } = {}) {
return renderToStaticMarkup(
<Composer
busy={opts.busy ?? false}
@@ -22,6 +22,7 @@ function render(opts: { busy?: boolean; canSend?: boolean } = {}) {
onSend={() => {}}
onStop={() => {}}
canSend={opts.canSend ?? true}
{...(opts.placeholder ? { placeholder: opts.placeholder } : {})}
/>,
);
}
@@ -29,10 +30,16 @@ function render(opts: { busy?: boolean; canSend?: boolean } = {}) {
describe("Composer", () => {
it("chat placeholder + send button", () => {
const html = render();
assert.match(html, /Ask, answer, or steer the lesson/);
assert.match(html, /Ask, answer, or steer the lesson/, "the default — active sessions");
assert.ok(html.includes('aria-label="Send"'), "send button present");
});
it("placeholder is overridable — the new-session page asks the graph view's question", () => {
const html = render({ placeholder: "What do you want to learn?" });
assert.match(html, /What do you want to learn\?/);
assert.ok(!html.includes("Ask, answer"), "the session placeholder must not ride along");
});
it("busy: send button becomes stop", () => {
const html = render({ busy: true });
assert.ok(html.includes('aria-label="Stop"'), "stop replaces send while streaming");