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
1 change: 1 addition & 0 deletions .changelog/next/changed-issue-4138.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Shared ui/ProgressBar primitive replaces six hand-rolled progress meters; every meter now carries role=progressbar with aria-valuenow/min/max and an accessible name
36 changes: 23 additions & 13 deletions client/src/components/meatspace/post/MemoryPractice.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, useMemo } from 'react';
import { ChevronLeft, Check, X, SkipForward, RotateCcw, Target, ChevronDown, Loader, ShieldCheck } from 'lucide-react';
import { submitMemoryPractice, getChunkMastery, getMemoryItem, attestMemoryMastery } from '../../../services/api';
import ProgressBar from '../../ui/ProgressBar';
import PostCompletionActions from './PostCompletionActions';
import { startRetryableSave } from './completionSave';

Expand Down Expand Up @@ -386,7 +387,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
</span>
</div>

<ProgressBar current={spacedChunkIdx * 10 + spacedLineIdx + 1} total={chunkMastery.length * 10} />
<PracticeProgress current={spacedChunkIdx * 10 + spacedLineIdx + 1} total={chunkMastery.length * 10} />

{/* Chunk info */}
<div className="flex items-center gap-3 text-xs">
Expand Down Expand Up @@ -480,7 +481,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
<span className="text-gray-500 text-sm ml-auto">{currentIdx + 1} / {lines.length}</span>
</div>

<ProgressBar current={currentIdx + 1} total={lines.length} />
<PracticeProgress current={currentIdx + 1} total={lines.length} />

<div className="bg-port-card border border-port-border rounded-lg p-6">
<div className="space-y-2">
Expand Down Expand Up @@ -547,7 +548,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
<span className="text-gray-500 text-sm ml-auto">{currentIdx + 1} / {lines.length - 1}</span>
</div>

<ProgressBar current={currentIdx + 1} total={lines.length - 1} />
<PracticeProgress current={currentIdx + 1} total={lines.length - 1} />

<div className="bg-port-card border border-port-border rounded-lg p-6">
<div className="text-gray-400 text-xs mb-2 uppercase tracking-wide">Current line:</div>
Expand Down Expand Up @@ -629,7 +630,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
<span className="text-gray-500 text-sm ml-auto">{currentIdx + 1} / {lines.length}</span>
</div>

<ProgressBar current={currentIdx + 1} total={lines.length} />
<PracticeProgress current={currentIdx + 1} total={lines.length} />

<div className="bg-port-card border border-port-border rounded-lg p-6">
<div className="text-white text-lg leading-relaxed mb-4 font-mono">{displayText}</div>
Expand Down Expand Up @@ -904,14 +905,18 @@ function ChunkMasteryOverview({ item }) {
const accuracy = typeof stats?.masteredAt === 'string'
? 100
: stats?.attempts > 0 ? Math.round((stats.correct / stats.attempts) * 100) : 0;
const barColor = accuracy >= 80 ? 'bg-port-success' : accuracy >= 40 ? 'bg-port-warning' : 'bg-gray-600';
const barTone = accuracy >= 80 ? 'success' : accuracy >= 40 ? 'warning' : 'muted';

return (
<div key={chunk.id} className="flex items-center gap-2">
<span className="text-xs text-gray-500 w-16 truncate">{chunk.label}</span>
<div className="flex-1 h-1.5 bg-port-border rounded-full overflow-hidden">
<div className={`h-full rounded-full ${barColor}`} style={{ width: `${accuracy}%` }} />
</div>
<ProgressBar
percent={accuracy}
tone={barTone}
track="border"
className="flex-1"
label={`${chunk.label} mastery`}
/>
<span className="text-xs font-mono text-gray-500 w-10 text-right">{accuracy}%</span>
</div>
);
Expand Down Expand Up @@ -962,12 +967,17 @@ function SpeedRunLine({ line, index, onResult }) {
);
}

function ProgressBar({ current, total }) {
const pct = Math.round((current / total) * 100);
// The name stays generic rather than "line 3 of 12": the spaced mode measures a
// synthetic chunk×line position, so only the percentage is meaningful across all
// four modes — and `aria-valuenow` already carries it.
function PracticeProgress({ current, total }) {
return (
<div className="w-full h-1.5 bg-port-border rounded-full overflow-hidden">
<div className="h-full bg-port-accent rounded-full transition-all" style={{ width: `${pct}%` }} />
</div>
<ProgressBar
percent={Math.round((current / total) * 100)}
track="border"
label="Practice progress"
duration={150}
/>
);
}

Expand Down
61 changes: 33 additions & 28 deletions client/src/components/meatspace/post/PostLlmDrillRunner.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { CheckCircle, XCircle } from 'lucide-react';
import ProgressBar from '../../ui/ProgressBar';
import { scorePostLlmDrill } from '../../../services/api';
import { DRILL_LABELS, WORDPLAY_LLM_DRILL_TYPES } from './constants';
import { AILoadingIndicator, MissedExamplesDisplay, CompoundChainUI, BridgeWordUI, DoubleMeaningUI, IdiomTwistUI, scoreWordplayResponse } from './WordplayDrillUI';
Expand Down Expand Up @@ -188,9 +189,9 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
}

