Documentation Index Fetch the complete documentation index at: https://docs.unleeshed.ai/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide covers best practices for displaying AI-generated commentary on your platform, ensuring a great user experience while meeting licensing requirements.
Required Attribution
All displayed commentaries must include Unleeshed attribution:
< footer class = "unleeshed-attribution" >
Powered by < a href = "https://unleeshed.ai" > Unleeshed </ a >
</ footer >
Display Patterns
For displaying one persona’s take:
function CommentaryCard ({ commentary }) {
return (
< article className = "commentary-card" >
< header className = "persona-header" >
< img
src = { commentary . persona . image_url }
alt = { commentary . persona . name }
className = "persona-avatar"
/>
< div className = "persona-info" >
< h3 > { commentary . persona . name } </ h3 >
< span className = "expertise" > NBA Analyst </ span >
</ div >
</ header >
< div className = "commentary-content" >
< p > { commentary . content } </ p >
</ div >
{ commentary . audio_url && (
< AudioPlayer src = { commentary . audio_url } />
) }
< footer className = "attribution" >
Powered by < a href = "https://unleeshed.ai" > Unleeshed </ a >
</ footer >
</ article >
);
}
Multiple Personas (Debate Style)
Show different perspectives side-by-side:
function DebateView ({ topic , commentaries }) {
return (
< section className = "debate-view" >
< header className = "topic" >
< h2 > { topic . content } </ h2 >
</ header >
< div className = "perspectives" >
{ commentaries . map ( c => (
< article key = { c . commentary_id } className = "perspective" >
< div className = "persona" >
< img src = { c . persona . image_url } alt = "" />
< h3 > { c . persona . name } </ h3 >
</ div >
< blockquote > { c . content } </ blockquote >
</ article >
)) }
</ div >
< footer className = "attribution" >
Powered by < a href = "https://unleeshed.ai" > Unleeshed </ a >
</ footer >
</ section >
);
}
Tabbed Navigation
Let users switch between personas:
function TabbedCommentary ({ commentaries }) {
const [ activeId , setActiveId ] = useState ( commentaries [ 0 ]?. persona . id );
const active = commentaries . find ( c => c . persona . id === activeId );
return (
< div className = "tabbed-commentary" >
< nav className = "persona-tabs" >
{ commentaries . map ( c => (
< button
key = { c . persona . id }
onClick = { () => setActiveId ( c . persona . id ) }
className = { c . persona . id === activeId ? 'active' : '' }
>
< img src = { c . persona . image_url } alt = "" />
< span > { c . persona . name } </ span >
</ button >
)) }
</ nav >
{ active && (
< article className = "active-commentary" >
< p > { active . content } </ p >
{ active . audio_url && < AudioPlayer src = { active . audio_url } /> }
</ article >
) }
< footer className = "attribution" >
Powered by < a href = "https://unleeshed.ai" > Unleeshed </ a >
</ footer >
</ div >
);
}
Audio Player
For commentaries with audio:
function AudioPlayer ({ src , personaName }) {
const [ isPlaying , setIsPlaying ] = useState ( false );
const audioRef = useRef ( null );
const togglePlay = () => {
if ( isPlaying ) {
audioRef . current . pause ();
} else {
audioRef . current . play ();
}
setIsPlaying ( ! isPlaying );
};
return (
< div className = "audio-player" >
< audio
ref = { audioRef }
src = { src }
onEnded = { () => setIsPlaying ( false ) }
/>
< button onClick = { togglePlay } aria-label = { isPlaying ? 'Pause' : 'Play' } >
{ isPlaying ? < PauseIcon /> : < PlayIcon /> }
</ button >
< span > Listen to { personaName } </ span >
</ div >
);
}
Styling Guidelines
Typography
Use readable font sizes (16px+ for body text)
Maintain good contrast (4.5:1 minimum)
Quote styling for commentary content
Persona Avatars
Minimum size: 48x48px
Use border-radius for circular avatars
Include alt text for accessibility
Responsive Design
.commentary-card {
max-width : 600 px ;
padding : 1.5 rem ;
}
@media ( max-width : 640 px ) {
.commentary-card {
padding : 1 rem ;
}
.persona-avatar {
width : 40 px ;
height : 40 px ;
}
}
Loading States
Show meaningful loading states during generation:
function CommentaryLoader ({ personas , status , summary }) {
return (
< div className = "loading-state" >
< div className = "progress" >
< div
className = "progress-bar"
style = { { width: ` ${ ( summary . completed / summary . total_personas ) * 100 } %` } }
/>
</ div >
< p >
Generating commentary...
( { summary . completed } / { summary . total_personas } ready)
</ p >
< div className = "persona-status" >
{ personas . map ( p => (
< div key = { p . id } className = { `persona ${ p . status } ` } >
< img src = { p . image_url } alt = "" />
{ p . status === 'completed' && < CheckIcon /> }
{ p . status === 'pending' && < Spinner /> }
</ div >
)) }
</ div >
</ div >
);
}
Accessibility
Use semantic HTML (article, header, blockquote)
Include alt text for all images
Announce dynamic content changes
All interactive elements focusable
Logical tab order
Visible focus indicators
Provide text transcript (commentary text)
Keyboard-accessible controls
Visual playback indicators
Next Steps
Error Handling Handle failures gracefully.
Rate Limits Understand API limits.