2026-05-14 00:14:25 +07:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Discord Moderation Watcher</ title >
< link href = "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel = "stylesheet" >
< link rel = "stylesheet" href = "/dashboard.css" >
</ head >
< body >
< main class = "shell" >
< section class = "hero" >
< div class = "brand-card" >
< div class = "eyebrow" >< span class = "pulse" ></ span > Discord moderation watcher</ div >
< h1 > Voice. Text. One Watch Floor.</ h1 >
2026-05-14 01:45:27 +07:00
< p class = "subtitle" > Static shared-state dashboard with legacy working voice bridge and captured Discord messages.</ p >
2026-05-14 00:14:25 +07:00
</ div >
< div class = "status-card" >
< div class = "status-row" >< span class = "status-label" > WebSocket</ span >< span class = "status-value" >< span id = "wsDot" class = "dot" ></ span >< span id = "wsStatusText" > Connecting</ span ></ span ></ div >
< div class = "status-row" >< span class = "status-label" > Voice Link</ span >< span id = "voiceStatusText" class = "status-value" > Not connected</ span ></ div >
< div class = "status-row" >< span class = "status-label" > Active Tab</ span >< span id = "activeTabLabel" class = "status-value" > Voice</ span ></ div >
</ div >
</ section >
< nav class = "tab-panel" >
< div class = "tabs" >< button class = "tab-btn active" data-tab = "voice" > Voice</ button >< button class = "tab-btn" data-tab = "text" > Text</ button ></ div >
2026-05-15 15:58:38 +07:00
< div class = "filter-row" >< span > Text Guild</ span >< select id = "textGuildSelect" >< option value = "" > Select guild</ option ></ select >< span > Channel / Thread</ span >< select id = "channelFilter" >< option value = "" > Select channel</ option ></ select ></ div >
2026-05-14 00:14:25 +07:00
</ nav >
< div id = "errorBox" class = "error" ></ div >
< section id = "voice" class = "tab-content active" >
< div class = "voice-layout" >
< div class = "content-card" >
< div class = "card-title" >< h2 > Voice Control</ h2 >< span class = "mini" > bridge</ span ></ div >
2026-05-15 15:58:38 +07:00
< div class = "field-group" >< label for = "voiceGuildSelect" > Voice Guild</ label >< select id = "voiceGuildSelect" >< option value = "" > Select guild</ option ></ select ></ div >
2026-05-14 00:14:25 +07:00
< div class = "field-group" >< label for = "channelSelect" > Voice Channel</ label >< select id = "channelSelect" >< option value = "" > Select voice channel</ option ></ select ></ div >
< div class = "button-row" >< button id = "joinVoiceBtn" class = "btn btn-success" > Join</ button >< button id = "disconnectVoiceBtn" class = "btn btn-danger" > Disconnect</ button ></ div >
< div class = "voice-status" id = "voiceStatusNote" > Idle</ div >
</ div >
< div class = "content-card" >
< div class = "card-title" >< h2 > Live Audio</ h2 >< span class = "mini" id = "listenStatus" > Speaker Off</ span ></ div >
< div style = "display:grid;gap:12px;grid-template-columns:1fr 1fr;margin-bottom:14px" >< button id = "toggleBtn" class = "btn btn-primary" > Start Transmitting</ button >< button id = "listenBtn" class = "btn btn-success" > Join Listen Channel</ button ></ div >
< div class = "visualizer" id = "visualizer" ></ div >
</ div >
2026-05-15 18:03:01 +07:00
< div class = "content-card" >
< div class = "card-title" >< h2 > Media</ h2 >< span class = "mini" id = "mediaStatus" > Idle</ span ></ div >
< div class = "field-group" >< label for = "mediaSourceInput" > Music URL / file path</ label >< input id = "mediaSourceInput" type = "text" placeholder = "https://example.com/song.mp3" ></ div >
< div class = "button-row" >< button id = "queueMediaBtn" class = "btn btn-primary" > Queue / Play</ button >< button id = "skipMediaBtn" class = "btn btn-success" > Skip</ button >< button id = "stopMediaBtn" class = "btn btn-danger" > Stop</ button ></ div >
< div id = "mediaQueueList" class = "feed" >< div class = "empty" > No media queued</ div ></ div >
</ div >
2026-05-14 00:14:25 +07:00
</ div >
< div class = "content-card" style = "margin-top:18px" >< div class = "card-title" >< h2 > Participants</ h2 >< span class = "mini" > speaking now</ span ></ div >< div id = "userList" class = "participants" ></ div ></ div >
</ section >
2026-05-14 04:33:48 +07:00
< section id = "text" class = "tab-content" >< div style = "display:grid;grid-template-columns:1fr 1fr;gap:16px;height:100%" >< div class = "content-card" >< div class = "card-title" >< h2 > All Messages</ h2 >< span class = "mini" > all captures</ span ></ div >< div id = "textList" class = "feed" >< div class = "empty" > Select channel to view text captures</ div ></ div ></ div >< div class = "content-card" >< div class = "card-title" >< h2 > Needs Review</ h2 >< span class = "mini" > warn + flagged</ span ></ div >< div id = "reviewList" class = "feed" >< div class = "empty" > No warned or flagged messages</ div ></ div ></ div ></ div ></ section >
2026-05-14 00:14:25 +07:00
</ main >
< script >
2026-05-14 01:45:27 +07:00
const state = {
socket : null ,
2026-05-15 15:58:38 +07:00
selectedVoiceGuild : '' ,
2026-05-14 01:45:27 +07:00
selectedVoiceChannel : '' ,
2026-05-15 15:58:38 +07:00
selectedTextGuild : '' ,
2026-05-14 01:45:27 +07:00
selectedTextChannel : '' ,
activeTab : 'voice' ,
text : [],
isStreaming : false ,
isListening : false ,
localStreaming : false ,
localListening : false ,
audioContextTransmit : null ,
audioContextListen : null ,
processor : null ,
userTimelines : new Map (),
applyingServerState : false ,
2026-05-15 18:03:01 +07:00
media : { playing : false , current : null , queue : [] },
2026-05-14 01:45:27 +07:00
};
2026-05-14 00:14:25 +07:00
const SAMPLE_RATE = 24000 ;
const CHANNELS = 1 ;
const el = {
2026-05-15 18:03:01 +07:00
wsDot : document . getElementById ( 'wsDot' ), wsStatusText : document . getElementById ( 'wsStatusText' ), activeTabLabel : document . getElementById ( 'activeTabLabel' ), errorBox : document . getElementById ( 'errorBox' ), voiceGuildSelect : document . getElementById ( 'voiceGuildSelect' ), textGuildSelect : document . getElementById ( 'textGuildSelect' ), channelSelect : document . getElementById ( 'channelSelect' ), channelFilter : document . getElementById ( 'channelFilter' ), joinVoiceBtn : document . getElementById ( 'joinVoiceBtn' ), disconnectVoiceBtn : document . getElementById ( 'disconnectVoiceBtn' ), voiceStatusText : document . getElementById ( 'voiceStatusText' ), voiceStatusNote : document . getElementById ( 'voiceStatusNote' ), toggleBtn : document . getElementById ( 'toggleBtn' ), listenBtn : document . getElementById ( 'listenBtn' ), listenStatus : document . getElementById ( 'listenStatus' ), visualizer : document . getElementById ( 'visualizer' ), userList : document . getElementById ( 'userList' ), textList : document . getElementById ( 'textList' ), reviewList : document . getElementById ( 'reviewList' ), mediaSourceInput : document . getElementById ( 'mediaSourceInput' ), mediaStatus : document . getElementById ( 'mediaStatus' ), queueMediaBtn : document . getElementById ( 'queueMediaBtn' ), skipMediaBtn : document . getElementById ( 'skipMediaBtn' ), stopMediaBtn : document . getElementById ( 'stopMediaBtn' ), mediaQueueList : document . getElementById ( 'mediaQueueList' )
2026-05-14 00:14:25 +07:00
};
for ( let i = 0 ; i < 32 ; i ++ ) { const bar = document . createElement ( 'div' ); bar . className = 'bar' ; el . visualizer . appendChild ( bar ); }
const bars = [... document . querySelectorAll ( '.bar' )];
async function apiRequest ( url , options = {}) { const response = await fetch ( url , { headers : { 'Content-Type' : 'application/json' , ...( options . headers || {}) }, ... options }); if ( ! response . ok ) { const error = await response . json (). catch (() => ({ message : response . statusText })); throw new Error ( error . message || response . statusText ); } return response . json (); }
function showError ( message ) { el . errorBox . textContent = message ; el . errorBox . style . display = 'block' ; setTimeout (() => { el . errorBox . style . display = 'none' ; }, 4500 ); }
2026-05-14 01:45:27 +07:00
function postUIState ( patch ) { return apiRequest ( '/api/ui-state' , { method : 'POST' , body : JSON . stringify ( patch ) }); }
2026-05-14 00:14:25 +07:00
function renderOptions ( select , items , placeholder ) { select . replaceChildren (); const first = document . createElement ( 'option' ); first . value = '' ; first . textContent = placeholder ; select . appendChild ( first ); for ( const item of items ) { const option = document . createElement ( 'option' ); option . value = item . id ; option . textContent = item . name ; select . appendChild ( option ); } }
function appendOptions ( select , items ) { const existing = new Set ([... select . options ]. map (( option ) => option . value )); for ( const item of items ) { if ( existing . has ( item . id )) continue ; const option = document . createElement ( 'option' ); option . value = item . id ; option . textContent = item . name ; select . appendChild ( option ); } }
function appendEmpty ( parent , message ) { const empty = document . createElement ( 'div' ); empty . className = 'empty' ; empty . textContent = message ; parent . appendChild ( empty ); }
function appendBadge ( parent , label , className ) { const badge = document . createElement ( 'span' ); badge . className = `badge ${ className } ` ; badge . textContent = label ; parent . appendChild ( badge ); }
function parseMetadata ( value ) { if ( ! value ) return {}; try { return JSON . parse ( value ); } catch { return {}; } }
2026-05-15 15:58:38 +07:00
async function loadGuilds () { const guilds = await apiRequest ( '/api/guilds' ); renderOptions ( el . voiceGuildSelect , guilds , 'Select guild' ); renderOptions ( el . textGuildSelect , guilds , 'Select guild' ); if ( state . selectedVoiceGuild ) { el . voiceGuildSelect . value = state . selectedVoiceGuild ; await loadVoiceChannels ( state . selectedVoiceGuild ); } if ( state . selectedTextGuild ) { el . textGuildSelect . value = state . selectedTextGuild ; await loadTextChannels ( state . selectedTextGuild ); } }
async function loadVoiceChannels ( guildId ) { if ( ! guildId ) return renderOptions ( el . channelSelect , [], 'Select voice channel' ); const voiceChannels = await apiRequest ( `/api/guilds/ ${ guildId } /voice-channels` ); renderOptions ( el . channelSelect , voiceChannels , 'Select voice channel' ); if ( state . selectedVoiceChannel ) el . channelSelect . value = state . selectedVoiceChannel ; }
async function loadTextChannels ( guildId ) { if ( ! guildId ) return renderOptions ( el . channelFilter , [], 'Select channel' ); const watchChannels = await apiRequest ( `/api/guilds/ ${ guildId } /channels` ); renderOptions ( el . channelFilter , watchChannels , 'Select channel' ); if ( state . selectedTextChannel ) el . channelFilter . value = state . selectedTextChannel ; apiRequest ( `/api/guilds/ ${ guildId } /threads` ). then (( threads ) => { appendOptions ( el . channelFilter , threads ); if ( state . selectedTextChannel ) el . channelFilter . value = state . selectedTextChannel ; }). catch (( error ) => showError ( `Thread discovery failed: ${ error . message } ` )); }
2026-05-14 00:14:25 +07:00
async function refreshStatus () { try { const status = await apiRequest ( '/api/status' ); el . voiceStatusText . textContent = status . connected ? status . activeChannelName || 'Connected' : 'Not connected' ; el . voiceStatusNote . textContent = status . connected ? `Connected to ${ status . activeChannelName } ` : 'Idle' ; } catch ( error ) { showError ( error . message ); } }
2026-05-15 15:58:38 +07:00
async function connectVoice () { const guildId = el . voiceGuildSelect . value ; const channelId = el . channelSelect . value ; if ( ! guildId || ! channelId ) return showError ( 'Select guild and voice channel first' ); await postUIState ({ selectedVoiceGuild : guildId , selectedVoiceChannel : channelId }); const status = await apiRequest ( '/api/connect' , { method : 'POST' , body : JSON . stringify ({ guildId , channelId }) }); el . voiceStatusText . textContent = status . activeChannelName || 'Connected' ; el . voiceStatusNote . textContent = `Connected to ${ status . activeChannelName } ` ; }
2026-05-14 00:14:25 +07:00
async function disconnectVoice () { await apiRequest ( '/api/disconnect' , { method : 'POST' }); await refreshStatus (); }
function connectWebSocket () { const protocol = location . protocol === 'https:' ? 'wss:' : 'ws:' ; state . socket = new WebSocket ( ` ${ protocol } // ${ location . host } /ws` ); state . socket . binaryType = 'arraybuffer' ; state . socket . onopen = () => { el . wsDot . classList . add ( 'on' ); el . wsStatusText . textContent = 'Connected' ; }; state . socket . onclose = () => { el . wsDot . classList . remove ( 'on' ); el . wsStatusText . textContent = 'Reconnecting' ; setTimeout ( connectWebSocket , 2500 ); }; state . socket . onerror = () => { el . wsDot . classList . remove ( 'on' ); el . wsDot . classList . add ( 'warn' ); el . wsStatusText . textContent = 'Socket error' ; }; state . socket . onmessage = ( event ) => { if ( event . data instanceof ArrayBuffer ) { handleIncomingPCM ( event . data ); return ; } try { handleJsonEvent ( event . data ); } catch {} }; }
2026-05-15 18:03:01 +07:00
function handleJsonEvent ( raw ) { const message = JSON . parse ( raw ); if ( message . type === 'ui_state' ) return applyServerState ( message . state ); if ( message . type === 'user_state' ) return renderUsers ( message . users || []); if ( message . type === 'message_created' ) { state . text . unshift ( message . data ); renderText (); } if ( message . type === 'message_updated' ) { const item = state . text . find (( entry ) => entry . id === message . data . id ); if ( item ) Object . assign ( item , { edited_content : message . data . edited_content , edited_at : message . data . edited_at , type : 'edited' }); renderText (); } if ( message . type === 'message_deleted' ) { const item = state . text . find (( entry ) => entry . id === message . data . id ); if ( item ) Object . assign ( item , { deleted_at : message . data . deleted_at , type : 'deleted' }); renderText (); } if ( message . type === 'attachment_uploaded' ) fetchText (); if ( message . type === 'message_analyzed' ) { const item = state . text . find (( entry ) => entry . id === message . data . id ); if ( item ) Object . assign ( item , message . data ); renderText (); } if ( message . type === 'media_state' ) { state . media = message . state ; renderMedia (); } }
2026-05-14 00:14:25 +07:00
2026-05-14 01:45:27 +07:00
async function applyServerState ( next ) {
if ( ! next || state . applyingServerState ) return ;
state . applyingServerState = true ;
2026-05-15 15:58:38 +07:00
const nextVoiceGuild = next . selectedVoiceGuild || next . selectedGuild || '' ;
const nextTextGuild = next . selectedTextGuild || next . selectedGuild || '' ;
const voiceGuildChanged = nextVoiceGuild !== state . selectedVoiceGuild ;
const textGuildChanged = nextTextGuild !== state . selectedTextGuild ;
2026-05-14 01:45:27 +07:00
const textChanged = next . selectedTextChannel !== state . selectedTextChannel ;
2026-05-15 15:58:38 +07:00
state . selectedVoiceGuild = nextVoiceGuild ;
2026-05-14 01:45:27 +07:00
state . selectedVoiceChannel = next . selectedVoiceChannel || '' ;
2026-05-15 15:58:38 +07:00
state . selectedTextGuild = nextTextGuild ;
2026-05-14 01:45:27 +07:00
state . selectedTextChannel = next . selectedTextChannel || '' ;
state . activeTab = next . activeTab || 'voice' ;
state . isListening = !! next . isListening ;
state . isStreaming = !! next . isStreaming ;
2026-05-15 15:58:38 +07:00
el . voiceGuildSelect . value = state . selectedVoiceGuild ;
el . textGuildSelect . value = state . selectedTextGuild ;
if ( voiceGuildChanged ) await loadVoiceChannels ( state . selectedVoiceGuild );
if ( textGuildChanged ) await loadTextChannels ( state . selectedTextGuild );
2026-05-14 01:45:27 +07:00
el . channelSelect . value = state . selectedVoiceChannel ;
el . channelFilter . value = state . selectedTextChannel ;
applyActiveTab ( state . activeTab );
2026-05-15 15:58:38 +07:00
if ( textChanged || textGuildChanged || state . activeTab === 'text' ) {
if ( state . selectedTextChannel && state . selectedTextGuild ) {
2026-05-14 04:12:33 +07:00
await apiRequest ( '/api/backlog-sync' , {
method : 'POST' ,
2026-05-15 15:58:38 +07:00
body : JSON . stringify ({ guildId : state . selectedTextGuild , channelId : state . selectedTextChannel }),
2026-05-14 04:12:33 +07:00
}). catch (( error ) => logger . warn ( 'Backlog sync failed:' , error . message ));
}
await fetchText (). catch (( error ) => showError ( error . message ));
}
2026-05-14 01:45:27 +07:00
await reconcileListenState ();
await reconcileStreamingState ();
state . applyingServerState = false ;
}
function applyActiveTab ( tab ) { document . querySelectorAll ( '.tab-btn' ). forEach (( item ) => item . classList . toggle ( 'active' , item . dataset . tab === tab )); document . querySelectorAll ( '.tab-content' ). forEach (( item ) => item . classList . toggle ( 'active' , item . id === tab )); el . activeTabLabel . textContent = tab === 'text' ? 'Text' : 'Voice' ; }
async function reconcileListenState () { if ( state . isListening && ! state . localListening ) { try { await startListeningLocal (); } catch ( error ) { showError ( `Speaker error: ${ error . message } ` ); await postUIState ({ isListening : false }); } } else if ( ! state . isListening && state . localListening ) { stopListeningLocal (); } }
async function reconcileStreamingState () { if ( state . isStreaming && ! state . localStreaming ) { try { await startStreamingLocal (); } catch ( error ) { showError ( `Microphone error: ${ error . message } ` ); await postUIState ({ isStreaming : false }); } } else if ( ! state . isStreaming && state . localStreaming ) { stopStreamingLocal (); } }
function renderUsers ( users ) { el . userList . replaceChildren (); if ( users . length === 0 ) return appendEmpty ( el . userList , 'No active speakers' ); for ( const user of users ) { const row = document . createElement ( 'div' ); row . className = `user-item ${ user . speaking ? ' speaking' : '' } ` ; const img = document . createElement ( 'img' ); img . src = user . avatar || '' ; img . alt = '' ; const name = document . createElement ( 'span' ); name . textContent = user . username ; row . append ( img , name ); el . userList . appendChild ( row ); } }
async function fetchText () { if ( ! state . selectedTextChannel ) return renderText (); const result = await apiRequest ( `/api/messages?channel= ${ encodeURIComponent ( state . selectedTextChannel ) } &type=text&limit=80` ); state . text = result . data || []; renderText (); }
2026-05-14 04:33:48 +07:00
function renderText () { el . textList . replaceChildren (); el . reviewList . replaceChildren (); if ( ! state . selectedTextChannel ) { appendEmpty ( el . textList , 'Select channel to view text captures' ); appendEmpty ( el . reviewList , 'No warned or flagged messages' ); return ; } if ( state . text . length === 0 ) { appendEmpty ( el . textList , 'No text captures yet' ); appendEmpty ( el . reviewList , 'No warned or flagged messages' ); return ; } const reviewMessages = []; for ( const msg of state . text ) { const metadata = parseMetadata ( msg . metadata ); const card = document . createElement ( 'article' ); card . className = 'event-card' ; const head = document . createElement ( 'div' ); head . className = 'event-head' ; const author = document . createElement ( 'div' ); author . className = 'author' ; const avatar = document . createElement ( 'div' ); avatar . className = 'avatar' ; if ( msg . avatar_url ) { const img = document . createElement ( 'img' ); img . src = msg . avatar_url ; img . alt = '' ; avatar . appendChild ( img ); } const name = document . createElement ( 'div' ); name . className = 'name' ; name . textContent = msg . username || msg . user_id ; author . append ( avatar , name ); const time = document . createElement ( 'div' ); time . className = 'time' ; time . textContent = new Date ( msg . created_at ). toLocaleString (); head . append ( author , time ); const text = document . createElement ( 'div' ); text . className = 'message-text' ; text . textContent = msg . edited_content || msg . content || '(empty message)' ; card . append ( head , text ); appendAIAnalysis ( card , msg ); appendMedia ( card , metadata ); const badges = document . createElement ( 'div' ); badges . className = 'badges' ; if ( metadata . reference ? . messageId ) appendBadge ( badges , 'reply' , '' ); if ( msg . thread_id ) appendBadge ( badges , metadata . channel ? . threadName ? `thread: ${ metadata . channel . threadName } ` : 'thread' , '' ); if ( msg . edited_at ) appendBadge ( badges , 'edited' , 'edit' ); if ( msg . deleted_at ) appendBadge ( badges , 'deleted' , 'delete' ); card . appendChild ( badges ); el . textList . appendChild ( card ); if ( msg . ai_status === 'warn' || msg . ai_status === 'flagged' ) reviewMessages . push ( card . cloneNode ( true )); } if ( reviewMessages . length === 0 ) { appendEmpty ( el . reviewList , 'No warned or flagged messages' ); } else { reviewMessages . forEach (( card ) => el . reviewList . appendChild ( card )); } }
2026-05-14 02:31:16 +07:00
function appendAIAnalysis ( card , msg ) { const status = msg . ai_status || 'pending' ; const wrap = document . createElement ( 'div' ); wrap . className = 'badges' ; const badge = document . createElement ( 'span' ); badge . className = `badge ${ status === 'flagged' ? 'delete' : status === 'clean' ? 'edit' : '' } ` ; badge . textContent = `AI: ${ status } ` ; wrap . appendChild ( badge ); if ( msg . ai_moderation_flags ) { const flags = document . createElement ( 'span' ); flags . className = 'badge delete' ; try { flags . textContent = JSON . parse ( msg . ai_moderation_flags ). join ( ', ' ); } catch { flags . textContent = msg . ai_moderation_flags ; } wrap . appendChild ( flags ); } card . appendChild ( wrap ); if ( msg . ai_analysis ) { const analysis = document . createElement ( 'div' ); analysis . className = 'embed-description' ; analysis . textContent = msg . ai_analysis ; card . appendChild ( analysis ); } if ( msg . ai_error ) { const error = document . createElement ( 'div' ); error . className = 'embed-description' ; error . textContent = `AI error: ${ msg . ai_error } ` ; card . appendChild ( error ); } }
2026-05-14 00:14:25 +07:00
function appendMedia ( card , metadata ) { const stickers = document . createElement ( 'div' ); stickers . className = 'sticker-strip' ; for ( const sticker of metadata . stickers || []) { const img = document . createElement ( 'img' ); img . className = 'sticker-img' ; img . src = sticker . url ; img . alt = sticker . name ; stickers . appendChild ( img ); } if ( stickers . childElementCount ) card . appendChild ( stickers ); const embeds = document . createElement ( 'div' ); embeds . className = 'feed' ; for ( const embed of metadata . embeds || []) { const item = document . createElement ( 'div' ); item . className = 'embed-card' ; if ( embed . title ) { const title = document . createElement ( embed . url ? 'a' : 'div' ); title . className = 'embed-title' ; title . textContent = embed . title ; if ( embed . url ) { title . href = embed . url ; title . target = '_blank' ; title . rel = 'noreferrer' ; } item . appendChild ( title ); } if ( embed . description ) { const desc = document . createElement ( 'div' ); desc . className = 'embed-description' ; desc . textContent = embed . description ; item . appendChild ( desc ); } if ( embed . image || embed . thumbnail ) { const img = document . createElement ( 'img' ); img . className = 'embed-image' ; img . src = embed . image || embed . thumbnail ; img . alt = embed . title || 'embed image' ; item . appendChild ( img ); } embeds . appendChild ( item ); } if ( embeds . childElementCount ) card . appendChild ( embeds ); const attachments = document . createElement ( 'div' ); attachments . className = 'attachment-strip' ; for ( const attachment of metadata . attachments || []) { const link = document . createElement ( 'a' ); link . className = 'attachment-chip' ; link . href = attachment . url ; link . target = '_blank' ; link . rel = 'noreferrer' ; link . textContent = ` ${ attachment . name } ( ${ ( attachment . size / 1024 ). toFixed ( 1 ) } KB)` ; attachments . appendChild ( link ); } if ( attachments . childElementCount ) card . appendChild ( attachments ); }
2026-05-14 01:45:27 +07:00
function handleIncomingPCM ( data ) { if ( ! state . localListening || ! state . audioContextListen ) return ; const headerView = new DataView ( data , 0 , 4 ); const userIdHash = headerView . getInt32 ( 0 , true ); const audioData = data . slice ( 4 ); const int16Array = new Int16Array ( audioData ); const float32Array = new Float32Array ( int16Array . length ); for ( let i = 0 ; i < int16Array . length ; i ++ ) float32Array [ i ] = int16Array [ i ] / 32768 ; const audioBuffer = state . audioContextListen . createBuffer ( CHANNELS , float32Array . length / CHANNELS , SAMPLE_RATE ); const nowBuffering = audioBuffer . getChannelData ( 0 ); for ( let i = 0 ; i < audioBuffer . length ; i ++ ) nowBuffering [ i ] = float32Array [ i ]; const source = state . audioContextListen . createBufferSource (); source . buffer = audioBuffer ; source . connect ( state . audioContextListen . destination ); const currentTime = state . audioContextListen . currentTime ; let userNextStartTime = state . userTimelines . get ( userIdHash ) || 0 ; if ( userNextStartTime < currentTime ) userNextStartTime = currentTime + 0.05 ; source . start ( userNextStartTime ); userNextStartTime += audioBuffer . duration ; state . userTimelines . set ( userIdHash , userNextStartTime ); }
async function startStreamingLocal () { const stream = await navigator . mediaDevices . getUserMedia ({ audio : true }); state . localStreaming = true ; el . toggleBtn . textContent = 'Stop Transmitting' ; state . audioContextTransmit = new ( window . AudioContext || window . webkitAudioContext )({ sampleRate : SAMPLE_RATE }); const source = state . audioContextTransmit . createMediaStreamSource ( stream ); const analyser = state . audioContextTransmit . createAnalyser (); analyser . fftSize = 64 ; source . connect ( analyser ); const dataArray = new Uint8Array ( analyser . frequencyBinCount ); state . processor = state . audioContextTransmit . createScriptProcessor ( 4096 , 1 , 1 ); source . connect ( state . processor ); state . processor . connect ( state . audioContextTransmit . destination ); state . processor . onaudioprocess = ( event ) => { if ( ! state . localStreaming || state . socket . readyState !== WebSocket . OPEN ) return ; const inputData = event . inputBuffer . getChannelData ( 0 ); const pcmData = new Int16Array ( inputData . length ); for ( let i = 0 ; i < inputData . length ; i ++ ) pcmData [ i ] = Math . max ( - 1 , Math . min ( 1 , inputData [ i ])) * 32767 ; state . socket . send ( pcmData . buffer ); analyser . getByteFrequencyData ( dataArray ); bars . forEach (( bar , index ) => { const percent = ( dataArray [ index ] / 255 ) * 100 ; bar . style . height = ` ${ Math . max ( 2 , percent ) } %` ; }); }; }
function stopStreamingLocal () { state . localStreaming = false ; if ( state . processor ) state . processor . disconnect (); if ( state . audioContextTransmit ) state . audioContextTransmit . close (); state . processor = null ; state . audioContextTransmit = null ; el . toggleBtn . textContent = 'Start Transmitting' ; bars . forEach (( bar ) => { bar . style . height = '2px' ; }); }
async function startListeningLocal () { if ( ! state . audioContextListen ) state . audioContextListen = new ( window . AudioContext || window . webkitAudioContext )({ sampleRate : SAMPLE_RATE }); await state . audioContextListen . resume (); state . localListening = true ; el . listenBtn . textContent = 'Stop Listening' ; el . listenStatus . textContent = 'Listening Live...' ; }
function stopListeningLocal () { state . audioContextListen ? . suspend (); state . userTimelines . clear (); state . localListening = false ; el . listenBtn . textContent = 'Join Listen Channel' ; el . listenStatus . textContent = 'Speaker Off' ; }
2026-05-14 00:14:25 +07:00
function updateVisualizer ( level ) { bars . forEach (( bar , index ) => { const wave = Math . sin ( index * 0.55 + Date . now () / 140 ) * 0.35 + 0.65 ; bar . style . height = ` ${ Math . max ( 3 , level * 190 * wave ) } px` ; }); }
2026-05-14 01:45:27 +07:00
document . querySelectorAll ( '.tab-btn' ). forEach (( button ) => { button . addEventListener ( 'click' , () => postUIState ({ activeTab : button . dataset . tab }). catch (( error ) => showError ( error . message ))); });
2026-05-15 15:58:38 +07:00
el . voiceGuildSelect . addEventListener ( 'change' , () => postUIState ({ selectedVoiceGuild : el . voiceGuildSelect . value , selectedVoiceChannel : '' }). catch (( error ) => showError ( error . message )));
el . textGuildSelect . addEventListener ( 'change' , () => postUIState ({ selectedTextGuild : el . textGuildSelect . value , selectedTextChannel : '' }). catch (( error ) => showError ( error . message )));
2026-05-14 01:45:27 +07:00
el . channelSelect . addEventListener ( 'change' , () => postUIState ({ selectedVoiceChannel : el . channelSelect . value }). catch (( error ) => showError ( error . message )));
2026-05-14 00:14:25 +07:00
el . joinVoiceBtn . addEventListener ( 'click' , () => connectVoice (). catch (( error ) => showError ( error . message )));
el . disconnectVoiceBtn . addEventListener ( 'click' , () => disconnectVoice (). catch (( error ) => showError ( error . message )));
2026-05-14 01:45:27 +07:00
el . toggleBtn . addEventListener ( 'click' , () => postUIState ({ isStreaming : ! state . isStreaming }). catch (( error ) => showError ( error . message )));
el . listenBtn . addEventListener ( 'click' , () => postUIState ({ isListening : ! state . isListening }). catch (( error ) => showError ( error . message )));
2026-05-15 15:58:38 +07:00
el . channelFilter . addEventListener ( 'change' , () => { const selectedTextChannel = el . channelFilter . value ; const url = new URL ( location . href ); if ( selectedTextChannel ) url . searchParams . set ( 'channel' , selectedTextChannel ); else url . searchParams . delete ( 'channel' ); if ( el . textGuildSelect . value ) url . searchParams . set ( 'guild' , el . textGuildSelect . value ); history . replaceState ({}, '' , url ); postUIState ({ selectedTextChannel }). catch (( error ) => showError ( error . message )); });
2026-05-14 01:45:27 +07:00
2026-05-15 18:03:01 +07:00
async function fetchMediaStatus () { state . media = await apiRequest ( '/api/media/status' ); renderMedia (); }
async function queueMedia () { const source = el . mediaSourceInput . value . trim (); if ( ! source ) return showError ( 'Enter a music URL or local file path' ); state . media = await apiRequest ( '/api/media/queue' , { method : 'POST' , body : JSON . stringify ({ source }) }); el . mediaSourceInput . value = '' ; renderMedia (); }
async function skipMedia () { state . media = await apiRequest ( '/api/media/skip' , { method : 'POST' }); renderMedia (); }
async function stopMedia () { state . media = await apiRequest ( '/api/media/stop' , { method : 'POST' }); renderMedia (); }
function renderMedia () { el . mediaQueueList . replaceChildren (); const current = state . media . current ; el . mediaStatus . textContent = current ? `Playing ${ current . title } ` : 'Idle' ; if ( current ) { const item = document . createElement ( 'div' ); item . className = 'event-card' ; item . textContent = `Now: ${ current . title } ` ; el . mediaQueueList . appendChild ( item ); } for ( const queued of state . media . queue || []) { const item = document . createElement ( 'div' ); item . className = 'event-card' ; item . textContent = queued . title ; el . mediaQueueList . appendChild ( item ); } if ( ! current && ( ! state . media . queue || state . media . queue . length === 0 )) appendEmpty ( el . mediaQueueList , 'No media queued' ); }
el . queueMediaBtn . addEventListener ( 'click' , () => queueMedia (). catch (( error ) => showError ( error . message )));
el . skipMediaBtn . addEventListener ( 'click' , () => skipMedia (). catch (( error ) => showError ( error . message )));
el . stopMediaBtn . addEventListener ( 'click' , () => stopMedia (). catch (( error ) => showError ( error . message )));
async function fetchMediaStatus () { state . media = await apiRequest ( '/api/media/status' ); renderMedia (); }
async function queueMedia () { const source = el . mediaSourceInput . value . trim (); if ( ! source ) return showError ( 'Enter a music URL or local file path' ); state . media = await apiRequest ( '/api/media/queue' , { method : 'POST' , body : JSON . stringify ({ source }) }); el . mediaSourceInput . value = '' ; renderMedia (); }
async function skipMedia () { state . media = await apiRequest ( '/api/media/skip' , { method : 'POST' }); renderMedia (); }
async function stopMedia () { state . media = await apiRequest ( '/api/media/stop' , { method : 'POST' }); renderMedia (); }
function renderMedia () { el . mediaQueueList . replaceChildren (); const current = state . media . current ; el . mediaStatus . textContent = current ? `Playing ${ current . title } ` : 'Idle' ; if ( current ) { const item = document . createElement ( 'div' ); item . className = 'event-card' ; item . textContent = `Now: ${ current . title } ` ; el . mediaQueueList . appendChild ( item ); } for ( const queued of state . media . queue || []) { const item = document . createElement ( 'div' ); item . className = 'event-card' ; item . textContent = queued . title ; el . mediaQueueList . appendChild ( item ); } if ( ! current && ( ! state . media . queue || state . media . queue . length === 0 )) appendEmpty ( el . mediaQueueList , 'No media queued' ); }
el . queueMediaBtn . addEventListener ( 'click' , () => queueMedia (). catch (( error ) => showError ( error . message )));
el . skipMediaBtn . addEventListener ( 'click' , () => skipMedia (). catch (( error ) => showError ( error . message )));
el . stopMediaBtn . addEventListener ( 'click' , () => stopMedia (). catch (( error ) => showError ( error . message )));
2026-05-14 01:45:27 +07:00
connectWebSocket ();
2026-05-15 18:03:01 +07:00
apiRequest ( '/api/ui-state' ). then ( applyServerState ). then (() => loadGuilds ()). then ( refreshStatus ). then ( fetchMediaStatus ). catch (( error ) => showError ( error . message ));
2026-05-14 01:45:27 +07:00
setInterval (() => { if ( state . activeTab === 'text' ) fetchText (). catch (() => {}); }, 7000 );
2026-05-14 00:14:25 +07:00
</ script >
</ body >
</ html >