ui: the document look — one language for every thinking, no bubbles

Owner round 2 (2026-10-05), Slice F:

- USER TEXT: the right-aligned bubble is gone. User messages are
  full-width document rows — muted 'You' label, muted quote rail,
  full-opacity text. Learner words are content, not a chat-app tell.
- USER THINKING (everywhere): arrives COLLAPSED as a trigger row in the
  exact register of the agent's reasoning rows (muted label + chevron +
  hover affordance); expanding opens the quote block — rail, 50%
  opacity, italics. The inner-voice signature. The ThinkingBlock loses
  its shell/border: pure document material.
- QUIZ CARDS: the answer's thinking is the SAME collapsible row, same
  placement, just inside the container; the explanation drops to 50%
  opacity (upright — italics stays the thinking signature).
- AGENT'S THINKING: the duration bookkeeping is gone (it was live-turn
  state that died to 'a few seconds' on remount anyway) — the settled
  row reads static 'Agent's thinking', pairing with 'Your thinking'.
  Spinner + shimmer stay the working signal while streaming.
- Glance law: user thinking = rail + 50% italic; agent thinking = no
  rail; user text = upright, full opacity, 'You' label.
This commit is contained in:
amosblomqvist
2026-10-05 03:41:39 +03:00
parent a5a554582e
commit e2284e723a
7 changed files with 97 additions and 96 deletions
+25 -13
View File
@@ -1249,16 +1249,28 @@ export function Chat({
{m.role === "assistant" && mi > 0 && phases[mi] === "teach" && phases[mi - 1] !== "teach" ? (
<PhaseDivider key={`teach-${m.id || `m-${mi}`}`} phase="teach" />
) : null}
<Message from={m.role} className={m.role === "assistant" ? "max-w-full" : undefined}>
<MessageContent className={m.role === "assistant" ? "w-full" : undefined}>
{m.role === "user" ? (
// User text renders plain: the model's LaTeX pipeline is not
// applied to learner-authored text ($ stays $, no surprises).
<div className="lh-prose whitespace-pre-wrap">
{m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")}
{m.role === "user" ? (
/* THE DOCUMENT ROW (owner round, 2026-10-05): no right-aligned
bubble — the transcript reads as a document. User text is a
full-width quote block: muted "You" label, muted rail,
FULL-opacity text (learner words are content, not whisper).
Distinguished from user thinking at a glance: thinking is
collapsed + 50% italic; this is upright and present.
User text renders plain — the model's LaTeX pipeline is not
applied to learner-authored text ($ stays $, no surprises).
Rendered WITHOUT the Message wrapper — that is the bubble. */
<div data-testid="user-message" className="my-1 w-full">
<div className="mb-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">You</div>
<div className="border-l-2 border-muted-foreground/30 pl-3 text-foreground">
<div className="lh-prose whitespace-pre-wrap">
{m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")}
</div>
</div>
) : (
m.parts.map((part, pi) => {
</div>
) : (
<Message from="assistant" className="max-w-full">
<MessageContent className="w-full">
{m.parts.map((part, pi) => {
const isLastPart = mi === messages.length - 1 && pi === m.parts.length - 1;
// A tool call that errored BACK TO THE AGENT (validation
// failure, malformed args) is invisible work — the agent
@@ -1329,10 +1341,10 @@ export function Chat({
return null;
}
}
})
)}
</MessageContent>
})}
</MessageContent>
</Message>
)}
</Fragment>
))}
@@ -1983,7 +1995,7 @@ function DockOverlay({
measureBlock();
if (p && Math.abs(p.height - h) > 1) {
el.animate([{ height: `${p.height}px` }, { height: `${h}px` }], {
duration: 180,
duration: 120,
easing: "cubic-bezier(0.2, 0.8, 0.2, 1)",
});
}