diff --git a/index.html b/index.html index 8918dc9..31fe6a5 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,23 @@ + @@ -1050,84 +1067,6 @@ gap: 8px; } - .branch-navigation { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - color: #94a3b8; - } - - .branch-indicator { - font-weight: 500; - color: #e2e8f0; - } - - .branch-buttons { - display: flex; - gap: 4px; - } - - .branch-btn { - background: rgba(99, 102, 241, 0.2); - border: 1px solid rgba(99, 102, 241, 0.3); - color: #94a3b8; - padding: 2px 6px; - border-radius: 12px; - font-size: 10px; - cursor: pointer; - transition: all 0.2s; - min-width: 20px; - text-align: center; - } - - .branch-btn:hover { - background: rgba(99, 102, 241, 0.4); - color: #e2e8f0; - } - - .branch-btn.active { - background: rgba(99, 102, 241, 0.6); - border-color: rgba(99, 102, 241, 0.8); - color: #fff; - } - - .action-buttons { - display: flex; - gap: 6px; - align-items: center; - } - - .bottom-action-btn { - background: rgba(30, 41, 59, 0.6); - border: 1px solid rgba(99, 102, 241, 0.2); - color: #94a3b8; - padding: 4px 8px; - border-radius: 6px; - font-size: 11px; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 4px; - } - - .bottom-action-btn:hover { - background: rgba(99, 102, 241, 0.3); - color: #e2e8f0; - border-color: rgba(99, 102, 241, 0.5); - } - - .bottom-action-btn svg { - width: 12px; - height: 12px; - } - - /* Hide old message actions */ - .message-actions { - display: none !important; - } - /* Message content wrapper */ .message-content { position: relative; @@ -1743,142 +1682,17 @@

Settings

const rateLimitInfo = document.getElementById('rateLimitInfo'); const sidebarSettings = document.getElementById('sidebarSettings'); - // Enhanced marked renderer for codeblocks - const renderer = new marked.Renderer(); - const originalCode = renderer.code; - - renderer.code = function(code, language, escaped) { - const lang = language || 'text'; - const langDisplay = lang.charAt(0).toUpperCase() + lang.slice(1); - const codeId = 'code-' + Math.random().toString(36).substr(2, 9); - - // Apply syntax highlighting if hljs is available - let highlightedCode = escaped ? code : marked.escapeTest(code); - if (typeof hljs !== 'undefined' && lang !== 'text') { - try { - highlightedCode = hljs.highlight(code, { language: lang }).value; - } catch (e) { - // Fall back to plain text if language not supported - highlightedCode = escaped ? code : marked.escapeTest(code); - } - } - - return ` -
-          
- ${langDisplay} -
- - - -
-
- ${highlightedCode} -
- `; - }; - - marked.setOptions({ renderer }); - - // Codeblock action functions - window.copyCode = function(codeId) { - const codeElement = document.getElementById(codeId); - if (!codeElement) return; - - const code = codeElement.textContent; - navigator.clipboard.writeText(code).then(() => { - const btn = event.target.closest('.code-action-btn'); - const originalText = btn.innerHTML; - btn.innerHTML = `Copied`; - btn.classList.add('success'); - setTimeout(() => { - btn.innerHTML = originalText; - btn.classList.remove('success'); - }, 2000); - }); - }; - - window.downloadCode = function(codeId, language) { - const codeElement = document.getElementById(codeId); - if (!codeElement) return; - - const code = codeElement.textContent; - const extensions = { - javascript: 'js', typescript: 'ts', python: 'py', - java: 'java', cpp: 'cpp', csharp: 'cs', - php: 'php', ruby: 'rb', go: 'go', rust: 'rs', - html: 'html', css: 'css', sql: 'sql', json: 'json', - xml: 'xml', yaml: 'yml', markdown: 'md', shell: 'sh', - bash: 'sh', powershell: 'ps1', text: 'txt' - }; - - const ext = extensions[language.toLowerCase()] || 'txt'; - const filename = `code.${ext}`; - - const blob = new Blob([code], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(url); - document.body.removeChild(a); - }; - - window.toggleWrap = function(codeId) { - const codeElement = document.getElementById(codeId); - if (!codeElement) return; - - const pre = codeElement.closest('pre'); - pre.classList.toggle('code-wrap'); - - const btn = event.target.closest('.code-action-btn'); - const isWrapped = pre.classList.contains('code-wrap'); - btn.innerHTML = isWrapped ? - `Unwrap` : - `Wrap`; - }; - - // State - let chats = JSON.parse(localStorage.getItem('chat_history') || '[]'); - let currentChatId = null; + // Data initialization let models = []; + window.chats = JSON.parse(localStorage.getItem('chat_history') || '[]'); + window.currentChatId = null; let currentAbortController = null; - // Migrate old chats to support branching - chats.forEach(chat => { - if (!chat.branches) { - // Convert linear messages to branched format - chat.branches = [chat.messages || []]; - chat.currentBranch = 0; - delete chat.messages; // Remove old format - } - }); - // Helper functions function loadUserProfile(){ const token=localStorage.getItem('gh_token'); if(!token || !token.trim() || token.trim()=="" || token == "") return; - fetch('https://api.github.com/user',{headers:{Authorization:`Bearer ${token}`}}) + fetch('https://api.github.com/user',{headers:{Authorization:'Bearer ' + token}}) .then(r=> r.ok ? r.json(): Promise.reject()) .then(u=>{ if(u.login){ document.getElementById('usernameDisplay').textContent=u.login; } @@ -1899,2011 +1713,344 @@

Settings

chatPage?.classList.remove('hidden'); } + // Check for stored token on load + const token = localStorage.getItem('gh_token'); + if (token && token.trim() && token.trim()!="" && token != "") { + loadUserProfile(); + fetchModels(); + showChatPage(); + } else { + showLoginPage(); + } + // Auth handlers const patInput = document.getElementById('patToken'); document.getElementById('usePat')?.addEventListener('click', () => { const token = patInput.value.trim(); if (token.startsWith('github_pat_')) { - const octokit = new Octokit({ auth: token }); localStorage.setItem('gh_token', token); + patInput.value = ''; fetchModels(); loadUserProfile(); showChatPage(); - } else { - alert('Invalid token (must start with github_pat_)'); + alert('Please enter a valid GitHub Personal Access Token that starts with "github_pat_"'); } }); - // Auth init - const storedToken = localStorage.getItem('gh_token'); - if (storedToken) { - fetchModels(); - loadUserProfile(); - showChatPage(); - } else { - showLoginPage(); + // OAuth & Fetch + const GITHUB_CLIENT_ID = 'Ov23li2MmzqUhjwLwwMB'; + const OAUTH_REDIRECT_URI = window.location.origin + window.location.pathname; + + function startOAuth(){ + if(location.protocol==='file:') { alert('Serve over http:// (not file://) for OAuth'); return; } + const state = crypto.randomUUID(); + localStorage.setItem('oauth_state', state); + const scope = 'read:user read:org models'; + const authUrl = `https://github.com/login/oauth/authorize?client_id=${encodeURIComponent(GITHUB_CLIENT_ID)}&redirect_uri=${encodeURIComponent(OAUTH_REDIRECT_URI)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`; + window.location = authUrl; } - // Profile menu - userProfile?.addEventListener('click', (e) => { - e.stopPropagation(); - profileMenu?.classList.toggle('hidden'); + function handleOAuthCallback(){ + const params = new URLSearchParams(location.search); + const code = params.get('code'); + const state = params.get('state'); + if(!code) return; + const expected = localStorage.getItem('oauth_state'); + if(!state || state !== expected){ alert('OAuth state mismatch. Retry sign in.'); return; } - if(!profileMenu?.classList.contains('hidden')) { - const rect = userProfile.getBoundingClientRect(); - profileMenu.style.left = rect.left + 'px'; - profileMenu.style.top = (rect.top - profileMenu.offsetHeight - 8) + 'px'; - } - }); - document.getElementById('signoutBtn')?.addEventListener('click', () => { - localStorage.clear(); // Clear all stored data including tokens - profileMenu?.classList.add('hidden'); - showLoginPage(); - }); - document.getElementById('settingsBtn')?.addEventListener('click', (e) => { - e.stopPropagation(); // Prevent closing the menu because of outside click - profileMenu?.classList.add('hidden'); - settingsModal?.classList.remove('hidden'); - }); + fetch(`${API_BASE}/api/exchange?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`) + .then(r=>{ if(!r.ok) throw new Error('Exchange failed'); return r.json(); }) + .then(data=>{ + if(!data.access_token){ throw new Error('No access token in response'); } + localStorage.setItem('gh_token', data.access_token); + const octokit = new Octokit({ auth: data.access_token }); + history.replaceState({}, document.title, OAUTH_REDIRECT_URI); + fetchModels(); loadUserProfile(); showChatPage(); + }) + .catch(err=>{ console.error(err); alert('OAuth exchange error: '+err.message); }); + } - // Outside click - document.addEventListener('click', (e) => { - if (profileMenu && !profileMenu.classList.contains('hidden') && !profileMenu.contains(e.target) && e.target !== userProfile) { - profileMenu.classList.add('hidden'); - } + function fetchModels(){ + const token = localStorage.getItem('gh_token'); + if(!token || !token.trim() || token.trim()=="" || token == "") return; + + const fallback = [{"id":"openai/gpt-4.1","name":"OpenAI GPT-4.1","publisher":"OpenAI","rate_limit_tier":"high","limits":{"max_input_tokens":1048576,"max_output_tokens":32768}},{"id":"openai/gpt-4.1-mini","name":"OpenAI GPT-4.1-mini","publisher":"OpenAI","rate_limit_tier":"low","limits":{"max_input_tokens":1048576,"max_output_tokens":32768}}]; + + fetch(`${API_BASE}/api/models`,{headers:{Authorization:'Bearer ' + token}}) + .then(r=>{ if(!r.ok) throw new Error(r.status); return r.json(); }) + .then(data=>{ + models = Array.isArray(data)? data : (data.models||fallback); + const savedModelId = localStorage.getItem('selected_model'); + const modelToSelect = savedModelId && models.find(m => m.id === savedModelId) ? savedModelId : (models.length ? models[0].id : null); + if(modelToSelect) selectModel(modelToSelect); + }) + .catch(err=>{ + console.warn('Model fetch failed', err); + models = fallback; + if(models.length) selectModel(models[0].id); + }); + } - // Desktop dropdown - if (dropdownContent?.classList.contains('show') && !dropdownSelected?.contains(e.target) && !dropdownContent.contains(e.target)) { - dropdownContent.classList.remove('show'); - dropdownSelected?.classList.remove('active'); + function selectModel(id){ + const m = models.find(model=>model.id===id); + if(!m) return; + + const selectedModelName = document.getElementById('selectedModelName'); + if(selectedModelName) { + selectedModelName.textContent = m.name; + selectedModelName.dataset.modelId = m.id; } + localStorage.setItem('selected_model', m.id); + } - // Title dropdown - const titleDropdownContent = document.getElementById('titleDropdownContent'); - const titleDropdownSelected = document.getElementById('titleDropdownSelected'); - if (titleDropdownContent?.classList.contains('show') && !titleDropdownSelected?.contains(e.target) && !titleDropdownContent.contains(e.target)) { - titleDropdownContent.classList.remove('show'); - titleDropdownSelected?.classList.remove('active'); - } + function sendMessage(){ + const msg = messageInput.value.trim(); + if(!msg) return; + + const token = localStorage.getItem('gh_token'); + const selectedModelElement = document.getElementById('selectedModelName'); + const modelId = selectedModelElement?.dataset.modelId; + const model = models.find(m => m.id === modelId); - // Mobile dropdown - const mobileContent = document.getElementById('mobileDropdownContent'); - const mobileSelected = document.getElementById('mobileDropdownSelected'); - if (mobileContent?.classList.contains('show') && !mobileSelected?.contains(e.target) && !mobileContent.contains(e.target)) { - mobileContent.classList.remove('show'); - mobileSelected?.classList.remove('active'); + if(!token || !token.trim()) { + alert('Please sign in with GitHub first'); + return; } - // Mobile sidebar - const sidebar = document.getElementById('sidebar'); - const mobileMenuBtn = document.getElementById('mobileMenuBtn'); - if (sidebar?.classList.contains('mobile-open') && !sidebar.contains(e.target) && e.target !== mobileMenuBtn) { - sidebar.classList.remove('mobile-open'); + if(!model || !model.id) { + alert('Please select a model first'); + return; } - if (settingsModal && !settingsModal.classList.contains('hidden')) { - const glass = settingsModal.querySelector('.glass'); - if (glass && !glass.contains(e.target)) { - settingsModal.classList.add('hidden'); - } - } - }); + // Send the message + appendUserMessage(msg); + messageInput.value = ''; + generateAssistantForLastUser(); + } - // Settings modal - document.getElementById('closeSettings')?.addEventListener('click', () => settingsModal?.classList.add('hidden')); - document.getElementById('saveToken')?.addEventListener('click', () => { - const newToken = document.getElementById('settingsPat').value.trim(); - if (newToken.startsWith('github_pat_')) { - localStorage.setItem('gh_token', newToken); - location.reload(); - } else { - alert('Token must start with github_pat_'); - } - }); - document.getElementById('exportAll')?.addEventListener('click', () => { - const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(chats, null, 2)); - const a = document.createElement('a'); - a.href = dataStr; a.download = 'chat_history.json'; - document.body.appendChild(a); a.click(); a.remove(); - }); - document.getElementById('importChats')?.addEventListener('click', () => { - const input = document.createElement('input'); input.type='file'; input.accept='.json'; - input.onchange = evt => { - const file = evt.target.files[0]; if(!file) return; - const reader = new FileReader(); - reader.onload = () => { try { const imported = JSON.parse(reader.result); chats = imported; localStorage.setItem('chat_history', JSON.stringify(chats)); location.reload(); } catch { alert('Invalid JSON'); } }; - reader.readAsText(file,'utf-8'); - }; - input.click(); - }); + document.getElementById('githubLogin')?.addEventListener('click', (e)=>{ e.preventDefault(); startOAuth(); }); + handleOAuthCallback(); - // ESC close - document.addEventListener('keydown', (e)=>{ - if (e.key === 'Escape') { - dropdownContent?.classList.remove('show'); - dropdownSelected?.classList.remove('active'); - document.getElementById('titleDropdownContent')?.classList.remove('show'); - document.getElementById('titleDropdownSelected')?.classList.remove('active'); - document.getElementById('mobileDropdownContent')?.classList.remove('show'); - document.getElementById('mobileDropdownSelected')?.classList.remove('active'); - document.getElementById('sidebar')?.classList.remove('mobile-open'); - profileMenu?.classList.add('hidden'); - if (!settingsModal?.classList.contains('hidden')) settingsModal.classList.add('hidden'); - } - }); + // Event Listeners + sendBtn?.addEventListener('click', sendMessage); + newChatBtn?.addEventListener('click', createNewChat); - // Model dropdown - dropdownSelected?.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - dropdownContent?.classList.toggle('show'); - dropdownSelected?.classList.toggle('active'); - }); - dropdownSearch?.addEventListener('input', e => filterModels(e.target.value)); - - // Title generation model dropdown - const titleDropdownSelected = document.getElementById('titleDropdownSelected'); - const titleDropdownContent = document.getElementById('titleDropdownContent'); - const titleDropdownSearch = document.getElementById('titleDropdownSearch'); - const titleDropdownItems = document.getElementById('titleDropdownItems'); - - titleDropdownSelected?.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - titleDropdownContent?.classList.toggle('show'); - titleDropdownSelected?.classList.toggle('active'); + messageInput?.addEventListener('keydown', e=>{ + if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); } }); - titleDropdownSearch?.addEventListener('input', e => filterTitleModels(e.target.value)); - - function filterTitleModels(query) { - const q = query.toLowerCase(); - renderTitleDropdownItems(models.filter(m => m.name.toLowerCase().includes(q) || m.publisher.toLowerCase().includes(q))); - function appendUserMessage(content){ - const welcome=document.getElementById('welcomeMessage'); - if(welcome) welcome.remove(); - - const chat = chats.find(c => c.id === currentChatId); - if (!chat) return; + // Chat management functions + if(!window.chats) window.chats = []; + if(!window.currentChatId) window.currentChatId = null; + + function createNewChat() { + const chat = { + id: Date.now().toString(), + title: `Chat ${window.chats.length + 1}`, + branches: [{ + id: 'main', + name: 'Main', + messages: [], + parent: null + }], + currentBranch: 'main', + createdAt: new Date().toISOString() + }; + + window.chats.unshift(chat); + window.currentChatId = chat.id; + saveChatHistory(); + renderChatHistory(); + switchToChat(chat.id); + } - // Initialize branches if needed - initializeBranches(chat); - const currentBranch = getCurrentBranch(chat); + function appendUserMessage(content) { + if(!window.currentChatId) return; - // Add message to current branch - currentBranch.push({role: 'user', content}); - const messageIndex = currentBranch.length - 1; - - const bubble = document.createElement('div'); - bubble.className = 'user-bubble'; - bubble.dataset.role = 'user'; - bubble.dataset.messageIndex = messageIndex; + const chat = window.chats.find(c => c.id === window.currentChatId); + if(!chat) return; - const branchButtons = renderBranchButtons(chat, messageIndex); + const branch = chat.branches.find(b => b.id === chat.currentBranch); + if(!branch) return; - bubble.innerHTML = ` -
-
${marked.parse(content)}
-
-
-
- Branch: -
${branchButtons}
-
-
- - -
-
- `; + const message = { + id: Date.now().toString(), + role: 'user', + content: content, + timestamp: new Date().toISOString() + }; - chatBox.appendChild(bubble); - chatBox.scrollTop = chatBox.scrollHeight; - - // Save to localStorage - localStorage.setItem('chat_history', JSON.stringify(chats)); - - // Generate title for new chats after first user message - if (chat.title === 'New Chat' && currentBranch.filter(m => m.role === 'user').length === 1) { - updateChatTitle(currentChatId, content); - } - } chatBoxContainer.innerHTML = '

How can I help you today?

Start a conversation by typing a message below

'; - } - } else { - // Recreate message container without welcome message - const chatBoxContainer = chatBox.querySelector('.max-w-3xl'); - if (chatBoxContainer) { - chatBoxContainer.innerHTML = ''; - } + branch.messages.push(message); + saveChatHistory(); + renderCurrentChat(); + } - // Load messages from current branch - currentBranch.forEach((m, index) => { - if(m.role === 'user') { - const bubble = document.createElement('div'); - bubble.className = 'user-bubble'; - bubble.dataset.role = 'user'; - bubble.dataset.messageIndex = index; - - const branchButtons = renderBranchButtons(chat, index); - - bubble.innerHTML = ` -
-
${marked.parse(m.content)}
-
-
-
- Branch: -
${branchButtons}
-
-
- - -
-
- `; - chatBox.appendChild(bubble); - } else if(m.role === 'assistant') { - const response = document.createElement('div'); - response.className = 'assistant-response'; - response.dataset.role = 'assistant'; - response.dataset.messageIndex = index; - - const branchButtons = renderBranchButtons(chat, index); - - response.innerHTML = ` -
-
${marked.parse(m.content)}
-
-
-
- Branch: -
${branchButtons}
-
-
- `; - }).join(''); } - function updateBranchNavigation() { - const chat = chats.find(c => c.id === currentChatId); - if (!chat || !chat.branches) return; + function switchToChat(chatId) { + window.currentChatId = chatId; + renderCurrentChat(); - const messageElements = chatBox.querySelectorAll('.user-bubble, .assistant-response'); - messageElements.forEach((element, index) => { - const branchNav = element.querySelector('.branch-navigation'); - const branchButtons = element.querySelector('.branch-buttons'); - - if (branchNav && branchButtons) { - const buttonsHtml = renderBranchButtons(chat, index); - branchButtons.innerHTML = buttonsHtml; - branchNav.style.display = buttonsHtml ? 'flex' : 'none'; - } + // Update active state in sidebar + document.querySelectorAll('.chat-item').forEach(item => { + item.classList.toggle('active', item.dataset.chatId === chatId); }); } - // Make functions globally available - window.switchToBranch = switchToBranch; - window.createNewBranch = createNewBranch; - window.updateBranchNavigation = updateBranchNavigation; - function createNewBranch(chat, fromIndex, newMessage) { - if (!chat.branches) initializeBranches(chat); + function renderCurrentChat() { + if(!window.currentChatId || !chatBox) return; - const currentBranch = getCurrentBranch(chat); - const newBranch = currentBranch.slice(0, fromIndex); - if (newMessage) { - newBranch.push(newMessage); - } - window.editMessage = (btn) => { - const container = btn.closest('.user-bubble, .assistant-response'); - if (!container || container.querySelector('.inline-edit-container')) return; - - const messageContent = container.querySelector('.message-content .markdown-body'); - if (!messageContent) return; - - const originalText = messageContent.innerText.trim(); - const messageIndex = parseInt(container.dataset.messageIndex); - - // Create inline editor - const editor = document.createElement('textarea'); - editor.className = 'inline-edit-container'; - editor.value = originalText; - - // Auto-resize function - const autoResize = () => { - editor.style.height = 'auto'; - editor.style.height = editor.scrollHeight + 'px'; - }; - - editor.addEventListener('input', autoResize); - - // Create control buttons - const controls = document.createElement('div'); - controls.className = 'edit-controls'; - controls.innerHTML = ` - - - `; - - // Save function - const saveEdit = () => { - const newText = editor.value.trim(); - if (!newText) return; - - const chat = chats.find(c => c.id === currentChatId); - if (!chat) return; - - // Create new branch for the edit - const currentBranch = getCurrentBranch(chat); - const newMessage = { - role: container.dataset.role, - content: newText - }; - - // Create branch with edited message - const branchIndex = createNewBranch(chat, messageIndex, newMessage); - - // Update display - messageContent.innerHTML = marked.parse(newText); - editor.remove(); - controls.remove(); - - // Regenerate assistant response if user message was edited - if (container.dataset.role === 'user') { - // Remove all messages after this one in current view - const allMessages = chatBox.querySelectorAll('.user-bubble, .assistant-response'); - for (let i = messageIndex + 1; i < allMessages.length; i++) { - allMessages[i].remove(); - } - - // Update the chat data to only include messages up to the edit - const newBranch = chat.branches[chat.currentBranch]; - newBranch.splice(messageIndex + 1); - localStorage.setItem('chat_history', JSON.stringify(chats)); - - // Generate new assistant response - setTimeout(() => generateAssistantForLastUser(), 100); - } - - // Update branch navigation - updateBranchNavigation(); - }; - - // Cancel function - const cancelEdit = () => { - editor.remove(); - controls.remove(); - }; - - // Event handlers - controls.querySelector('.save').addEventListener('click', saveEdit); - controls.querySelector('.cancel').addEventListener('click', cancelEdit); - - editor.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - e.preventDefault(); - cancelEdit(); - } else if (e.key === 'Enter' && e.ctrlKey) { - e.preventDefault(); - saveEdit(); - } - }); - - // Replace content with editor - messageContent.style.display = 'none'; - messageContent.parentNode.insertBefore(editor, messageContent); - messageContent.parentNode.insertBefore(controls, messageContent.nextSibling); - - // Focus and resize - editor.focus(); - autoResize(); - };"What's the weather like?" → "Weather Information" -User: "What is the best way to learn JavaScript?" → "Learning JavaScript"`; - - return fetch('https://models.github.ai/inference/chat/completions', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - model: titleGenerationModel, - messages: [ - { role: 'system', content: titleSystemPrompt }, - { role: 'user', content: firstUserMessage } - ], - temperature: 0.7 - }) - }) - .then(response => { - if (!response.ok) throw new Error('Title generation failed'); - return response.json(); - }) - .then(data => { - const title = data.choices?.[0]?.message?.content?.trim(); - return title || null; - }) - .catch(error => { - console.warn('Failed to generate chat title:', error); - return null; - }); - } - // Edit functionality for chat history - document.getElementById('editChatsBtn')?.addEventListener('click', () => { - const sidebar = document.getElementById('sidebar'); - const btn = document.getElementById('editChatsBtn'); - sidebar.classList.toggle('editing'); - btn.classList.toggle('active'); - - // Toggle icon between edit and check - const svg = btn.querySelector('svg'); - if (sidebar.classList.contains('editing')) { - svg.innerHTML = ''; - } else { - svg.innerHTML = ''; - } - // Toggle delete buttons - document.querySelectorAll('.del-btn').forEach(elem => { - elem.style.opacity = sidebar.classList.contains('editing') ? '1' : '0'; - function loadChat(id){ - const chat = chats.find(c=>c.id===id); + const chat = window.chats.find(c => c.id === window.currentChatId); if(!chat) return; - currentChatId = id; - - // Migrate to branching system if needed - if (!chat.branches && chat.messages) { - chat.branches = [chat.messages]; - chat.currentBranch = 0; - // Show welcome message if chat is empty, otherwise hide it - // Initialize branches if needed (for legacy chat format compatibility) - if (!chat.branches && chat.messages) { - chat.branches = [chat.messages]; - chat.currentBranch = 0; - delete chat.messages; - } - - const currentBranch = chat.branches ? chat.branches[chat.currentBranch || 0] : []; - if (currentBranch.length === 0) { - - // Clear any existing message content to prevent duplication - // Load messages without triggering storage save - currentBranch.forEach(m => { el.remove()); - - const currentBranch = chat.branches?.[chat.currentBranch] || []; - - // Show welcome message if chat is empty, otherwise hide it - if (currentBranch.length === 0) { - const chatBoxContainer = chatBox.querySelector('.max-w-3xl'); - if (chatBoxContainer) { - chatBoxContainer.innerHTML = '

