Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion src/components/ProposalSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StatTile } from "@/components/StatTile";
import { Surface } from "@/components/Surface";
import { TitledSurface } from "@/components/TitledSurface";
import { formatDateTime } from "@/lib/dateTime";
import { Link } from "react-router";

export type ProposalSummaryStat = {
label: string;
Expand Down Expand Up @@ -41,6 +42,13 @@ export type ProposalTimelineItem = {
title: string;
detail?: string;
actor?: string;
snapshot?: {
fromStage: "pool" | "vote" | "build";
toStage: "vote" | "build" | "passed" | "failed";
reason?: string;
milestoneIndex?: number | null;
metrics: Array<{ label: string; value: string }>;
};
};

type ProposalSummaryCardProps = {
Expand Down Expand Up @@ -228,13 +236,29 @@ export function ProposalInvisionInsightCard({

type ProposalTimelineCardProps = {
items: ProposalTimelineItem[];
proposalId?: string;
};

function isLikelyAddress(value: string): boolean {
return /^[a-z0-9]{6,}$/i.test(value) && value.length >= 20;
}

export function ProposalTimelineCard({ items }: ProposalTimelineCardProps) {
function snapshotStageHref(
proposalId: string,
stage: "pool" | "vote" | "build",
): string | null {
if (stage === "pool")
return `/app/proposals/${proposalId}/pp?snapshotStage=pool`;
if (stage === "vote")
return `/app/proposals/${proposalId}/chamber?snapshotStage=vote`;
// `build` stage can become unavailable after terminal transition.
return null;
}

export function ProposalTimelineCard({
items,
proposalId,
}: ProposalTimelineCardProps) {
return (
<section className="space-y-3 text-sm text-text">
<h2 className="text-lg font-semibold text-text">Timeline</h2>
Expand Down Expand Up @@ -271,6 +295,49 @@ export function ProposalTimelineCard({ items }: ProposalTimelineCardProps) {
)}
</p>
) : null}
{item.snapshot ? (
<div className="space-y-2 rounded-lg border border-border/70 bg-panel-alt px-2 py-2">
<p className="text-xs font-semibold text-text">
Stage transition: {item.snapshot.fromStage} →{" "}
{item.snapshot.toStage}
</p>
{item.snapshot.reason ? (
<p className="text-xs text-muted">{item.snapshot.reason}</p>
) : null}
{item.snapshot.metrics.length > 0 ? (
<ul className="grid gap-1 sm:grid-cols-2">
{item.snapshot.metrics.map((metric) => (
<li
key={`${item.id}-${metric.label}`}
className="text-xs text-muted"
>
<span className="font-semibold text-text">
{metric.label}:
</span>{" "}
{metric.value}
</li>
))}
</ul>
) : null}
{proposalId
? (() => {
const href = snapshotStageHref(
proposalId,
item.snapshot.fromStage,
);
if (!href) return null;
return (
<Link
to={href}
className="inline-flex text-xs font-semibold text-primary underline-offset-2 hover:underline"
>
Open {item.snapshot.fromStage} snapshot
</Link>
);
})()
: null}
</div>
) : null}
</Surface>
))}
{items.length === 0 && (
Expand Down
2 changes: 1 addition & 1 deletion src/data/inlineHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export type InlineHelpRegistry = Record<string, Record<string, string>>;
export const inlineHelp: InlineHelpRegistry = {
chambers: {
metrics:
"A quick snapshot of chamber activity for this era (mock data) to help you spot where attention is needed.",
"A quick snapshot of chamber activity for this era to help you spot where attention is needed.",
filters:
"Use filters to find chambers with active proposal pools, votes, or Formation work.",
cards:
Expand Down
2 changes: 1 addition & 1 deletion src/data/pageHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export const pageHints: Record<string, PageHintEntry> = {
items: [
"Review case context and filings.",
"See jury composition and timeline; track status badges.",
"Submit statements or view decisions (UI placeholder actions).",
"Submit statements or view decisions based on your current role and permissions.",
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion src/data/vortexopedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
"A cognitocratic system contains multiple specialization chambers, so CM/LCM from different chambers cannot be treated as equal by default.",
"A CM of 5 in one chamber can be meaningfully different from a CM of 5 in another depending on what the system values at that time.",
"The chamber multiplier defines these proportions between chambers so contributions can be normalized for aggregation.",
"In this demo, the multiplier is set collectively by cognitocrats who have not received LCM in that chamber (average of their inputs).",
"The multiplier is set collectively by cognitocrats who have not received LCM in that chamber (average of their inputs).",
],
tags: ["cm", "multiplier", "chamber", "weighting"],
related: ["cognitocratic_measure", "lcm", "mcm", "acm"],
Expand Down
7 changes: 7 additions & 0 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
GetProposalTimelineResponse,
HumanNodeProfileDto,
ProposalDraftDetailDto,
ProposalStatusDto,
PoolProposalPageDto,
} from "@/types/api";

Expand Down Expand Up @@ -279,6 +280,12 @@ export async function apiProposalTimeline(
);
}

export async function apiProposalStatus(
id: string,
): Promise<ProposalStatusDto> {
return await apiGet<ProposalStatusDto>(`/api/proposals/${id}/status`);
}

export type PoolVoteDirection = "up" | "down";

export async function apiPoolVote(input: {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/dateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ function parseDate(value: string | number | Date): Date | null {
return Number.isNaN(parsed.getTime()) ? null : parsed;
}

export function toTimestampMs(
value: string | number | Date,
fallback = 0,
): number {
const parsed = parseDate(value);
return parsed ? parsed.getTime() : fallback;
}

export function getStoredDateFormat(): DateFormat {
try {
const raw = localStorage.getItem(DATE_FORMAT_KEY);
Expand Down
10 changes: 10 additions & 0 deletions src/lib/errorFormatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function formatLoadError(
error: string | null | undefined,
fallback = "Request failed",
): string {
const raw = (error ?? "").trim();
if (!raw) return fallback;
const stripped = raw.replace(/^HTTP\s+\d+\s*:\s*/i, "").trim();
if (stripped) return stripped;
return fallback;
}
30 changes: 15 additions & 15 deletions src/pages/Guide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ const Guide: React.FC = () => {
Vortex Guide
</h1>
<p className="mt-3 text-sm leading-relaxed text-white! [text-shadow:0_0_18px_rgba(244,179,127,0.14)] sm:text-base">
A short, human-readable map of what you’re seeing in this demo, why
Vortex exists, and how each page works.
A short, human-readable map of what you’re seeing in the simulator,
why Vortex exists, and how each page works.
</p>

<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
Expand Down Expand Up @@ -325,8 +325,9 @@ const Guide: React.FC = () => {
simplistic voting surface.
</p>
<p className="text-white">
This repo is a <strong>demo mockup</strong> of that experience.
Numbers, identities, and statuses are illustrative.
This repo is a <strong>living simulator UI</strong> of that
experience. Data and governance state come from the current
simulation backend.
</p>
</GuideSection>

Expand Down Expand Up @@ -401,8 +402,8 @@ const Guide: React.FC = () => {
>
Proposal Creation
</Link>{" "}
is a multi-step wizard in this demo. Steps are navigable so you
can explore the structure even if you don’t fill everything in.
is a multi-step wizard. Steps are navigable so you can explore
structure and submit complete proposals.
</p>
<p>
Drafts:{" "}
Expand Down Expand Up @@ -521,8 +522,7 @@ const Guide: React.FC = () => {
>
<p>
Invision is where governance becomes measurable: it summarizes
behavior and delivery patterns into scannable insights (still a
mock in this repo).
behavior and delivery patterns into scannable insights.
</p>
<p>
In the app you’ll see “Invision insight” snippets attached to
Expand All @@ -534,7 +534,7 @@ const Guide: React.FC = () => {
<GuideSection
id="courts"
title="Courts"
subtitle="Disputes, evidence, and verdicts (mock UI)."
subtitle="Disputes, evidence, and verdicts."
>
<p>
Courts is the judicial layer. Cases are structured around a
Expand All @@ -546,16 +546,16 @@ const Guide: React.FC = () => {
Statuses (jury / live / ended) reflect the state of the case.
</li>
<li>
Verdict buttons are present as a UI mock to show how an action
might look.
Verdict actions appear based on case stage and participant
permissions.
</li>
</ul>
</GuideSection>

<GuideSection
id="cm-panel"
title="CM Panel"
subtitle="A control surface for chamber multipliers (mock UI)."
subtitle="A control surface for chamber multipliers."
>
<p>
The CM Panel is a specialized admin-style surface. In a real
Expand All @@ -566,7 +566,7 @@ const Guide: React.FC = () => {
<GuideSection
id="profile-settings"
title="Profile & Settings"
subtitle="Personal details and global preferences (demo)."
subtitle="Personal details and global preferences."
>
<p>
Profile is your personal view. Settings is where future
Expand Down Expand Up @@ -627,8 +627,8 @@ const Guide: React.FC = () => {
</div>

<MarketingFooter className="mt-12 text-white">
This guide describes the demo mockups shipped in this repo and will
evolve as the community tests and gives feedback.
This guide describes the live simulator surfaces and will evolve as
the community tests and gives feedback.
</MarketingFooter>
</div>
</MarketingPage>
Expand Down
8 changes: 6 additions & 2 deletions src/pages/MyGovernance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
apiCmMe,
apiMyGovernance,
} from "@/lib/apiClient";
import { formatLoadError } from "@/lib/errorFormatting";
import { toTimestampMs } from "@/lib/dateTime";
import type {
ChamberDto,
CmSummaryDto,
Expand Down Expand Up @@ -205,7 +207,7 @@ const MyGovernance: React.FC = () => {

const timeLeftValue = useMemo(() => {
const targetMs = clock?.nextEraAt
? new Date(clock.nextEraAt).getTime()
? toTimestampMs(clock.nextEraAt, NaN)
: NaN;
if (Number.isFinite(targetMs)) {
return formatDayHourMinute(targetMs, nowMs);
Expand Down Expand Up @@ -255,7 +257,9 @@ const MyGovernance: React.FC = () => {
loadError ? "text-destructive" : undefined,
)}
>
{loadError ? `My governance unavailable: ${loadError}` : "Loading…"}
{loadError
? `My governance unavailable: ${formatLoadError(loadError)}`
: "Loading…"}
</Surface>
) : null}
<Card>
Expand Down
Loading