// ========================================== // API Functions // ========================================== async function fetchModels(showInModal = true) { const endpoint = showInModal ? document.getElementById('apiEndpoint').value.trim() : State.settings.apiEndpoint; const apiKey = showInModal ? document.getElementById('apiKey').value.trim() : State.settings.apiKey; if (!endpoint || !apiKey) { showToast('⚠️ Configure API endpoint and key first', 'warning'); if (!showInModal) openSettings(); return; } const btn = showInModal ? document.getElementById('fetchModelsBtn') : document.getElementById('quickFetchBtn'); const originalText = btn.innerHTML; btn.innerHTML = '⏳'; btn.disabled = true; try { const response = await fetch(endpoint.replace(/\/$/, '') + '/models', { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`API Error: ${response.status}`); } const data = await response.json(); const models = data.data || data.models || data || []; State.models = Array.isArray(models) ? models.map(m => ({ id: m.id || m.name || m, owned_by: m.owned_by || null })).sort((a, b) => a.id.localeCompare(b.id)) : []; saveModels(); if (showInModal) { populateModelSelect(document.getElementById('model')); } updateQuickModelSelector(); showToast(`✅ Loaded ${State.models.length} models`, 'success'); } catch (error) { console.error('Fetch models error:', error); showToast(`❌ Failed: ${error.message}`, 'error'); } finally { btn.innerHTML = originalText; btn.disabled = false; } } async function sendApiRequest(messages) { const chatContainer = document.getElementById('chatMessages'); // Add typing indicator const typingDiv = document.createElement('div'); typingDiv.className = 'message assistant'; typingDiv.id = 'typingIndicator'; typingDiv.innerHTML = `
🤖
Assistant
`; chatContainer.appendChild(typingDiv); scrollToBottom(); State.isGenerating = true; State.abortController = new AbortController(); document.getElementById('stopBtn').classList.add('visible'); document.getElementById('sendBtn').disabled = true; try { const endpoint = State.settings.apiEndpoint.replace(/\/$/, '') + '/chat/completions'; const model = document.getElementById('quickModel').value || State.settings.model; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${State.settings.apiKey}` }, body: JSON.stringify({ model, messages: messages.map(m => ({ role: m.role, content: m.content })), max_tokens: State.settings.maxTokens, temperature: State.settings.temperature, top_p: State.settings.topP, presence_penalty: State.settings.presencePenalty, stream: State.settings.stream }), signal: State.abortController.signal }); if (!response.ok) { const error = await response.text(); throw new Error(`API Error: ${response.status} - ${error}`); } document.getElementById('typingIndicator')?.remove(); let assistantContent = ''; if (State.settings.stream) { const assistantDiv = document.createElement('div'); assistantDiv.className = 'message assistant'; assistantDiv.innerHTML = `
🤖
Assistant
`; chatContainer.appendChild(assistantDiv); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.delta?.content || ''; assistantContent += content; document.getElementById('streamingContent').innerHTML = formatMessage(assistantContent); scrollToBottom(); } catch (e) {} } } } } else { const data = await response.json(); assistantContent = data.choices?.[0]?.message?.content || 'No response'; } messages.push({ role: 'assistant', content: assistantContent, timestamp: new Date().toISOString() }); updateChat(State.currentChatId, { messages, model }); if (!State.settings.stream) { renderMessages(messages); } updateRegenerateButton(messages); } catch (error) { document.getElementById('typingIndicator')?.remove(); if (error.name === 'AbortError') { showToast('⚠️ Generation stopped', 'warning'); } else { console.error('API Error:', error); showToast(`❌ ${error.message}`, 'error'); } } finally { State.isGenerating = false; State.abortController = null; document.getElementById('stopBtn').classList.remove('visible'); document.getElementById('sendBtn').disabled = false; } } function stopGeneration() { if (State.abortController) { State.abortController.abort(); State.abortController = null; } State.isGenerating = false; document.getElementById('stopBtn').classList.remove('visible'); document.getElementById('sendBtn').disabled = false; }