MVP M1–M3: visual port, TipTap text editor, React Flow diagram

apps/web — Next.js 16 + TypeScript + React 19 (no Tailwind)

M1 Visual port
- Manuscript theme + base styles ported verbatim from design-source
- 100dvh layout with internal scroll regions (rail / narrative / dock thread)
- TopBar, LeftRail (collapsible sections + collapsed strip), CanvasHeader,
  StatusBar, EditorShell composing the dual-canvas
- SocratesDock (subtle/default/prominent) with bubbles + numbered options
- Sigil (Σ + laurel) used across editor and seed screens
- SeedScreen with emerging-seed rail, mini-graph, confidence bar, thread,
  typing indicator, input row
- Routes: / (homepage), /editor/[projectId], /seed
- Aristotle fixture in lib/fixtures (ported from data.js)

M2 Real text editor
- TipTap (StarterKit + custom Chip atom inline node + ReactNodeViewRenderer)
- Slash-menu insertion via @tiptap/suggestion with arrow / number-key shortcuts
- ChipFocusContext bridges narrative ↔ diagram hover/selection across the
  TipTap render boundary
- fixtureToDoc converts the fixture narrative → ProseMirror JSON

M3 Real diagram
- React Flow (@xyflow/react) custom node (block / actor / constraint / system)
  matching prototype's softened look
- Custom edge with bezier path + label renderer (association / composition /
  constraint variants)
- Drag-create from a left-side palette via HTML5 DnD + screenToFlowPosition
- NodeInspector panel: edit kind / label / properties (or expression for
  constraints); delete cascades to incident edges
- Bidirectional focus highlighting between chips (narrative) and blocks
  (diagram)
- Removed redundant legend (stereotype labels on nodes serve same purpose)

