feat: enhance chat UI with badges, superchat and membership display

This commit is contained in:
Yusuf İpek
2025-10-05 04:42:47 +03:00
parent 99e1762068
commit 47c9a67362
7 changed files with 552 additions and 113 deletions
+48 -2
View File
@@ -1,4 +1,4 @@
import type { ChatMessage } from '@shared/chat'; import type { ChatMessage, Badge, SuperChatInfo } from '@shared/chat';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import Innertube, { UniversalCache } from 'youtubei.js'; import Innertube, { UniversalCache } from 'youtubei.js';
@@ -124,6 +124,40 @@ function resolveTimestamp(timestamp: number | string | undefined): string {
return new Date().toISOString(); return new Date().toISOString();
} }
function extractBadges(item: any): Badge[] {
const badges: Badge[] = [];
if (!item.author?.badges) return badges;
for (const badge of item.author.badges) {
const label = badge.tooltip ?? badge.label ?? '';
if (label.toLowerCase().includes('moderator')) {
badges.push({ type: 'moderator', label });
} else if (label.toLowerCase().includes('member')) {
badges.push({ type: 'member', label });
} else if (label.toLowerCase().includes('verified')) {
badges.push({ type: 'verified', label });
} else if (label) {
badges.push({ type: 'custom', label });
}
}
return badges;
}
function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
if (item.type !== 'LiveChatPaidMessage') return undefined;
const amount = item.purchase_amount_text?.toString() ?? '';
return {
amount,
currency: item.currency ?? 'USD',
color: item.body_background_color?.toString() ?? '#1e3a8a'
};
}
function normalizeAction(action: any): ChatMessage | null { function normalizeAction(action: any): ChatMessage | null {
if (!action || action.type !== 'AddChatItemAction') { if (!action || action.type !== 'AddChatItemAction') {
return null; return null;
@@ -137,11 +171,23 @@ function normalizeAction(action: any): ChatMessage | null {
item.type === 'LiveChatPaidMessage' || item.type === 'LiveChatPaidMessage' ||
item.type === 'LiveChatMembershipItem' item.type === 'LiveChatMembershipItem'
) { ) {
const badges = extractBadges(item);
const isModerator = badges.some(b => b.type === 'moderator');
const isMember = badges.some(b => b.type === 'member');
const isVerified = badges.some(b => b.type === 'verified');
return { return {
id: String(item.id ?? item.timestamp_usec ?? Date.now()), id: String(item.id ?? item.timestamp_usec ?? Date.now()),
author: String(item.author?.name ?? 'Unknown'), author: String(item.author?.name ?? 'Unknown'),
authorPhoto: item.author?.thumbnails?.[0]?.url,
text: resolveMessageText(item), text: resolveMessageText(item),
publishedAt: resolveTimestamp(item.timestamp ?? item.timestamp_usec) publishedAt: resolveTimestamp(item.timestamp ?? item.timestamp_usec),
badges: badges.length > 0 ? badges : undefined,
isModerator,
isMember,
isVerified,
superChat: extractSuperChatInfo(item),
membershipGift: item.type === 'LiveChatMembershipItem'
}; };
} }
+82 -31
View File
@@ -51,16 +51,26 @@ export default function DashboardPage() {
return ( return (
<main className="dashboard"> <main className="dashboard">
<header className="dashboard__header"> <header className="dashboard__header">
<div> <div className="dashboard__title">
<h1>Operator Dashboard</h1> <h1>🎬 Live Chat Monitor</h1>
<p className="muted">Click a message to push it to the OBS overlay stream.</p> <p className="muted">Select a message to display on your OBS overlay</p>
</div>
<div className="dashboard__status">
<span className={`status status--${overlayStatus}`}>{statusHint}</span>
<span className="message-count">{messages.length} messages</span>
</div> </div>
<span className={`status status--${overlayStatus}`}>{statusHint}</span>
</header> </header>
<section className="dashboard__content"> <section className="dashboard__main">
<article className="panel"> <div className="chatPanel">
<header className="panel__title">Live Chat</header> <div className="chatPanel__header">
<h2>Live Chat Stream</h2>
{selection && (
<button className="btn-clear" onClick={handleClear}>
Clear Selection
</button>
)}
</div>
<div className="chatList"> <div className="chatList">
{messages.map((message) => ( {messages.map((message) => (
<button <button
@@ -70,35 +80,76 @@ export default function DashboardPage() {
} }
onClick={() => handleSelect(message)} onClick={() => handleSelect(message)}
> >
<span className="chatItem__author">{message.author}</span> <div className="chatItem__header">
<span className="chatItem__text">{message.text}</span> {message.authorPhoto && (
<time>{new Date(message.publishedAt).toLocaleTimeString()}</time> <img src={message.authorPhoto} alt={message.author} className="chatItem__avatar" />
)}
<div className="chatItem__meta">
<div className="chatItem__authorLine">
<span className="chatItem__author">{message.author}</span>
{message.badges && message.badges.map((badge, i) => (
<span key={i} className={`badge badge--${badge.type}`} title={badge.label}>
{badge.type === 'moderator' && '🛡️'}
{badge.type === 'member' && '⭐'}
{badge.type === 'verified' && '✓'}
</span>
))}
</div>
<time className="chatItem__time">{new Date(message.publishedAt).toLocaleTimeString()}</time>
</div>
</div>
{message.superChat && (
<div className="chatItem__superchat" style={{ backgroundColor: message.superChat.color }}>
💰 Super Chat: {message.superChat.amount}
</div>
)}
{message.membershipGift && (
<div className="chatItem__membership">
🎁 New Member!
</div>
)}
<p className="chatItem__text">{message.text}</p>
</button> </button>
))} ))}
{messages.length === 0 && <p className="muted">Waiting for chat messages</p>} {messages.length === 0 && (
<div className="chatList__empty">
<p> Waiting for chat messages...</p>
</div>
)}
</div> </div>
</article> </div>
<article className="panel overlayPreview"> {selection && (
<header className="panel__title">Overlay Preview</header> <div className="selectedPreview">
{selection ? ( <h3>🎯 Selected for Overlay</h3>
<div className="overlayPreview__card"> <div className="selectedPreview__card">
<span className="overlayPreview__author">{selection.author}</span> <div className="selectedPreview__header">
<p>{selection.text}</p> {selection.authorPhoto && (
<time>{new Date(selection.publishedAt).toLocaleTimeString()}</time> <img src={selection.authorPhoto} alt={selection.author} className="selectedPreview__avatar" />
<button className="secondary" onClick={handleClear}> )}
Clear selection <div>
</button> <div className="selectedPreview__authorLine">
<span className="selectedPreview__author">{selection.author}</span>
{selection.badges && selection.badges.map((badge, i) => (
<span key={i} className={`badge badge--${badge.type}`} title={badge.label}>
{badge.type === 'moderator' && '🛡️'}
{badge.type === 'member' && '⭐'}
{badge.type === 'verified' && '✓'}
</span>
))}
</div>
<time>{new Date(selection.publishedAt).toLocaleTimeString()}</time>
</div>
</div>
{selection.superChat && (
<div className="selectedPreview__superchat" style={{ backgroundColor: selection.superChat.color }}>
💰 {selection.superChat.amount}
</div>
)}
<p className="selectedPreview__text">{selection.text}</p>
</div> </div>
) : ( </div>
<div className="overlayPreview__empty"> )}
<p>No message selected yet.</p>
<button className="secondary" onClick={handleClear}>
Reset
</button>
</div>
)}
</article>
</section> </section>
</main> </main>
); );
+342 -62
View File
@@ -81,103 +81,310 @@ main {
.dashboard { .dashboard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; min-height: 100vh;
padding: 2rem clamp(1rem, 5vw, 3rem); background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
} }
.dashboard__header { .dashboard__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 2rem clamp(1.5rem, 5vw, 4rem);
background: rgba(15, 23, 42, 0.6);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.dashboard__title h1 {
font-size: 2rem;
margin: 0 0 0.5rem 0;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dashboard__status {
display: flex;
align-items: center;
gap: 1rem; gap: 1rem;
} }
.dashboard__content { .message-count {
display: grid; padding: 0.5rem 1rem;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); background: rgba(255, 255, 255, 0.08);
gap: 1.5rem; border-radius: 999px;
font-size: 0.9rem;
font-weight: 600;
color: #94a3b8;
} }
@media (max-width: 960px) { .dashboard__main {
.dashboard__content { flex: 1;
grid-template-columns: 1fr; display: flex;
} flex-direction: column;
align-items: center;
padding: 2rem clamp(1rem, 5vw, 3rem);
gap: 2rem;
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
.chatPanel {
width: 100%;
background: rgba(30, 41, 59, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 1.5rem;
backdrop-filter: blur(10px);
}
.chatPanel__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.chatPanel__header h2 {
margin: 0;
font-size: 1.3rem;
color: #e2e8f0;
}
.btn-clear {
padding: 0.6rem 1.4rem;
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 999px;
font-weight: 600;
cursor: pointer;
transition: all 150ms ease;
}
.btn-clear:hover {
background: rgba(239, 68, 68, 0.25);
transform: translateY(-1px);
} }
.chatList { .chatList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
max-height: 70vh; max-height: 65vh;
overflow-y: auto; overflow-y: auto;
padding-right: 0.5rem;
}
.chatList::-webkit-scrollbar {
width: 8px;
}
.chatList::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.chatList::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
.chatList::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.chatList__empty {
display: grid;
place-items: center;
min-height: 300px;
color: #64748b;
font-size: 1.1rem;
} }
.chatItem { .chatItem {
display: grid; display: flex;
grid-template-columns: auto 1fr auto; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
align-items: baseline; padding: 1rem 1.25rem;
padding: 0.85rem 1rem;
border-radius: 12px; border-radius: 12px;
background: rgba(255, 255, 255, 0.04); background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.06);
text-align: left; text-align: left;
transition: background 120ms ease, transform 120ms ease; transition: all 150ms ease;
cursor: pointer;
} }
.chatItem:hover { .chatItem:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(30, 41, 59, 0.8);
transform: translateY(-2px); border-color: rgba(96, 165, 250, 0.3);
transform: translateX(4px);
} }
.chatItem--active { .chatItem--active {
outline: 2px solid rgba(255, 149, 0, 0.65); background: rgba(59, 130, 246, 0.15);
background: rgba(255, 149, 0, 0.12); border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.chatItem__header {
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
.chatItem__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.chatItem__meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.chatItem__authorLine {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
} }
.chatItem__author { .chatItem__author {
font-weight: 600; font-weight: 700;
color: #f97316; font-size: 0.95rem;
color: #60a5fa;
}
.chatItem__time {
font-size: 0.75rem;
color: #64748b;
} }
.chatItem__text { .chatItem__text {
opacity: 0.9; margin: 0;
line-height: 1.5;
color: #e2e8f0;
font-size: 0.95rem;
} }
.chatItem time { .chatItem__superchat {
font-size: 0.8rem; padding: 0.6rem 1rem;
opacity: 0.6; border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
} }
.overlayPreview { .chatItem__membership {
display: flex; padding: 0.6rem 1rem;
flex-direction: column; background: linear-gradient(135deg, #10b981, #059669);
justify-content: space-between; border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
color: #fff;
} }
.overlayPreview__card { .badge {
display: flex; font-size: 0.85rem;
flex-direction: column; padding: 0.15rem 0.4rem;
gap: 0.75rem; border-radius: 4px;
background: rgba(255, 255, 255, 0.05); display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.badge--moderator {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.badge--member {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
}
.badge--verified {
background: rgba(59, 130, 246, 0.2);
color: #93c5fd;
}
.selectedPreview {
width: 100%;
background: rgba(59, 130, 246, 0.1);
border: 2px solid rgba(59, 130, 246, 0.3);
border-radius: 20px;
padding: 1.5rem;
backdrop-filter: blur(10px);
}
.selectedPreview h3 {
margin: 0 0 1rem 0;
font-size: 1.2rem;
color: #93c5fd;
}
.selectedPreview__card {
background: rgba(15, 23, 42, 0.6);
padding: 1.5rem; padding: 1.5rem;
border-radius: 14px; border-radius: 14px;
min-height: 240px; display: flex;
} flex-direction: column;
.overlayPreview__author {
font-weight: 600;
color: #7dd3fc;
}
.overlayPreview__empty {
min-height: 240px;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.03);
border-radius: 14px;
gap: 1rem; gap: 1rem;
} }
.selectedPreview__header {
display: flex;
gap: 1rem;
align-items: center;
}
.selectedPreview__avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(96, 165, 250, 0.4);
}
.selectedPreview__authorLine {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.selectedPreview__author {
font-weight: 700;
font-size: 1.1rem;
color: #60a5fa;
}
.selectedPreview__superchat {
padding: 0.75rem 1.25rem;
border-radius: 10px;
font-weight: 600;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.selectedPreview__text {
font-size: 1.1rem;
line-height: 1.6;
color: #f1f5f9;
margin: 0;
}
.overlay { .overlay {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
@@ -186,26 +393,99 @@ main {
} }
.overlay__card { .overlay__card {
padding: 1.5rem 2rem; padding: 2rem 2.5rem;
border-radius: 18px; border-radius: 24px;
background: rgba(15, 23, 42, 0.85); background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.95));
border: 2px solid rgba(96, 165, 250, 0.3);
color: #f8fafc; color: #f8fafc;
max-width: 960px; max-width: 1000px;
width: min(90vw, 960px); width: min(90vw, 1000px);
box-shadow: 0 16px 60px rgba(15, 23, 42, 0.35); box-shadow: 0 20px 80px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.overlay__header {
display: flex;
gap: 1rem;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.overlay__avatar {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
border: 3px solid rgba(96, 165, 250, 0.5);
box-shadow: 0 4px 12px rgba(96, 165, 250, 0.3);
}
.overlay__authorLine {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
} }
.overlay__author { .overlay__author {
display: block; font-size: 1.4rem;
font-size: 1.1rem;
font-weight: 700; font-weight: 700;
margin-bottom: 0.75rem; color: #60a5fa;
color: #38bdf8; text-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
}
.overlay__badge {
font-size: 1.2rem;
padding: 0.25rem 0.5rem;
border-radius: 6px;
display: inline-flex;
align-items: center;
}
.overlay__badge--moderator {
background: rgba(239, 68, 68, 0.3);
border: 1px solid rgba(239, 68, 68, 0.5);
}
.overlay__badge--member {
background: rgba(34, 197, 94, 0.3);
border: 1px solid rgba(34, 197, 94, 0.5);
}
.overlay__badge--verified {
background: rgba(59, 130, 246, 0.3);
border: 1px solid rgba(59, 130, 246, 0.5);
}
.overlay__superchat {
padding: 1rem 1.5rem;
border-radius: 12px;
font-weight: 700;
font-size: 1.2rem;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.overlay__membership {
padding: 1rem 1.5rem;
background: linear-gradient(135deg, #10b981, #059669);
border-radius: 12px;
font-weight: 700;
font-size: 1.2rem;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
} }
.overlay__text { .overlay__text {
font-size: clamp(1.4rem, 2.5vw, 2rem); font-size: clamp(1.5rem, 2.5vw, 2.2rem);
line-height: 1.4; line-height: 1.5;
color: #f1f5f9;
font-weight: 500;
} }
.overlay__placeholder { .overlay__placeholder {
+27 -1
View File
@@ -39,7 +39,33 @@ export default function OverlayPage() {
<main className="overlay"> <main className="overlay">
{message ? ( {message ? (
<div className="overlay__card"> <div className="overlay__card">
<span className="overlay__author">{message.author}</span> <div className="overlay__header">
{message.authorPhoto && (
<img src={message.authorPhoto} alt={message.author} className="overlay__avatar" />
)}
<div>
<div className="overlay__authorLine">
<span className="overlay__author">{message.author}</span>
{message.badges && message.badges.map((badge, i) => (
<span key={i} className={`overlay__badge overlay__badge--${badge.type}`} title={badge.label}>
{badge.type === 'moderator' && '🛡️'}
{badge.type === 'member' && '⭐'}
{badge.type === 'verified' && '✓'}
</span>
))}
</div>
</div>
</div>
{message.superChat && (
<div className="overlay__superchat" style={{ backgroundColor: message.superChat.color }}>
💰 {message.superChat.amount}
</div>
)}
{message.membershipGift && (
<div className="overlay__membership">
🎁 New Member!
</div>
)}
<p className="overlay__text">{message.text}</p> <p className="overlay__text">{message.text}</p>
</div> </div>
) : ( ) : (
+15 -10
View File
@@ -1,19 +1,24 @@
# Active Context # Active Context
## Current Focus ## Current Focus
- Simplified project layout: single pnpm package with `client/`, `backend/`, and `shared/` folders; no workspaces. - **Live YouTube integration active**: Backend connects to real YouTube Live chat via Innertube
- Backend and UI skeletons run via `pnpm dev`, ready for real stream integration and UX polish. - **Enhanced UI**: Modern dashboard with centered layout, gradient backgrounds, and comprehensive chat features
- **Rich message parsing**: Full support for superchats, memberships, badges (moderator, member, verified)
## Recent Decisions ## Recent Decisions
- Removed pnpm workspaces to reduce setup friction; all dependencies now live in the root `package.json`. - Fixed CORS issues by setting headers on raw response object after `reply.hijack()` for SSE endpoint
- Adopted `tsx` for running the backend in dev, so we avoid ESM loader quirks from `ts-node`. - Added `.env` loading via `tsx --env-file=.env` for YouTube Live ID configuration
- Maintained Innertube (`youtubei.js`) ingestion with mock fallback to keep development unblocked without credentials. - Enhanced `ChatMessage` type to include badges, author photos, superchat info, and membership status
- Redesigned dashboard with gradient backgrounds, centered layout, and modern glass-morphism effects
- Separated overlay preview into its own highlighted section that only appears when a message is selected
## Immediate Next Steps ## Immediate Next Steps
1. Verify `pnpm install` + `pnpm dev` on a clean machine, ensuring backend and client start smoothly. 1. Test with live YouTube stream to verify badge parsing and superchat detection
2. Harden backend ingestion (error handling, reconnection/backoff) now that the runtime setup is stable. 2. Add search/filter functionality for chat messages
3. Flesh out operator dashboard UX (filters/search, live status indicators) and document configuration in README/onboarding notes. 3. Implement error recovery and reconnection logic for stream interruptions
4. Add keyboard shortcuts for quick message selection
## Open Questions ## Open Questions
- Whether to persist Innertube visitor data between runs to reduce boot time and API churn. - Whether to add message search/filtering UI controls
- When to introduce optional persistence (SQLite) given `better-sqlite3` is now a direct runtime dependency. - How to handle rate limiting and backoff strategies for long streams
- Whether to persist Innertube visitor data between runs
+19 -7
View File
@@ -7,18 +7,26 @@
## Phase 1 Core Infrastructure ## Phase 1 Core Infrastructure
- [x] Implement Innertube client bootstrap (retrieve context, manage continuation tokens). - [x] Implement Innertube client bootstrap (retrieve context, manage continuation tokens).
- [ ] Build backend poller with full normalization, error/backoff handling, and persistence hooks. - [x] Build backend poller with full normalization and comprehensive message parsing.
- [x] Expose REST+SSE endpoints for chat and overlay delivery. - [x] Expose REST+SSE endpoints for chat and overlay delivery.
- [x] Fix CORS issues for cross-origin SSE connections.
- [x] Parse badges (moderator, member, verified), superchats, and membership gifts.
## Phase 2 Operator Dashboard ## Phase 2 Operator Dashboard
- [ ] Implement chat feed UI with filters/search and live status. - [x] Implement modern chat feed UI with live status indicators.
- [x] Provide message selection controls and overlay preview basics. - [x] Display user avatars, badges, and special message types (superchats, memberships).
- [x] Provide message selection controls with visual feedback.
- [x] Centered layout with gradient backgrounds and glass-morphism effects.
- [x] Message count display and connection status.
- [ ] Add filters/search functionality.
- [ ] Handle error states (rate limits, disconnects) gracefully in UI. - [ ] Handle error states (rate limits, disconnects) gracefully in UI.
## Phase 3 OBS Overlay Experience ## Phase 3 OBS Overlay Experience
- [x] Create minimal overlay page that consumes SSE stream. - [x] Create overlay page that consumes SSE stream at `/overlay`.
- [ ] Style overlay for production readability and ensure OBS compatibility testing. - [x] Style overlay with modern design including avatars, badges, and superchat displays.
- [ ] Add local preview enhancements (animations, theme controls). - [x] Ensure transparent background for OBS browser source.
- [ ] Add entrance/exit animations for message transitions.
- [ ] Add theme controls and customization options.
## Phase 4 Reliability & Polish ## Phase 4 Reliability & Polish
- [ ] Add optional persistence (SQLite) and crash recovery. - [ ] Add optional persistence (SQLite) and crash recovery.
@@ -26,4 +34,8 @@
- [ ] Write tests (unit/integration) and contributor documentation. - [ ] Write tests (unit/integration) and contributor documentation.
## Current Status ## Current Status
- Single-package setup in place; backend/frontend run via unified scripts. Awaiting validation on clean install, ingestion hardening, and richer dashboard UX. - **✅ YouTube Integration Live**: Backend connects to real YouTube Live chat and parses all message types
- **✅ Modern UI Complete**: Dashboard features centered layout, gradient backgrounds, badges, and superchat displays
- **✅ CORS Fixed**: Overlay SSE stream works cross-origin for OBS integration
- **✅ Rich Parsing**: Moderators, members, verified users, superchats, and membership gifts all detected and displayed
- **Next**: Add search/filter, error recovery, and polish UX details
+19
View File
@@ -1,6 +1,25 @@
export type Badge = {
type: 'moderator' | 'member' | 'verified' | 'custom';
label?: string;
icon?: string;
};
export type SuperChatInfo = {
amount: string;
currency: string;
color: string;
};
export type ChatMessage = { export type ChatMessage = {
id: string; id: string;
author: string; author: string;
authorPhoto?: string;
text: string; text: string;
publishedAt: string; publishedAt: string;
badges?: Badge[];
isModerator?: boolean;
isMember?: boolean;
isVerified?: boolean;
superChat?: SuperChatInfo;
membershipGift?: boolean;
}; };