diff --git a/Dockerfile b/Dockerfile index 638eebc..593c2c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,5 @@ RUN apt-get update && apt-get install -y --no-install-recommends libpython3.11 p echo 'alias pip="pip3"' >> /root/.bashrc && \ echo 'alias python="python3"' >> /root/.bashrc +WORKDIR /tmp CMD ["npm", "run", "start"] diff --git a/package-lock.json b/package-lock.json index a8725c9..dc12e24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@ztimson/ai-utils": "^0.8.7", + "@ztimson/utils": "^0.28.14", "cors": "^2.8.5", "express": "^4.18.2", "socket.io": "^4.6.1" @@ -324,9 +325,9 @@ "license": "MIT" }, "node_modules/@ztimson/utils": { - "version": "0.28.13", - "resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.28.13.tgz", - "integrity": "sha512-6nk7mW1vPX5QltTjCNK6u6y8UGYTOgeDadWHbrD+1QM5PZ1PbosMqVMQ6l4dhsQT7IHFxvLI3+U720T2fLGwoA==", + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.28.14.tgz", + "integrity": "sha512-ZI8kT1CV9La22w/E+HoPOsSV0ewgz0Rl4LXKIxFxH2dJVkXDCajgjfSHEfIHniifeOMma2nF+VO6yqT9BtvorQ==", "license": "MIT", "dependencies": { "var-persist": "^1.0.1" diff --git a/package.json b/package.json index 9530161..d2ae02e 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "dependencies": { "@ztimson/ai-utils": "^0.8.7", + "@ztimson/utils": "^0.28.14", "cors": "^2.8.5", "express": "^4.18.2", "socket.io": "^4.6.1" diff --git a/public/components/btn.mjs b/public/components/btn.mjs new file mode 100644 index 0000000..04f6c9b --- /dev/null +++ b/public/components/btn.mjs @@ -0,0 +1,162 @@ +function contrast(color) { + const exploded = color?.match(color.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g); + if(!exploded || exploded?.length < 3) return 'black'; + const [r, g, b] = exploded.map(hex => parseInt(hex.length === 1 ? `${hex}${hex}` : hex, 16)); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.5 ? 'black' : 'white'; +} + +function shadeColor(hex, amount) { + function dec2Hex(num) { + const hex = Math.round(num * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + } + + function hex2Int(hex) { + let r = 0, g = 0, b = 0; + if (hex.length === 4) { + r = parseInt(hex[1] + hex[1], 16); + g = parseInt(hex[2] + hex[2], 16); + b = parseInt(hex[3] + hex[3], 16); + } else { + r = parseInt(hex.slice(1, 3), 16); + g = parseInt(hex.slice(3, 5), 16); + b = parseInt(hex.slice(5, 7), 16); + } + return { r, g, b }; + } + + function hue2rgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + + function int2Hex(r, g, b) { + return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b); + } + + let { r, g, b } = hex2Int(hex); + r /= 255; g /= 255; b /= 255; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + default: h = 0; break; + } + h /= 6; + } + + l = Math.max(0, Math.min(1, l + amount)); + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + return int2Hex(hue2rgb(p, q, h + 1 / 3), hue2rgb(p, q, h), hue2rgb(p, q, h - 1 / 3)); +} + +class BtnComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + this.shadowRoot.innerHTML = ` + +
+ +
+ `; + + this.shadowRoot.querySelector('.btn').addEventListener('click', (e) => { + if(this.hasAttribute('disabled')) { + e.stopPropagation(); + e.preventDefault(); + } + }); + + this.updateColors(); + } + + static get observedAttributes() { + return ['color', 'disabled']; + } + + attributeChangedCallback(name, oldValue, newValue) { + if(name === 'color') this.updateColors(); + if(name === 'disabled') { + const disabled = this.hasAttribute('disabled'); + this.shadowRoot.querySelector('.btn').classList[disabled ? 'add' : 'remove']('disabled'); + } + } + + updateColors() { + const hex = this.getAttribute('color'); + this.shadowRoot.host.style.setProperty('--base-color', hex); + this.shadowRoot.host.style.setProperty('--dark-color', shadeColor(hex, -.1)); + this.shadowRoot.host.style.setProperty('--light-color', shadeColor(hex, .1)); + this.shadowRoot.host.style.setProperty('--text-color', contrast(hex)); + } +} + +customElements.define('btn-component', BtnComponent); diff --git a/public/jukebox.js b/public/components/jukebox.mjs similarity index 72% rename from public/jukebox.js rename to public/components/jukebox.mjs index 90e8f57..57af2ea 100644 --- a/public/jukebox.js +++ b/public/components/jukebox.mjs @@ -1,35 +1,28 @@ +import './btn.mjs'; + class JukeboxComponent extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); - - // Use global singleton 🎵 - this.api = window.netNaviAPI; + this.navi = window.navi; + this.navi.init(); + this.unsubscribeWorld = this.navi.on('world:data', (data) => { + console.log(data, this.navi.world.data); + this.render() + }); this.playlist = []; this.currentTrackIndex = 0; this.bgMusic = null; this.isMuted = false; this.hasInteracted = false; - this.theme = null; this.isPlaylistMode = false; } - connectedCallback() { - this.render(); - this.setupAPIListeners(); - } - disconnectedCallback() { if(this.unsubscribeWorld) this.unsubscribeWorld(); } - setupAPIListeners() { - this.unsubscribeWorld = this.api.on('world:loaded', (data) => { - if(data.theme?.music) this.loadMusic(data.theme.music, data.theme); - }); - } - render() { this.shadowRoot.innerHTML = ` - + `; @@ -147,18 +110,15 @@ class JukeboxComponent extends HTMLElement { this.shadowRoot.getElementById('mute-btn').addEventListener('click', () => this.toggleMute()); this.shadowRoot.getElementById('prev-btn').addEventListener('click', () => this.previousTrack()); this.shadowRoot.getElementById('next-btn').addEventListener('click', () => this.nextTrack()); + this.loadMusic(this.navi.world.data.theme.music); } - loadMusic(musicConfig, theme) { - if (!musicConfig) return; - - this.theme = theme; + loadMusic(musicConfig) { + if(!musicConfig) return; this.isPlaylistMode = Array.isArray(musicConfig); this.playlist = Array.isArray(musicConfig) ? musicConfig : [musicConfig]; this.currentTrackIndex = 0; - this.applyThemeColors(); - if (this.isPlaylistMode) { this.shadowRoot.getElementById('simple-mute-btn').classList.add('hidden'); this.shadowRoot.getElementById('playlist-controls').classList.remove('hidden'); @@ -205,15 +165,6 @@ class JukeboxComponent extends HTMLElement { trackName.textContent = `[${trackNum}] ${fileName}`; } - applyThemeColors() { - if (!this.theme) return; - const root = this.shadowRoot.host.style; - Object.entries(this.theme.colors).forEach(([key, value]) => { - const cssVar = '--' + key.replace(/([A-Z])/g, '-$1').toLowerCase(); - root.setProperty(cssVar, value); - }); - } - setupAutoplayHandler() { const startMusic = () => { if (!this.hasInteracted && !this.isMuted && this.bgMusic) { diff --git a/public/llm.js b/public/components/llm.mjs similarity index 71% rename from public/llm.js rename to public/components/llm.mjs index 85ea7d2..5c23aea 100644 --- a/public/llm.js +++ b/public/components/llm.mjs @@ -1,3 +1,5 @@ +import './btn.mjs'; + class LlmComponent extends HTMLElement { hideTools = ['adjust_personality', 'recall', 'remember'] @@ -6,11 +8,9 @@ class LlmComponent extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); + this.navi = window.navi; + this.navi.init().then(() => this.render()); - // Use global singleton 🔥 - this.api = window.netNaviAPI; - - this.isTyping = false; this.isReceiving = false; this.streamComplete = false; this.isDialogueOpen = false; @@ -23,9 +23,6 @@ class LlmComponent extends HTMLElement { this.currentRequest = null; this.attachedFiles = []; this.currentStreamingMessage = null; - - this.render(); - this.initEventListeners(); } render() { @@ -41,8 +38,8 @@ class LlmComponent extends HTMLElement { } ::-webkit-scrollbar-thumb { - background: var(--dialogue-header-bg, #fff); - border: 2px solid var(--dialog-border, #000); + background: ${this.navi.theme.primary}; + border: 2px solid ${this.navi.theme.border}; border-radius: 9px; } @@ -76,8 +73,8 @@ class LlmComponent extends HTMLElement { } .dialogue-content { - background: var(--dialogue-bg, #fff); - border: 5px solid var(--dialogue-border, #000); + background: ${this.navi.theme.background}; + border: 5px solid ${this.navi.theme.border}; border-radius: 16px 16px 0 0; padding: 0; box-shadow: 0 -4px 20px rgba(0,0,0,0.5); @@ -95,9 +92,9 @@ class LlmComponent extends HTMLElement { .dialogue-header { user-select: none; - padding: 12px 20px; - background: var(--dialogue-header-bg, #fff); - border-bottom: 3px solid var(--dialogue-border, #000); + padding: 10px 10px 10px 15px; + background: ${this.navi.theme.primary}; + border-bottom: 3px solid ${this.navi.theme.border}; border-radius: 12px 12px 0 0; display: flex; align-items: center; @@ -109,47 +106,13 @@ class LlmComponent extends HTMLElement { border-radius: 0; } - .speaker-name { - font-weight: bold; - font-size: 16px; - color: var(--dialogue-text, #000); - font-family: 'Courier New', monospace; - pointer-events: none; - } - - .expand-btn { - background: var(--button-bg, #4a90e2); - border: 2px solid var(--dialogue-border, #000); - color: var(--button-text, #fff); - padding: 8px; - font-family: 'Courier New', monospace; - font-weight: bold; - font-size: 14px; - border-radius: 4px; - box-shadow: 0 3px 0 var(--button-shadow, #2a5a9a); - display: flex; - align-items: center; - gap: 4px; - } - - .expand-btn:active { - transform: translateY(2px); - box-shadow: 0 1px 0 var(--button-shadow, #2a5a9a); - } - - .expand-icon { - width: 20px; - height: 20px; - flex-shrink: 0; - } - .message-body { - padding: 20px; + padding: 1.75rem 1.25rem; flex: 1; overflow-y: auto; display: flex; flex-direction: column; - gap: 16px; + gap: 1.75rem; min-height: 200px; max-height: 400px; } @@ -176,51 +139,41 @@ class LlmComponent extends HTMLElement { .message-label { font-size: 12px; font-weight: bold; - color: var(--dialogue-text, #000); + color: ${this.navi.theme.text}; font-family: 'Courier New', monospace; opacity: 0.7; padding: 0 8px; } .message-bubble { - max-width: 80%; - padding: 12px 16px; - border-radius: 12px; font-size: 15px; line-height: 1.5; font-family: 'Courier New', monospace; word-wrap: break-word; - border: 2px solid var(--dialogue-border, #000); - box-shadow: 2px 2px 0 rgba(0,0,0,0.2); user-select: text; } .message-wrapper.user .message-bubble { - background: var(--button-bg, #4a90e2); - color: var(--button-text, #fff); + background: ${this.navi.theme.primary}; + color: ${this.navi.theme.primaryContrast}; + padding: 0.5rem 0.75rem; + border-radius: 12px; + border: 2px solid ${this.navi.theme.border}; } - .message-wrapper.assistant .message-bubble { - background: var(--dialogue-bg, #fff); - color: var(--dialogue-text, #000); - } - - .message-timestamp { - font-size: 10px; - opacity: 0.6; - padding: 0 8px; - font-family: 'Courier New', monospace; - color: var(--dialogue-text, #000); + .message-wrapper.navi .message-bubble { + color: ${this.navi.theme.text}; } .empty-state { display: flex; align-items: center; justify-content: center; + text-align: center;; height: 100%; font-size: 32px; font-weight: bold; - color: var(--dialogue-text, #000); + color: ${this.navi.theme.text}; font-family: 'Courier New', monospace; text-shadow: 3px 3px 0 rgba(74, 144, 226, 0.3), @@ -262,7 +215,7 @@ class LlmComponent extends HTMLElement { .tool-call { display: inline-block; - background: var(--button-bg, #000); + background: ${this.navi.theme.primary}; color: #fff; padding: 2px 8px; margin: 2px; @@ -281,11 +234,11 @@ class LlmComponent extends HTMLElement { .file-badge { display: flex; align-items: center; - background: var(--button-bg, #4a90e2); - color: var(--button-text, #fff); + background: ${this.navi.theme.primary}; + color: ${this.navi.theme.primaryContrast}; padding: 4px 10px; margin: 0; - border: 2px solid var(--dialogue-border, #000); + border: 2px solid ${this.navi.theme.border}; border-radius: 6px; font-size: 13px; font-family: 'Courier New', monospace; @@ -304,14 +257,14 @@ class LlmComponent extends HTMLElement { justify-content: flex-end; } - .message-wrapper.assistant .message-files { + .message-wrapper.navi .message-files { justify-content: flex-start; } .attached-files { padding: 8px 12px; - border-top: 3px solid var(--dialogue-border, #000); - background: var(--dialogue-input-bg, #f0f0f0); + border-top: 3px solid ${this.navi.theme.border}; + background: ${this.navi.theme.backgroundDark}; display: none; flex-wrap: wrap; gap: 6px; @@ -325,8 +278,9 @@ class LlmComponent extends HTMLElement { .attached-file { display: inline-flex; align-items: center; - background: var(--dialogue-bg, #fff); - border: 2px solid var(--dialogue-border, #000); + background: ${this.navi.theme.primary}; + color: ${this.navi.theme.primaryContrast}; + border: 2px solid ${this.navi.theme.border}; padding: 6px 10px; border-radius: 6px; font-family: 'Courier New', monospace; @@ -343,7 +297,7 @@ class LlmComponent extends HTMLElement { .attached-file .remove-file { background: #e74c3c; - border: none; + border: 1px solid black; color: #fff; padding: 2px 6px; border-radius: 3px; @@ -357,8 +311,8 @@ class LlmComponent extends HTMLElement { .dialogue-input { padding: 12px 20px; - border-top: 3px solid var(--dialogue-border, #000); - background: var(--dialogue-input-bg, #f0f0f0); + border-top: 3px solid ${this.navi.theme.border}; + background: ${this.navi.theme.backgroundDark}; flex-shrink: 0; } @@ -368,13 +322,13 @@ class LlmComponent extends HTMLElement { .dialogue-input textarea { width: 100%; - background: var(--dialogue-bg, #fff); - border: 3px solid var(--dialogue-border, #000); + background: ${this.navi.theme.background}; + border: 3px solid ${this.navi.theme.border}; padding: 10px 14px; font-family: 'Courier New', monospace; font-size: 16px; border-radius: 4px; - color: var(--dialogue-text, #000); + color: ${this.navi.theme.text}; resize: none; min-height: 44px; max-height: 120px; @@ -384,7 +338,6 @@ class LlmComponent extends HTMLElement { .dialogue-input textarea:focus { outline: none; - box-shadow: 0 0 0 2px var(--button-bg, #4a90e2); } .dialogue-input textarea:disabled { @@ -395,78 +348,57 @@ class LlmComponent extends HTMLElement { .button-row { display: flex; gap: 8px; + padding-bottom: 5px; } .clear-btn, .attach-btn, .dialogue-send-btn { - background: var(--button-bg, #4a90e2); - border: 3px solid var(--dialogue-border, #000); - color: var(--button-text, #fff); + background: ${this.navi.theme.accent}; + border: 3px solid ${this.navi.theme.border}; + color: ${this.navi.theme.accentContrast}; padding: 10px 18px; font-family: 'Courier New', monospace; font-weight: bold; font-size: 14px; border-radius: 4px; - box-shadow: 0 3px 0 var(--button-shadow, #2a5a9a); + box-shadow: 0 3px 0 ${this.navi.theme.accentDark}; transition: background 0.2s; white-space: nowrap; flex: 1; } - .clear-btn { - background: #e74c3c; - box-shadow: 0 3px 0 #c0392b; - } - - .clear-btn:active { - transform: translateY(2px); - box-shadow: 0 1px 0 #c0392b; - } - - .attach-btn:active, .dialogue-send-btn:active { - transform: translateY(2px); - box-shadow: 0 1px 0 var(--button-shadow, #2a5a9a); - } - - .attach-btn:disabled, .dialogue-send-btn:disabled, .clear-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - } - .dialogue-send-btn.stop { background: #e74c3c; - box-shadow: 0 3px 0 #c0392b; + box-shadow: 0 3px 0 #; } .dialogue-send-btn.stop:active { box-shadow: 0 1px 0 #c0392b; } - .dialogue-send-btn.skip { - background: #f39c12; - box-shadow: 0 3px 0 #d68910; - } - - .dialogue-send-btn.skip:active { - box-shadow: 0 1px 0 #d68910; - } - #file-input { display: none; } + + .flex-fill-even { + flex: 1 1 0; + }
- logo - +
NetNavi v1.0.0
@@ -477,89 +409,84 @@ class LlmComponent extends HTMLElement {
- - - + CLEAR + ATTACH + SEND
`; - } - initEventListeners() { - const dialogueBox = this.shadowRoot.getElementById('dialogue-box'); - const dialogueHeader = this.shadowRoot.getElementById('dialogue-header'); - const dialogueInput = this.shadowRoot.getElementById('dialogue-input'); - const dialogueSend = this.shadowRoot.getElementById('dialogue-send'); - const attachBtn = this.shadowRoot.getElementById('attach-btn'); - const fileInput = this.shadowRoot.getElementById('file-input'); - const clearBtn = this.shadowRoot.getElementById('clear-btn'); - const expandBtn = this.shadowRoot.getElementById('expand-btn'); + const dialogueHeader = this.shadowRoot.getElementById('dialogue-header'); + const dialogueInput = this.shadowRoot.getElementById('dialogue-input'); + const dialogueSend = this.shadowRoot.getElementById('dialogue-send'); + const attachBtn = this.shadowRoot.getElementById('attach-btn'); + const fileInput = this.shadowRoot.getElementById('file-input'); + const clearBtn = this.shadowRoot.getElementById('clear-btn'); + const expandBtn = this.shadowRoot.getElementById('expand-btn'); - dialogueInput.addEventListener('input', () => { - dialogueInput.style.height = 'auto'; - dialogueInput.style.height = Math.min(dialogueInput.scrollHeight, 120) + 'px'; - }); + dialogueInput.addEventListener('input', () => { + dialogueInput.style.height = 'auto'; + dialogueInput.style.height = Math.min(dialogueInput.scrollHeight, 120) + 'px'; + }); - dialogueInput.addEventListener('paste', (e) => { - const text = e.clipboardData.getData('text'); - if (text.length > 1000) { - e.preventDefault(); - const blob = new Blob([text], { type: 'text/plain' }); - const file = new File([blob], 'pasted_text.txt', { type: 'text/plain' }); - this.addFile(file); - } - }); + dialogueInput.addEventListener('paste', (e) => { + const text = e.clipboardData.getData('text'); + if (text.length > 1000) { + e.preventDefault(); + const blob = new Blob([text], { type: 'text/plain' }); + const file = new File([blob], 'pasted_text.txt', { type: 'text/plain' }); + this.addFile(file); + } + }); - dialogueHeader.addEventListener('click', (e) => { - if (e.target === dialogueHeader || e.target.classList.contains('speaker-name')) { - this.toggleDialogue(); - } - }); + dialogueHeader.addEventListener('click', (e) => { + this.toggleDialogue(); + }); - dialogueInput.addEventListener('focus', () => { - if (!this.isDialogueOpen) this.openDialogue(); - }); + dialogueInput.addEventListener('focus', () => { + if(!this.isDialogueOpen) this.openDialogue(); + }); - clearBtn.addEventListener('click', (e) => { - e.stopPropagation(); - this.clearChat(); - }); + clearBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.clearChat(); + }); - expandBtn.addEventListener('click', (e) => { - e.stopPropagation(); - this.toggleExpand(); - }); + expandBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleExpand(); + }); - attachBtn.addEventListener('click', () => fileInput.click()); + attachBtn.addEventListener('click', () => fileInput.click()); - fileInput.addEventListener('change', (e) => { - Array.from(e.target.files).forEach(file => this.addFile(file)); - fileInput.value = ''; - }); + fileInput.addEventListener('change', (e) => { + Array.from(e.target.files).forEach(file => this.addFile(file)); + fileInput.value = ''; + }); - dialogueSend.addEventListener('click', () => { - const buttonText = dialogueSend.textContent; - if (buttonText === 'SKIP') this.skipToEnd(); - else if (buttonText === 'STOP') this.abortStream(); - else this.sendMessage(); - }); + dialogueSend.addEventListener('click', () => { + const buttonText = dialogueSend.textContent; + if (buttonText === 'SKIP') this.skipToEnd(); + else if (buttonText === 'STOP') this.abortStream(); + else this.sendMessage(); + }); - dialogueInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter' && !e.shiftKey && !this.isReceiving) { - e.preventDefault(); - this.sendMessage(); - } - }); + dialogueInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !e.shiftKey && !this.isReceiving) { + e.preventDefault(); + this.sendMessage(); + } + }); } clearChat() { this.messageHistory = []; const messageBody = this.shadowRoot.getElementById('message-body'); messageBody.innerHTML = '
NetNavi v1.0.0
'; - this.api.clearLLMHistory(); + this.navi.clearChat(); } toggleExpand() { @@ -607,7 +534,7 @@ class LlmComponent extends HTMLElement { container.classList.add('has-files'); container.innerHTML = this.attachedFiles.map((file, i) => `
- 📄 ${file.name} + ${file.name}
`).join(''); @@ -628,11 +555,6 @@ class LlmComponent extends HTMLElement { }); } - processMessageForDisplay(text) { - return text.replace(/[\s\S]*?<\/file>/g, - '📄 $1'); - } - formatTime(date) { const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); @@ -659,16 +581,6 @@ class LlmComponent extends HTMLElement { })); } - closeDialogue() { - this.isDialogueOpen = false; - const dialogueBox = this.shadowRoot.getElementById('dialogue-box'); - dialogueBox.classList.add('minimized'); - - this.dispatchEvent(new CustomEvent('dialogue-toggle', { - detail: { isOpen: this.isDialogueOpen } - })); - } - playTextBeep() { const oscillator = this.audioCtx.createOscillator(); const gainNode = this.audioCtx.createGain(); @@ -709,8 +621,7 @@ class LlmComponent extends HTMLElement { const cleanText = text.replace(fileRegex, '').trim(); const messageWrapper = document.createElement('div'); - messageWrapper.className = `message-wrapper ${isUser ? 'user' : 'assistant'}`; - const timestamp = this.formatTime(new Date()); + messageWrapper.className = `message-wrapper ${isUser ? 'user' : 'navi'}`; const fileBadgesHtml = fileBadges.length > 0 ? `
${fileBadges.map(name => @@ -718,14 +629,11 @@ class LlmComponent extends HTMLElement { : ''; messageWrapper.innerHTML = ` -
${isUser ? 'You' : 'PET'}
${fileBadgesHtml} -
${cleanText}
-
${timestamp}
- `; +
${cleanText}
`; messageBody.appendChild(messageWrapper); - this.messageHistory.push({ text, html: cleanText, isUser, element: messageWrapper, timestamp }); + this.messageHistory.push({ text, html: cleanText, isUser, element: messageWrapper, timestamp: Date.now() }); this.scrollToBottom(); } @@ -745,24 +653,20 @@ class LlmComponent extends HTMLElement { attachBtn.disabled = true; clearBtn.disabled = true; dialogueSend.textContent = 'STOP'; - dialogueSend.classList.add('stop'); - dialogueSend.classList.remove('skip'); + dialogueSend.setAttribute('color', '#c0392b'); + attachBtn.setAttribute('disabled', true); + clearBtn.setAttribute('disabled', true); const emptyState = messageBody.querySelector('.empty-state'); if (emptyState) messageBody.innerHTML = ''; - const timestamp = this.formatTime(new Date()); const messageWrapper = document.createElement('div'); - messageWrapper.className = 'message-wrapper assistant'; - messageWrapper.innerHTML = ` -
PET
-
-
${timestamp}
- `; + messageWrapper.className = 'message-wrapper navi'; + messageWrapper.innerHTML = `
`; messageBody.appendChild(messageWrapper); - this.currentStreamingMessage = { text: '', html: '', isUser: false, element: messageWrapper, timestamp }; + this.currentStreamingMessage = { text: '', html: '', isUser: false, element: messageWrapper, timestamp: Date.now() }; this.messageHistory.push(this.currentStreamingMessage); this.scrollToBottom(); @@ -779,14 +683,12 @@ class LlmComponent extends HTMLElement { handleStreamComplete(response) { this.streamComplete = true; - if (this.typingIndex >= this.streamBuffer.length) { this.cleanupStreaming(); } else { const dialogueSend = this.shadowRoot.getElementById('dialogue-send'); dialogueSend.textContent = 'SKIP'; - dialogueSend.classList.remove('stop'); - dialogueSend.classList.add('skip'); + dialogueSend.setAttribute('color', '#f39c12'); } } @@ -861,7 +763,9 @@ class LlmComponent extends HTMLElement { attachBtn.disabled = false; clearBtn.disabled = false; dialogueSend.textContent = 'SEND'; - dialogueSend.classList.remove('stop', 'skip'); + dialogueSend.setAttribute('color', this.navi.theme.accent); + attachBtn.removeAttribute('disabled'); + clearBtn.removeAttribute('disabled'); if (bubble) { bubble.id = ''; @@ -913,9 +817,7 @@ class LlmComponent extends HTMLElement { this.renderAttachedFiles(); // Send via API with streaming callback 💬 - this.currentRequest = this.api.sendPetMessage(text, (chunk) => { - this.handleStreamChunk(chunk); - }); + this.currentRequest = this.navi.ask(text, (chunk) => this.handleStreamChunk(chunk)); // Handle completion/errors with promise try { diff --git a/public/world.js b/public/components/world.mjs similarity index 85% rename from public/world.js rename to public/components/world.mjs index f48aaab..e81f49b 100644 --- a/public/world.js +++ b/public/components/world.mjs @@ -5,15 +5,6 @@ const TILE_WIDTH = 64; const TILE_HEIGHT = 32; const TILE_DEPTH = 16; -// ============================================ -// HAPTIC FEEDBACK -// ============================================ -function triggerHaptic() { - if ('vibrate' in navigator) { - navigator.vibrate(10); - } -} - // ============================================ // THEME HANDLER // ============================================ @@ -183,7 +174,6 @@ function createPet(gridX, gridY, name = 'PET') { class Game { constructor() { this.worldId = ''; - this.theme = null; this.world = null; this.app = null; this.pet = null; @@ -193,57 +183,51 @@ class Game { this.keys = {}; // Use global singleton 🌍 - this.api = window.netNaviAPI; + this.navi = window.navi; this.worldActions = null; this.playerInfo = { name: 'Guest', - apiUrl: this.api.baseUrl + apiUrl: this.navi.navi }; } async init() { try { // Join world with callbacks 🌍 - this.worldActions = this.api.joinWorld(this.worldId, { - onWorldLoaded: (data) => { - this.world = data.world; - this.theme = data.theme; - applyTheme(this.theme); - this.initializeRenderer(); - }, + await this.navi.init(); + this.worldActions = this.navi.connect(this.worldId); - onCurrentPlayers: (players) => { - players.forEach(player => { - if (player.socketId !== this.api.worldSocket.id) { - this.addOtherPlayer(player); - } - }); - }, + this.worldActions.onData = (data) => { + this.world = data; + applyTheme(this.world.theme); + this.initializeRenderer(); + } - onPlayerJoined: (player) => { - this.addOtherPlayer(player); - }, + this.worldActions.onPlayers = (players) => { + players.forEach(player => { + if (player.name !== this.navi.info.name) { + this.addOtherPlayer(player); + } + }); + } - onPlayerMoved: (data) => { - const sprite = this.otherPlayers.get(data.socketId); - if (sprite) { - this.moveOtherPlayer(sprite, data.x, data.y); - } - }, + this.worldActions.onJoined = (player) => { + this.addOtherPlayer(player); + } - onPlayerLeft: (data) => { - const sprite = this.otherPlayers.get(data.socketId); - if (sprite) { - this.app.stage.removeChild(sprite); - this.otherPlayers.delete(data.socketId); - } - }, + this.worldActions.onMoved = (data) => { + const sprite = this.otherPlayers.get(data.socketId); + if (sprite) { + this.moveOtherPlayer(sprite, data.x, data.y); + } + }; - onError: (error) => { - console.error('❌ World error:', error); - } - }); + this.worldActions.onLeft = (data) => { + const sprite = this.otherPlayers.get(data.socketId); + if(sprite) this.otherPlayers.delete(data.socketId); + } + this.worldActions.onError = (error) => console.error('❌ World error:', error); console.log('✨ Game initializing...'); } catch (error) { @@ -299,12 +283,13 @@ class Game { this.app.stage.addChild(tiles); this.world.tiles.forEach(tileData => { - const tile = createTile(tileData, this.theme); + const tile = createTile(tileData, this.world.theme); tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY)); tiles.addChild(tile); }); - this.pet = createPet(this.world.pet.startX, this.world.pet.startY, this.playerInfo.name); + const spawn = this.world.tiles.find(t => t.type === 'spawn'); + this.pet = createPet(spawn.x, spawn.y, this.playerInfo.name); this.app.stage.addChild(this.pet); this.dialogue = document.getElementById('llm'); diff --git a/public/index.html b/public/index.html index b33821b..060188f 100644 --- a/public/index.html +++ b/public/index.html @@ -12,9 +12,7 @@