State for M3 lives in component memory; persistence + bidirectional sync land
in M4–M5.
This commit is contained in:
2026-04-28 22:54:36 +02:00
parent f1c4566576
commit a0566ce64c
38 changed files with 7988 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
// Sample project content — a realistic PM idea: an AI study companion for university students.
// Ported from docs/design-source/socrata/project/data.js.
// Used to populate the textual narrative + diagram during M1's static visual port.
export type ChipKind = "block" | "property" | "association" | "requirement";
export interface ChipToken {
t: "chip";
kind: ChipKind;
id: string;
label: string;
}
export interface TextToken {
t: "text";
v: string;
}
export type NarrativeInline = ChipToken | TextToken;
export interface NarrativeNode {
type: "h1" | "h2" | "p";
text?: string;
children?: NarrativeInline[];
}
export type BlockKind = "block" | "actor" | "constraint";
export interface FixtureBlock {
id: string;
label: string;
kind: BlockKind;
/** Position normalized to a 720×460 board (matches diagram.jsx). */
x: number;
y: number;
w: number;
h: number;
properties: string[];
}
export type AssociationKind = "association" | "composition" | "constraint";
export interface FixtureAssociation {
id: string;
from: string;
to: string;
label: string;
kind: AssociationKind;
}
export interface FixtureAssumption {
id: string;
text: string;
status: "open" | "validated" | "invalidated";
linked: string[];
}
export interface FixtureRisk {
id: string;
text: string;
severity: "low" | "med" | "high";
linked: string[];
}
export interface FixtureSocratesTurn {
who: "socrates" | "user";
text: string;
options?: Array<{ n: number; label: string; sub: string }>;
}
export interface FixtureProposal {
title: string;
impactedBlocks: string[];
addedConstraints: number;
affectedRequirements: string[];
iteration: number;
}
export interface FixtureData {
project: {
name: string;
tagline: string;
scope: string;
owner: string;
branch: string;
lastSync: string;
};
narrative: NarrativeNode[];
blocks: FixtureBlock[];
associations: FixtureAssociation[];
assumptions: FixtureAssumption[];
risks: FixtureRisk[];
socratesThread: FixtureSocratesTurn[];
proposal: FixtureProposal;
}
export const aristotleFixture: FixtureData = {
project: {
name: "Aristotle",
tagline: "AI study companion for first-year STEM students",
scope: "Higher education · undergraduate",
owner: "M. Chen · PM",
branch: "main",
lastSync: "2 min ago",
},
narrative: [
{ type: "h1", text: "Problem framing" },
{
type: "p",
children: [
{ t: "text", v: "First-year STEM students at large public universities frequently disengage from coursework not because the material is intractable, but because the " },
{ t: "chip", kind: "block", id: "student", label: "Student" },
{ t: "text", v: " lacks a low-stakes thinking partner during the long tail between lectures and office hours. The " },
{ t: "chip", kind: "block", id: "course", label: "Course" },
{ t: "text", v: " produces problem sets that assume mastery of prerequisite scaffolding, and a " },
{ t: "chip", kind: "property", id: "selfEfficacy", label: "self_efficacy" },
{ t: "text", v: " gap forms quickly." },
],
},
{
type: "p",
children: [
{ t: "text", v: "Aristotle is a study companion that " },
{ t: "chip", kind: "association", id: "guides", label: "guides" },
{ t: "text", v: " the student through Socratic prompts rather than answers, scoped to their current " },
{ t: "chip", kind: "block", id: "assignment", label: "Assignment" },
{ t: "text", v: ". It refuses to produce solutions; it only produces questions calibrated to a student's evolving understanding." },
],
},
{ type: "h2", text: "Constraints" },
{
type: "p",
children: [
{ t: "chip", kind: "requirement", id: "REQ-001", label: "REQ-001" },
{ t: "text", v: " The companion must never output a complete solution to a graded problem. " },
{ t: "chip", kind: "requirement", id: "REQ-002", label: "REQ-002" },
{ t: "text", v: " Response latency under 1.2s P50 to preserve flow. " },
{ t: "chip", kind: "requirement", id: "REQ-003", label: "REQ-003" },
{ t: "text", v: " Operates within FERPA boundaries; coursework never leaves institutional tenancy." },
],
},
{ type: "h2", text: "Why now" },
{
type: "p",
children: [
{ t: "text", v: "Two large public-university pilots indicated that students would adopt a tool that explicitly does not solve their homework — an inversion of the prevailing market." },
],
},
],
blocks: [
{ id: "student", label: "Student", kind: "block", x: 0.10, y: 0.18, w: 168, h: 96,
properties: ["self_efficacy", "course_load", "prior_grade"] },
{ id: "aristotle", label: "Aristotle", kind: "block", x: 0.42, y: 0.18, w: 184, h: 110,
properties: ["interaction_style", "scope_window", "refusal_policy"] },
{ id: "course", label: "Course", kind: "block", x: 0.74, y: 0.10, w: 168, h: 96,
properties: ["syllabus", "prerequisites"] },
{ id: "assignment", label: "Assignment", kind: "block", x: 0.74, y: 0.58, w: 168, h: 96,
properties: ["due_at", "rubric", "graded"] },
{ id: "instructor", label: "Instructor", kind: "actor", x: 0.10, y: 0.62, w: 144, h: 76,
properties: ["policy_set"] },
{ id: "ferpa", label: "FERPA boundary", kind: "constraint", x: 0.42, y: 0.66, w: 184, h: 72,
properties: [] },
],
associations: [
{ id: "a1", from: "student", to: "aristotle", label: "consults", kind: "association" },
{ id: "a2", from: "aristotle", to: "assignment", label: "scoped_to", kind: "association" },
{ id: "a3", from: "course", to: "assignment", label: "contains", kind: "composition" },
{ id: "a4", from: "instructor", to: "aristotle", label: "configures", kind: "association" },
{ id: "a5", from: "aristotle", to: "ferpa", label: "obeys", kind: "constraint" },
{ id: "a6", from: "student", to: "course", label: "enrolled_in", kind: "association" },
],
assumptions: [
{ id: "ASM-1", text: "Students will accept a tool that refuses to give answers", status: "open", linked: ["aristotle"] },
{ id: "ASM-2", text: "Faculty will not classify Socratic prompts as academic dishonesty", status: "validated", linked: ["aristotle", "instructor"] },
{ id: "ASM-3", text: "1.2s P50 latency is achievable with on-prem inference", status: "open", linked: ["aristotle"] },
],
risks: [
{ id: "RSK-1", text: "Refusal policy circumvented via prompt injection", severity: "high", linked: ["aristotle"] },
{ id: "RSK-2", text: "FERPA tenancy adds 46 weeks of procurement per institution", severity: "med", linked: ["ferpa"] },
],
socratesThread: [
{ who: "socrates", text: "You've defined Aristotle as scoped to a single Assignment. What happens when a student wants to compare techniques across two assignments — does the scope expand, or does the student switch contexts?" },
{ who: "user", text: "Switch contexts. We don't want cross-assignment leakage of solution patterns." },
{ who: "socrates", text: "Good. That decision has an implication: the property scope_window on Aristotle becomes single-valued, and we may want a Constraint that forbids cross-Assignment retrieval. Should I draft that as a proposal?" },
{
who: "socrates",
text: "Given the context-switch decision, three things follow. Pick where to go next:",
options: [
{ n: 1, label: "Draft the constraint", sub: "no cross-Assignment retrieval" },
{ n: 2, label: "Revisit scope_window", sub: "single-valued vs. multi-valued" },
{ n: 3, label: "Park it", sub: "I'll come back later" },
],
},
],
proposal: {
title: "Add constraint: no cross-Assignment retrieval",
impactedBlocks: ["aristotle", "assignment"],
addedConstraints: 1,
affectedRequirements: ["REQ-001"],
iteration: 2,
},
};