ButterM40's picture
Add per-token alternatives + hover tooltip UI
5a6a589
let debounceTimer;
const DEBOUNCE_DELAY = 300;
let isDebugMode = {
chat: false,
summary: false
};
document.addEventListener('DOMContentLoaded', () => {
setupTabNavigation();
setupChatPredictions();
setupSummaryPredictions();
setupImageUpload();
initializeChatInterface();
});
function setupTabNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
navItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
const tabId = item.getAttribute('data-tab');
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
});
});
}
function initializeChatInterface() {
const chatOutput = document.getElementById('chat-output');
chatOutput.innerHTML = `
<div class="message system">
Hello! I'm your AI assistant. How can I help you today?
</div>
`;
}
function setupChatPredictions() {
const chatInput = document.getElementById('chat-input');
const toggleDebug = document.querySelector('#chat .toggle-debug');
const wordPredictions = document.querySelector('#chat .word-predictions');
toggleDebug.addEventListener('click', () => {
isDebugMode.chat = !isDebugMode.chat;
wordPredictions.style.display = isDebugMode.chat ? 'block' : 'none';
toggleDebug.classList.toggle('active');
});
chatInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (isDebugMode.chat) {
const words = chatInput.value.split(/\s+/);
if (words.length > 0) {
const lastWord = words[words.length - 1];
getPredictions(lastWord, 'chat');
}
}
}, DEBOUNCE_DELAY);
});
}
function setupSummaryPredictions() {
const summaryInput = document.getElementById('summary-input');
const toggleDebug = document.querySelector('#summary .toggle-debug');
const wordPredictions = document.querySelector('#summary .word-predictions');
toggleDebug.addEventListener('click', () => {
isDebugMode.summary = !isDebugMode.summary;
wordPredictions.style.display = isDebugMode.summary ? 'block' : 'none';
toggleDebug.classList.toggle('active');
});
summaryInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (isDebugMode.summary) {
const words = summaryInput.value.split(/\s+/);
if (words.length > 0) {
const lastWord = words[words.length - 1];
getPredictions(lastWord, 'summary');
}
}
}, DEBOUNCE_DELAY);
});
}
async function getPredictions(word, section) {
try {
const response = await fetch('/predict_words', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ word: word })
});
const predictions = await response.json();
displayPredictions(predictions, section);
} catch (error) {
console.error('Error getting predictions:', error);
}
}
function displayPredictions(predictions, section) {
const predictionsContent = document.querySelector(`#${section} .predictions-content`);
predictionsContent.innerHTML = '';
predictions.forEach((pred, index) => {
const predictionItem = document.createElement('div');
predictionItem.className = 'prediction-item';
predictionItem.innerHTML = `
<span>${index + 1}. ${pred.word}</span>
<span>${(pred.probability * 100).toFixed(2)}%</span>
`;
predictionsContent.appendChild(predictionItem);
});
}
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Add user message to chat
const chatOutput = document.getElementById('chat-output');
const userMessage = document.createElement('div');
userMessage.className = 'message user';
userMessage.textContent = message;
chatOutput.appendChild(userMessage);
// Show loading
document.getElementById('loading').classList.add('show');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
});
const data = await response.json();
const botMessage = document.createElement('div');
botMessage.className = 'message assistant';
// If the server returned per-token info, render tokens individually so we
// can show alternative tokens on hover. Otherwise, fall back to plain text.
if (data.tokens && Array.isArray(data.tokens) && data.tokens.length > 0) {
const frag = document.createDocumentFragment();
const wrapper = document.createElement('div');
wrapper.className = 'generated-text';
data.tokens.forEach((t, idx) => {
const span = document.createElement('span');
span.className = 'generated-token';
span.setAttribute('data-token-index', idx);
span.textContent = t.token || '';
// store alternatives on the element for quick access
span._alternatives = t.alternatives || [];
wrapper.appendChild(span);
});
frag.appendChild(wrapper);
botMessage.appendChild(frag);
chatOutput.appendChild(botMessage);
// Tooltip element for showing alternatives
let tooltip = document.getElementById('alt-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'alt-tooltip';
tooltip.className = 'alt-tooltip';
document.body.appendChild(tooltip);
}
// Attach hover listeners
wrapper.querySelectorAll('.generated-token').forEach(el => {
el.addEventListener('mouseenter', (ev) => {
const alts = el._alternatives || [];
if (!alts.length) return;
// build tooltip html
tooltip.innerHTML = '';
const title = document.createElement('div');
title.className = 'alt-title';
title.textContent = 'Alternatives';
tooltip.appendChild(title);
alts.forEach(a => {
const row = document.createElement('div');
row.className = 'alt-row';
const tok = document.createElement('span');
tok.className = 'alt-token';
tok.textContent = a.token || '';
const prob = document.createElement('span');
prob.className = 'alt-prob';
prob.textContent = `${(a.probability * 100).toFixed(2)}%`;
row.appendChild(tok);
row.appendChild(prob);
// click to insert token into input (optional UX)
row.addEventListener('click', () => {
const chatInput = document.getElementById('chat-input');
insertAtCursor(chatInput, a.token || '');
});
tooltip.appendChild(row);
});
// Position tooltip near the hovered token
const rect = el.getBoundingClientRect();
tooltip.style.display = 'block';
tooltip.style.left = `${rect.left + window.scrollX}px`;
tooltip.style.top = `${rect.bottom + window.scrollY + 6}px`;
});
el.addEventListener('mouseleave', () => {
const tooltip = document.getElementById('alt-tooltip');
if (tooltip) tooltip.style.display = 'none';
});
});
} else {
botMessage.textContent = data.response || 'Sorry, I could not process your request.';
chatOutput.appendChild(botMessage);
}
// Clear input
input.value = '';
chatOutput.scrollTop = chatOutput.scrollHeight;
} catch (error) {
console.error('Error:', error);
const errorMessage = document.createElement('div');
errorMessage.className = 'message error';
errorMessage.textContent = 'Error: Could not send message.';
chatOutput.appendChild(errorMessage);
} finally {
document.getElementById('loading').classList.remove('show');
}
}
async function generateSummary() {
const input = document.getElementById('summary-input');
const text = input.value.trim();
if (!text) {
alert('Please enter some text to summarize');
return;
}
const outputDiv = document.getElementById('summary-output');
document.getElementById('loading').classList.add('show');
try {
const response = await fetch('/api/summarize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: text,
max_length: 150,
min_length: 30
})
});
const data = await response.json();
if (data.success) {
outputDiv.innerHTML = `
<div class="output-content">
${data.summary}
</div>
`;
} else {
throw new Error(data.error || 'Could not generate summary');
}
} catch (error) {
console.error('Error:', error);
outputDiv.innerHTML = `
<div class="output-content error">
Error: ${error.message || 'Could not generate summary.'}
</div>
`;
} finally {
document.getElementById('loading').classList.remove('show');
}
}
function setupImageUpload() {
const imageInput = document.getElementById('image-input');
const imagePreview = document.getElementById('image-preview');
const imageOutput = document.getElementById('image-output');
imageInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
// Show preview
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imagePreview.style.display = 'block';
};
reader.readAsDataURL(file);
}
});
}
async function processImage() {
const imageInput = document.getElementById('image-input');
const imageOutput = document.getElementById('image-output');
if (!imageInput.files[0]) {
alert('Please select an image first');
return;
}
// Validate file type
const file = imageInput.files[0];
if (!file.type.startsWith('image/')) {
alert('Please select a valid image file');
return;
}
const formData = new FormData();
formData.append('image', file);
document.getElementById('loading').classList.add('show');
try {
// Make sure to use the correct endpoint
const response = await fetch('/process_image', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
imageOutput.innerHTML = `
<div class="output-content">
<h3>Image Analysis Results:</h3>
<p>${result.description}</p>
</div>
`;
} else {
throw new Error(result.error || 'Failed to analyze image');
}
} catch (error) {
console.error('Error processing image:', error);
imageOutput.innerHTML = `
<div class="output-content error">
Error: ${error.message || 'Could not process image.'}
</div>
`;
} finally {
document.getElementById('loading').classList.remove('show');
}
}
function displayImageResults(results) {
const output = document.getElementById('image-output');
if (results.success) {
output.innerHTML = `
<div class="output-content">
<h3>Recognition Results:</h3>
<p>${results.description}</p>
</div>
`;
} else {
output.innerHTML = `
<div class="output-content error">
Error: ${results.error || 'Could not analyze image.'}
</div>
`;
}
}
// Add event listeners for Enter key
document.getElementById('chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
document.getElementById('summary-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
generateSummary();
}
});
// Helper to insert text at the cursor position for input/textarea
function insertAtCursor(el, text) {
if (!el) return;
const start = typeof el.selectionStart === 'number' ? el.selectionStart : el.value.length;
const end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
const before = el.value.substring(0, start);
const after = el.value.substring(end);
el.value = before + text + after;
const pos = before.length + text.length;
el.selectionStart = el.selectionEnd = pos;
el.focus();
}