diff --git a/web/js/diana.js b/web/js/diana.js deleted file mode 100644 index a30cb07..0000000 --- a/web/js/diana.js +++ /dev/null @@ -1,741 +0,0 @@ -import { app } from "../../../scripts/app.js"; - -console.log("%c🧝 DIANA: v16.0 - Interactive Assistant", "color: #9b59b6; font-size: 16px; font-weight: bold"); - -// ==================== CORE STATE ==================== -let diana = { - // UI elements - container: null, - canvas: null, - ctx: null, - emotionSpan: null, - phraseSpan: null, - pinButton: null, - audioButton: null, - - // Assets - sprites: {}, - audio: {}, - phrases: {}, - emotions: [], - frameCount: 16, - - // State - currentEmotion: "neutral", - currentFrame: 0, - speed: 1.0, - ready: false, - audioEnabled: false, - userActivated: false, - - // Cycle counters - cyclesRemaining: 0, - nextEmotion: null, - - // Phrase timer - phraseTimer: 0, - lastTimestamp: 0, - - // Animation cycles config - cycles: { - speaking: 1, - happy: 1, - surprised: 1, - waiting: 0, - greeting: 2, - drag: 1 // drag reaction - }, - - // WebSocket - ws: null, - - // UI state - isPinned: false, - isDragging: false, - - // Counters - eventCount: 0, - connected: false, - reconnectAttempts: 0 -}; - -const emotionIcons = { - 'neutral': '😐', - 'waiting': '⏳', - 'speaking': 'πŸ—£οΈ', - 'happy': '😊', - 'surprised': '😲' -}; - -// ==================== UTILITIES ==================== -function log(level, module, msg, data = null) { - const timestamp = new Date().toISOString().substr(11, 8); - const prefix = `[${timestamp}] [${module}]`; - - if (data) { - console.log(`${prefix} ${msg}`, data); - } else { - console.log(`${prefix} ${msg}`); - } -} - -// ==================== ASSET LOADING ==================== -async function loadLibrary() { - log('info', 'LIB', 'πŸ“₯ Loading from /diana/library...'); - - try { - const response = await fetch('/diana/library'); - log('info', 'LIB', `πŸ“Š Status: ${response.status}`); - - if (!response.ok) { - log('error', 'LIB', '❌ Failed to load library'); - return false; - } - - const data = await response.json(); - log('info', 'LIB', 'βœ… Library loaded'); - - diana.sprites = data.sprites; - diana.audio = data.audio; - diana.phrases = data.phrases; - diana.emotions = data.metadata.emotions; - diana.frameCount = data.metadata.frame_count; - - log('info', 'LIB', ` β€’ Emotions: ${diana.emotions.length} (${diana.emotions.join(', ')})`); - log('info', 'LIB', ` β€’ Audio files: ${Object.keys(diana.audio).length} (${Object.keys(diana.audio).join(', ')})`); - log('info', 'LIB', ` β€’ Phrases: ${Object.keys(diana.phrases).length}`); - - return true; - - } catch (e) { - log('error', 'LIB', `❌ Error: ${e.message}`); - return false; - } -} - -// ==================== WEBSOCKET CONNECTION ==================== -function connectToDiana() { - const wsUrl = `ws://${window.location.host}/diana/events`; - log('info', 'WS', `πŸ”Œ Connecting to ${wsUrl} (attempt ${diana.reconnectAttempts + 1})`); - - diana.ws = new WebSocket(wsUrl); - - diana.ws.onopen = () => { - log('ok', 'WS', 'βœ… WebSocket OPEN'); - diana.connected = true; - diana.reconnectAttempts = 0; - updateStatusBadge('connected'); - - // Send ping to test connection - diana.ws.send(JSON.stringify({ type: "ping" })); - }; - - diana.ws.onmessage = (event) => { - diana.eventCount++; - - log('raw', 'WS', `πŸ“¨ RAW #${diana.eventCount}: ${event.data}`); - - try { - const data = JSON.parse(event.data); - log('event', 'WS', `πŸ“¨ #${diana.eventCount}: ${data.type || 'unknown'}`); - - // Ignore events until user activates - if (!diana.userActivated) { - log('system', 'DIANA', `⏸️ Event ${data.type} ignored (user not activated)`); - return; - } - - if (data.type === 'start') { - log('event', 'HANDLER', 'πŸ”₯ START detected'); - handleEvent('start'); - - } else if (data.type === 'complete') { - log('event', 'HANDLER', 'πŸ”₯ COMPLETE detected'); - handleEvent('complete'); - - } else if (data.type === 'error') { - log('event', 'HANDLER', 'πŸ”₯ ERROR detected'); - handleEvent('error'); - - } else if (data.type === 'waiting') { - log('event', 'HANDLER', '⏳ WAITING detected'); - handleEvent('waiting'); - - } else if (data.type === 'pong') { - log('ok', 'WS', 'βœ… Connection alive'); - } - - } catch (e) { - log('error', 'WS', `❌ Parse error: ${e.message}`); - log('error', 'WS', `❌ Raw data: ${event.data}`); - } - }; - - diana.ws.onerror = (error) => { - log('error', 'WS', `❌ WebSocket error:`, error); - diana.connected = false; - updateStatusBadge('error'); - }; - - diana.ws.onclose = () => { - diana.reconnectAttempts++; - log('warn', 'WS', `πŸ”Œ WebSocket closed, reconnecting in 3s... (attempt ${diana.reconnectAttempts})`); - diana.connected = false; - updateStatusBadge('disconnected'); - setTimeout(connectToDiana, 3000); - }; -} - -// ==================== USER ACTIVATION ==================== -function activateUser() { - if (diana.userActivated) return; - - diana.userActivated = true; - log('system', 'DIANA', '🌟 User activated! Diana is now alive'); - - // Enable audio - enableAudio(); - - // Show greeting (animation + phrase) - sayGreeting(); - - // Remove activation handlers - const events = ['click', 'keydown', 'touchstart', 'mousedown']; - events.forEach(event => { - document.removeEventListener(event, activateUser); - }); -} - -// ==================== EVENT HANDLING ==================== -function handleEvent(eventType) { - log('event', 'HANDLER', `πŸ”₯ Processing event: ${eventType}`); - - if (!diana.sprites || Object.keys(diana.sprites).length === 0) { - log('error', 'HANDLER', '❌ No sprites loaded!'); - return; - } - - let emotion, cycles, nextEmotion, phraseKey; - - switch(eventType) { - case 'start': - emotion = 'speaking'; - cycles = 1; - nextEmotion = 'waiting'; - phraseKey = 'start'; - log('event', 'HANDLER', ` β†’ speaking (${cycles} cycle) β†’ waiting`); - break; - - case 'complete': - emotion = 'happy'; - cycles = 1; - nextEmotion = 'neutral'; - phraseKey = 'complete'; - log('event', 'HANDLER', ` β†’ happy (${cycles} cycle) β†’ neutral`); - break; - - case 'error': - emotion = 'surprised'; - cycles = 1; - nextEmotion = 'neutral'; - phraseKey = 'error'; - log('event', 'HANDLER', ` β†’ surprised (${cycles} cycle) β†’ neutral`); - break; - - case 'waiting': - emotion = 'waiting'; - cycles = 0; - nextEmotion = null; - phraseKey = 'waiting'; - log('event', 'HANDLER', ` β†’ waiting (infinite)`); - break; - - default: - log('warn', 'HANDLER', ` ⏭️ Unknown event: ${eventType}`); - return; - } - - // Set emotion - setEmotion(emotion, cycles, nextEmotion); - - // Play audio - playAudio(eventType); - - // Show phrase - showRandomPhrase(phraseKey); -} - -// ==================== EMOTIONS ==================== -function setEmotion(emotion, cycles, nextEmotion = null) { - log('emotion', 'EMOTION', `🎭 Setting: ${emotion} (${cycles} cycles) β†’ ${nextEmotion || 'none'}`); - - if (!diana.sprites[emotion]) { - log('warn', 'EMOTION', `⚠️ No sprites for emotion: ${emotion}`); - if (diana.sprites['neutral']) { - emotion = 'neutral'; - } else { - return; - } - } - - diana.currentEmotion = emotion; - diana.currentFrame = 0; - diana.cyclesRemaining = cycles; - diana.nextEmotion = nextEmotion; - - if (diana.emotionSpan) { - const icon = emotionIcons[emotion] || '🧝'; - diana.emotionSpan.innerText = `${icon} ${emotion}`; - log('emotion', 'EMOTION', ` βœ… UI updated to: ${icon} ${emotion}`); - } -} - -// ==================== ANIMATION LOOP ==================== -function animate(timestamp) { - if (!diana.ctx) { - requestAnimationFrame(animate); - return; - } - - // Update phrase timer - if (diana.lastTimestamp) { - const deltaTime = (timestamp - diana.lastTimestamp) / 1000; - - if (diana.phraseTimer > 0) { - diana.phraseTimer -= deltaTime; - if (diana.phraseTimer <= 0 && diana.phraseSpan) { - diana.phraseSpan.innerText = "πŸ’­ ..."; - log('timer', 'ANIM', `⏱️ Phrase timer done`); - } - } - } - diana.lastTimestamp = timestamp; - - // Draw sprite - if (diana.sprites[diana.currentEmotion]) { - const frames = diana.sprites[diana.currentEmotion]; - if (frames && frames.length > 0) { - const frameDelay = 1000 / 12 / diana.speed; - - if (!diana.lastFrameTime || timestamp - diana.lastFrameTime > frameDelay) { - diana.currentFrame = (diana.currentFrame + 1) % frames.length; - diana.lastFrameTime = timestamp; - - if (diana.currentFrame === 0 && diana.cyclesRemaining > 0) { - diana.cyclesRemaining--; - log('cycles', 'ANIM', `⏱️ Cycles left: ${diana.cyclesRemaining}`); - - if (diana.cyclesRemaining === 0 && diana.nextEmotion) { - log('cycles', 'ANIM', `↩️ Switching to ${diana.nextEmotion}`); - setEmotion(diana.nextEmotion, - diana.nextEmotion === 'waiting' ? 0 : 0, - null); - } - } - } - - const img = new Image(); - img.src = 'data:image/png;base64,' + frames[diana.currentFrame]; - img.onload = () => { - diana.ctx.clearRect(0, 0, 128, 128); - diana.ctx.drawImage(img, 0, 0, 128, 128); - }; - } - } else { - // Placeholder - diana.ctx.clearRect(0, 0, 128, 128); - diana.ctx.fillStyle = "#333"; - diana.ctx.fillRect(0, 0, 128, 128); - diana.ctx.fillStyle = "#9b59b6"; - diana.ctx.font = "bold 20px Arial"; - diana.ctx.fillText("🧝", 64, 64); - } - - requestAnimationFrame(animate); -} - -// ==================== AUDIO ==================== -const audioContext = new (window.AudioContext || window.webkitAudioContext)(); - -function enableAudio() { - if (diana.audioEnabled) return; - - diana.audioEnabled = true; - - if (diana.audioButton) { - diana.audioButton.style.color = '#00ff00'; - diana.audioButton.innerHTML = 'πŸ”Š'; - diana.audioButton.title = 'Sound enabled'; - } - - log('audio', 'AUDIO', 'πŸ”Š Audio enabled'); -} - -async function playAudio(eventType) { - if (!diana.audioEnabled) return; - - const audioMap = { - 'start': 'start', - 'complete': 'complete', - 'error': 'error', - 'greeting': 'greeting', - 'waiting': 'waiting' - }; - - const audioName = audioMap[eventType]; - log('audio', 'AUDIO', `πŸ”Š Attempting to play: ${audioName}`); - - if (!audioName) { - log('warn', 'AUDIO', ` ⚠️ No mapping for event: ${eventType}`); - return; - } - - if (!diana.audio[audioName]) { - log('warn', 'AUDIO', ` ⚠️ Audio file not found: ${audioName}`); - return; - } - - try { - const base64Data = diana.audio[audioName]; - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - log('audio', 'AUDIO', ` β€’ Decoded ${bytes.length} bytes`); - - const blob = new Blob([bytes], { type: 'audio/mpeg' }); - const url = URL.createObjectURL(blob); - const audio = new Audio(url); - audio.volume = 0.7; - - audio.onplay = () => log('ok', 'AUDIO', `βœ… Playing: ${audioName}`); - audio.onerror = (e) => log('error', 'AUDIO', `❌ Play error: ${e.message}`); - - await audio.play(); - - } catch (e) { - log('error', 'AUDIO', `❌ Error: ${e.message}`); - } -} - -// ==================== PHRASES ==================== -function showRandomPhrase(key) { - log('phrase', 'PHRASE', `πŸ’¬ Getting phrase for: ${key}`); - - if (!diana.phrases[key]) { - log('warn', 'PHRASE', ` ⚠️ No phrase category: ${key}`); - return; - } - - const phrases = diana.phrases[key]; - if (!phrases || phrases.length === 0) { - log('warn', 'PHRASE', ` ⚠️ Empty phrase list for: ${key}`); - return; - } - - const phrase = phrases[Math.floor(Math.random() * phrases.length)]; - log('ok', 'PHRASE', ` βœ… Selected: "${phrase}"`); - - if (diana.phraseSpan) { - diana.phraseSpan.innerText = `"${phrase}"`; - diana.phraseTimer = 15.0; - log('phrase', 'PHRASE', ` πŸ“ Displaying for 15s`); - } -} - -// ==================== GREETING ==================== -function sayGreeting() { - log('system', 'DIANA', 'πŸ‘‹ Diana here!'); - - if (diana.phraseSpan) { - if (diana.phrases && diana.phrases['greeting'] && diana.phrases['greeting'].length > 0) { - const phrases = diana.phrases['greeting']; - const phrase = phrases[Math.floor(Math.random() * phrases.length)]; - diana.phraseSpan.innerText = `"${phrase}"`; - diana.phraseTimer = 15.0; - } else { - diana.phraseSpan.innerText = '"Diana here!"'; - diana.phraseTimer = 15.0; - } - } - - playAudio('greeting'); - setEmotion('speaking', 2, 'neutral'); -} - -// ==================== DRAG REACTION ==================== -function handleDragStart() { - if (!diana.userActivated || diana.isDragging) return; - - diana.isDragging = true; - log('emotion', 'DRAG', 'πŸ–οΈ Drag started - showing surprise'); - - // Show surprise for 1 cycle - setEmotion('surprised', 1, diana.currentEmotion); -} - -function handleDragEnd() { - if (!diana.isDragging) return; - - diana.isDragging = false; - log('emotion', 'DRAG', 'βœ… Drag ended'); -} - -// ==================== UI ==================== -function updateStatusBadge(status) { - const badge = document.getElementById('diana-status-badge'); - if (!badge) return; - - const colors = { - 'connected': '#00ff00', - 'disconnected': '#ffff00', - 'error': '#ff0000' - }; - badge.style.color = colors[status] || '#ffff00'; - log('ui', 'UI', `πŸ“Š Status badge: ${status}`); -} - -function togglePin() { - diana.isPinned = !diana.isPinned; - - if (diana.pinButton) { - diana.pinButton.style.color = diana.isPinned ? '#ffaa00' : '#ffffff'; - } - - if (diana.isPinned && diana.container) { - diana.container.style.top = "auto"; - diana.container.style.bottom = "20px"; - diana.container.style.left = "auto"; - diana.container.style.right = "20px"; - log('ui', 'UI', `πŸ“Œ Pinned`); - } else { - log('ui', 'UI', `πŸ“Œ Unpinned`); - } -} - -// ==================== WINDOW CREATION ==================== -async function createDianaWindow() { - if (document.getElementById('diana-container')) return; - - log('ui', 'UI', 'πŸͺŸ Creating window...'); - - const container = document.createElement("div"); - container.id = "diana-container"; - container.style.cssText = ` - position: fixed; - top: 80px; - left: 50%; - transform: translateX(-50%); - width: 220px; - background: #2a2a2a; - border: 2px solid #9b59b6; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0,0,0,0.5); - z-index: 999999; - overflow: hidden; - font-family: 'Segoe UI', sans-serif; - user-select: none; - cursor: default; - `; - - const header = document.createElement("div"); - header.style.cssText = ` - background: #9b59b6; - color: white; - padding: 8px; - font-weight: bold; - display: flex; - justify-content: space-between; - cursor: ${diana.isPinned ? 'default' : 'move'}; - align-items: center; - `; - - const headerLeft = document.createElement("span"); - headerLeft.style.cssText = ` - display: flex; - align-items: center; - gap: 5px; - cursor: ${diana.isPinned ? 'default' : 'move'}; - `; - headerLeft.innerHTML = ` - 🧝 Diana - ● - `; - - const headerRight = document.createElement("span"); - headerRight.style.cssText = ` - display: flex; - align-items: center; - gap: 8px; - `; - - // Sound button - const audioBtn = document.createElement("span"); - audioBtn.innerHTML = "πŸ”‡"; - audioBtn.style.cssText = ` - cursor: pointer; - padding: 0 4px; - font-size: 14px; - transition: all 0.2s; - color: #ffffff; - `; - audioBtn.title = "Enable sound"; - audioBtn.onclick = activateUser; - diana.audioButton = audioBtn; - - const pinBtn = document.createElement("span"); - pinBtn.innerHTML = "πŸ“Œ"; - pinBtn.style.cssText = ` - cursor: pointer; - padding: 0 4px; - font-size: 14px; - transition: all 0.2s; - color: #ffffff; - `; - pinBtn.title = "Pin to corner / Free movement"; - pinBtn.onclick = togglePin; - diana.pinButton = pinBtn; - - const closeBtn = document.createElement("span"); - closeBtn.innerHTML = "βœ•"; - closeBtn.style.cssText = ` - cursor: pointer; - padding: 0 4px; - font-size: 14px; - `; - closeBtn.onclick = () => container.remove(); - - headerRight.appendChild(audioBtn); - headerRight.appendChild(pinBtn); - headerRight.appendChild(closeBtn); - header.appendChild(headerLeft); - header.appendChild(headerRight); - - const canvas = document.createElement("canvas"); - canvas.width = 128; - canvas.height = 128; - canvas.style.cssText = ` - width: 128px; - height: 128px; - display: block; - margin: 10px auto; - background: #1a1a1a; - border-radius: 5px; - `; - - const emotionDiv = document.createElement("div"); - emotionDiv.id = "diana-emotion"; - emotionDiv.style.cssText = ` - text-align: center; - color: #9b59b6; - padding: 5px; - font-size: 12px; - font-weight: bold; - background: #1a1a1a; - margin: 0 5px 5px 5px; - border-radius: 3px; - `; - emotionDiv.innerText = "😐 neutral"; - - const phraseDiv = document.createElement("div"); - phraseDiv.id = "diana-phrase"; - phraseDiv.style.cssText = ` - text-align: center; - color: #aaa; - padding: 8px; - font-size: 11px; - font-style: italic; - border-top: 1px solid #333; - min-height: 40px; - background: #1a1a1a; - word-break: break-word; - `; - phraseDiv.innerText = "πŸ’­ ..."; - - container.appendChild(header); - container.appendChild(canvas); - container.appendChild(emotionDiv); - container.appendChild(phraseDiv); - document.body.appendChild(container); - - diana.container = container; - diana.canvas = canvas; - diana.ctx = canvas.getContext("2d"); - diana.emotionSpan = emotionDiv; - diana.phraseSpan = phraseDiv; - - // Draggable with reaction - let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; - header.onmousedown = (e) => { - if (diana.isPinned) return; - - handleDragStart(); - - e.preventDefault(); - pos3 = e.clientX; - pos4 = e.clientY; - - document.onmouseup = () => { - handleDragEnd(); - document.onmouseup = null; - document.onmousemove = null; - }; - - document.onmousemove = (e) => { - e.preventDefault(); - pos1 = pos3 - e.clientX; - pos2 = pos4 - e.clientY; - pos3 = e.clientX; - pos4 = e.clientY; - - let newTop = container.offsetTop - pos2; - let newLeft = container.offsetLeft - pos1; - - newTop = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, newTop)); - newLeft = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, newLeft)); - - container.style.top = newTop + "px"; - container.style.left = newLeft + "px"; - container.style.bottom = "auto"; - container.style.right = "auto"; - }; - }; - - // User activation on first click - const events = ['click', 'keydown', 'touchstart', 'mousedown']; - events.forEach(event => { - document.addEventListener(event, activateUser, { once: true }); - }); - - // Load assets - const libLoaded = await loadLibrary(); - - if (libLoaded) { - requestAnimationFrame(animate); - - setTimeout(() => { - log('system', 'MAIN', 'πŸ”Œ Connecting to Diana...'); - connectToDiana(); - }, 2000); - } else { - log('error', 'MAIN', '❌ Library loading failed'); - } -} - -// ==================== INIT ==================== -log('system', 'MAIN', 'πŸš€ Initializing...'); - -setTimeout(createDianaWindow, 2000); -window.addEventListener('load', () => { - log('system', 'MAIN', 'πŸ“„ Window load event'); - setTimeout(createDianaWindow, 1000); -}); - -app.registerExtension({ - name: "ComfyUI.Diana", - async setup() { - log('system', 'MAIN', '🧩 Extension ready'); - } -}); \ No newline at end of file