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 = `
-
+
-
+
+
+
No track loaded
-
`;
@@ -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;
+ }
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 @@