feat: Built CreatorTransparency page with four collapsible sections, AP…

- "frontend/src/api/creator-transparency.ts"
- "frontend/src/pages/CreatorTransparency.tsx"
- "frontend/src/pages/CreatorTransparency.module.css"
- "frontend/src/App.tsx"
- "frontend/src/pages/CreatorDashboard.tsx"

GSD-Task: S05/T02
This commit is contained in:
jlightner 2026-04-04 13:58:33 +00:00
parent b32fc5134b
commit 6f3a0cc3d2
9 changed files with 894 additions and 2 deletions

View file

@ -37,7 +37,7 @@
- Estimate: 45m - Estimate: 45m
- Files: backend/schemas.py, backend/routers/creator_dashboard.py - Files: backend/schemas.py, backend/routers/creator_dashboard.py
- Verify: docker exec chrysopedia-api python -c "from routers.creator_dashboard import router; from schemas import CreatorTransparencyResponse" && echo 'OK' - Verify: docker exec chrysopedia-api python -c "from routers.creator_dashboard import router; from schemas import CreatorTransparencyResponse" && echo 'OK'
- [ ] **T02: Build transparency page with collapsible sections and wire into creator dashboard** — Create the CreatorTransparency page component with four collapsible sections (Technique Pages, Key Moments, Cross-References, Source Videos), an API client function, route registration in App.tsx, and a sidebar nav link. - [x] **T02: Built CreatorTransparency page with four collapsible sections, API client, route, and sidebar nav link** — Create the CreatorTransparency page component with four collapsible sections (Technique Pages, Key Moments, Cross-References, Source Videos), an API client function, route registration in App.tsx, and a sidebar nav link.
## Steps ## Steps

View file

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"taskId": "T01",
"unitId": "M025/S05/T01",
"timestamp": 1775310913898,
"passed": true,
"discoverySource": "task-plan",
"checks": [
{
"command": "echo 'OK'",
"exitCode": 0,
"durationMs": 9,
"verdict": "pass"
}
]
}

View file

