From ce4bccf292a161bf14cacd8149ddd31d107ff727 Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 13:58:33 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Built=20CreatorTransparency=20page=20wi?= =?UTF-8?q?th=20four=20collapsible=20sections,=20AP=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "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 --- frontend/src/App.tsx | 2 + frontend/src/api/creator-transparency.ts | 51 +++ frontend/src/pages/CreatorDashboard.tsx | 7 + .../src/pages/CreatorTransparency.module.css | 332 +++++++++++++++ frontend/src/pages/CreatorTransparency.tsx | 399 ++++++++++++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 6 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 frontend/src/api/creator-transparency.ts create mode 100644 frontend/src/pages/CreatorTransparency.module.css create mode 100644 frontend/src/pages/CreatorTransparency.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4b5b3bc..9205b29 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { }>} /> }>} /> }>} /> + }>} /> }>} /> }>} /> }>} /> diff --git a/frontend/src/api/creator-transparency.ts b/frontend/src/api/creator-transparency.ts new file mode 100644 index 0000000..15fa6ef --- /dev/null +++ b/frontend/src/api/creator-transparency.ts @@ -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 { + return request(`${BASE}/creator/transparency`); +} diff --git a/frontend/src/pages/CreatorDashboard.tsx b/frontend/src/pages/CreatorDashboard.tsx index a041f6b..54586b6 100644 --- a/frontend/src/pages/CreatorDashboard.tsx +++ b/frontend/src/pages/CreatorDashboard.tsx @@ -59,6 +59,13 @@ function SidebarNav() { Tiers + + + + + + Transparency + diff --git a/frontend/src/pages/CreatorTransparency.module.css b/frontend/src/pages/CreatorTransparency.module.css new file mode 100644 index 0000000..9df93e7 --- /dev/null +++ b/frontend/src/pages/CreatorTransparency.module.css @@ -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; + } +} diff --git a/frontend/src/pages/CreatorTransparency.tsx b/frontend/src/pages/CreatorTransparency.tsx new file mode 100644 index 0000000..51ada76 --- /dev/null +++ b/frontend/src/pages/CreatorTransparency.tsx @@ -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 ( + + + + ); +} + +/* ── 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 ( +
+
{count} + +
+
+
{children}
+
+
+ + ); +} + +/* ── Section: Technique Pages ──────────────────────────────────────────────── */ + +function TechniqueSection({ data }: { data: CreatorTransparencyResponse }) { + if (data.techniques.length === 0) { + return

No technique pages derived from your content yet.

; + } + return ( +
+ + + + + + + + + + + + {data.techniques.map((t) => ( + + + + + + + + ))} + +
TitleCategoryTagsMomentsCreated
+ + {t.title} + + {t.topic_category} +
+ {t.topic_tags.slice(0, 4).map((tag) => ( + {tag} + ))} + {t.topic_tags.length > 4 && ( + +{t.topic_tags.length - 4} + )} +
+
{t.key_moment_count}{formatDate(t.created_at)}
+
+ ); +} + +/* ── Section: Key Moments (grouped by source video) ────────────────────────── */ + +function KeyMomentSection({ data }: { data: CreatorTransparencyResponse }) { + if (data.key_moments.length === 0) { + return

No key moments extracted yet.

; + } + + // Group by source video + const grouped = new Map(); + 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]) => ( +
+

{videoFilename}

+
+ + + + + + + + + + + {moments.map((m, i) => ( + + + + + + + ))} + +
TitleTypeTimeTechnique Page
{m.title} + + {m.content_type} + + + + {formatTime(m.start_time)} – {formatTime(m.end_time)} + + + {m.technique_page_title ?? ( + + )} +
+
+
+ ))} + + ); +} + +/* ── Section: Cross-References ─────────────────────────────────────────────── */ + +function RelationshipSection({ data }: { data: CreatorTransparencyResponse }) { + if (data.relationships.length === 0) { + return

No cross-references found yet.

; + } + return ( +
+ + + + + + + + + + {data.relationships.map((r, i) => ( + + + + + + ))} + +
TypeSourceTarget
+ + {r.relationship_type} + + + + {r.source_page_title} + + + + {r.target_page_title} + +
+
+ ); +} + +/* ── Section: Source Videos ─────────────────────────────────────────────────── */ + +function SourceVideoSection({ data }: { data: CreatorTransparencyResponse }) { + if (data.source_videos.length === 0) { + return

No source videos uploaded yet.

; + } + return ( +
+ + + + + + + + + + {data.source_videos.map((v) => ( + + + + + + ))} + +
FilenameStatusUploaded
{v.filename} + + {v.processing_status} + + {formatDate(v.created_at)}
+
+ ); +} + +/* ── Main component ────────────────────────────────────────────────────────── */ + +export default function CreatorTransparency() { + useDocumentTitle("AI Transparency"); + const { user } = useAuth(); + + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+ +
+

AI Transparency

+

+ Everything our AI derived from {user?.display_name ? `${user.display_name}'s` : "your"} content +

+ + {loading && } + + {!loading && error === "not_linked" && ( +
+

No Creator Profile

+

Your account isn't linked to a creator profile yet. Contact an admin to get set up.

+
+ )} + + {!loading && error && error !== "not_linked" && ( +
+

Could not load transparency data: {error}

+
+ )} + + {!loading && !error && data && ( + <> + {/* ── Tag summary ────────────────────────────────────────── */} + {data.tags.length > 0 && ( +
+ {data.tags.map((tag) => ( + {tag} + ))} +
+ )} + + {/* ── Collapsible sections ──────────────────────────────── */} + + + + + + + + + + + + + + + + + )} +
+
+ ); +} + +/* ── Loading skeleton ──────────────────────────────────────────────────────── */ + +function TransparencySkeleton() { + return ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ ); +} diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 31ad412..1815196 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file