graph context: a separate marker message before the first user turn
The map picks no longer glue into the opening prompt as seed text
(owner, 2026-10-04 late). POST /api/chats accepts graphContext
{propositions}; the route resolves the picks against the live graph,
derives the reasoning chains among them (chainsForSelection →
formatChainLine), and writes a graph-context custom marker as
message[0] — before the learner's first user message, in either mode.
- Core: newGraphContextMessage (model-facing content = the
contextualization framing + proposition lines + chain lines;
details = the structured pick); toLlmMessages flattens it to a user
message like every marker, so the model reads it for free.
- View: projects to a user-side data-graph-context part.
- UI: GraphContextCard — minimal, muted, dashed, collapsed by default
(counts only); expand reveals name — statement rows and the chain
lines. Same compact-expandable pattern as the practice cards.
- Home: sends graphContext with the creation whenever picks exist;
the prompt param carries the learner's own words only.
This commit is contained in:
@@ -38,6 +38,7 @@ import type { PlanBlockData } from "@/src/core";
|
||||
import { QuestionDock } from "@/components/question-dock";
|
||||
import { PracticeDock } from "@/components/practice-dock";
|
||||
import { ThinkingCard, type PracticeThinkingData } from "@/components/practice-cards";
|
||||
import { GraphContextCard, type GraphContextData } from "@/components/graph-context-card";
|
||||
import { TopBar } from "@/components/top-bar";
|
||||
import type { ArtifactPartProps, JobRecord, Phase3, QuestionAnswerData, TurnUsage } from "@/src/core";
|
||||
import type { LhViewMessage } from "@/src/core";
|
||||
@@ -1258,6 +1259,13 @@ export function Chat({
|
||||
);
|
||||
case "step-start":
|
||||
return pi > 0 ? <Separator key={pi} className="my-3" /> : null;
|
||||
case "data-graph-context":
|
||||
// The session's map pick (2026-10-04): a minimal
|
||||
// collapsible card — scaffolding above the first
|
||||
// user message, never a bubble.
|
||||
return (
|
||||
<GraphContextCard key={pi} data={part.data as GraphContextData} />
|
||||
);
|
||||
case "data-practice-thinking":
|
||||
// The learner's practice submission (2026-10-04):
|
||||
// user-side — the register (thinking trace /
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* The graph-context card (owner, 2026-10-04): the session's map pick —
|
||||
* message[0] of a session started with propositions picked from the
|
||||
* graph. Scaffolding, not a turn: minimal, muted, collapsed by default;
|
||||
* expand reveals the picked propositions (name — statement) and the
|
||||
* reasoning chains among them (premises → conclusion lines). The model
|
||||
* reads the same content through the marker's own text (toLlmMessages);
|
||||
* this card is the learner's face of it.
|
||||
*/
|
||||
|
||||
/** The graph-context marker's details (native-messages.ts). */
|
||||
export interface GraphContextData {
|
||||
propositions: { id: string; name: string; statement: string }[];
|
||||
chains: string[];
|
||||
}
|
||||
|
||||
export function GraphContextCard({ data }: { data: GraphContextData }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { propositions = [], chains = [] } = data ?? {};
|
||||
const noun = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
|
||||
return (
|
||||
<div data-testid="graph-context-card" className="my-1">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="graph-context-toggle"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full cursor-pointer items-center gap-2 rounded-lg border border-dashed border-border/70 px-3 py-2 text-left text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<ChevronRightIcon aria-hidden="true" className={cn("size-3.5 shrink-0 transition-transform", open && "rotate-90")} />
|
||||
<span className="font-medium">Graph context</span>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{noun(propositions.length, "proposition", "propositions")}
|
||||
{chains.length > 0 ? ` · ${noun(chains.length, "chain", "chains")}` : ""}
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div data-testid="graph-context-body" className="mt-1 space-y-1 rounded-lg border border-dashed border-border/70 px-4 py-3 text-xs">
|
||||
{propositions.map((p) => (
|
||||
<div key={p.id} data-testid={`graph-context-prop-${p.id}`} className="leading-relaxed">
|
||||
<span className="font-medium text-foreground/80">{p.name}</span>
|
||||
<span className="text-muted-foreground"> — {p.statement}</span>
|
||||
</div>
|
||||
))}
|
||||
{chains.length > 0 && (
|
||||
<div className="pt-1">
|
||||
{chains.map((c, i) => (
|
||||
<div key={i} className="font-mono text-[11px] text-muted-foreground">
|
||||
{c}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+11
-12
@@ -112,26 +112,25 @@ export function Home({ initialConfigured }: { initialConfigured: boolean }) {
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
// The practice seed: the picks ride the OPENING PROMPT (never
|
||||
// the title — the title stays the learner's own words).
|
||||
const seed =
|
||||
mode === "practice" && practiceProps.length > 0
|
||||
? `\n\nPropositions from the map:\n${practiceProps
|
||||
.map((p) => `- ${p.name}: ${p.statement}`)
|
||||
.join("\n")}`
|
||||
: "";
|
||||
// The map context (owner, 2026-10-04): picks in EITHER mode ride
|
||||
// the CREATION as graphContext — the server writes a
|
||||
// graph-context marker as message[0] (a separate custom message
|
||||
// before the learner's first user message, propositions + the
|
||||
// chains among them); the prompt stays the learner's own words.
|
||||
const res = await fetch("/api/chats", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(
|
||||
mode === "practice" ? { title: t, kind: "practice" } : { title: t },
|
||||
),
|
||||
body: JSON.stringify({
|
||||
title: t,
|
||||
...(mode === "practice" ? { kind: "practice" } : {}),
|
||||
...(practiceProps.length > 0 ? { graphContext: { propositions: practiceProps } } : {}),
|
||||
}),
|
||||
});
|
||||
const { id } = (await res.json()) as { id: string };
|
||||
// No settings PUT on create (per-session models, owner
|
||||
// 2026-09-28): the SERVER stamps the newborn from the strong
|
||||
// slot — the same default this surface's model control edits.
|
||||
router.push(`/chat/${id}?prompt=${encodeURIComponent(t + seed)}`);
|
||||
router.push(`/chat/${id}?prompt=${encodeURIComponent(t)}`);
|
||||
} catch {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user