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 `
-
-
- ${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}
-
-