master planner: pink brain, agent tab, mode-scoped topologies + svg illustrations
Six planner-modal fixes in one pass:
- Header icon swap: Sparkles → Brain, styled pink (rgba(255,105,180,*))
to match the "brain" framing the user wanted.
- Drop the "Claude Opus 4.8 designs & deploys." subtitle — the title
carries enough weight on its own.
- Rename the first mode tab "Specialists" → "Agent". This card is
specifically for building a single specialist. Updated INTRO to
ask for one agent (job title + system prompt), so the planner
chat behaves accordingly.
- Team hint: "4–8 agents, balanced" → "4–6 agents, balanced" so the
UI matches the intro text ("4–6 person growth team").
- Topology strip is now mode-scoped. Team mode surfaces org-shaped
structures (hierarchical, hub_spoke, star_moe, pipeline, ring,
holacratic, debate, flat); Swarm mode surfaces formation-shaped
ones (swarm, mesh, blackboard, market, flat). `flat` bridges
both because it serves either shape. The strip header now
reads "Topology · N available for {mode}".
- Replaced the ASCII `<pre>` diagram with inline SVG illustrations
per kind. Coordinator nodes render in the accent pink (#ff69b4);
worker/peer nodes are neutral gray. Twelve illustrations total,
one per topology kind, in a lookup map (`TOPO_ILLUSTRATION`).
If we later want the exact clawbernetes.work marketing images,
drop them into /public/topologies/<kind>.svg and swap the
renderer to an <img/>.
This commit is contained in:
@@ -7,20 +7,20 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Activity, CalendarClock, Send, Sparkles, Users, Webhook } from "lucide-react";
|
import { Activity, Brain, CalendarClock, Send, Users, Webhook } from "lucide-react";
|
||||||
|
|
||||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||||
|
|
||||||
type Mode = "specialists" | "team" | "swarm" | "scheduled" | "triggered";
|
type Mode = "specialists" | "team" | "swarm" | "scheduled" | "triggered";
|
||||||
const MODES: { key: Mode; label: string; hint: string }[] = [
|
const MODES: { key: Mode; label: string; hint: string }[] = [
|
||||||
{ key: "specialists", label: "Specialists", hint: "2–3 domain experts" },
|
{ key: "specialists", label: "Agent", hint: "one specialist agent" },
|
||||||
{ key: "team", label: "Team", hint: "4–8 agents, balanced" },
|
{ key: "team", label: "Team", hint: "4–6 agents, balanced" },
|
||||||
{ key: "swarm", label: "Swarm", hint: "10+ workers, self-verifying" },
|
{ key: "swarm", label: "Swarm", hint: "10+ workers, self-verifying" },
|
||||||
{ key: "scheduled", label: "Scheduled", hint: "ephemeral · date/time" },
|
{ key: "scheduled", label: "Scheduled", hint: "ephemeral · date/time" },
|
||||||
{ key: "triggered", label: "Triggered", hint: "ephemeral · webhook" },
|
{ key: "triggered", label: "Triggered", hint: "ephemeral · webhook" },
|
||||||
];
|
];
|
||||||
const INTRO: Record<Mode, string> = {
|
const INTRO: Record<Mode, string> = {
|
||||||
specialists: "Describe what you need — e.g. “a group of 2–3 specialists for a React Native app: a Rust backend expert, a RN UI dev, and a release engineer.” I'll write high-quality prompts for each.",
|
specialists: "Describe the single agent you need — e.g. “a Rust backend expert who knows Actix and Diesel deeply, focused on reviewing our API layer for correctness bugs.” I'll write a high-quality system prompt and job title for one agent.",
|
||||||
team: "Describe the goal — e.g. “build me a 4–6 person growth team: a coordinator, a researcher, a writer, an analyst, and a critic.” I'll pick roles that actually get exercised each iteration.",
|
team: "Describe the goal — e.g. “build me a 4–6 person growth team: a coordinator, a researcher, a writer, an analyst, and a critic.” I'll pick roles that actually get exercised each iteration.",
|
||||||
swarm: "Describe a job to verify-loop across 10+ workers — e.g. “analyze 100 EV companies; every figure needs a resolvable source URL.” I'll turn it into a checklist the verifier enforces.",
|
swarm: "Describe a job to verify-loop across 10+ workers — e.g. “analyze 100 EV companies; every figure needs a resolvable source URL.” I'll turn it into a checklist the verifier enforces.",
|
||||||
scheduled: "Describe a team and when it should run — e.g. “a market-news digest team, every weekday at 7am.” The team is spun up at fire time and torn down after.",
|
scheduled: "Describe a team and when it should run — e.g. “a market-news digest team, every weekday at 7am.” The team is spun up at fire time and torn down after.",
|
||||||
@@ -91,6 +91,28 @@ type CatalogEntry = {
|
|||||||
role_distribution: { role: string; weight: number }[];
|
role_distribution: { role: string; weight: number }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Topologies split by mode. Teams read as organizational structures (a
|
||||||
|
// coordinator, a workflow, a role rotation); swarms read as formations —
|
||||||
|
// broad, parallel, self-organizing. `flat` lives in both because a flat
|
||||||
|
// spread of peers can serve either shape depending on the task.
|
||||||
|
const TEAM_TOPOLOGIES: readonly string[] = [
|
||||||
|
"hierarchical",
|
||||||
|
"hub_spoke",
|
||||||
|
"star_moe",
|
||||||
|
"pipeline",
|
||||||
|
"ring",
|
||||||
|
"holacratic",
|
||||||
|
"debate",
|
||||||
|
"flat",
|
||||||
|
];
|
||||||
|
const SWARM_TOPOLOGIES: readonly string[] = [
|
||||||
|
"swarm",
|
||||||
|
"mesh",
|
||||||
|
"blackboard",
|
||||||
|
"market",
|
||||||
|
"flat",
|
||||||
|
];
|
||||||
|
|
||||||
// Per-topology comm/coordinator/usage cheat sheet. Kept in the client so the
|
// Per-topology comm/coordinator/usage cheat sheet. Kept in the client so the
|
||||||
// info panel can render immediately after a selection without a second fetch;
|
// info panel can render immediately after a selection without a second fetch;
|
||||||
// mirrors the semantics baked into cm-orchestrator's plan_steps().
|
// mirrors the semantics baked into cm-orchestrator's plan_steps().
|
||||||
@@ -350,10 +372,9 @@ export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
|
|||||||
<div onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Master Planner" style={{ width: "100%", maxWidth: 960, height: "86vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0d0d10", border: "1px solid rgba(255,255,255,.1)", boxShadow: "0 30px 90px rgba(0,0,0,.6)", overflow: "hidden", animation: "scale-in .18s ease" }}>
|
<div onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Master Planner" style={{ width: "100%", maxWidth: 960, height: "86vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0d0d10", border: "1px solid rgba(255,255,255,.1)", boxShadow: "0 30px 90px rgba(0,0,0,.6)", overflow: "hidden", animation: "scale-in .18s ease" }}>
|
||||||
{/* Header + mode selector */}
|
{/* Header + mode selector */}
|
||||||
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid rgba(255,255,255,.07)" }}>
|
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid rgba(255,255,255,.07)" }}>
|
||||||
<span style={{ width: 32, height: 32, borderRadius: 9, background: "rgba(201,138,240,.14)", border: "1px solid rgba(201,138,240,.32)", display: "flex", alignItems: "center", justifyContent: "center", color: "#c98af0" }}><Sparkles size={16} /></span>
|
<span style={{ width: 32, height: 32, borderRadius: 9, background: "rgba(255,105,180,.14)", border: "1px solid rgba(255,105,180,.36)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff69b4" }}><Brain size={16} /></span>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>Master Planner</div>
|
<div style={{ fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>Master Planner</div>
|
||||||
<div style={{ fontSize: 11.5, color: "#8a8a92" }}>Claude Opus 4.8 designs & deploys.</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: 3, padding: 3, borderRadius: 9, background: "rgba(255,255,255,.04)", border: "1px solid rgba(255,255,255,.08)" }}>
|
<div style={{ display: "flex", gap: 3, padding: 3, borderRadius: 9, background: "rgba(255,255,255,.04)", border: "1px solid rgba(255,255,255,.08)" }}>
|
||||||
{MODES.map((m) => (
|
{MODES.map((m) => (
|
||||||
@@ -365,6 +386,7 @@ export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
|
|||||||
|
|
||||||
{(mode === "team" || mode === "swarm") ? (
|
{(mode === "team" || mode === "swarm") ? (
|
||||||
<TopologyStrip
|
<TopologyStrip
|
||||||
|
mode={mode}
|
||||||
catalog={catalog}
|
catalog={catalog}
|
||||||
selected={topologyKind}
|
selected={topologyKind}
|
||||||
onSelect={setTopologyKind}
|
onSelect={setTopologyKind}
|
||||||
@@ -462,14 +484,18 @@ export function MasterPlannerModal({ onClose }: { onClose: () => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TopologyStrip({
|
function TopologyStrip({
|
||||||
|
mode,
|
||||||
catalog,
|
catalog,
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
|
mode: "team" | "swarm";
|
||||||
catalog: CatalogEntry[];
|
catalog: CatalogEntry[];
|
||||||
selected: string | null;
|
selected: string | null;
|
||||||
onSelect: (kind: string | null) => void;
|
onSelect: (kind: string | null) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const allowed = mode === "team" ? TEAM_TOPOLOGIES : SWARM_TOPOLOGIES;
|
||||||
|
const filtered = catalog.filter((c) => allowed.includes(c.kind));
|
||||||
if (catalog.length === 0) {
|
if (catalog.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -508,7 +534,7 @@ function TopologyStrip({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Topology · {catalog.length} available</span>
|
<span>Topology · {filtered.length} available for {mode}</span>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -538,7 +564,7 @@ function TopologyStrip({
|
|||||||
scrollbarWidth: "thin",
|
scrollbarWidth: "thin",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{catalog.map((c) => {
|
{filtered.map((c) => {
|
||||||
const active = selected === c.kind;
|
const active = selected === c.kind;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -584,7 +610,7 @@ function TopologyStrip({
|
|||||||
</div>
|
</div>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<TopologyInfoPanel
|
<TopologyInfoPanel
|
||||||
entry={catalog.find((c) => c.kind === selected)}
|
entry={filtered.find((c) => c.kind === selected)}
|
||||||
detail={TOPO_DETAIL[selected]}
|
detail={TOPO_DETAIL[selected]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -592,6 +618,203 @@ function TopologyStrip({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Inline topology illustrations --------------------------------
|
||||||
|
// Small SVG glyphs, roughly 168×120. Coordinator role uses accent pink
|
||||||
|
// (#ff69b4); worker/peer nodes are neutral gray. If a project ever wants
|
||||||
|
// the exact clawbernetes.work marketing images, drop them into
|
||||||
|
// /public/topologies/<kind>.svg and replace this map with an <img/>.
|
||||||
|
const PINK = "#ff69b4";
|
||||||
|
const NODE = "#8a8a92";
|
||||||
|
const EDGE = "rgba(201,138,240,.55)";
|
||||||
|
const NODE_TEXT = "#eaeaee";
|
||||||
|
|
||||||
|
function Circle({ cx, cy, r, fill, stroke, label }: { cx: number; cy: number; r: number; fill: string; stroke?: string; label?: string }) {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<circle cx={cx} cy={cy} r={r} fill={fill} stroke={stroke ?? "rgba(255,255,255,.14)"} strokeWidth="1" />
|
||||||
|
{label ? <text x={cx} y={cy + 3.5} textAnchor="middle" fontSize="8.5" fill={NODE_TEXT} fontFamily="ui-monospace, monospace">{label}</text> : null}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Arrow({ x1, y1, x2, y2 }: { x1: number; y1: number; x2: number; y2: number }) {
|
||||||
|
return <line x1={x1} y1={y1} x2={x2} y2={y2} stroke={EDGE} strokeWidth="1.4" markerEnd="url(#tp-arrow)" />;
|
||||||
|
}
|
||||||
|
function Wire({ x1, y1, x2, y2 }: { x1: number; y1: number; x2: number; y2: number }) {
|
||||||
|
return <line x1={x1} y1={y1} x2={x2} y2={y2} stroke={EDGE} strokeWidth="1.4" />;
|
||||||
|
}
|
||||||
|
function TopoSvg({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 168 120" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<marker id="tp-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||||
|
<path d="M0,0 L10,5 L0,10 z" fill={EDGE} />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOPO_ILLUSTRATION: Record<string, React.FC> = {
|
||||||
|
hierarchical: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Arrow x1={84} y1={22} x2={30} y2={62} />
|
||||||
|
<Arrow x1={84} y1={22} x2={84} y2={62} />
|
||||||
|
<Arrow x1={84} y1={22} x2={138} y2={62} />
|
||||||
|
<Arrow x1={30} y1={78} x2={84} y2={106} />
|
||||||
|
<Arrow x1={84} y1={78} x2={84} y2={106} />
|
||||||
|
<Arrow x1={138} y1={78} x2={84} y2={106} />
|
||||||
|
<Circle cx={84} cy={18} r={12} fill={PINK} label="R" />
|
||||||
|
<Circle cx={30} cy={70} r={10} fill={NODE} label="c1" />
|
||||||
|
<Circle cx={84} cy={70} r={10} fill={NODE} label="c2" />
|
||||||
|
<Circle cx={138} cy={70} r={10} fill={NODE} label="c3" />
|
||||||
|
<Circle cx={84} cy={106} r={10} fill={PINK} label="R" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
hub_spoke: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Wire x1={84} y1={60} x2={30} y2={24} />
|
||||||
|
<Wire x1={84} y1={60} x2={138} y2={24} />
|
||||||
|
<Wire x1={84} y1={60} x2={30} y2={96} />
|
||||||
|
<Wire x1={84} y1={60} x2={138} y2={96} />
|
||||||
|
<Circle cx={30} cy={24} r={10} fill={NODE} label="s1" />
|
||||||
|
<Circle cx={138} cy={24} r={10} fill={NODE} label="s2" />
|
||||||
|
<Circle cx={30} cy={96} r={10} fill={NODE} label="s3" />
|
||||||
|
<Circle cx={138} cy={96} r={10} fill={NODE} label="s4" />
|
||||||
|
<Circle cx={84} cy={60} r={14} fill={PINK} label="hub" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
star_moe: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Arrow x1={84} y1={30} x2={22} y2={92} />
|
||||||
|
<Arrow x1={84} y1={30} x2={62} y2={92} />
|
||||||
|
<Arrow x1={84} y1={30} x2={106} y2={92} />
|
||||||
|
<Arrow x1={84} y1={30} x2={146} y2={92} />
|
||||||
|
<Circle cx={84} cy={22} r={13} fill={PINK} label="rt" />
|
||||||
|
<Circle cx={22} cy={100} r={10} fill={NODE} label="e1" />
|
||||||
|
<Circle cx={62} cy={100} r={10} fill={NODE} label="e2" />
|
||||||
|
<Circle cx={106} cy={100} r={10} fill={NODE} label="e3" />
|
||||||
|
<Circle cx={146} cy={100} r={10} fill={NODE} label="e4" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
pipeline: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Arrow x1={30} y1={60} x2={64} y2={60} />
|
||||||
|
<Arrow x1={78} y1={60} x2={112} y2={60} />
|
||||||
|
<Arrow x1={126} y1={60} x2={148} y2={60} />
|
||||||
|
<Circle cx={22} cy={60} r={11} fill={NODE} label="s1" />
|
||||||
|
<Circle cx={72} cy={60} r={11} fill={NODE} label="s2" />
|
||||||
|
<Circle cx={122} cy={60} r={11} fill={NODE} label="s3" />
|
||||||
|
<Circle cx={156} cy={60} r={11} fill={PINK} label="✓" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
ring: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Arrow x1={84} y1={22} x2={132} y2={54} />
|
||||||
|
<Arrow x1={140} y1={70} x2={110} y2={102} />
|
||||||
|
<Arrow x1={70} y1={106} x2={38} y2={78} />
|
||||||
|
<Arrow x1={30} y1={62} x2={62} y2={30} />
|
||||||
|
<Circle cx={84} cy={18} r={11} fill={NODE} label="a1" />
|
||||||
|
<Circle cx={140} cy={60} r={11} fill={NODE} label="a2" />
|
||||||
|
<Circle cx={100} cy={106} r={11} fill={NODE} label="a3" />
|
||||||
|
<Circle cx={28} cy={60} r={11} fill={NODE} label="a4" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
swarm: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
{Array.from({ length: 7 }).map((_, i) => {
|
||||||
|
const x = 14 + i * 23;
|
||||||
|
return <Wire key={`w${i}`} x1={x} y1={38} x2={84} y2={92} />;
|
||||||
|
})}
|
||||||
|
{Array.from({ length: 7 }).map((_, i) => {
|
||||||
|
const x = 14 + i * 23;
|
||||||
|
return <Circle key={`c${i}`} cx={x} cy={30} r={8} fill={NODE} />;
|
||||||
|
})}
|
||||||
|
<Circle cx={84} cy={98} r={13} fill={PINK} label="∑" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
flat: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Circle cx={26} cy={60} r={11} fill={NODE} label="a1" />
|
||||||
|
<Circle cx={70} cy={60} r={11} fill={NODE} label="a2" />
|
||||||
|
<Circle cx={114} cy={60} r={11} fill={NODE} label="a3" />
|
||||||
|
<Circle cx={156} cy={60} r={11} fill={NODE} label="a4" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
holacratic: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<circle cx={84} cy={60} r={50} fill="none" stroke={EDGE} strokeDasharray="3 4" strokeWidth="1.2" />
|
||||||
|
<circle cx={64} cy={54} r={26} fill="none" stroke={PINK} strokeWidth="1.4" />
|
||||||
|
<circle cx={106} cy={68} r={26} fill="none" stroke="#c98af0" strokeWidth="1.4" />
|
||||||
|
<Circle cx={60} cy={44} r={8} fill={NODE} />
|
||||||
|
<Circle cx={78} cy={62} r={8} fill={NODE} />
|
||||||
|
<Circle cx={104} cy={54} r={8} fill={NODE} />
|
||||||
|
<Circle cx={112} cy={80} r={8} fill={NODE} />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
mesh: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
{(() => {
|
||||||
|
const p = [
|
||||||
|
[42, 26], [126, 26], [42, 94], [126, 94],
|
||||||
|
] as [number, number][];
|
||||||
|
const lines: React.ReactNode[] = [];
|
||||||
|
for (let i = 0; i < p.length; i++) {
|
||||||
|
for (let j = i + 1; j < p.length; j++) {
|
||||||
|
lines.push(<Wire key={`m${i}${j}`} x1={p[i][0]} y1={p[i][1]} x2={p[j][0]} y2={p[j][1]} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
})()}
|
||||||
|
<Circle cx={42} cy={26} r={11} fill={NODE} label="a1" />
|
||||||
|
<Circle cx={126} cy={26} r={11} fill={NODE} label="a2" />
|
||||||
|
<Circle cx={42} cy={94} r={11} fill={NODE} label="a3" />
|
||||||
|
<Circle cx={126} cy={94} r={11} fill={NODE} label="a4" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
blackboard: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<rect x={38} y={48} width={92} height={30} rx={4} fill="rgba(201,138,240,.10)" stroke={PINK} strokeWidth="1.2" />
|
||||||
|
<text x={84} y={68} textAnchor="middle" fontSize="10" fill={PINK} fontFamily="ui-monospace, monospace">blackboard</text>
|
||||||
|
<Wire x1={30} y1={26} x2={60} y2={48} />
|
||||||
|
<Wire x1={84} y1={22} x2={84} y2={48} />
|
||||||
|
<Wire x1={138} y1={26} x2={110} y2={48} />
|
||||||
|
<Wire x1={30} y1={104} x2={60} y2={78} />
|
||||||
|
<Wire x1={138} y1={104} x2={110} y2={78} />
|
||||||
|
<Circle cx={26} cy={22} r={9} fill={NODE} />
|
||||||
|
<Circle cx={84} cy={16} r={9} fill={NODE} />
|
||||||
|
<Circle cx={142} cy={22} r={9} fill={NODE} />
|
||||||
|
<Circle cx={26} cy={104} r={9} fill={NODE} />
|
||||||
|
<Circle cx={142} cy={104} r={9} fill={NODE} />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
debate: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Arrow x1={54} y1={38} x2={110} y2={38} />
|
||||||
|
<Arrow x1={110} y1={50} x2={54} y2={50} />
|
||||||
|
<Arrow x1={54} y1={54} x2={84} y2={90} />
|
||||||
|
<Arrow x1={110} y1={54} x2={84} y2={90} />
|
||||||
|
<Circle cx={40} cy={44} r={14} fill={NODE} label="prop" />
|
||||||
|
<Circle cx={124} cy={44} r={14} fill={NODE} label="crit" />
|
||||||
|
<Circle cx={84} cy={102} r={14} fill={PINK} label="judge" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
market: () => (
|
||||||
|
<TopoSvg>
|
||||||
|
<Circle cx={84} cy={22} r={14} fill={PINK} label="auc" />
|
||||||
|
<Wire x1={84} y1={36} x2={30} y2={80} />
|
||||||
|
<Wire x1={84} y1={36} x2={84} y2={80} />
|
||||||
|
<Wire x1={84} y1={36} x2={138} y2={80} />
|
||||||
|
<text x={30} y={62} textAnchor="middle" fontSize="8" fill={EDGE} fontFamily="ui-monospace, monospace">bid</text>
|
||||||
|
<text x={84} y={62} textAnchor="middle" fontSize="8" fill={EDGE} fontFamily="ui-monospace, monospace">bid</text>
|
||||||
|
<text x={138} y={62} textAnchor="middle" fontSize="8" fill={EDGE} fontFamily="ui-monospace, monospace">bid</text>
|
||||||
|
<Circle cx={30} cy={94} r={11} fill={NODE} label="a1" />
|
||||||
|
<Circle cx={84} cy={94} r={11} fill={NODE} label="a2" />
|
||||||
|
<Circle cx={138} cy={94} r={11} fill={NODE} label="a3" />
|
||||||
|
</TopoSvg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
function TopologyInfoPanel({
|
function TopologyInfoPanel({
|
||||||
entry,
|
entry,
|
||||||
detail,
|
detail,
|
||||||
@@ -659,23 +882,25 @@ function TopologyInfoPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<div
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
padding: 6,
|
||||||
padding: 8,
|
borderRadius: 8,
|
||||||
borderRadius: 6,
|
|
||||||
background: "#0a0a0d",
|
background: "#0a0a0d",
|
||||||
border: "1px solid rgba(255,255,255,.06)",
|
border: "1px solid rgba(255,255,255,.06)",
|
||||||
color: "#c98af0",
|
display: "flex",
|
||||||
fontFamily: mono,
|
alignItems: "center",
|
||||||
fontSize: 10.5,
|
justifyContent: "center",
|
||||||
lineHeight: 1.35,
|
minHeight: 118,
|
||||||
whiteSpace: "pre",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{detail?.diagram ?? "—"}
|
{(() => {
|
||||||
</pre>
|
const Ill = TOPO_ILLUSTRATION[entry.kind];
|
||||||
|
return Ill ? <Ill /> : (
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 10.5, color: "#6a6a72" }}>—</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user