@ -0,0 +1,85 @@
---
id: T02
parent: S05
milestone: M025
provides: []
requires: []
affects: []
key_files: ["frontend/src/api/creator-transparency.ts", "frontend/src/pages/CreatorTransparency.tsx", "frontend/src/pages/CreatorTransparency.module.css", "frontend/src/App.tsx", "frontend/src/pages/CreatorDashboard.tsx"]
key_decisions: ["Used CSS grid-template-rows 0fr/1fr for smooth collapsible section animation", "Grouped key moments by source video filename for scannability"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "Frontend build passes with zero TypeScript errors. grep confirms route registered in App.tsx and sidebar link added in CreatorDashboard.tsx."
completed_at: 2026-04-04T13:58:28.416Z
blocker_discovered: false
---
# T02: Built CreatorTransparency page with four collapsible sections, API client, route, and sidebar nav link
> Built CreatorTransparency page with four collapsible sections, API client, route, and sidebar nav link
## What Happened
---
id: T02
parent: S05
milestone: M025
key_files:
- frontend/src/api/creator-transparency.ts
- frontend/src/pages/CreatorTransparency.tsx
- frontend/src/pages/CreatorTransparency.module.css
- frontend/src/App.tsx
- frontend/src/pages/CreatorDashboard.tsx
key_decisions:
- Used CSS grid-template-rows 0fr/1fr for smooth collapsible section animation
- Grouped key moments by source video filename for scannability
duration: ""
verification_result: passed
completed_at: 2026-04-04T13:58:28.416Z
blocker_discovered: false
---
# T02: Built CreatorTransparency page with four collapsible sections, API client, route, and sidebar nav link
**Built CreatorTransparency page with four collapsible sections, API client, route, and sidebar nav link**
## What Happened
Created the API client with TypeScript interfaces matching all backend Transparency schemas. Built the CreatorTransparency page with tag summary bar, four collapsible sections (Technique Pages, Key Moments grouped by video, Cross-References, Source Videos) using CSS grid-template-rows animation. Added route at /creator/transparency with ProtectedRoute wrapper and Transparency NavLink in the sidebar between Tiers and Posts.
## Verification
Frontend build passes with zero TypeScript errors. grep confirms route registered in App.tsx and sidebar link added in CreatorDashboard.tsx.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 7100ms |
| 2 | `grep -q 'transparency' frontend/src/App.tsx` | 0 | ✅ pass | 10ms |
| 3 | `grep -q 'Transparency' frontend/src/pages/CreatorDashboard.tsx` | 0 | ✅ pass | 10ms |
## Deviations
None.
## Known Issues
None.
## Files Created/Modified
- `frontend/src/api/creator-transparency.ts`
- `frontend/src/pages/CreatorTransparency.tsx`
- `frontend/src/pages/CreatorTransparency.module.css`
- `frontend/src/App.tsx`
- `frontend/src/pages/CreatorDashboard.tsx`
## Deviations
None.
## Known Issues
None.

View file

@ -31,6 +31,7 @@ const PostsList = React.lazy(() => import("./pages/PostsList"));
const ShortPlayer = React.lazy(() => import("./pages/ShortPlayer")); const ShortPlayer = React.lazy(() => import("./pages/ShortPlayer"));
const EmbedPlayer = React.lazy(() => import("./pages/EmbedPlayer")); const EmbedPlayer = React.lazy(() => import("./pages/EmbedPlayer"));
const CreatorOnboarding = React.lazy(() => import("./pages/CreatorOnboarding")); const CreatorOnboarding = React.lazy(() => import("./pages/CreatorOnboarding"));
const CreatorTransparency = React.lazy(() => import("./pages/CreatorTransparency"));
import AdminDropdown from "./components/AdminDropdown"; import AdminDropdown from "./components/AdminDropdown";
import ImpersonationBanner from "./components/ImpersonationBanner"; import ImpersonationBanner from "./components/ImpersonationBanner";
import AppFooter from "./components/AppFooter"; import AppFooter from "./components/AppFooter";
@ -216,6 +217,7 @@ function AppShell() {
<Route path="/creator/chapters/:videoId" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ChapterReview /></Suspense></ProtectedRoute>} /> <Route path="/creator/chapters/:videoId" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ChapterReview /></Suspense></ProtectedRoute>} />
<Route path="/creator/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></Suspense></ProtectedRoute>} /> <Route path="/creator/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></Suspense></ProtectedRoute>} />
<Route path="/creator/tiers" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorTiers /></Suspense></ProtectedRoute>} /> <Route path="/creator/tiers" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorTiers /></Suspense></ProtectedRoute>} />
<Route path="/creator/transparency" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorTransparency /></Suspense></ProtectedRoute>} />
<Route path="/creator/posts" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostsList /></Suspense></ProtectedRoute>} /> <Route path="/creator/posts" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostsList /></Suspense></ProtectedRoute>} />
<Route path="/creator/posts/new" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} /> <Route path="/creator/posts/new" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
<Route path="/creator/posts/:postId/edit" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} /> <Route path="/creator/posts/:postId/edit" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />

View file

@ -0,0 +1,51 @@
import { request, BASE } from "./client";
// ── Types ────────────────────────────────────────────────────────────────────
export interface TransparencyTechnique {
title: string;
slug: string;
topic_category: string;
topic_tags: string[];
summary: string;
created_at: string;
key_moment_count: number;
}
export interface TransparencyKeyMoment {
title: string;
summary: string;
content_type: string;
start_time: number;
end_time: number;
source_video_filename: string;
technique_page_title: string | null;
}
export interface TransparencyRelationship {
relationship_type: string;
source_page_title: string;
source_page_slug: string;
target_page_title: string;
target_page_slug: string;
}
export interface TransparencySourceVideo {
filename: string;
processing_status: string;
created_at: string;
}
export interface CreatorTransparencyResponse {
techniques: TransparencyTechnique[];
key_moments: TransparencyKeyMoment[];
relationships: TransparencyRelationship[];
source_videos: TransparencySourceVideo[];
tags: string[];
}
// ── Functions ────────────────────────────────────────────────────────────────
export async function fetchCreatorTransparency(): Promise<CreatorTransparencyResponse> {
return request<CreatorTransparencyResponse>(`${BASE}/creator/transparency`);
}

View file

