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:
parent
b32fc5134b
commit
6f3a0cc3d2
9 changed files with 894 additions and 2 deletions
|
|
@ -37,7 +37,7 @@
|
|||
- Estimate: 45m
|
||||
- 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'
|
||||
- [ ] **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
|
||||
|
||||
|
|
|
|||
16
.gsd/milestones/M025/slices/S05/tasks/T01-VERIFY.json
Normal file
16
.gsd/milestones/M025/slices/S05/tasks/T01-VERIFY.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
85
.gsd/milestones/M025/slices/S05/tasks/T02-SUMMARY.md
Normal file
85
.gsd/milestones/M025/slices/S05/tasks/T02-SUMMARY.md
Normal 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.
|
||||
|
|
@ -31,6 +31,7 @@ const PostsList = React.lazy(() => import("./pages/PostsList"));
|
|||
const ShortPlayer = React.lazy(() => import("./pages/ShortPlayer"));
|
||||
const EmbedPlayer = React.lazy(() => import("./pages/EmbedPlayer"));
|
||||
const CreatorOnboarding = React.lazy(() => import("./pages/CreatorOnboarding"));
|
||||
const CreatorTransparency = React.lazy(() => import("./pages/CreatorTransparency"));
|
||||
import AdminDropdown from "./components/AdminDropdown";
|
||||
import ImpersonationBanner from "./components/ImpersonationBanner";
|
||||
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/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></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/new" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/creator/posts/:postId/edit" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
|
||||
|
|
|
|||
51
frontend/src/api/creator-transparency.ts
Normal file
51
frontend/src/api/creator-transparency.ts
Normal 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`);
|
||||
}
|
||||
|
|
@ -59,6 +59,13 @@ function SidebarNav() {
|
|||
</svg>
|
||||
Tiers
|
||||
</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}>
|
||||
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 20h9" />
|
||||
|
|
|
|||
332
frontend/src/pages/CreatorTransparency.module.css
Normal file
332
frontend/src/pages/CreatorTransparency.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
399
frontend/src/pages/CreatorTransparency.tsx
Normal file
399
frontend/src/pages/CreatorTransparency.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"}
|
||||
Loading…
Add table
Reference in a new issue