Some plugin work
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
/* Nebot dedicated page logic */
|
||||
(function(){
|
||||
console.log('[Nebot Page] Starting initialization...');
|
||||
console.log('[Nebot Page] window.ollamaChat:', window.ollamaChat);
|
||||
console.log('[Nebot Page] window.electronAPI:', window.electronAPI);
|
||||
|
||||
// Try multiple ways to access the API
|
||||
let api = window.ollamaChat;
|
||||
|
||||
// If not available directly, try accessing through electronAPI
|
||||
if (!api && window.electronAPI) {
|
||||
console.log('[Nebot Page] Creating proxy API using electronAPI...');
|
||||
// Create a proxy API that uses IPC directly
|
||||
api = {
|
||||
listChats: () => {
|
||||
console.log('[Nebot Page] Calling listChats via IPC...');
|
||||
return window.electronAPI.invoke('ollama-chat:list-chats');
|
||||
},
|
||||
getChat: (id) => {
|
||||
console.log('[Nebot Page] Calling getChat via IPC...', id);
|
||||
return window.electronAPI.invoke('ollama-chat:get-chat', { id });
|
||||
},
|
||||
createChat: (title) => {
|
||||
console.log('[Nebot Page] Calling createChat via IPC...', title);
|
||||
return window.electronAPI.invoke('ollama-chat:create-chat', { title });
|
||||
},
|
||||
deleteChat: (id) => {
|
||||
console.log('[Nebot Page] Calling deleteChat via IPC...', id);
|
||||
return window.electronAPI.invoke('ollama-chat:delete-chat', { id });
|
||||
},
|
||||
getSettings: () => {
|
||||
console.log('[Nebot Page] Calling getSettings via IPC...');
|
||||
return window.electronAPI.invoke('ollama-chat:get-settings');
|
||||
},
|
||||
setSettings: (s) => {
|
||||
console.log('[Nebot Page] Calling setSettings via IPC...', s);
|
||||
return window.electronAPI.invoke('ollama-chat:set-settings', s);
|
||||
},
|
||||
send: (id, content) => {
|
||||
console.log('[Nebot Page] Calling send via IPC...', id, content);
|
||||
return window.electronAPI.invoke('ollama-chat:send', { id, content });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if(!api){
|
||||
document.body.innerHTML = '<div style="padding:20px;font-family:system-ui;background:#12141c;color:#e6e8ef;"><h2>Nebot Plugin API Not Available</h2><p>The Nebot plugin may be disabled or not properly loaded.</p><p>Try:</p><ul><li>Check that the plugin is enabled in settings</li><li>Restart the browser</li><li>Use the floating panel instead (Ctrl+Shift+O)</li></ul></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Nebot Page] API available, proceeding with initialization...');
|
||||
|
||||
const els = {
|
||||
chatList: document.getElementById('chat-list'),
|
||||
messages: document.getElementById('messages'),
|
||||
input: document.getElementById('input'),
|
||||
newChat: document.getElementById('new-chat'),
|
||||
form: document.getElementById('composer'),
|
||||
send: document.getElementById('send'),
|
||||
settingsBtn: document.getElementById('settings-btn')
|
||||
};
|
||||
|
||||
const state = { chats: [], currentId: null };
|
||||
|
||||
function h(tag, attrs={}, ...children){
|
||||
const el = document.createElement(tag);
|
||||
for(const [k,v] of Object.entries(attrs)){
|
||||
if(k==='class') el.className=v; else if(k==='onclick') el.addEventListener('click',v); else if(v!=null) el.setAttribute(k,v);
|
||||
}
|
||||
for(const c of children){ if(c==null) continue; el.appendChild(typeof c==='string'?document.createTextNode(c):c);} return el;
|
||||
}
|
||||
|
||||
function formatTime(ts){ try { return new Date(ts).toLocaleString(); } catch { return ''; } }
|
||||
|
||||
async function refreshList(){
|
||||
console.log('[Nebot Page] refreshList called...');
|
||||
try {
|
||||
const result = await api.listChats();
|
||||
console.log('[Nebot Page] listChats result:', result);
|
||||
state.chats = result.chats || [];
|
||||
renderChatList();
|
||||
} catch (e) {
|
||||
console.error('[Nebot Page] refreshList error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChatList(){
|
||||
els.chatList.innerHTML='';
|
||||
state.chats.forEach(c => {
|
||||
const li = h('li',{class:'chat-item'+(c.id===state.currentId?' active':'')});
|
||||
li.appendChild(h('div',{class:'chat-title'}, c.title||'Untitled'));
|
||||
li.appendChild(h('button',{class:'delete-btn',title:'Delete',onclick:(e)=>{e.stopPropagation();deleteChat(c.id);}},'✕'));
|
||||
li.onclick=()=>openChat(c.id);
|
||||
els.chatList.appendChild(li);
|
||||
});
|
||||
if(!state.chats.length){
|
||||
els.chatList.appendChild(h('div',{class:'empty'},'No chats yet. Start one below.'));}
|
||||
}
|
||||
|
||||
async function openChat(id){
|
||||
console.log('[Nebot Page] openChat called with id:', id);
|
||||
state.currentId=id;
|
||||
try {
|
||||
const result = await api.getChat(id);
|
||||
console.log('[Nebot Page] getChat result:', result);
|
||||
if(result.error){
|
||||
console.error('[Nebot Page] Error getting chat:', result.error);
|
||||
return;
|
||||
}
|
||||
renderMessages(result.chat);
|
||||
renderChatList();
|
||||
subscribeStream(id);
|
||||
} catch (e) {
|
||||
console.error('[Nebot Page] openChat error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function newChat(){
|
||||
const { chat } = await api.createChat('New chat');
|
||||
await refreshList();
|
||||
await openChat(chat.id);
|
||||
}
|
||||
|
||||
async function deleteChat(id){
|
||||
await api.deleteChat(id);
|
||||
await refreshList();
|
||||
if(state.currentId===id){ state.currentId=state.chats[0]?.id||null; if(state.currentId) openChat(state.currentId); else els.messages.innerHTML=''; }
|
||||
}
|
||||
|
||||
function mdEscape(s){ return (s||'').replace(/[&<>]/g,m=>({"&":"&","<":"<",">":">"}[m])); }
|
||||
|
||||
function renderMarkdown(md){
|
||||
if(!md) return '';
|
||||
|
||||
// Check if libraries are loaded
|
||||
if(window.marked && window.DOMPurify){
|
||||
try {
|
||||
// Configure marked if not already done
|
||||
if(!window.marked.configured) {
|
||||
window.marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
headerIds: false,
|
||||
mangle: false,
|
||||
highlight: function(code, lang) {
|
||||
if (window.hljs && lang && window.hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return window.hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.warn('Highlight.js error:', e);
|
||||
}
|
||||
}
|
||||
// Try auto-detection
|
||||
if (window.hljs) {
|
||||
try {
|
||||
return window.hljs.highlightAuto(code).value;
|
||||
} catch (e) {
|
||||
console.warn('Highlight.js auto error:', e);
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
});
|
||||
window.marked.configured = true;
|
||||
}
|
||||
|
||||
const raw = window.marked.parse(md);
|
||||
return window.DOMPurify.sanitize(raw, {
|
||||
ADD_ATTR: ['target', 'rel', 'class'],
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'a', 'span', 'div'],
|
||||
ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'title']
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Markdown parsing error:', e);
|
||||
return mdEscape(md);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: basic markdown-like parsing
|
||||
console.warn('Markdown libraries not loaded, using fallback parsing');
|
||||
return md
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/^(.*)$/, '<p>$1</p>');
|
||||
}
|
||||
|
||||
function renderMessages(chat){
|
||||
els.messages.innerHTML='';
|
||||
if(!chat){ return; }
|
||||
chat.messages.forEach(m=>{
|
||||
const div = h('div',{class:'msg '+m.role});
|
||||
div.innerHTML = '<div class="markdown">'+renderMarkdown(m.content)+'</div>';
|
||||
|
||||
// Enhance links for security
|
||||
div.querySelectorAll('a[href]').forEach(a => {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
els.messages.appendChild(div);
|
||||
});
|
||||
els.messages.scrollTop = els.messages.scrollHeight;
|
||||
}
|
||||
|
||||
// --- Deferred Markdown Enhancement Support ---
|
||||
// Some CDN scripts (marked / highlight.js) may not be ready when we first render.
|
||||
// We keep raw text and upgrade once libraries are available.
|
||||
const deferredMarkdown = new Set();
|
||||
let deferredTimer = null;
|
||||
function scheduleDeferredMarkdownCheck(){
|
||||
if(deferredTimer) return;
|
||||
deferredTimer = setInterval(()=>{
|
||||
if(window.marked && window.DOMPurify){
|
||||
deferredMarkdown.forEach(el=>{
|
||||
const raw = el.dataset.raw;
|
||||
try {
|
||||
el.innerHTML = renderMarkdown(raw);
|
||||
// Enhance links again
|
||||
el.closest('.msg')?.querySelectorAll('a[href]').forEach(a=>{ a.setAttribute('target','_blank'); a.setAttribute('rel','noopener noreferrer'); });
|
||||
el.removeAttribute('data-raw');
|
||||
deferredMarkdown.delete(el);
|
||||
} catch(e){ console.warn('[Nebot Page] Deferred markdown render failed', e); }
|
||||
});
|
||||
if(!deferredMarkdown.size){ clearInterval(deferredTimer); deferredTimer=null; }
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Typing animation state
|
||||
let typingQueue = [];
|
||||
let isTyping = false;
|
||||
let typingSpeed = 25; // milliseconds per character (base speed)
|
||||
let typingEnabled = true; // can be toggled in settings
|
||||
let currentCharIndex = 0; // track current position for adaptive speed
|
||||
let lastComputedDelay = typingSpeed;
|
||||
|
||||
function calculateTypingDelay(charIndex, element) {
|
||||
// Dynamic words-per-second scaling based on total word count of (displayed + queued)
|
||||
const currentText = element.textContent + typingQueue.join('');
|
||||
const words = currentText.trim().length ? currentText.trim().split(/\s+/).length : 0;
|
||||
if (words === 0) return typingSpeed; // fallback
|
||||
|
||||
// Derive average chars per word (include space) for conversion
|
||||
const avgWordChars = Math.max(3.5, Math.min(8, currentText.length / Math.max(1, words)) + 0.8); // small bias for trailing spaces
|
||||
|
||||
// Base slider (typingSpeed currently ms per char) corresponds to baseWordsPerSec for small replies.
|
||||
// Convert baseSpeed (ms/char) to base words/sec using avgWordChars
|
||||
const baseWordsPerSec = 1000 / (typingSpeed * avgWordChars);
|
||||
|
||||
// Target words per second scales with total words:
|
||||
// 0 -> baseWordsPerSec
|
||||
// 1000 -> 100 wps cap (user example: 1000 words => 100 wps)
|
||||
// Linear interpolation then clamp.
|
||||
const targetWps = Math.min(100, baseWordsPerSec + (words / 1000) * (100 - baseWordsPerSec));
|
||||
|
||||
// Convert target words/sec to per-char delay.
|
||||
const delayPerChar = 1000 / (targetWps * avgWordChars);
|
||||
|
||||
// Slight smoothing to avoid jitter (EMA)
|
||||
const alpha = 0.25;
|
||||
lastComputedDelay = lastComputedDelay ? (alpha * delayPerChar + (1 - alpha) * lastComputedDelay) : delayPerChar;
|
||||
|
||||
return Math.max(2, lastComputedDelay); // minimum 2ms
|
||||
}
|
||||
|
||||
function startTypingAnimation(element) {
|
||||
console.log('[Nebot Page] startTypingAnimation called, queue length:', typingQueue.length);
|
||||
if (isTyping || typingQueue.length === 0) return;
|
||||
|
||||
isTyping = true;
|
||||
currentCharIndex = 0;
|
||||
const totalLength = typingQueue.length;
|
||||
element.classList.add('typing');
|
||||
console.log('[Nebot Page] Starting typing animation with', totalLength, 'characters (word-count adaptive speed)');
|
||||
|
||||
function typeNext() {
|
||||
if (typingQueue.length === 0) {
|
||||
isTyping = false;
|
||||
currentCharIndex = 0;
|
||||
element.classList.remove('typing');
|
||||
console.log('[Nebot Page] Typing animation completed');
|
||||
return;
|
||||
}
|
||||
|
||||
const char = typingQueue.shift();
|
||||
element.textContent += char;
|
||||
els.messages.scrollTop = els.messages.scrollHeight;
|
||||
|
||||
// Calculate dynamic delay based on live word count
|
||||
const delay = calculateTypingDelay(currentCharIndex, element);
|
||||
currentCharIndex++;
|
||||
|
||||
// Log speed changes for debugging
|
||||
if (currentCharIndex % 20 === 0) {
|
||||
console.log(`[Nebot Page] Char ${currentCharIndex}/${totalLength}, adaptive delay: ${delay.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
setTimeout(typeNext, delay);
|
||||
}
|
||||
|
||||
typeNext();
|
||||
}
|
||||
|
||||
function subscribeStream(id){
|
||||
const channel = 'ollama-chat:stream:' + id;
|
||||
console.log('[Nebot Page] Subscribing to stream channel:', channel);
|
||||
|
||||
// Reset typing state for new stream
|
||||
typingQueue = [];
|
||||
isTyping = false;
|
||||
|
||||
// Remove any existing listeners for this channel
|
||||
if (window.electronAPI && window.electronAPI.removeListener) {
|
||||
window.electronAPI.removeListener(channel, handleStreamPayload);
|
||||
}
|
||||
|
||||
function handleStreamPayload(...args) {
|
||||
// Handle both (event, payload) and (payload) argument patterns
|
||||
const payload = args.length > 1 ? args[1] : args[0];
|
||||
console.log('[Nebot Page] Stream payload received:', payload);
|
||||
|
||||
if(!els.messages) return;
|
||||
if(payload.type==='token'){
|
||||
let last = els.messages.querySelector('.msg.assistant.streaming');
|
||||
if(!last){
|
||||
last = h('div',{class:'msg assistant streaming'});
|
||||
els.messages.appendChild(last);
|
||||
last.innerHTML='<div class="markdown"></div>';
|
||||
console.log('[Nebot Page] Created new streaming message element');
|
||||
}
|
||||
const md = last.querySelector('.markdown');
|
||||
|
||||
if (typingEnabled) {
|
||||
// Add tokens to typing queue instead of directly appending
|
||||
console.log('[Nebot Page] Adding token to queue:', payload.token);
|
||||
for (const char of payload.token) {
|
||||
typingQueue.push(char);
|
||||
}
|
||||
console.log('[Nebot Page] Queue length now:', typingQueue.length);
|
||||
|
||||
// Start typing animation if not already running
|
||||
if (!isTyping) {
|
||||
console.log('[Nebot Page] Starting typing animation...');
|
||||
startTypingAnimation(md);
|
||||
}
|
||||
} else {
|
||||
// Direct append if typing is disabled
|
||||
console.log('[Nebot Page] Typing disabled, appending directly:', payload.token);
|
||||
md.textContent += payload.token;
|
||||
}
|
||||
} else if(payload.type==='done') {
|
||||
console.log('[Nebot Page] Stream done, finalizing message');
|
||||
const last = els.messages.querySelector('.msg.assistant.streaming');
|
||||
if(last){
|
||||
const mdEl = last.querySelector('.markdown');
|
||||
|
||||
// Wait for typing animation to complete before rendering markdown
|
||||
const waitForTyping = () => {
|
||||
if (typingEnabled && (isTyping || typingQueue.length > 0)) {
|
||||
setTimeout(waitForTyping, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now render the markdown (or defer if libs not ready)
|
||||
const raw = mdEl.textContent;
|
||||
if(window.marked && window.DOMPurify){
|
||||
mdEl.innerHTML = renderMarkdown(raw);
|
||||
} else {
|
||||
mdEl.dataset.raw = raw;
|
||||
deferredMarkdown.add(mdEl);
|
||||
scheduleDeferredMarkdownCheck();
|
||||
}
|
||||
|
||||
// Enhance links for security
|
||||
last.querySelectorAll('a[href]').forEach(a => {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
last.classList.remove('streaming');
|
||||
};
|
||||
|
||||
waitForTyping();
|
||||
}
|
||||
} else if(payload.type==='error') {
|
||||
console.error('[Nebot Page] Stream error:', payload.message);
|
||||
}
|
||||
els.messages.scrollTop = els.messages.scrollHeight;
|
||||
}
|
||||
|
||||
if (window.electronAPI && window.electronAPI.on) {
|
||||
console.log('[Nebot Page] Setting up stream listener via electronAPI');
|
||||
window.electronAPI.on(channel, handleStreamPayload);
|
||||
} else {
|
||||
console.warn('[Nebot Page] electronAPI.on not available for stream subscription');
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(e){
|
||||
e.preventDefault();
|
||||
console.log('[Nebot Page] sendMessage called...');
|
||||
const content = els.input.value.trim();
|
||||
if(!content) return;
|
||||
if(!state.currentId){
|
||||
console.log('[Nebot Page] Creating new chat...');
|
||||
const result = await api.createChat('New chat');
|
||||
console.log('[Nebot Page] createChat result:', result);
|
||||
await refreshList();
|
||||
state.currentId = result.chat.id;
|
||||
}
|
||||
const userDiv = h('div',{class:'msg user'}); userDiv.textContent=content; els.messages.appendChild(userDiv);
|
||||
els.input.value='';
|
||||
els.messages.scrollTop = els.messages.scrollHeight;
|
||||
|
||||
// Subscribe to stream BEFORE sending
|
||||
subscribeStream(state.currentId);
|
||||
|
||||
console.log('[Nebot Page] Sending message...', state.currentId, content);
|
||||
try {
|
||||
const result = await api.send(state.currentId, content);
|
||||
console.log('[Nebot Page] send result:', result);
|
||||
|
||||
// If no streaming response appears after 2 seconds, reload the chat to show the full response
|
||||
setTimeout(async () => {
|
||||
if (!els.messages.querySelector('.msg.assistant.streaming')) {
|
||||
console.log('[Nebot Page] No streaming response detected, reloading chat...');
|
||||
const result = await api.getChat(state.currentId);
|
||||
if (result.chat && result.chat.messages) {
|
||||
const lastMessage = result.chat.messages[result.chat.messages.length - 1];
|
||||
if (lastMessage && lastMessage.role === 'assistant') {
|
||||
console.log('[Nebot Page] Found new assistant message, simulating typing animation...');
|
||||
|
||||
// Create a streaming message element
|
||||
const assistantDiv = h('div',{class:'msg assistant streaming'});
|
||||
assistantDiv.innerHTML='<div class="markdown"></div>';
|
||||
els.messages.appendChild(assistantDiv);
|
||||
const md = assistantDiv.querySelector('.markdown');
|
||||
|
||||
// Simulate typing animation with the full response
|
||||
if (typingEnabled) {
|
||||
typingQueue = [];
|
||||
for (const char of lastMessage.content) {
|
||||
typingQueue.push(char);
|
||||
}
|
||||
console.log('[Nebot Page] Simulating typing for', typingQueue.length, 'characters with word-count adaptive speed');
|
||||
startTypingAnimation(md);
|
||||
|
||||
// Rough duration estimate using dynamic words/sec model (cap 8s)
|
||||
const msgWords = lastMessage.content.trim().split(/\s+/).length;
|
||||
const estWps = Math.min(100, 10 + (msgWords / 1000) * 90);
|
||||
const estimatedDuration = Math.min(8000, (msgWords / estWps) * 1000);
|
||||
|
||||
// Wait for typing to complete, then render markdown
|
||||
setTimeout(() => {
|
||||
assistantDiv.classList.remove('streaming');
|
||||
const raw = md.textContent;
|
||||
if(window.marked && window.DOMPurify){
|
||||
md.innerHTML = renderMarkdown(raw);
|
||||
} else {
|
||||
md.dataset.raw = raw;
|
||||
deferredMarkdown.add(md);
|
||||
scheduleDeferredMarkdownCheck();
|
||||
}
|
||||
assistantDiv.querySelectorAll('a[href]').forEach(a => {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
}, estimatedDuration + 1000);
|
||||
} else {
|
||||
// No typing animation, just show the message
|
||||
md.textContent = lastMessage.content;
|
||||
assistantDiv.classList.remove('streaming');
|
||||
const raw = md.textContent;
|
||||
if(window.marked && window.DOMPurify){
|
||||
md.innerHTML = renderMarkdown(raw);
|
||||
} else {
|
||||
md.dataset.raw = raw;
|
||||
deferredMarkdown.add(md);
|
||||
scheduleDeferredMarkdownCheck();
|
||||
}
|
||||
assistantDiv.querySelectorAll('a[href]').forEach(a => {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback to full reload
|
||||
await openChat(state.currentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
refreshList();
|
||||
} catch (e) {
|
||||
console.error('[Nebot Page] sendMessage error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function openSettings(){
|
||||
const { settings } = await api.getSettings();
|
||||
const modal = h('div',{class:'settings-modal'},
|
||||
h('div',{class:'settings-card'},
|
||||
h('h2',{},'Nebot Settings'),
|
||||
h('div',{},
|
||||
h('label',{},'Ollama Base URL'),
|
||||
h('input',{id:'set-base',value:settings.ollamaBaseUrl||'http://localhost:11434'})
|
||||
),
|
||||
h('div',{},
|
||||
h('label',{},'System Prompt'),
|
||||
h('textarea',{id:'set-sys'}, settings.systemPrompt||'')
|
||||
),
|
||||
h('div',{},
|
||||
h('label',{},'Typing Animation'),
|
||||
h('div',{style:'display:flex;align-items:center;gap:8px;margin-top:6px;'},
|
||||
h('input',{type:'checkbox',id:'set-typing',checked:settings.typingEnabled!==false}),
|
||||
h('span',{style:'font-size:13px;'},'Enable typing animation for responses')
|
||||
)
|
||||
),
|
||||
h('div',{},
|
||||
h('label',{},'Typing Speed (characters per second)'),
|
||||
h('input',{type:'range',id:'set-speed',min:'10',max:'200',value:settings.typingSpeed||40,style:'margin-top:6px;'}),
|
||||
h('span',{id:'speed-display',style:'font-size:12px;color:var(--muted);margin-top:4px;display:block;'},(settings.typingSpeed||40)+' chars/sec'),
|
||||
h('div',{style:'font-size:11px;color:var(--muted);margin-top:4px;line-height:1.4;'},'💡 Speed scales with total words (up to 100 words/sec at ~1000 words)')
|
||||
),
|
||||
h('div',{class:'settings-actions'},
|
||||
h('button',{onclick:()=>modal.remove()},'Cancel'),
|
||||
h('button',{class:'primary',onclick:async()=>{
|
||||
const next = {
|
||||
ollamaBaseUrl: modal.querySelector('#set-base').value.trim(),
|
||||
systemPrompt: modal.querySelector('#set-sys').value,
|
||||
typingEnabled: modal.querySelector('#set-typing').checked,
|
||||
typingSpeed: parseInt(modal.querySelector('#set-speed').value)
|
||||
};
|
||||
// Update local settings
|
||||
typingEnabled = next.typingEnabled;
|
||||
typingSpeed = 1000 / next.typingSpeed; // convert chars/sec to ms per char
|
||||
|
||||
await api.setSettings(next);
|
||||
modal.remove();
|
||||
}},'Save')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Update speed display when slider changes
|
||||
const speedSlider = modal.querySelector('#set-speed');
|
||||
const speedDisplay = modal.querySelector('#speed-display');
|
||||
speedSlider.addEventListener('input', () => {
|
||||
speedDisplay.textContent = speedSlider.value + ' chars/sec';
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
els.newChat.addEventListener('click', newChat);
|
||||
els.form.addEventListener('submit', sendMessage);
|
||||
els.settingsBtn.addEventListener('click', openSettings);
|
||||
// Removed temporary "Test Typing" debug button now that feature is stable.
|
||||
|
||||
// Auto grow textarea
|
||||
els.input.addEventListener('input', ()=>{ els.input.style.height='auto'; els.input.style.height=Math.min(200, els.input.scrollHeight)+'px'; });
|
||||
|
||||
// Load settings and initialize
|
||||
async function initializeSettings() {
|
||||
try {
|
||||
const { settings } = await api.getSettings();
|
||||
typingEnabled = settings.typingEnabled !== false; // default to true
|
||||
typingSpeed = settings.typingSpeed ? (1000 / settings.typingSpeed) : 25; // convert chars/sec to ms per char, default 40 chars/sec
|
||||
console.log('[Nebot Page] Loaded settings - typing enabled:', typingEnabled, 'speed:', typingSpeed + 'ms per char');
|
||||
} catch (e) {
|
||||
console.warn('[Nebot Page] Could not load settings, using defaults:', e);
|
||||
}
|
||||
}
|
||||
|
||||
initializeSettings().then(() => {
|
||||
refreshList().then(()=>{ if(state.chats[0]) openChat(state.chats[0].id); });
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user