const timePct = timeLimitMs > 0 ? (timeLeft / timeLimitMs) * 100 : 0;
let timerColor = 'bg-port-accent';
if (timePct <= 10) timerColor = 'bg-port-error';
else if (timePct <= 25) timerColor = 'bg-port-warning';
let timerTone = 'accent';
if (timePct <= 10) timerTone = 'error';
else if (timePct <= 25) timerTone = 'warning';

// Training mode: feedback overlay
if (isTraining && trainingFeedback) {
Expand Down Expand Up @@ -231,14 +232,7 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
>
Next
</button>
<div className="space-y-1">
<div className="flex justify-between text-xs text-gray-500">
<span>Prompt {questionIndex + 1} of {totalPrompts}</span>
</div>
<div className="w-full h-1.5 bg-port-border rounded-full overflow-hidden">
<div className="h-full bg-port-accent-2/60 transition-all" style={{ width: `${((questionIndex + 1) / totalPrompts) * 100}%` }} />
</div>
</div>
<PromptProgress index={questionIndex} total={totalPrompts} tone="accent2" showPercent={false} />
</div>
);
}
Expand All @@ -257,9 +251,14 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
{/* Timer bar (hidden in training mode) */}
{!isTraining && (
<>
<div className="w-full h-2 bg-port-border rounded-full overflow-hidden">
<div className={`h-full ${timerColor} transition-all duration-100`} style={{ width: `${timePct}%` }} />
</div>
<ProgressBar
percent={timePct}
tone={timerTone}
size="md"
track="border"
duration={100}
label={`Time remaining: ${Math.ceil(timeLeft / 1000)}s`}
/>
<div className="text-center text-sm text-gray-500">{Math.ceil(timeLeft / 1000)}s remaining</div>
</>
)}
Expand Down Expand Up @@ -427,7 +426,7 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
</div>
</div>
<TextInput inputRef={inputRef} value={inputValue} onChange={setInputValue} onSubmit={handleSubmit} placeholder="Your micro-story (2-4 sentences)..." buttonLabel="Next" />
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
)}