How can I help you today?

Start a conversation by typing a message below

'; - } - } else { - // Recreate message container without welcome message - const chatBoxContainer = chatBox.querySelector('.max-w-3xl'); - if (chatBoxContainer) { - chatBoxContainer.innerHTML = ''; - } - - // Load messages without triggering storage save - currentBranch.forEach((m, index) => { - if(m.role === 'user') { - const bubble = document.createElement('div'); - bubble.className = 'user-bubble'; - bubble.innerHTML = ` -
${marked.parse(m.content)}
- `; - bubble.dataset.role = 'user'; - - // Add action buttons below content - const actions = document.createElement('div'); - actions.className = 'message-actions'; - actions.innerHTML = ` - - - - `; - - bubble.appendChild(actions); - - // Add branch navigation if applicable - const branchNav = renderBranchNavigation(bubble, index); - if (branchNav) { - bubble.appendChild(branchNav); - } - - chatBox.appendChild(bubble); - } else if(m.role === 'assistant') { - const response = document.createElement('div'); - response.className = 'assistant-response'; - response.innerHTML = ` -
${marked.parse(m.content)}
- `; - response.dataset.role = 'assistant'; - - // Add action buttons below content - const actions = document.createElement('div'); - actions.className = 'message-actions'; - function createNewChat(){ - currentChatId='chat_'+Date.now(); - const newChat={ - id:currentChatId, - title:'New Chat', - branches:[[]], - currentBranch:0, - createdAt:new Date().toISOString() - }; - chats.push(newChat); - localStorage.setItem('chat_history', JSON.stringify(chats)); - chatBox.innerHTML=`

