// ═══════════════════════════════════════════════ // PIXELAI CHAT v2.1 — script.js // API-Ready · Chat History · Minimal Pixel Design // ═══════════════════════════════════════════════ (function () { "use strict"; // ── Quick DOM selector ── const $ = (s) => document.querySelector(s); const $$ = (s) => document.querySelectorAll(s); // ── DOM Elements ── const cursor = $("#cursor"); const sidebar = $("#sidebar"); const sidebarOverlay= $("#sidebarOverlay"); const menuBtn = $("#menuBtn"); const sidebarClose = $("#sidebarClose"); const newChatBtn = $("#newChatBtn"); const chatHistoryList = $("#chatHistoryList"); const chatArea = $("#chatArea"); const chatInput = $("#chatInput"); const sendBtn = $("#sendBtn"); const typingEl = $("#typing"); const typingAvatar = $("#typingAvatar"); const welcomeEl = $("#welcome"); const pixelArtEl = $("#pixelArt"); const quickActions = $("#quickActions"); const currentChatTitle = $("#currentChatTitle"); const statusDot = $("#statusDot"); const statusText = $("#statusText"); const toastContainer = $("#toastContainer"); const floatingContainer = $("#floatingPixels"); // API settings const apiProviderEl = $("#apiProvider"); const apiKeyEl = $("#apiKey"); const apiModelEl = $("#apiModel"); const apiEndpointEl = $("#apiEndpoint"); const systemPromptEl = $("#systemPrompt"); const saveApiBtn = $("#saveApiBtn"); const apiStatusEl = $("#apiStatus"); const toggleApiKeyBtn= $("#toggleApiKey"); // Dialog const dialogOverlay = $("#dialogOverlay"); const dialogTitle = $("#dialogTitle"); const dialogMsg = $("#dialogMsg"); const dialogCancel = $("#dialogCancel"); const dialogConfirm = $("#dialogConfirm"); // Buttons const exportBtn = $("#exportBtn"); const clearAllBtn = $("#clearAllBtn"); // ── State ── let currentChatId = null; let isProcessing = false; let abortController = null; // ═══════════════════════════════ // STORAGE // ═══════════════════════════════ const KEYS = { CHATS: "pixelai_chats", ACTIVE: "pixelai_active_chat", CONFIG: "pixelai_api_config", }; function load(key, fallback) { try { const d = localStorage.getItem(key); return d ? JSON.parse(d) : fallback; } catch { return fallback; } } function save(key, data) { try { localStorage.setItem(key, JSON.stringify(data)); } catch (e) { console.error("Storage error:", e); } } // ═══════════════════════════════ // CHAT DATA // ═══════════════════════════════ function allChats() { return load(KEYS.CHATS, {}); } function saveAllChats(c) { save(KEYS.CHATS, c); } function getChat(id) { return allChats()[id] || null; } function saveChat(id, chat) { const c = allChats(); c[id] = chat; saveAllChats(c); } function deleteChat(id) { const c = allChats(); delete c[id]; saveAllChats(c); } function genId() { return "chat_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6); } function createChat() { const id = genId(); const chat = { id, title: "New Chat", messages: [], createdAt: Date.now(), updatedAt: Date.now(), }; saveChat(id, chat); return chat; } function chatTitle(chat) { if (chat.messages.length > 0) { const first = chat.messages.find((m) => m.role === "user"); if (first) { const t = first.content.substring(0, 45); return t + (first.content.length > 45 ? "..." : ""); } } return "New Chat"; } function fmtDate(ts) { const diff = Date.now() - ts; if (diff < 60000) return "Just now"; if (diff < 3600000) return Math.floor(diff / 60000) + "m ago"; if (diff < 86400000) return Math.floor(diff / 3600000) + "h ago"; if (diff < 604800000) return Math.floor(diff / 86400000) + "d ago"; return new Date(ts).toLocaleDateString(); } function getTime() { return new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function escHtml(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } // ═══════════════════════════════ // RENDER SIDEBAR HISTORY // ═══════════════════════════════ function renderHistory() { const chats = allChats(); const sorted = Object.values(chats).sort((a, b) => b.updatedAt - a.updatedAt); if (!sorted.length) { chatHistoryList.innerHTML = '
No chats yet.
Start a new conversation!
'; return; } chatHistoryList.innerHTML = sorted.map((ch) => `
${escHtml(chatTitle(ch))}
${fmtDate(ch.updatedAt)} · ${ch.messages.length} msgs
`).join(""); } // ═══════════════════════════════ // LOAD / NEW CHAT // ═══════════════════════════════ function loadChat(id) { const chat = getChat(id); if (!chat) return; currentChatId = id; save(KEYS.ACTIVE, id); chatArea.innerHTML = ""; if (chat.messages.length === 0) { // Show welcome const w = document.createElement("div"); w.className = "welcome"; w.innerHTML = `