@ -59,6 +59,13 @@ function SidebarNav() {
</svg> </svg>
Tiers Tiers
</NavLink> </NavLink>
<NavLink to="/creator/transparency" className={linkClass}>
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
Transparency
</NavLink>
<NavLink to="/creator/posts" className={linkClass}> <NavLink to="/creator/posts" className={linkClass}>
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9" /> <path d="M12 20h9" />

View file

@ -0,0 +1,332 @@
/* ── Page layout (reuses sidebar from CreatorDashboard) ─────────────────── */
.layout {
display: flex;
gap: 0;
min-height: 60vh;
}
.content {
flex: 1;
min-width: 0;
padding: 2rem;
}
.pageTitle {
margin: 0 0 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
}
.subtitle {
margin: 0 0 1.5rem;
font-size: 0.875rem;
color: var(--color-text-muted);
}
/* ── Tag pills ─────────────────────────────────────────────────────────────── */
.tagBar {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-bottom: 1.5rem;
}
.tagPill {
display: inline-block;
padding: 0.1875rem 0.625rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
background: var(--color-badge-category-bg);
color: var(--color-badge-category-text);
white-space: nowrap;
}
/* ── Collapsible section ───────────────────────────────────────────────────── */
.section {
margin-bottom: 1rem;
border: 1px solid var(--color-border);
border-radius: 10px;
overflow: hidden;
}
.sectionHeader {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
padding: 0.875rem 1rem;
background: var(--color-bg-surface);
border: none;
cursor: pointer;
text-align: left;
color: var(--color-text-primary);
font-size: 0.9375rem;
font-weight: 600;
transition: background 0.15s;
}
.sectionHeader:hover {
background: var(--color-bg-surface-hover);
}
.chevron {
width: 16px;
height: 16px;
flex-shrink: 0;
transition: transform 0.2s ease;
color: var(--color-text-muted);
}
.chevronOpen {
transform: rotate(90deg);
}
.sectionCount {
margin-left: auto;
font-size: 0.75rem;
font-weight: 500;
color: var(--color-text-muted);
background: var(--color-bg-elevated, var(--color-bg-surface));
padding: 0.125rem 0.5rem;
border-radius: 9999px;
}
.sectionBody {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.25s ease;
}
.sectionBodyOpen {
grid-template-rows: 1fr;
}
.sectionInner {
overflow: hidden;
min-height: 0;
}
.sectionContent {
padding: 0 1rem 1rem;
}
/* ── Table ─────────────────────────────────────────────────────────────────── */
.tableWrap {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.table th {
text-align: left;
padding: 0.5rem 0.625rem;
font-weight: 600;
color: var(--color-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1px solid var(--color-border);
}
.table td {
padding: 0.5rem 0.625rem;
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border-subtle, var(--color-border));
}
.table tbody tr:hover {
background: var(--color-bg-surface-hover);
}
.link {
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
}
.link:hover {
text-decoration: underline;
}
/* ── Badges ────────────────────────────────────────────────────────────────── */
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.badgeContentType {
background: var(--color-badge-edited-bg);
color: var(--color-badge-edited-text);
}
.badgeRelType {
background: var(--color-badge-pending-bg);
color: var(--color-badge-pending-text);
}
.badgeComplete {
background: var(--color-badge-approved-bg);
color: var(--color-badge-approved-text);
}
.badgeProcessing {
background: var(--color-badge-edited-bg);
color: var(--color-badge-edited-text);
}
.badgeError {
background: var(--color-badge-rejected-bg);
color: var(--color-badge-rejected-text);
}
.badgePending {
background: var(--color-badge-pending-bg);
color: var(--color-badge-pending-text);
}
/* ── Tags in table cells ──────────────────────────────────────────────────── */
.cellTags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: center;
}
.miniTag {
display: inline-block;
padding: 0.0625rem 0.375rem;
border-radius: 4px;
font-size: 0.6875rem;
background: var(--color-badge-category-bg);
color: var(--color-badge-category-text);
}
.tagOverflow {
font-size: 0.6875rem;
color: var(--color-text-muted);
}
/* ── Time range ────────────────────────────────────────────────────────────── */
.timeRange {
font-family: var(--font-mono, monospace);
font-size: 0.8125rem;
color: var(--color-text-muted);
white-space: nowrap;
}
/* ── Video group ───────────────────────────────────────────────────────────── */
.videoGroup {
margin-bottom: 1rem;
}
.videoGroup:last-child {
margin-bottom: 0;
}
.videoGroupTitle {
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text-muted);
margin: 0 0 0.375rem;
font-family: var(--font-mono, monospace);
}
/* ── Filename ──────────────────────────────────────────────────────────────── */
.filename {
font-family: var(--font-mono, monospace);
font-size: 0.8125rem;
word-break: break-all;
}
/* ── States ────────────────────────────────────────────────────────────────── */
.emptyText {
color: var(--color-text-muted);
font-size: 0.875rem;
padding: 0.5rem 0;
}
.emptyState {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-muted);
}
.emptyState h2 {
font-size: 1.125rem;
color: var(--color-text-secondary);
margin: 0 0 0.5rem;
}
.emptyState p {
font-size: 0.875rem;
margin: 0;
}
.errorState {
background: var(--color-error-bg, rgba(220, 38, 38, 0.1));
color: var(--color-error, #ef4444);
padding: 1rem;
border-radius: 8px;
font-size: 0.875rem;
}
/* ── Skeleton ──────────────────────────────────────────────────────────────── */
.skeleton {
display: flex;
flex-direction: column;
gap: 1rem;
}
.skeletonPulse {
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeletonBlock {
height: 56px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ── Responsive ────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.layout {
flex-direction: column;
}
.content {
padding: 1.25rem;
}
.table th:nth-child(n+4),
.table td:nth-child(n+4) {
display: none;
}
}

View file

@ -0,0 +1,399 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import { SidebarNav } from "./CreatorDashboard";
import {
fetchCreatorTransparency,
type CreatorTransparencyResponse,
type TransparencyKeyMoment,
} from "../api/creator-transparency";
import { ApiError } from "../api/client";
import styles from "./CreatorTransparency.module.css";
/* ── Helpers ────────────────────────────────────────────────────────────────── */
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
function statusBadgeClass(status: string): string {
switch (status.toLowerCase()) {
case "complete":
case "completed":
return styles.badgeComplete ?? "";
case "processing":
return styles.badgeProcessing ?? "";
case "error":
case "failed":
return styles.badgeError ?? "";
default:
return styles.badgePending ?? "";
}
}
/* ── Chevron icon ──────────────────────────────────────────────────────────── */
function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
className={`${styles.chevron}${open ? ` ${styles.chevronOpen}` : ""}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6" />
</svg>
);
}
/* ── Collapsible section wrapper ───────────────────────────────────────────── */
function CollapsibleSection({
title,
count,
defaultOpen = false,
children,
}: {
title: string;
count: number;
defaultOpen?: boolean;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<section className={styles.section}>
<button
className={styles.sectionHeader}
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
>
<ChevronIcon open={open} />
{title}
<span className={styles.sectionCount}>{count}</span>
</button>
<div className={`${styles.sectionBody}${open ? ` ${styles.sectionBodyOpen}` : ""}`}>
<div className={styles.sectionInner}>
<div className={styles.sectionContent}>{children}</div>
</div>
</div>
</section>
);
}
/* ── Section: Technique Pages ──────────────────────────────────────────────── */
function TechniqueSection({ data }: { data: CreatorTransparencyResponse }) {
if (data.techniques.length === 0) {
return <p className={styles.emptyText}>No technique pages derived from your content yet.</p>;
}
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Tags</th>
<th>Moments</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{data.techniques.map((t) => (
<tr key={t.slug}>
<td>
<Link to={`/techniques/${t.slug}`} className={styles.link}>
{t.title}
</Link>
</td>
<td>{t.topic_category}</td>
<td>
<div className={styles.cellTags}>
{t.topic_tags.slice(0, 4).map((tag) => (
<span key={tag} className={styles.miniTag}>{tag}</span>
))}
{t.topic_tags.length > 4 && (
<span className={styles.tagOverflow}>+{t.topic_tags.length - 4}</span>
)}
</div>
</td>
<td>{t.key_moment_count}</td>
<td>{formatDate(t.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
/* ── Section: Key Moments (grouped by source video) ────────────────────────── */
function KeyMomentSection({ data }: { data: CreatorTransparencyResponse }) {
if (data.key_moments.length === 0) {
return <p className={styles.emptyText}>No key moments extracted yet.</p>;
}
// Group by source video
const grouped = new Map<string, TransparencyKeyMoment[]>();
for (const m of data.key_moments) {
const key = m.source_video_filename || "Unknown source";
const arr = grouped.get(key);
if (arr) arr.push(m);
else grouped.set(key, [m]);
}
return (
<>
{Array.from(grouped.entries()).map(([videoFilename, moments]) => (
<div key={videoFilename} className={styles.videoGroup}>
<p className={styles.videoGroupTitle}>{videoFilename}</p>
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Time</th>
<th>Technique Page</th>
</tr>
</thead>
<tbody>
{moments.map((m, i) => (
<tr key={`${m.title}-${i}`}>
<td>{m.title}</td>
<td>
<span className={`${styles.badge} ${styles.badgeContentType}`}>
{m.content_type}
</span>
</td>
<td>
<span className={styles.timeRange}>
{formatTime(m.start_time)} {formatTime(m.end_time)}
</span>
</td>
<td>
{m.technique_page_title ?? (
<span className={styles.emptyText}></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</>
);
}
/* ── Section: Cross-References ─────────────────────────────────────────────── */
function RelationshipSection({ data }: { data: CreatorTransparencyResponse }) {
if (data.relationships.length === 0) {
return <p className={styles.emptyText}>No cross-references found yet.</p>;
}
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Type</th>
<th>Source</th>
<th>Target</th>
</tr>
</thead>
<tbody>
{data.relationships.map((r, i) => (
<tr key={`${r.source_page_slug}-${r.target_page_slug}-${i}`}>
<td>
<span className={`${styles.badge} ${styles.badgeRelType}`}>
{r.relationship_type}
</span>
</td>
<td>
<Link to={`/techniques/${r.source_page_slug}`} className={styles.link}>
{r.source_page_title}
</Link>
</td>
<td>
<Link to={`/techniques/${r.target_page_slug}`} className={styles.link}>
{r.target_page_title}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
/* ── Section: Source Videos ─────────────────────────────────────────────────── */
function SourceVideoSection({ data }: { data: CreatorTransparencyResponse }) {
if (data.source_videos.length === 0) {
return <p className={styles.emptyText}>No source videos uploaded yet.</p>;
}
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Filename</th>
<th>Status</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
{data.source_videos.map((v) => (
<tr key={v.filename}>
<td className={styles.filename}>{v.filename}</td>
<td>
<span className={`${styles.badge} ${statusBadgeClass(v.processing_status)}`}>
{v.processing_status}
</span>
</td>
<td>{formatDate(v.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
/* ── Main component ────────────────────────────────────────────────────────── */
export default function CreatorTransparency() {
useDocumentTitle("AI Transparency");
const { user } = useAuth();
const [data, setData] = useState<CreatorTransparencyResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetchCreatorTransparency()
.then((res) => {
if (!cancelled) setData(res);
})
.catch((err) => {
if (cancelled) return;
if (err instanceof ApiError && err.status === 404) {
setError("not_linked");
} else {
setError(err instanceof ApiError ? err.detail : "Failed to load transparency data");
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return (
<div className={styles.layout}>
<SidebarNav />
<div className={styles.content}>
<h1 className={styles.pageTitle}>AI Transparency</h1>
<p className={styles.subtitle}>
Everything our AI derived from {user?.display_name ? `${user.display_name}'s` : "your"} content
</p>
{loading && <TransparencySkeleton />}
{!loading && error === "not_linked" && (
<div className={styles.emptyState}>
<h2>No Creator Profile</h2>
<p>Your account isn't linked to a creator profile yet. Contact an admin to get set up.</p>
</div>
)}
{!loading && error && error !== "not_linked" && (
<div className={styles.errorState}>
<p>Could not load transparency data: {error}</p>
</div>
)}
{!loading && !error && data && (
<>
{/* ── Tag summary ────────────────────────────────────────── */}
{data.tags.length > 0 && (
<div className={styles.tagBar}>
{data.tags.map((tag) => (
<span key={tag} className={styles.tagPill}>{tag}</span>
))}
</div>
)}
{/* ── Collapsible sections ──────────────────────────────── */}
<CollapsibleSection
title="Technique Pages"
count={data.techniques.length}
defaultOpen={true}
>
<TechniqueSection data={data} />
</CollapsibleSection>
<CollapsibleSection
title="Key Moments"
count={data.key_moments.length}
>
<KeyMomentSection data={data} />
</CollapsibleSection>
<CollapsibleSection
title="Cross-References"
count={data.relationships.length}
>
<RelationshipSection data={data} />
</CollapsibleSection>
<CollapsibleSection
title="Source Videos"
count={data.source_videos.length}
>
<SourceVideoSection data={data} />
</CollapsibleSection>
</>
)}
</div>
</div>
);
}
/* ── Loading skeleton ──────────────────────────────────────────────────────── */
function TransparencySkeleton() {
return (
<div className={styles.skeleton}>
{[1, 2, 3, 4].map((i) => (
<div key={i} className={`${styles.skeletonBlock} ${styles.skeletonPulse}`} />
))}
</div>
);
}

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/notifications.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/shorts.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/templates.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/EmbedPlayer.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/ShortPlayer.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/chatCitations.tsx","./src/utils/citations.tsx","./src/utils/clipboard.ts","./src/utils/formatTime.ts"],"version":"5.6.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/admin-usage.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creator-transparency.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/notifications.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/shorts.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/templates.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsage.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorOnboarding.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorTransparency.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/EmbedPlayer.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/ShortPlayer.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/chatCitations.tsx","./src/utils/citations.tsx","./src/utils/clipboard.ts","./src/utils/formatTime.ts"],"version":"5.6.3"}