How can I help you today?

Start a conversation by typing a message below

`; - loadChatHistory(); - } - - - - - -
- `; - streamingResponse.dataset.role = 'assistant'; - - // Save to chat history - if(fullContent && !chat.messages.some(m => m.content === fullContent && m.role === 'assistant')) { - chat.messages.push({role: 'assistant', content: fullContent}); - localStorage.setItem('chat_history', JSON.stringify(chats)); - } - resetSendButton(); - return; - } - - const chunk = decoder.decode(value, {stream: true}); - const lines = chunk.split('\n').filter(line => line.trim() !== ''); - - for(const line of lines) { - if(line.startsWith('data: ')) { - const data = line.slice(6); - if(data === '[DONE]') { - // Stream completed - handled above - return; - } - - try { - const parsed = JSON.parse(data); - const content = parsed.choices?.[0]?.delta?.content || ''; - if(content) { - fullContent += content; - streamingResponse.innerHTML = `
${marked.parse(fullContent)}
`; - chatBox.scrollTop = chatBox.scrollHeight; - } - } catch(e) { - // Ignore parsing errors - } - } - } - - readStream(); - }).catch(error => { - if (error.name === 'AbortError') { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = `
Generation stopped
`; - } else { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = `
❌ Stream error: ${error.message}
`; - } - resetSendButton(); - }); - } - - readStream(); - }) - .catch(error => { - if (error.name === 'AbortError') { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = `
⚠️ Generation stopped
`; - } else { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = `
❌ Request failed: ${error.message}
`; - } - resetSendButton(); - }); - - messageInput.value = ''; - messageInput.style.height = '60px'; - } - - function resetSendButton() { - sendBtn.classList.remove('hidden'); - stopBtn.classList.add('hidden'); - currentAbortController = null; - } - - function stopGeneration() { - if (currentAbortController) { - currentAbortController.abort(); - } - } - - function generateAssistantForLastUser(){ - const chat = chats.find(c => c.id === currentChatId); - if(!chat) return; - - // Create streaming container for regeneration - const streamingResponse = document.createElement('div'); - streamingResponse.className = 'assistant-response streaming'; - streamingResponse.innerHTML = '
Regenerating response...
'; - chatBox.appendChild(streamingResponse); - chatBox.scrollTop = chatBox.scrollHeight; - - const token = localStorage.getItem('gh_token'); - const selectedModelElement = document.getElementById('selectedModelName'); - const modelId = selectedModelElement?.dataset.modelId; - const model = models.find(m => m.id === modelId); - - if(!token || !token.trim() || token.trim()=="" || token == "") { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = '
❌ No authentication token found.
'; - return; - } - - if(!model || !model.id) { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = '
❌ No valid model selected.
'; - return; - } - - let messages = [...chat.messages]; - const sys = document.getElementById('systemPromptText')?.value.trim(); - if(sys && (!messages[0] || messages[0].role !== 'system')) { - messages.unshift({role: 'system', content: sys}); - } - - fetch('https://models.github.ai/inference/chat/completions', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - messages, - model: model.id, - stream: true - }) - }) - .then(response => { - if(!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let fullContent = ''; - - function readStream() { - reader.read().then(({done, value}) => { - if(done) { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = ` -
${marked.parse(fullContent || '*No response received*')}
-
- - - -
- `; - streamingResponse.dataset.role = 'assistant'; - - if(fullContent) { - chat.messages.push({role: 'assistant', content: fullContent}); - localStorage.setItem('chat_history', JSON.stringify(chats)); - } - return; - } - - const chunk = decoder.decode(value, {stream: true}); - const lines = chunk.split('\n').filter(line => line.trim() !== ''); - - for(const line of lines) { - if(line.startsWith('data: ')) { - const data = line.slice(6); - if(data === '[DONE]') { - return; - } - - try { - const parsed = JSON.parse(data); - const content = parsed.choices?.[0]?.delta?.content || ''; - if(content) { - fullContent += content; - streamingResponse.innerHTML = `
${marked.parse(fullContent)}
`; - chatBox.scrollTop = chatBox.scrollHeight; - } - } catch(e) { - // Ignore parsing errors - } - } - } - - readStream(); - }).catch(error => { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = `
❌ Stream error: ${error.message}
`; - }); - } - - readStream(); - }) - .catch(error => { - streamingResponse.classList.remove('streaming'); - streamingResponse.innerHTML = `
❌ Request failed: ${error.message}
`; - }); - } - - // OAuth & Fetch - const GITHUB_CLIENT_ID = 'Ov23li2MmzqUhjwLwwMB'; - const OAUTH_REDIRECT_URI = window.location.origin + window.location.pathname; - - function startOAuth(){ - if(location.protocol==='file:') { alert('Serve over http:// (not file://) for OAuth'); return; } - const state = crypto.randomUUID(); - localStorage.setItem('oauth_state', state); - const scope = 'read:user read:org models'; - const authUrl = `https://github.com/login/oauth/authorize?client_id=${encodeURIComponent(GITHUB_CLIENT_ID)}&redirect_uri=${encodeURIComponent(OAUTH_REDIRECT_URI)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`; - window.location = authUrl; - } - - function handleOAuthCallback(){ - const params = new URLSearchParams(location.search); - const code = params.get('code'); - const state = params.get('state'); - if(!code) return; - const expected = localStorage.getItem('oauth_state'); - if(!state || state !== expected){ alert('OAuth state mismatch. Retry sign in.'); return; } - - fetch(`${API_BASE}/api/exchange?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`) - .then(r=>{ if(!r.ok) throw new Error('Exchange failed'); return r.json(); }) - .then(data=>{ - if(!data.access_token){ throw new Error('No access token in response'); } - localStorage.setItem('gh_token', data.access_token); - const octokit = new Octokit({ auth: data.access_token }); - history.replaceState({}, document.title, OAUTH_REDIRECT_URI); - fetchModels(); loadUserProfile(); showChatPage(); - }) - .catch(err=>{ console.error(err); alert('OAuth exchange error: '+err.message); }); - } - - function fetchModels(){ - const token = localStorage.getItem('gh_token'); - if(!token || !token.trim() || token.trim()=="" || token == "") return; - const statusEl = document.getElementById('networkStatus'); - - // TODO: Add Cerebras models when external API keys are implemented - // Cerebras offers better rate limits per model (especially for less used models like llama 70B) - const fallback = [{"id":"openai/gpt-4.1","name":"OpenAI GPT-4.1","publisher":"OpenAI","summary":"gpt-4.1 outperforms gpt-4o across the board, with major gains in coding, instruction following, and long-context understanding","rate_limit_tier":"high","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-04-14","capabilities":["streaming","tool-calling"],"limits":{"max_input_tokens":1048576,"max_output_tokens":32768},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-4-1"},{"id":"openai/gpt-4.1-mini","name":"OpenAI GPT-4.1-mini","publisher":"OpenAI","summary":"gpt-4.1-mini outperform gpt-4o-mini across the board, with major gains in coding, instruction following, and long-context handling","rate_limit_tier":"low","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-04-14","capabilities":["streaming","tool-calling"],"limits":{"max_input_tokens":1048576,"max_output_tokens":32768},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-4-1-mini"},{"id":"openai/gpt-4.1-nano","name":"OpenAI GPT-4.1-nano","publisher":"OpenAI","summary":"gpt-4.1-nano provides gains in coding, instruction following, and long-context handling along with lower latency and cost","rate_limit_tier":"low","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-04-14","capabilities":["streaming","tool-calling"],"limits":{"max_input_tokens":1048576,"max_output_tokens":32768},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-4-1-nano"},{"id":"openai/gpt-4o","name":"OpenAI GPT-4o","publisher":"OpenAI","summary":"OpenAI's most advanced multimodal model in the gpt-4o family. Can handle both text and image inputs.","rate_limit_tier":"high","supported_input_modalities":["text","image","audio"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2024-11-20","capabilities":["agents","assistants","streaming","tool-calling"],"limits":{"max_input_tokens":131072,"max_output_tokens":16384},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-4o"},{"id":"openai/gpt-4o-mini","name":"OpenAI GPT-4o mini","publisher":"OpenAI","summary":"An affordable, efficient AI solution for diverse text and image tasks.","rate_limit_tier":"low","supported_input_modalities":["text","image","audio"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2024-07-18","capabilities":["agents","assistants","streaming","tool-calling"],"limits":{"max_input_tokens":131072,"max_output_tokens":4096},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-4o-mini"},{"id":"openai/gpt-5","name":"OpenAI gpt-5","publisher":"OpenAI","summary":"gpt-5 is designed for logic-heavy and multi-step tasks. ","rate_limit_tier":"custom","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-08-07","capabilities":["reasoning","tool-calling","streaming"],"limits":{"max_input_tokens":200000,"max_output_tokens":100000},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-5"},{"id":"openai/gpt-5-chat","name":"OpenAI gpt-5-chat (preview)","publisher":"OpenAI","summary":"gpt-5-chat (preview) is an advanced, natural, multimodal, and context-aware conversations for enterprise applications.","rate_limit_tier":"custom","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-08-07","capabilities":["reasoning","tool-calling","streaming"],"limits":{"max_input_tokens":200000,"max_output_tokens":100000},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-5-chat"},{"id":"openai/gpt-5-mini","name":"OpenAI gpt-5-mini","publisher":"OpenAI","summary":"gpt-5-mini is a lightweight version for cost-sensitive applications.","rate_limit_tier":"custom","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-08-07","capabilities":["reasoning","tool-calling","streaming"],"limits":{"max_input_tokens":200000,"max_output_tokens":100000},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-5-mini"},{"id":"openai/gpt-5-nano","name":"OpenAI gpt-5-nano","publisher":"OpenAI","summary":"gpt-5-nano is optimized for speed, ideal for applications requiring low latency. ","rate_limit_tier":"custom","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["multipurpose","multilingual","multimodal"],"registry":"azure-openai","version":"2025-08-07","capabilities":["reasoning","tool-calling","streaming"],"limits":{"max_input_tokens":200000,"max_output_tokens":100000},"html_url":"https://github.com/marketplace/models/azure-openai/gpt-5-nano"},{"id":"openai/o1","name":"OpenAI o1","publisher":"OpenAI","summary":"Focused on advanced reasoning and solving complex problems, including math and science tasks. Ideal for applications that require deep contextual understanding and agentic workflows.","rate_limit_tier":"custom","supported_input_modalities":["text","image"],"supported_output_modalities":["text"],"tags":["reasoning","multilingual","coding"],"registry":"azure-openai","version":"2024-12-17","capabilities":["reasoning","tool-calling"],"limits":{"max_input_tokens":200000,"max_output_tokens":100000},"html_url":"https://github.com/marketplace/models/azure-openai/o1"},{"id":"openai/o1-mini","name":"OpenAI o1-mini","publisher":"OpenAI","summary":"Smaller, faster, and 80% cheaper than o1-preview, performs well at code generation and small context operations.","rate_limit_tier":"custom","supported_input_modalities":["text"],"supported_output_modalities":["text"],"tags":["reasoning","multilingual","coding"],"registry":"azure-openai","version":"2024-09-12","capabilities":["reasoning","streaming"],"limits":{"max_input_tokens":128000,"max_output_tokens":65536},"html_url":"https://github.com/marketplace/models/azure-openai/o1-mini"}] - fetch(`${API_BASE}/api/models`,{headers:{Authorization:`Bearer ${token}`}}) - .then(r=>{ if(!r.ok) throw new Error(r.status); return r.json(); }) - .then(data=>{ - models = Array.isArray(data)? data : (data.models||fallback); - renderDropdownItems(models); - renderTitleDropdownItems(models); - - // Restore previously selected model or select first available - const savedModelId = localStorage.getItem('selected_model'); - const modelToSelect = savedModelId && models.find(m => m.id === savedModelId) - ? savedModelId - : (models.length ? models[0].id : null); - - if(modelToSelect) selectModel(modelToSelect); - - // Restore previously selected title model or select GPT-4.1 Mini as default - const savedTitleModelId = localStorage.getItem('selected_title_model'); - const titleModelToSelect = savedTitleModelId && models.find(m => m.id === savedTitleModelId) - ? savedTitleModelId - : models.find(m => m.id === 'openai/gpt-4.1-mini')?.id || (models.length ? models[0].id : null); - - if(titleModelToSelect) selectTitleModel(titleModelToSelect); - - statusEl?.classList.add('hidden'); - }) - .catch(err=>{ - console.warn('Model fetch failed', err); - models = fallback; - renderDropdownItems(models); - renderTitleDropdownItems(models); - - // Restore previously selected model or select first available - const savedModelId = localStorage.getItem('selected_model'); - const modelToSelect = savedModelId && models.find(m => m.id === savedModelId) - ? savedModelId - : (models.length ? models[0].id : null); - - if(modelToSelect) selectModel(modelToSelect); - - // Restore previously selected title model or select GPT-4.1 Mini as default - const savedTitleModelId = localStorage.getItem('selected_title_model'); - const titleModelToSelect = savedTitleModelId && models.find(m => m.id === savedTitleModelId) - ? savedTitleModelId - : models.find(m => m.id === 'openai/gpt-4.1-mini')?.id || (models.length ? models[0].id : null); - - if(titleModelToSelect) selectTitleModel(titleModelToSelect); - - if(statusEl){ statusEl.textContent = 'Using fallback models'; statusEl.classList.remove('hidden'); } - }); - } - - document.getElementById('githubLogin')?.addEventListener('click', (e)=>{ e.preventDefault(); startOAuth(); }); - handleOAuthCallback(); - - // Event Listeners - sendBtn?.addEventListener('click', sendMessage); - stopBtn?.addEventListener('click', stopGeneration); - newChatBtn?.addEventListener('click', goToHomepage); - - - // Mobile dropdown - document.getElementById('mobileDropdownSelected')?.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const mobileContent = document.getElementById('mobileDropdownContent'); - const mobileSelected = document.getElementById('mobileDropdownSelected'); - mobileContent?.classList.toggle('show'); - mobileSelected?.classList.toggle('active'); - }); - - // Mobile hamburger menu - document.getElementById('mobileMenuBtn')?.addEventListener('click', () => { - const sidebar = document.getElementById('sidebar'); - sidebar?.classList.toggle('mobile-open'); - }); - - dropdownSearch?.addEventListener('input', e => filterModels(e.target.value)); - document.getElementById('mobileDropdownSearch')?.addEventListener('input', e => filterModels(e.target.value)); - - messageInput?.addEventListener('keydown', e=>{ - if(e.key==='Enter'){ - if(e.ctrlKey){ e.preventDefault(); sendMessage(); } - else if(e.shiftKey){ - const start=messageInput.selectionStart; - const end=messageInput.selectionEnd; - messageInput.value=messageInput.value.substring(0,start)+'\n'+messageInput.value.substring(end); - messageInput.selectionStart=messageInput.selectionEnd=start+1; - // Auto-resize after adding newline - setTimeout(() => { - messageInput.style.height = 'auto'; - messageInput.style.height = messageInput.scrollHeight + 'px'; - }, 0); - e.preventDefault(); - } - else { e.preventDefault(); sendMessage(); } - } - }); - - // Auto-resize textarea on input - messageInput?.addEventListener('input', function(){ - this.style.height='auto'; - this.style.height=Math.min(this.scrollHeight, 200)+'px'; - }); - - // Keyboard navigation - let historyFocusIndex=-1; - document.addEventListener('keydown', e=>{ - if(e.altKey && e.key==='n'){ e.preventDefault(); newChatBtn?.click(); } - if(e.altKey && e.key==='m'){ e.preventDefault(); dropdownSelected?.click(); } - if(e.altKey && e.key==='/'){ e.preventDefault(); messageInput?.focus(); } - if(e.altKey && (e.key==='ArrowDown'||e.key==='ArrowUp')){ - e.preventDefault(); - const items=[...chatHistory.querySelectorAll('.chat-history-item')]; - if(!items.length) return; - historyFocusIndex = e.key==='ArrowDown' ? (historyFocusIndex+1)%items.length : (historyFocusIndex-1+items.length)%items.length; - items.forEach(i=>i.classList.remove('focused')); - const t=items[historyFocusIndex]; - t.classList.add('focused'); - t.scrollIntoView({block:'nearest'}); - if(e.altKey&&e.shiftKey) t.click(); - } - }); - - // Initialize - if (typeof hljs !== 'undefined') { - hljs.highlightAll(); } + // Initialize chat system loadChatHistory(); - if(!chats.length) createNewChat(); + if(!window.chats.length) createNewChat(); + + console.log('LLM Dump authentication system loaded successfully'); })();