WELCOME TO
PIXELAI CHAT

Your retro-futuristic AI companion.
Open the ☰ MENU to configure your API key.

← Click the menu button
`; chatArea.appendChild(w); buildPixelArt(w.querySelector(".pixel-art-display")); quickActions.style.display = "flex"; } else { quickActions.style.display = "none"; chat.messages.forEach((msg) => { appendMsg(msg.content, msg.role === "user", msg.time, false, msg.role === "error"); }); chatArea.scrollTop = chatArea.scrollHeight; } currentChatTitle.textContent = chatTitle(chat); renderHistory(); closeSidebar(); } function startNewChat() { const chat = createChat(); loadChat(chat.id); toast("New chat created!", "success"); } // ═══════════════════════════════ // API CONFIG // ═══════════════════════════════ const DEFAULT_CFG = { provider: "openai", apiKey: "", model: "gpt-4o-mini", endpoint: "", systemPrompt: "You are a helpful AI assistant. Keep responses concise and clear.", }; function loadCfg() { return load(KEYS.CONFIG, { ...DEFAULT_CFG }); } function saveCfg(c) { save(KEYS.CONFIG, c); } function populateSettings() { const c = loadCfg(); apiProviderEl.value = c.provider; apiKeyEl.value = c.apiKey; apiModelEl.value = c.model; apiEndpointEl.value = c.endpoint; systemPromptEl.value = c.systemPrompt; updateDefaults(c.provider); } function updateDefaults(provider) { const map = { openai: { model: "gpt-4o-mini", ep: "https://api.openai.com/v1/chat/completions" }, anthropic: { model: "claude-3-5-sonnet-20241022", ep: "https://api.anthropic.com/v1/messages" }, google: { model: "gemini-1.5-flash", ep: "" }, custom: { model: "", ep: "" }, }; const d = map[provider] || map.openai; apiModelEl.placeholder = d.model || "model name"; apiEndpointEl.placeholder = d.ep || "https://your-api.com/v1/chat"; } // ═══════════════════════════════ // API CALL // ═══════════════════════════════ function buildRequest(config, messages) { const p = config.provider; if (p === "openai" || p === "custom") { return { url: config.endpoint || "https://api.openai.com/v1/chat/completions", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.apiKey}`, }, body: { model: config.model, messages: [ { role: "system", content: config.systemPrompt }, ...messages.map((m) => ({ role: m.role, content: m.content })), ], max_tokens: 2048, temperature: 0.7, }, parse: (d) => { if (d.choices?.[0]) return d.choices[0].message.content; throw new Error(d.error?.message || "Invalid API response"); }, }; } if (p === "anthropic") { return { url: config.endpoint || "https://api.anthropic.com/v1/messages", headers: { "Content-Type": "application/json", "x-api-key": config.apiKey, "anthropic-version": "2023-06-01", "anthropic-dangerous-direct-browser-access": "true", }, body: { model: config.model, max_tokens: 2048, system: config.systemPrompt, messages: messages.map((m) => ({ role: m.role, content: m.content })), }, parse: (d) => { if (d.content?.[0]) return d.content[0].text; throw new Error(d.error?.message || "Invalid API response"); }, }; } if (p === "google") { return { url: `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`, headers: { "Content-Type": "application/json" }, body: { system_instruction: { parts: [{ text: config.systemPrompt }] }, contents: messages.map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }], })), generationConfig: { maxOutputTokens: 2048, temperature: 0.7 }, }, parse: (d) => { if (d.candidates?.[0]?.content?.parts?.[0]) return d.candidates[0].content.parts[0].text; throw new Error(d.error?.message || "Invalid Gemini response"); }, }; } throw new Error("Unknown provider: " + p); } async function callAPI(messages) { const config = loadCfg(); if (!config.apiKey) { throw new Error("No API key! Open ☰ MENU → API Settings to configure."); } const req = buildRequest(config, messages); abortController = new AbortController(); const res = await fetch(req.url, { method: "POST", headers: req.headers, body: JSON.stringify(req.body), signal: abortController.signal, }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || `API Error ${res.status}: ${res.statusText}`); } return req.parse(await res.json()); } // ═══════════════════════════════ // SEND MESSAGE // ═══════════════════════════════ async function sendMessage() { const text = chatInput.value.trim(); if (!text || isProcessing) return; isProcessing = true; setUI("loading"); // Hide welcome const w = chatArea.querySelector(".welcome"); if (w) w.remove(); quickActions.style.display = "none"; // Ensure active chat if (!currentChatId) { const ch = createChat(); currentChatId = ch.id; save(KEYS.ACTIVE, currentChatId); } const time = getTime(); const userMsg = { role: "user", content: text, time }; appendMsg(text, true, time, true); addMsgToChat(currentChatId, userMsg); chatInput.value = ""; chatInput.focus(); showTyping(); try { const chat = getChat(currentChatId); const reply = await callAPI(chat.messages); hideTyping(); const aiTime = getTime(); const aiMsg = { role: "assistant", content: reply, time: aiTime }; appendMsg(reply, false, aiTime, true); addMsgToChat(currentChatId, aiMsg); currentChatTitle.textContent = chatTitle(getChat(currentChatId)); renderHistory(); setUI("ready"); } catch (err) { hideTyping(); if (err.name === "AbortError") { toast("Request cancelled.", "info"); } else { appendMsg("⚠ " + err.message, false, getTime(), true, true); toast(err.message, "error"); } setUI("error"); setTimeout(() => setUI("ready"), 3000); } isProcessing = false; } function addMsgToChat(id, msg) { const chat = getChat(id); if (!chat) return; chat.messages.push(msg); chat.updatedAt = Date.now(); chat.title = chatTitle(chat); saveChat(id, chat); } // ═══════════════════════════════ // DOM: APPEND MESSAGE // ═══════════════════════════════ function appendMsg(text, isUser, time, animate = true, isError = false) { const msg = document.createElement("div"); msg.className = `message ${isUser ? "user" : "ai"} ${isError ? "error" : ""}`; if (!animate) msg.style.animation = "none"; const av = document.createElement("div"); av.className = "avatar"; createAvatar(av, !isUser); const bubble = document.createElement("div"); bubble.className = "bubble"; const msgSpan = document.createElement("span"); msgSpan.className = "msg-text"; const timeSpan = document.createElement("span"); timeSpan.className = "time"; timeSpan.textContent = time; bubble.appendChild(msgSpan); bubble.appendChild(timeSpan); msg.appendChild(av); msg.appendChild(bubble); chatArea.appendChild(msg); if (!isUser && animate && !isError) { typewriter(msgSpan, text); } else { msgSpan.textContent = text; } chatArea.scrollTop = chatArea.scrollHeight; } function typewriter(el, text) { let i = 0; function tick() { if (i < text.length) { el.textContent += text[i++]; chatArea.scrollTop = chatArea.scrollHeight; setTimeout(tick, 14); } } tick(); } // ═══════════════════════════════ // UI HELPERS // ═══════════════════════════════ function showTyping() { typingEl.classList.add("active"); chatArea.scrollTop = chatArea.scrollHeight; } function hideTyping() { typingEl.classList.remove("active"); } function setUI(state) { const map = { ready: { cls: "status-dot", label: "READY", dis: false }, loading: { cls: "status-dot loading", label: "THINKING", dis: true }, error: { cls: "status-dot error", label: "ERROR", dis: false }, }; const s = map[state] || map.ready; statusDot.className = s.cls; statusText.textContent = s.label; chatInput.disabled = s.dis; sendBtn.disabled = s.dis; } // ═══════════════════════════════ // TOAST // ═══════════════════════════════ function toast(msg, type = "info") { const t = document.createElement("div"); t.className = `toast ${type}`; t.textContent = msg; toastContainer.appendChild(t); setTimeout(() => t.remove(), 3200); } // ═══════════════════════════════ // CONFIRM DIALOG // ═══════════════════════════════ function confirm(title, message) { return new Promise((resolve) => { dialogTitle.textContent = title; dialogMsg.textContent = message; dialogOverlay.classList.add("active"); function done(val) { dialogOverlay.classList.remove("active"); dialogCancel.removeEventListener("click", onNo); dialogConfirm.removeEventListener("click", onYes); resolve(val); } function onNo() { done(false); } function onYes() { done(true); } dialogCancel.addEventListener("click", onNo); dialogConfirm.addEventListener("click", onYes); }); } // ═══════════════════════════════ // SIDEBAR // ═══════════════════════════════ function openSidebar() { sidebar.classList.add("open"); sidebarOverlay.classList.add("active"); renderHistory(); } function closeSidebar() { sidebar.classList.remove("open"); sidebarOverlay.classList.remove("active"); } // ═══════════════════════════════ // EXPORT // ═══════════════════════════════ function exportChats() { const data = JSON.stringify(allChats(), null, 2); const blob = new Blob([data], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `pixelai_export_${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); toast("Chats exported!", "success"); } // ═══════════════════════════════ // PIXEL ART & AVATARS // ═══════════════════════════════ const ROBOT = [ 0,0,1,1,1,1,0,0, 0,1,0,1,1,0,1,0, 0,1,1,1,1,1,1,0, 0,1,2,1,1,2,1,0, 0,1,1,1,1,1,1,0, 0,0,1,0,0,1,0,0, 0,1,1,1,1,1,1,0, 0,1,0,0,0,0,1,0, ]; const ART_C = ["transparent", "#00ff88", "#ff00aa"]; function buildPixelArt(container) { if (!container) return; container.innerHTML = ""; ROBOT.forEach((v) => { const p = document.createElement("div"); p.className = "p"; p.style.background = ART_C[v]; container.appendChild(p); }); } function createAvatar(container, isAI) { const ai = [0,1,1,1,1,0, 1,0,1,1,0,1, 1,1,1,1,1,1, 1,2,1,1,2,1, 1,1,1,1,1,1, 0,1,0,0,1,0]; const user = [0,0,1,1,0,0, 0,1,1,1,1,0, 1,2,1,1,2,1, 1,1,1,1,1,1, 0,1,2,2,1,0, 0,0,1,1,0,0]; const pat = isAI ? ai : user; const cols = isAI ? ["transparent","#00ff88","#ff00aa"] : ["transparent","#ff00aa","#00ff88"]; container.innerHTML = ""; pat.forEach((v) => { const p = document.createElement("div"); p.className = "avatar-pixel"; p.style.background = cols[v]; container.appendChild(p); }); } // ═══════════════════════════════ // CUSTOM CURSOR // ═══════════════════════════════ let trailN = 0; document.addEventListener("mousemove", (e) => { cursor.style.left = (e.clientX - 2) + "px"; cursor.style.top = (e.clientY - 2) + "px"; if (++trailN % 3 === 0) { const t = document.createElement("div"); t.className = "cursor-trail"; t.style.left = (e.clientX - 2) + "px"; t.style.top = (e.clientY - 2) + "px"; document.body.appendChild(t); setTimeout(() => { t.style.opacity = "0"; }, 80); setTimeout(() => t.remove(), 500); } }); document.addEventListener("mousedown", () => cursor.classList.add("click")); document.addEventListener("mouseup", () => cursor.classList.remove("click")); // ═══════════════════════════════ // FLOATING PIXELS // ═══════════════════════════════ function spawnPixel() { const p = document.createElement("div"); p.className = "floating-pixel"; p.style.left = Math.random() * 100 + "%"; p.style.animationDuration = (8 + Math.random() * 15) + "s"; const size = (2 + Math.random() * 4) + "px"; p.style.width = p.style.height = size; p.style.background = Math.random() > 0.5 ? "#00ff88" : "#ff00aa"; floatingContainer.appendChild(p); p.addEventListener("animationend", () => { p.remove(); spawnPixel(); }); } for (let i = 0; i < 18; i++) setTimeout(spawnPixel, Math.random() * 5000); // ═══════════════════════════════ // LOGO GLITCH // ═══════════════════════════════ const logoText = $(".logo-text"); setInterval(() => { if (Math.random() > 0.95 && logoText) { logoText.style.textShadow = ` ${(Math.random()*4-2).toFixed(1)}px 0 rgba(255,0,170,0.7), ${(Math.random()*4-2).toFixed(1)}px 0 rgba(0,255,136,0.7)`; setTimeout(() => { logoText.style.textShadow = "2px 2px 0 rgba(0,255,136,0.15)"; }, 100); } }, 200); // ═══════════════════════════════ // EVENT LISTENERS // ═══════════════════════════════ // Send message sendBtn.addEventListener("click", sendMessage); chatInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Quick actions quickActions.addEventListener("click", (e) => { const btn = e.target.closest(".quick-btn"); if (btn) { chatInput.value = btn.dataset.prompt; sendMessage(); } }); // Sidebar open/close menuBtn.addEventListener("click", openSidebar); sidebarClose.addEventListener("click", closeSidebar); sidebarOverlay.addEventListener("click", closeSidebar); // New chat newChatBtn.addEventListener("click", startNewChat); // History list clicks chatHistoryList.addEventListener("click", async (e) => { const target = e.target.closest("[data-action]"); if (!target) return; const action = target.dataset.action; const id = target.dataset.id; if (action === "load") { loadChat(id); } else if (action === "delete") { const ok = await confirm("Delete Chat", "Permanently delete this chat?"); if (ok) { deleteChat(id); if (id === currentChatId) startNewChat(); else renderHistory(); toast("Chat deleted.", "info"); } } }); // Export exportBtn.addEventListener("click", exportChats); // Clear all clearAllBtn.addEventListener("click", async () => { const ok = await confirm("Clear All", "Delete ALL chat history? This cannot be undone."); if (ok) { saveAllChats({}); startNewChat(); toast("All chats cleared.", "info"); } }); // API provider change apiProviderEl.addEventListener("change", () => updateDefaults(apiProviderEl.value)); // Toggle API key toggleApiKeyBtn.addEventListener("click", () => { const show = apiKeyEl.type === "password"; apiKeyEl.type = show ? "text" : "password"; toggleApiKeyBtn.textContent = show ? "◎" : "◉"; }); // Save API config saveApiBtn.addEventListener("click", () => { const cfg = { provider: apiProviderEl.value, apiKey: apiKeyEl.value.trim(), model: apiModelEl.value.trim() || DEFAULT_CFG.model, endpoint: apiEndpointEl.value.trim(), systemPrompt: systemPromptEl.value.trim() || DEFAULT_CFG.systemPrompt, }; saveCfg(cfg); apiStatusEl.textContent = "✓ Saved!"; apiStatusEl.style.color = "var(--accent)"; setTimeout(() => { apiStatusEl.textContent = ""; }, 2500); toast("API config saved!", "success"); }); // Keyboard: Escape document.addEventListener("keydown", (e) => { if (e.key === "Escape") { if (sidebar.classList.contains("open")) closeSidebar(); if (dialogOverlay.classList.contains("active")) { dialogOverlay.classList.remove("active"); } if (isProcessing && abortController) { abortController.abort(); isProcessing = false; hideTyping(); setUI("ready"); } } }); // ═══════════════════════════════ // INIT // ═══════════════════════════════ function init() { buildPixelArt(pixelArtEl); createAvatar(typingAvatar, true); populateSettings(); const lastId = load(KEYS.ACTIVE, null); const chats = allChats(); if (lastId && chats[lastId]) { loadChat(lastId); } else if (Object.keys(chats).length) { const latest = Object.values(chats).sort((a, b) => b.updatedAt - a.updatedAt); loadChat(latest[0].id); } else { startNewChat(); } chatInput.focus(); console.log( "%c◈ PixelAI v2.1 Ready ◈", "color:#00ff88; font-size:14px; font-family:monospace; background:#0a0a0a; padding:8px 16px;" ); } init(); })();