From ad2f8a629b46b36e814de787f4ace7b628d1aab2 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Mon, 19 Jan 2026 05:43:43 +1000 Subject: [PATCH] feat(web): add ScenariosSection component to collection page - Create ScenariosSection component with scenario list, status badges, and metrics - Allow generating new scenarios with AI (owner only) - Allow running scenarios and showing run progress - Display quality scores and pass/fail streaks - Link to scenario detail pages for run history --- .../[username]/collections/[slug]/page.tsx | 16 +- apps/web/src/components/ScenariosSection.tsx | 338 ++++++++++++++++++ 2 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/ScenariosSection.tsx diff --git a/apps/web/src/app/(profile)/[username]/collections/[slug]/page.tsx b/apps/web/src/app/(profile)/[username]/collections/[slug]/page.tsx index 2e10129..dbaea37 100644 --- a/apps/web/src/app/(profile)/[username]/collections/[slug]/page.tsx +++ b/apps/web/src/app/(profile)/[username]/collections/[slug]/page.tsx @@ -11,6 +11,7 @@ import { AppHeader } from '~/components/AppHeader'; import { ForkButton } from '~/components/ForkButton'; import { ForkedFromBadge } from '~/components/ForkedFromBadge'; import { LikeButton } from '~/components/LikeButton'; +import { ScenariosSection } from '~/components/ScenariosSection'; import { UseCasesSection } from '~/components/UseCasesSection'; import { useSession } from '~/lib/auth-client'; @@ -196,8 +197,9 @@ const response = await fetch("${httpUrl}", {

You'll need to provide your own API keys for any tools that require them. Pass - credentials via the env{' '} - parameter in your API calls. + credentials via the{' '} + env parameter in your + API calls.

)} @@ -422,6 +424,16 @@ export default function PrettyCollectionDetailPage(): React.ReactElement { )} + {/* Scenarios Section */} + {collection.tools.length > 0 && ( + + )} + {/* Use Cases Section - at the bottom */} {collection.tools.length > 0 && ( + Not run + + ); + } + + switch (status) { + case 'pass': + return ( + + + Pass + + ); + case 'fail': + return ( + + + Fail + + ); + case 'error': + return ( + + + Error + + ); + case 'running': + return ( + + + Running + + ); + default: + return ( + + {status} + + ); + } +} + +function QualityIndicator({ score }: { score: number }) { + const percentage = Math.round(score * 100); + const color = + percentage >= 70 + ? 'text-success' + : percentage >= 40 + ? 'text-warning' + : 'text-foreground-tertiary'; + + return ( +
+ + {percentage}% +
+ ); +} + +export function ScenariosSection({ + collectionId, + collectionOwnerId, + username, + slug, +}: ScenariosSectionProps) { + const { data: session } = useSession(); + const [scenarios, setScenarios] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isGenerating, setIsGenerating] = useState(false); + const [runningScenarioId, setRunningScenarioId] = useState(null); + const [error, setError] = useState(null); + + const isOwner = session?.user?.id === collectionOwnerId; + + const fetchScenarios = useCallback(async () => { + try { + const response = await fetch(`/api/collections/${collectionId}/scenarios`); + if (!response.ok) { + throw new Error('Failed to fetch scenarios'); + } + const data = await response.json(); + if (data.success) { + setScenarios(data.data.scenarios); + } + } catch { + setError('Failed to load scenarios'); + } finally { + setIsLoading(false); + } + }, [collectionId]); + + useEffect(() => { + fetchScenarios(); + }, [fetchScenarios]); + + const handleGenerate = async () => { + setIsGenerating(true); + setError(null); + + try { + const response = await fetch(`/api/collections/${collectionId}/scenarios/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ count: 1 }), + }); + + if (response.status === 429) { + const data = await response.json(); + setError(`Rate limited. Try again in ${Math.ceil(data.retryAfter / 60)} minute(s).`); + return; + } + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error?.message || 'Failed to generate scenario'); + } + + // Refresh the scenarios list + await fetchScenarios(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to generate scenario'); + } finally { + setIsGenerating(false); + } + }; + + const handleRunScenario = async (scenarioId: string) => { + setRunningScenarioId(scenarioId); + setError(null); + + try { + const response = await fetch(`/api/scenarios/${scenarioId}/run`, { + method: 'POST', + }); + + if (response.status === 429) { + const data = await response.json(); + setError(`Rate limited. Try again in ${Math.ceil(data.retryAfter / 60)} minute(s).`); + return; + } + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error?.message || 'Failed to run scenario'); + } + + // Refresh to show updated status + await fetchScenarios(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to run scenario'); + } finally { + setRunningScenarioId(null); + } + }; + + return ( +
+
+
+
+ +
+

Test Scenarios

+ {scenarios.length > 0 && ( + + {scenarios.length} + + )} +
+ {isOwner && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + + {isLoading ? ( +
+ +
+ ) : scenarios.length === 0 ? ( +
+ +

No test scenarios yet

+ {isOwner && ( + + )} +
+ ) : ( +
+ {scenarios.map((scenario) => ( +
+ {/* Header */} +
+
+
+ {scenario.name && ( +

{scenario.name}

+ )} + +
+

+ {scenario.prompt} +

+
+
+ + {isOwner && ( + + )} +
+
+ + {/* Tags */} + {scenario.tags.length > 0 && ( +
+ {scenario.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + + {/* Metrics */} +
+ + + {scenario.metrics.totalRuns} runs + + {scenario.metrics.consecutivePasses > 0 && ( + + + {scenario.metrics.consecutivePasses} streak + + )} + {scenario.metrics.consecutiveFails > 0 && ( + + + {scenario.metrics.consecutiveFails} fails + + )} + {scenario.metrics.lastRunAt && ( + Last: {new Date(scenario.metrics.lastRunAt).toLocaleDateString()} + )} +
+ + {/* View Details Link */} +
+ + View details & run history + + +
+
+ ))} +
+ )} +
+ ); +}