Expand Down Expand Up @@ -532,17 +531,23 @@ export function buildLlmResponseObj({ drillType, questionIndex, items, inputValu
// DRILL-SPECIFIC UI COMPONENTS
// ─────────────────────────────────────────────────────────────────────────────

function ProgressBar({ index, total }) {
// "Prompt N of M" counter over the shared meter. Both knobs exist for the
// training-feedback screen, which draws the same counter without the numeric
// readout and in the accent-2 tone that marks training throughout this runner.
function PromptProgress({ index, total, tone = 'accent', showPercent = true }) {
const pct = total > 0 ? ((index + 1) / total) * 100 : 0;
return (
<div className="space-y-1">
<div className="flex justify-between text-xs text-gray-500">
<span>Prompt {index + 1} of {total}</span>
<span>{Math.round(pct)}%</span>
</div>
<div className="w-full h-1.5 bg-port-border rounded-full overflow-hidden">
<div className="h-full bg-port-accent/60 transition-all" style={{ width: `${pct}%` }} />
{showPercent && <span>{Math.round(pct)}%</span>}
</div>
<ProgressBar
percent={pct}
tone={tone}
track="border"
label={`Prompt ${index + 1} of ${total}`}
/>
</div>
);
}
Expand Down Expand Up @@ -586,7 +591,7 @@ function WordAssociationUI({ prompt, inputValue, setInputValue, onSubmit, inputR
placeholder="Type your associations..."
buttonLabel="Next"
/>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand All @@ -605,7 +610,7 @@ function StoryRecallUI({ exercise, phase, onStartRecall, items, inputValue, setI
>
I'm Ready — Show Questions
</button>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand Down Expand Up @@ -647,7 +652,7 @@ function StoryRecallUI({ exercise, phase, onStartRecall, items, inputValue, setI
Submit All Answers
</button>
)}
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand Down Expand Up @@ -701,7 +706,7 @@ function VerbalFluencyUI({ category, items, inputValue, setInputValue, onAddItem
>
Done — Submit {items.length} items
</button>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand Down Expand Up @@ -729,7 +734,7 @@ function WitComebackUI({ scenario, inputValue, setInputValue, onSubmit, inputRef
placeholder="Your witty response..."
buttonLabel="Next"
/>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand All @@ -753,7 +758,7 @@ function PunWordplayUI({ challenge, inputValue, setInputValue, onSubmit, inputRe
placeholder="Your pun or wordplay..."
buttonLabel="Next"
/>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand All @@ -776,7 +781,7 @@ function ImaginationUI({ label, prompt, badge, badgeColor, placeholder, inputVal
placeholder={placeholder}
buttonLabel="Next"
/>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
Expand Down Expand Up @@ -827,7 +832,7 @@ function AlternativeUsesUI({ object, items, inputValue, setInputValue, onAddItem
>
Done — Submit {items.length} uses
</button>
<ProgressBar index={questionIndex} total={totalPrompts} />
<PromptProgress index={questionIndex} total={totalPrompts} />
</>
);
}
21 changes: 8 additions & 13 deletions client/src/components/pipeline/AutopilotMilestones.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isStoppedTerminal,
MILESTONE_STATUS,
} from '../../lib/autopilotMilestones';
import ProgressBar from '../ui/ProgressBar';

// Per-status chrome in ONE table — icon and tones together, so a new status
// can't get a row color and no icon (or the reverse). `blocked` reuses the
Expand Down Expand Up @@ -53,19 +54,13 @@ export default function AutopilotMilestones({ plan, planTotals, progress, termin
{/* Overall meter. A plan snapshot can under-count a step the run repeats,
so this is honest about milestones settled — not a time estimate. */}
{!dryRun ? (
<div
className="mt-1.5 h-1.5 w-full rounded-full bg-port-bg overflow-hidden"
role="progressbar"
aria-label="Autopilot story progress"
aria-valuenow={summary.percent}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className={`h-full rounded-full transition-all duration-500 ${isStoppedTerminal(terminal) ? 'bg-port-warning' : 'bg-port-accent'}`}
style={{ width: `${summary.percent}%` }}
/>
</div>
<ProgressBar
percent={summary.percent}
tone={isStoppedTerminal(terminal) ? 'warning' : 'accent'}
label="Autopilot story progress"
duration={500}
className="mt-1.5"
/>
) : null}

{/* Capped so a long plan (a comic series can project 16 milestones) can't
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Play, Pause, Square, Loader2, Volume2, AlertTriangle } from 'lucide-rea
import Modal from '../../ui/Modal';
import VoicePicker from '../../voice/VoicePicker';
import toast from '../../ui/Toast';
import ProgressBar from '../../ui/ProgressBar';
import { formatDurationMs } from '../../../utils/formatters';
import { narratePipelineProse } from '../../../services/api';
import { STAGE_LABEL } from './constants';
Expand Down Expand Up @@ -293,9 +294,7 @@ export default function ManuscriptReadAloud({ open, onClose, section }) {

{segments ? (
<div className="space-y-1">
<div className="h-1.5 w-full rounded bg-port-bg overflow-hidden">
<div className="h-full bg-port-accent transition-[width] duration-150" style={{ width: `${progressPct}%` }} />
</div>
<ProgressBar percent={progressPct} label="Narration progress" duration={150} />
<div className="flex items-center justify-between text-[11px] text-gray-500">
<span>
{segments.length} sentence{segments.length === 1 ? '' : 's'}
Expand Down
12 changes: 6 additions & 6 deletions client/src/components/pipeline/stages/EpisodeVideoStage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link } from 'react-router';
import { Film, ExternalLink, Loader2, Sparkles, AlertCircle, CheckCircle2 } from 'lucide-react';
import toast from '../../ui/Toast';
import Banner from '../../ui/Banner';
import ProgressBar from '../../ui/ProgressBar';
import { generatePipelineVisualImage, listVideoModels } from '../../../services/api';
import { getCreativeDirectorProject } from '../../../services/apiCreativeDirector';
import { getSceneStatusBadge, PROJECT_STATUS_LABEL } from '../../creative-director/sceneStatus';
Expand Down Expand Up @@ -360,12 +361,11 @@ export default function EpisodeVideoStage({ issue, onStageUpdate }) {

{total > 0 && (
<div className="space-y-1">
<div className="h-1.5 w-full bg-port-bg rounded-full overflow-hidden">
<div
className="h-full bg-port-accent transition-all"
style={{ width: `${total ? (accepted / total) * 100 : 0}%` }}
/>
</div>
<ProgressBar
percent={(accepted / total) * 100}
label={`Scenes accepted: ${accepted} of ${total}`}
duration={150}
/>
<ul className="flex flex-wrap gap-1.5 pt-1">
{sortedScenes.map((s) => {
const badge = getSceneStatusBadge(s.status);
Expand Down
Loading