chromaticai / script.js
Candle6468's picture
The sidebar doesn’t extend and the chat function doesn’t work. Make sure that it can handle conversations with context and not just single messages
a8c3c60 verified
Raw
History Blame Contribute Delete
12.3 kB
// App State
const state = {
currentChatId: null,
chats: [],
isGenerating: false,
theme: localStorage.getItem('theme') || 'system',
sidebarOpen: false
};
// DOM Elements
const elements = {
chatContainer: document.getElementById('chat-container'),
messagesWrapper: document.getElementById('messages-wrapper'),
messageInput: document.getElementById('message-input'),
sendBtn: document.getElementById('send-btn'),
welcomeScreen: document.getElementById('welcome-screen'),
sidebar: document.getElementById('sidebar'),
toggleSidebar: document.getElementById('toggle-sidebar'),
themeToggle: document.getElementById('theme-toggle'),
newChatBtn: document.getElementById('new-chat-btn'),
suggestionBtns: document.querySelectorAll('.suggestion-btn'),
attachBtn: document.getElementById('attach-btn')
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeTheme();
loadChats();
setupEventListeners();
setupTextareaAutoResize();
});
// Theme Management
function initializeTheme() {
if (state.theme === 'dark' || (state.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
document.getElementById('hljs-theme').href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css';
} else {
document.documentElement.classList.remove('dark');
document.getElementById('hljs-theme').href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
}
}
function toggleTheme() {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
state.theme = 'light';
document.getElementById('hljs-theme').href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
} else {
document.documentElement.classList.add('dark');
state.theme = 'dark';
document.getElementById('hljs-theme').href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css';
}
localStorage.setItem('theme', state.theme);
feather.replace();
}
// Chat Management
function loadChats() {
const saved = localStorage.getItem('chats');
if (saved) {
state.chats = JSON.parse(saved);
if (state.chats.length > 0 && !state.currentChatId) {
loadChat(state.chats[0].id);
}
}
if (!state.currentChatId) {
createNewChat();
}
updateSidebar();
}
function saveChats() {
localStorage.setItem('chats', JSON.stringify(state.chats));
updateSidebar();
}
function createNewChat() {
const id = Date.now().toString();
const chat = {
id,
title: 'New Chat',
messages: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
state.chats.unshift(chat);
state.currentChatId = id;
saveChats();
renderMessages();
elements.messageInput.focus();
}
function loadChat(id) {
state.currentChatId = id;
renderMessages();
updateSidebar();
// On mobile, close sidebar
if (window.innerWidth < 1024) {
elements.sidebar.shadowRoot.querySelector('aside').classList.add('-translate-x-full');
}
}
function deleteChat(id, event) {
event.stopPropagation();
state.chats = state.chats.filter(c => c.id !== id);
if (state.currentChatId === id) {
if (state.chats.length > 0) {
loadChat(state.chats[0].id);
} else {
createNewChat();
}
}
saveChats();
}
function updateChatTitle(chatId, firstMessage) {
const chat = state.chats.find(c => c.id === chatId);
if (chat && chat.title === 'New Chat') {
chat.title = firstMessage.slice(0, 30) + (firstMessage.length > 30 ? '...' : '');
saveChats();
}
}
// Message Handling
async function sendMessage(content = null) {
const text = content || elements.messageInput.value.trim();
if (!text || state.isGenerating) return;
elements.messageInput.value = '';
elements.messageInput.style.height = 'auto';
elements.welcomeScreen?.classList.add('hidden');
const userMessage = {
id: Date.now().toString(),
role: 'user',
content: text,
timestamp: new Date().toISOString()
};
const chat = state.chats.find(c => c.id === state.currentChatId);
if (!chat) return;
chat.messages.push(userMessage);
chat.updatedAt = new Date().toISOString();
updateChatTitle(state.currentChatId, text);
saveChats();
renderMessages();
scrollToBottom();
// Show typing indicator
showTypingIndicator();
try {
state.isGenerating = true;
updateSendButton();
// Build conversation context from previous messages
// Limit to last 10 messages to avoid token limits
const recentMessages = chat.messages.slice(-10);
const contextPrompt = buildContextPrompt(recentMessages);
// Use Pollinations AI with context
// The approach: include context in the system/instruction part of the prompt
const response = await fetch('https://text.pollinations.ai/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: [
{
role: "system",
content: "You are a helpful AI assistant. You have access to the conversation history and should maintain context throughout the conversation. Be concise but thorough in your responses."
},
...recentMessages.slice(0, -1).map(m => ({
role: m.role,
content: m.content
})),
{
role: "user",
content: text
}
],
model: "openai-large",
seed: Math.floor(Math.random() * 1000),
jsonMode: false
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.text();
hideTypingIndicator();
const assistantMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: data,
timestamp: new Date().toISOString()
};
chat.messages.push(assistantMessage);
chat.updatedAt = new Date().toISOString();
saveChats();
renderMessages();
scrollToBottom();
} catch (error) {
console.error('Error:', error);
hideTypingIndicator();
showError('Failed to get response. Please try again. Error: ' + error.message);
} finally {
state.isGenerating = false;
updateSendButton();
}
}
function buildContextPrompt(messages) {
// This function formats the conversation history for context
// It's used as a fallback if the API doesn't support message arrays
let context = "Previous conversation context:\n\n";
for (let i = 0; i < messages.length - 1; i++) {
const msg = messages[i];
const role = msg.role === 'user' ? 'User' : 'Assistant';
context += `${role}: ${msg.content}\n\n`;
}
const lastMessage = messages[messages.length - 1];
context += `User: ${lastMessage.content}\n\nAssistant:`;
return context;
}
// Rendering
function renderMessages() {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (!chat) return;
if (chat.messages.length === 0) {
elements.welcomeScreen?.classList.remove('hidden');
elements.messagesWrapper.innerHTML = '';
elements.messagesWrapper.appendChild(elements.welcomeScreen);
return;
}
elements.welcomeScreen?.classList.add('hidden');
// Clear current messages (except welcome screen)
Array.from(elements.messagesWrapper.children).forEach(child => {
if (child.id !== 'welcome-screen') {
child.remove();
}
});
chat.messages.forEach((msg, index) => {
const msgEl = document.createElement('chat-message');
msgEl.setAttribute('content', msg.content);
msgEl.setAttribute('role', msg.role);
msgEl.setAttribute('timestamp', msg.timestamp);
msgEl.classList.add('message-enter');
msgEl.style.animationDelay = `${index * 0.1}s`;
elements.messagesWrapper.appendChild(msgEl);
});
scrollToBottom();
}
function showTypingIndicator() {
const indicator = document.createElement('typing-indicator');
indicator.id = 'typing-indicator';
elements.messagesWrapper.appendChild(indicator);
scrollToBottom();
}
function hideTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
indicator?.remove();
}
function showError(message) {
const errorEl = document.createElement('div');
errorEl.className = 'p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl text-red-600 dark:text-red-400 text-center message-enter';
errorEl.textContent = message;
elements.messagesWrapper.appendChild(errorEl);
scrollToBottom();
}
function scrollToBottom() {
elements.chatContainer.scrollTo({
top: elements.chatContainer.scrollHeight,
behavior: 'smooth'
});
}
function updateSendButton() {
elements.sendBtn.disabled = state.isGenerating;
elements.sendBtn.innerHTML = state.isGenerating
? '<div class="typing-dot w-2 h-2 bg-white rounded-full inline-block mx-0.5"></div><div class="typing-dot w-2 h-2 bg-white rounded-full inline-block mx-0.5"></div><div class="typing-dot w-2 h-2 bg-white rounded-full inline-block mx-0.5"></div>'
: '<i data-feather="send" class="w-5 h-5"></i>';
feather.replace();
}
function updateSidebar() {
if (elements.sidebar) {
elements.sidebar.setAttribute('chats', JSON.stringify(state.chats));
elements.sidebar.setAttribute('active-chat', state.currentChatId);
}
}
// Event Listeners
function setupEventListeners() {
// Send button
elements.sendBtn.addEventListener('click', () => sendMessage());
// Enter key (Shift+Enter for new line)
elements.messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Suggestion buttons
elements.suggestionBtns.forEach(btn => {
btn.addEventListener('click', () => {
const text = btn.querySelector('.text-sm').textContent;
elements.messageInput.value = text;
sendMessage();
});
});
// Theme toggle
elements.themeToggle.addEventListener('click', toggleTheme);
// New chat
elements.newChatBtn.addEventListener('click', createNewChat);
// Sidebar toggle (mobile)
elements.toggleSidebar?.addEventListener('click', () => {
if (elements.sidebar) {
elements.sidebar.toggle();
}
});
// Attach button (placeholder)
elements.attachBtn.addEventListener('click', () => {
alert('File upload coming soon!');
});
// Handle sidebar events
elements.sidebar?.addEventListener('chat-selected', (e) => {
loadChat(e.detail.chatId);
});
elements.sidebar?.addEventListener('chat-deleted', (e) => {
deleteChat(e.detail.chatId, e.detail.event);
});
elements.sidebar?.addEventListener('new-chat', () => {
createNewChat();
});
}
// Textarea Auto-resize
function setupTextareaAutoResize() {
const textarea = elements.messageInput;
textarea.addEventListener('input', () => {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 128) + 'px';
});
}
// Expose to window for components
window.appState = state;
window.loadChat = loadChat;
window.deleteChat = deleteChat;
window.createNewChat = createNewChat;