feat: Added collapsible PersonalityProfile component to CreatorDetail p…

- "frontend/src/components/PersonalityProfile.tsx"
- "frontend/src/api/creators.ts"
- "frontend/src/pages/CreatorDetail.tsx"
- "frontend/src/App.css"

GSD-Task: S06/T03
This commit is contained in:
jlightner 2026-04-04 08:31:37 +00:00
parent 2d9076ae92
commit eab362d897
8 changed files with 447 additions and 2 deletions

View file

@ -141,7 +141,7 @@ Build the core extraction pipeline: a prompt template that analyzes creator tran
- Estimate: 1h30m
- Files: prompts/personality_extraction.txt, backend/pipeline/stages.py, backend/schemas.py, backend/routers/admin.py
- Verify: test -f prompts/personality_extraction.txt && cd backend && python -c "from pipeline.stages import extract_personality_profile; print('task OK')" && python -c "from schemas import PersonalityProfile; print('validator OK')" && grep -q 'extract-profile' routers/admin.py && echo 'all OK'
- [ ] **T03: Add personality profile display to CreatorDetail frontend page** — ## Description
- [x] **T03: Added collapsible PersonalityProfile component to CreatorDetail page with three sub-cards, pill badges, boolean indicators, and smooth CSS grid animation** — ## Description
Add a collapsible personality profile section to the CreatorDetail page. Update the TypeScript API type, create a PersonalityProfile component, and wire it into the page layout below the bio/social links section.

View file

@ -0,0 +1,36 @@
{
"schemaVersion": 1,
"taskId": "T02",
"unitId": "M022/S06/T02",
"timestamp": 1775291298640,
"passed": false,
"discoverySource": "task-plan",
"checks": [
{
"command": "test -f prompts/personality_extraction.txt",
"exitCode": 0,
"durationMs": 5,
"verdict": "pass"
},
{
"command": "cd backend",
"exitCode": 0,
"durationMs": 6,
"verdict": "pass"
},
{
"command": "grep -q 'extract-profile' routers/admin.py",
"exitCode": 2,
"durationMs": 6,
"verdict": "fail"
},
{
"command": "echo 'all OK'",
"exitCode": 0,
"durationMs": 4,
"verdict": "pass"
}
],
"retryAttempt": 1,
"maxRetries": 2
}

View file

