354 lines
11 KiB
HTML
354 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AI Chat - AI Cheat Sheet</title>
|
|
<link rel="stylesheet" href="../css/style.css">
|
|
</head>
|
|
<body>
|
|
|
|
<nav>
|
|
<div class="nav-inner">
|
|
<a href="../index.html" class="nav-brand">AI Cheat Sheet</a>
|
|
<div class="nav-links">
|
|
<a href="/pages/terminology.html">Terminology</a>
|
|
<a href="/pages/techniques.html">Techniques</a>
|
|
<a href="/pages/use-cases.html">Use Cases</a>
|
|
<a href="/pages/model-types.html">Model Types</a>
|
|
<a href="/pages/prompts.html">Prompt Guide</a>
|
|
<a href="/pages/math.html">Math & Concepts</a>
|
|
<a href="/pages/chat.html" class="active">Chat</a>
|
|
<a href="/pages/image-gen.html">Image Gen</a>
|
|
</div>
|
|
<button class="dark-toggle" id="darkToggle" aria-label="Toggle dark mode">🌙</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<button class="menu-toggle" id="menuToggle" aria-label="Toggle menu">☰</button>
|
|
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
|
|
|
<script>
|
|
(function(){
|
|
var btn = document.getElementById('darkToggle');
|
|
var saved = localStorage.getItem('theme');
|
|
if(saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)){
|
|
document.documentElement.setAttribute('data-theme','dark');
|
|
btn.textContent = '☀️';
|
|
}
|
|
btn.addEventListener('click', function(){
|
|
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
if(isDark){
|
|
document.documentElement.removeAttribute('data-theme');
|
|
btn.textContent = '🌙';
|
|
localStorage.setItem('theme','light');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme','dark');
|
|
btn.textContent = '☀️';
|
|
localStorage.setItem('theme','dark');
|
|
}
|
|
});
|
|
|
|
var menuToggle = document.getElementById('menuToggle');
|
|
var nav = document.querySelector('nav');
|
|
var backdrop = document.getElementById('sidebarBackdrop');
|
|
if(menuToggle && nav){
|
|
menuToggle.addEventListener('click', function(){
|
|
nav.classList.toggle('sidebar-open');
|
|
var isOpen = nav.classList.contains('sidebar-open');
|
|
menuToggle.textContent = isOpen ? '✕' : '☰';
|
|
if(backdrop){
|
|
backdrop.classList.toggle('visible', isOpen);
|
|
}
|
|
});
|
|
if(backdrop){
|
|
backdrop.addEventListener('click', function(){
|
|
nav.classList.remove('sidebar-open');
|
|
menuToggle.textContent = '☰';
|
|
backdrop.classList.remove('visible');
|
|
});
|
|
}
|
|
document.addEventListener('click', function(e){
|
|
if(nav.classList.contains('sidebar-open') && !nav.contains(e.target) && e.target !== menuToggle){
|
|
nav.classList.remove('sidebar-open');
|
|
menuToggle.textContent = '☰';
|
|
if(backdrop) backdrop.classList.remove('visible');
|
|
}
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<div class="hero">
|
|
<h1>AI Chat</h1>
|
|
<p>Chat with an LLM powered by your chosen model.</p>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="chat-config">
|
|
<div class="config-row">
|
|
<label for="apiUrl">API Endpoint</label>
|
|
<input type="text" id="apiUrl" value="https://llama-instruct.reeselink.com/v1">
|
|
</div>
|
|
<div class="config-row">
|
|
<label for="apiToken">API Token</label>
|
|
<input type="password" id="apiToken" placeholder="Enter your API token">
|
|
</div>
|
|
<div class="config-row">
|
|
<label for="modelName">Model</label>
|
|
<input type="text" id="modelName" value="instruct">
|
|
</div>
|
|
<div class="config-row">
|
|
<button id="clearBtn" class="clear-btn">Clear Chat</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-messages" id="chatMessages">
|
|
<div class="message assistant-message">
|
|
<div class="message-bubble">
|
|
<p>Hello! Enter your API token and model name, then start typing your message below.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-input-area">
|
|
<textarea id="userInput" placeholder="Type your message..." rows="1"></textarea>
|
|
<button id="sendBtn" class="send-btn">Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>AI Cheat Sheet — A learning reference for artificial intelligence</footer>
|
|
|
|
<div class="search-results-dropdown" id="searchResultsContainer"></div>
|
|
|
|
<script src="../lib/search.js"></script>
|
|
<script>Search.init();</script>
|
|
<script src="../lib/marked.min.js"></script>
|
|
|
|
<script>
|
|
(function(){
|
|
var chatMessages = document.getElementById('chatMessages');
|
|
var userInput = document.getElementById('userInput');
|
|
var sendBtn = document.getElementById('sendBtn');
|
|
var clearBtn = document.getElementById('clearBtn');
|
|
var apiUrlInput = document.getElementById('apiUrl');
|
|
var apiTokenInput = document.getElementById('apiToken');
|
|
var modelNameInput = document.getElementById('modelName');
|
|
|
|
// Parse markdown in initial welcome message
|
|
var initialBubble = chatMessages.querySelector('.message-bubble p');
|
|
if(initialBubble) {
|
|
initialBubble.innerHTML = marked.parseInline(initialBubble.textContent);
|
|
}
|
|
|
|
// Restore saved settings
|
|
var savedToken = localStorage.getItem('apiToken');
|
|
if(savedToken) apiTokenInput.value = savedToken;
|
|
var savedModel = localStorage.getItem('modelName');
|
|
if(savedModel) modelNameInput.value = savedModel;
|
|
var savedUrl = localStorage.getItem('apiUrl');
|
|
if(savedUrl) apiUrlInput.value = savedUrl;
|
|
|
|
var conversationHistory = [];
|
|
var isStreaming = false;
|
|
|
|
function addMessage(role, content) {
|
|
var msgDiv = document.createElement('div');
|
|
msgDiv.className = 'message ' + (role === 'user' ? 'user-message' : 'assistant-message');
|
|
|
|
var avatar = document.createElement('div');
|
|
avatar.className = 'message-avatar';
|
|
avatar.textContent = role === 'user' ? 'You' : 'AI';
|
|
|
|
var bubble = document.createElement('div');
|
|
bubble.className = 'message-bubble';
|
|
|
|
var p = document.createElement('p');
|
|
p.className = 'message-content';
|
|
if(role === 'assistant') {
|
|
p.innerHTML = marked.parse(content);
|
|
} else {
|
|
p.textContent = content;
|
|
}
|
|
bubble.appendChild(p);
|
|
|
|
msgDiv.appendChild(avatar);
|
|
msgDiv.appendChild(bubble);
|
|
chatMessages.appendChild(msgDiv);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
|
return p;
|
|
}
|
|
|
|
function addStreamingMessage() {
|
|
var msgDiv = document.createElement('div');
|
|
msgDiv.className = 'message assistant-message';
|
|
msgDiv.id = 'streaming-msg';
|
|
|
|
var avatar = document.createElement('div');
|
|
avatar.className = 'message-avatar';
|
|
avatar.textContent = 'AI';
|
|
|
|
var bubble = document.createElement('div');
|
|
bubble.className = 'message-bubble';
|
|
|
|
var p = document.createElement('p');
|
|
p.className = 'message-content streaming';
|
|
p.textContent = '';
|
|
|
|
bubble.appendChild(p);
|
|
msgDiv.appendChild(avatar);
|
|
msgDiv.appendChild(bubble);
|
|
chatMessages.appendChild(msgDiv);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
|
return p;
|
|
}
|
|
|
|
function setLoading(loading) {
|
|
isStreaming = loading;
|
|
sendBtn.disabled = loading;
|
|
sendBtn.textContent = loading ? '...' : 'Send';
|
|
userInput.disabled = loading;
|
|
if(!loading) {
|
|
userInput.focus();
|
|
}
|
|
}
|
|
|
|
function sendMessage() {
|
|
if(isStreaming) return;
|
|
|
|
var text = userInput.value.trim();
|
|
if(!text) return;
|
|
|
|
var model = modelNameInput.value.trim();
|
|
if(!model) {
|
|
alert('Please enter a model name.');
|
|
modelNameInput.focus();
|
|
return;
|
|
}
|
|
|
|
addMessage('user', text);
|
|
conversationHistory.push({ role: 'user', content: text });
|
|
userInput.value = '';
|
|
userInput.style.height = 'auto';
|
|
setLoading(true);
|
|
|
|
var apiEndpoint = apiUrlInput.value.trim().replace(/\/+$/, '');
|
|
var apiToken = apiTokenInput.value.trim();
|
|
|
|
var streamContent = '';
|
|
var contentEl = addStreamingMessage();
|
|
|
|
var headers = { 'Content-Type': 'application/json' };
|
|
if(apiToken) {
|
|
headers['Authorization'] = 'Bearer ' + apiToken;
|
|
}
|
|
|
|
fetch(apiEndpoint + '/chat/completions', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify({
|
|
messages: conversationHistory,
|
|
model: model,
|
|
stream: true
|
|
})
|
|
})
|
|
.then(function(response) {
|
|
if(!response.ok) {
|
|
throw new Error('API error: ' + response.status + ' ' + response.statusText);
|
|
}
|
|
var reader = response.body.getReader();
|
|
var decoder = new TextDecoder();
|
|
var buffer = '';
|
|
|
|
function read() {
|
|
return reader.read().then(function({ done, value }) {
|
|
if(done) {
|
|
setLoading(false);
|
|
contentEl.classList.remove('streaming');
|
|
return;
|
|
}
|
|
buffer += decoder.decode(value, { stream: true });
|
|
var lines = buffer.split('\n');
|
|
buffer = lines.pop();
|
|
|
|
for(var i = 0; i < lines.length; i++) {
|
|
var line = lines[i].trim();
|
|
if(line.startsWith('data: ')) {
|
|
var data = line.slice(6);
|
|
if(data === '[DONE]') {
|
|
setLoading(false);
|
|
contentEl.classList.remove('streaming');
|
|
return;
|
|
}
|
|
try {
|
|
var json = JSON.parse(data);
|
|
var delta = json.choices && json.choices[0] && json.choices[0].delta;
|
|
if(delta && delta.content) {
|
|
streamContent += delta.content;
|
|
contentEl.innerHTML = marked.parse(streamContent);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
} catch(e) {
|
|
// skip malformed JSON
|
|
}
|
|
}
|
|
}
|
|
return read();
|
|
});
|
|
}
|
|
return read();
|
|
})
|
|
.catch(function(err) {
|
|
setLoading(false);
|
|
contentEl.classList.remove('streaming');
|
|
contentEl.textContent = 'Error: ' + err.message;
|
|
contentEl.style.color = 'var(--pink-700)';
|
|
// Remove failed message from history
|
|
conversationHistory.pop();
|
|
});
|
|
}
|
|
|
|
sendBtn.addEventListener('click', sendMessage);
|
|
|
|
userInput.addEventListener('keydown', function(e) {
|
|
if(e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
userInput.addEventListener('input', function() {
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
|
|
});
|
|
|
|
clearBtn.addEventListener('click', function() {
|
|
conversationHistory = [];
|
|
chatMessages.innerHTML = '';
|
|
var welcomeMsg = document.createElement('div');
|
|
welcomeMsg.className = 'message assistant-message';
|
|
welcomeMsg.innerHTML = '<div class="message-avatar">AI</div><div class="message-bubble"><p>' + marked.parse('Hello! Chat cleared. How can I help you?') + '</p></div>';
|
|
chatMessages.appendChild(welcomeMsg);
|
|
});
|
|
|
|
// Focus input on page load
|
|
userInput.focus();
|
|
|
|
// Save settings on change
|
|
apiTokenInput.addEventListener('change', function() {
|
|
localStorage.setItem('apiToken', this.value);
|
|
});
|
|
modelNameInput.addEventListener('change', function() {
|
|
localStorage.setItem('modelName', this.value);
|
|
});
|
|
apiUrlInput.addEventListener('change', function() {
|
|
localStorage.setItem('apiUrl', this.value);
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|