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:
@@ -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,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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user