@ -0,0 +1,83 @@
---
id: T03
parent: S06
milestone: M022
provides: []
requires: []
affects: []
key_files: ["frontend/src/components/PersonalityProfile.tsx", "frontend/src/api/creators.ts", "frontend/src/pages/CreatorDetail.tsx", "frontend/src/App.css"]
key_decisions: ["Used dedicated TypeScript interfaces for personality profile sub-objects rather than inline types"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "TypeScript compiles clean (npx tsc --noEmit), production build succeeds (npm run build), component file exists, component is imported in CreatorDetail.tsx. All slice-level backend checks also pass."
completed_at: 2026-04-04T08:31:30.051Z
blocker_discovered: false
---
# T03: Added collapsible PersonalityProfile component to CreatorDetail page with three sub-cards, pill badges, boolean indicators, and smooth CSS grid animation
> Added collapsible PersonalityProfile component to CreatorDetail page with three sub-cards, pill badges, boolean indicators, and smooth CSS grid animation
## What Happened
---
id: T03
parent: S06
milestone: M022
key_files:
- frontend/src/components/PersonalityProfile.tsx
- frontend/src/api/creators.ts
- frontend/src/pages/CreatorDetail.tsx
- frontend/src/App.css
key_decisions:
- Used dedicated TypeScript interfaces for personality profile sub-objects rather than inline types
duration: ""
verification_result: passed
completed_at: 2026-04-04T08:31:30.051Z
blocker_discovered: false
---
# T03: Added collapsible PersonalityProfile component to CreatorDetail page with three sub-cards, pill badges, boolean indicators, and smooth CSS grid animation
**Added collapsible PersonalityProfile component to CreatorDetail page with three sub-cards, pill badges, boolean indicators, and smooth CSS grid animation**
## What Happened
Created PersonalityProfile.tsx with collapsible section (grid-template-rows 0fr/1fr), three cards (Teaching Style, Vocabulary, Style), pill badges for descriptors/phrases/terms, boolean checkmark indicators, and metadata footer. Updated CreatorDetailResponse type with dedicated interfaces. Wired into CreatorDetail.tsx between stats bar and techniques list. Added ~130 lines of CSS using existing design tokens.
## Verification
TypeScript compiles clean (npx tsc --noEmit), production build succeeds (npm run build), component file exists, component is imported in CreatorDetail.tsx. All slice-level backend checks also pass.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 3000ms |
| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 6200ms |
| 3 | `test -f frontend/src/components/PersonalityProfile.tsx` | 0 | ✅ pass | 100ms |
| 4 | `grep -q PersonalityProfile frontend/src/pages/CreatorDetail.tsx` | 0 | ✅ pass | 100ms |
## Deviations
None.
## Known Issues
Slice-level verification `grep -q 'extract-profile' routers/admin.py` uses wrong path (missing backend/ prefix) — file exists at backend/routers/admin.py.
## Files Created/Modified
- `frontend/src/components/PersonalityProfile.tsx`
- `frontend/src/api/creators.ts`
- `frontend/src/pages/CreatorDetail.tsx`
- `frontend/src/App.css`
## Deviations
None.
## Known Issues
Slice-level verification `grep -q 'extract-profile' routers/admin.py` uses wrong path (missing backend/ prefix) — file exists at backend/routers/admin.py.

View file

@ -5573,6 +5573,141 @@ a.app-footer__about:hover,
animation: pageEnter 250ms ease-out;
}
/* ── Personality Profile ───────────────────────────────────────────────────── */
.personality-profile {
margin: 1.5rem 0;
}
.personality-profile__toggle {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
cursor: pointer;
color: var(--color-text-primary);
transition: background 150ms ease;
}
.personality-profile__toggle:hover {
background: var(--color-bg-surface-hover);
}
.personality-profile__heading {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
.personality-profile__chevron {
margin-left: auto;
font-size: 0.9rem;
transition: transform 250ms ease;
color: var(--color-text-muted);
}
.personality-profile__chevron--open {
transform: rotate(90deg);
}
.personality-profile__collapse {
display: grid;
transition: grid-template-rows 300ms ease;
}
.personality-profile__inner {
overflow: hidden;
min-height: 0;
}
.personality-profile__summary {
color: var(--color-text-secondary);
font-size: 0.95rem;
line-height: 1.6;
margin: 1rem 0;
padding: 0 0.25rem;
}
.personality-profile__cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.personality-profile__card {
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1rem;
}
.personality-profile__card-title {
margin: 0 0 0.75rem;
font-size: 0.95rem;
font-weight: 600;
color: var(--color-accent);
}
.personality-profile__dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 0.75rem;
margin: 0 0 0.75rem;
font-size: 0.85rem;
}
.personality-profile__dl dt {
color: var(--color-text-muted);
font-weight: 500;
}
.personality-profile__dl dd {
margin: 0;
color: var(--color-text-primary);
}
.personality-profile__pill-label {
display: block;
font-size: 0.8rem;
color: var(--color-text-muted);
margin-bottom: 0.25rem;
}
.personality-profile__pills {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-bottom: 0.5rem;
}
.personality-profile__booleans {
display: flex;
flex-direction: column;
gap: 0.375rem;
font-size: 0.85rem;
margin-top: 0.5rem;
}
.personality-profile__bool {
color: var(--color-text-muted);
}
.personality-profile__bool--yes {
color: var(--color-badge-approved-text);
}
.personality-profile__meta {
color: var(--color-text-muted);
font-size: 0.8rem;
padding: 0 0.25rem;
margin-bottom: 0.5rem;
}
/* ── Admin Technique Pages ────────────────────────────────────────────────── */
.admin-page {

View file

@ -33,6 +33,39 @@ export interface CreatorTechniqueItem {
created_at: string;
}
export interface PersonalityVocabulary {
signature_phrases: string[];
technical_jargon_level: string;
filler_words: string[];
distinctive_terms: string[];
}
export interface PersonalityTone {
formality: string;
energy: string;
humor_frequency: string;
teaching_style: string;
descriptors: string[];
}
export interface PersonalityStyleMarkers {
explanation_approach: string;
uses_analogies: boolean;
uses_sound_words: boolean;
self_references_frequency: string;
audience_engagement: string;
}
export interface PersonalityProfile {
vocabulary: PersonalityVocabulary;
tone: PersonalityTone;
style_markers: PersonalityStyleMarkers;
summary: string;
extracted_at: string;
transcript_sample_size: number;
model_used: string;
}
export interface CreatorDetailResponse {
id: string;
name: string;
@ -52,6 +85,7 @@ export interface CreatorDetailResponse {
follower_count: number;
techniques: CreatorTechniqueItem[];
genre_breakdown: Record<string, number>;
personality_profile: PersonalityProfile | null;
}
export interface CreatorListParams {

View file

@ -0,0 +1,153 @@
/**
* Collapsible personality profile section for the CreatorDetail page.
*
* Shows vocabulary, tone, and style markers extracted from creator transcripts.
* Renders nothing if profile is null. Uses CSS grid-template-rows 0fr/1fr
* for smooth collapse/expand animation (per KNOWLEDGE.md).
*/
import { useState } from "react";
import type { PersonalityProfile as ProfileType } from "../api/creators";
interface Props {
profile: ProfileType | null;
}
export default function PersonalityProfile({ profile }: Props) {
const [expanded, setExpanded] = useState(false);
if (!profile) return null;
return (
<section className="personality-profile">
<button
className="personality-profile__toggle"
onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded}
>
<h2 className="personality-profile__heading">Personality Profile</h2>
<span
className={`personality-profile__chevron${expanded ? " personality-profile__chevron--open" : ""}`}
aria-hidden="true"
>
</span>
</button>
<div
className="personality-profile__collapse"
style={{
gridTemplateRows: expanded ? "1fr" : "0fr",
}}
>
<div className="personality-profile__inner">
{/* Summary */}
<p className="personality-profile__summary">{profile.summary}</p>
<div className="personality-profile__cards">
{/* Teaching Style */}
<div className="personality-profile__card">
<h3 className="personality-profile__card-title">Teaching Style</h3>
<dl className="personality-profile__dl">
<dt>Formality</dt>
<dd>{profile.tone.formality}</dd>
<dt>Energy</dt>
<dd>{profile.tone.energy}</dd>
<dt>Teaching style</dt>
<dd>{profile.tone.teaching_style}</dd>
<dt>Humor</dt>
<dd>{profile.tone.humor_frequency}</dd>
</dl>
{profile.tone.descriptors.length > 0 && (
<div className="personality-profile__pills">
{profile.tone.descriptors.map((d) => (
<span key={d} className="pill pill--tag">
{d}
</span>
))}
</div>
)}
</div>
{/* Vocabulary */}
<div className="personality-profile__card">
<h3 className="personality-profile__card-title">Vocabulary</h3>
<dl className="personality-profile__dl">
<dt>Jargon level</dt>
<dd>{profile.vocabulary.technical_jargon_level}</dd>
{profile.vocabulary.filler_words.length > 0 && (
<>
<dt>Filler words</dt>
<dd>{profile.vocabulary.filler_words.join(", ")}</dd>
</>
)}
</dl>
{profile.vocabulary.signature_phrases.length > 0 && (
<>
<span className="personality-profile__pill-label">
Signature phrases
</span>
<div className="personality-profile__pills">
{profile.vocabulary.signature_phrases.map((p) => (
<span key={p} className="pill pill--tag">
"{p}"
</span>
))}
</div>
</>
)}
{profile.vocabulary.distinctive_terms.length > 0 && (
<>
<span className="personality-profile__pill-label">
Distinctive terms
</span>
<div className="personality-profile__pills">
{profile.vocabulary.distinctive_terms.map((t) => (
<span key={t} className="pill pill--tag">
{t}
</span>
))}
</div>
</>
)}
</div>
{/* Style */}
<div className="personality-profile__card">
<h3 className="personality-profile__card-title">Style</h3>
<dl className="personality-profile__dl">
<dt>Explanation approach</dt>
<dd>{profile.style_markers.explanation_approach}</dd>
<dt>Audience engagement</dt>
<dd>{profile.style_markers.audience_engagement}</dd>
<dt>Self-references</dt>
<dd>{profile.style_markers.self_references_frequency}</dd>
</dl>
<div className="personality-profile__booleans">
<span
className={`personality-profile__bool${profile.style_markers.uses_analogies ? " personality-profile__bool--yes" : ""}`}
>
{profile.style_markers.uses_analogies ? "✓" : "✗"} Uses
analogies
</span>
<span
className={`personality-profile__bool${profile.style_markers.uses_sound_words ? " personality-profile__bool--yes" : ""}`}
>
{profile.style_markers.uses_sound_words ? "✓" : "✗"} Uses
sound words
</span>
</div>
</div>
</div>
{/* Metadata */}
<div className="personality-profile__meta">
Based on {profile.transcript_sample_size} transcript
{profile.transcript_sample_size !== 1 ? "s" : ""} · extracted{" "}
{new Date(profile.extracted_at).toLocaleDateString()}
</div>
</div>
</div>
</section>
);
}

View file

@ -19,6 +19,7 @@ import { useAuth } from "../context/AuthContext";
import CreatorAvatar from "../components/CreatorAvatar";
import { SocialIcon } from "../components/SocialIcons";
import ChatWidget from "../components/ChatWidget";
import PersonalityProfile from "../components/PersonalityProfile";
import SortDropdown from "../components/SortDropdown";
import TagList from "../components/TagList";
import { catSlug } from "../utils/catSlug";
@ -393,6 +394,9 @@ export default function CreatorDetail() {
)}
</div>
{/* Personality Profile */}
<PersonalityProfile profile={creator.personality_profile ?? null} />
{/* Technique pages */}
<section className="creator-techniques">
<div className="creator-techniques__header">

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/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.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/PlayerControls.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/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"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/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/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.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/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/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"}