Skip to main content

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:
Attribution example
<footer class="unleeshed-attribution">
  Powered by <a href="https://unleeshed.ai">Unleeshed</a>
</footer>

Display Patterns

Single Persona Commentary

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: 600px;
  padding: 1.5rem;
}

@media (max-width: 640px) {
  .commentary-card {
    padding: 1rem;
  }
  
  .persona-avatar {
    width: 40px;
    height: 40px;
  }
}

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