| |
| |
| |
| |
|
|
| (function () { |
| "use strict"; |
|
|
| |
| const $ = (s) => document.querySelector(s); |
| const $$ = (s) => document.querySelectorAll(s); |
|
|
| |
| 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"); |
|
|
| |
| const apiProviderEl = $("#apiProvider"); |
| const apiKeyEl = $("#apiKey"); |
| const apiModelEl = $("#apiModel"); |
| const apiEndpointEl = $("#apiEndpoint"); |
| const systemPromptEl = $("#systemPrompt"); |
| const saveApiBtn = $("#saveApiBtn"); |
| const apiStatusEl = $("#apiStatus"); |
| const toggleApiKeyBtn= $("#toggleApiKey"); |
|
|
| |
| const dialogOverlay = $("#dialogOverlay"); |
| const dialogTitle = $("#dialogTitle"); |
| const dialogMsg = $("#dialogMsg"); |
| const dialogCancel = $("#dialogCancel"); |
| const dialogConfirm = $("#dialogConfirm"); |
|
|
| |
| const exportBtn = $("#exportBtn"); |
| const clearAllBtn = $("#clearAllBtn"); |
|
|
| |
| let currentChatId = null; |
| let isProcessing = false; |
| let abortController = null; |
|
|
| |
| |
| |
| 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); } |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| function renderHistory() { |
| const chats = allChats(); |
| const sorted = Object.values(chats).sort((a, b) => b.updatedAt - a.updatedAt); |
|
|
| if (!sorted.length) { |
| chatHistoryList.innerHTML = |
| '<div class="history-empty">No chats yet.<br>Start a new conversation!</div>'; |
| return; |
| } |
|
|
| chatHistoryList.innerHTML = sorted.map((ch) => ` |
| <div class="history-item ${ch.id === currentChatId ? "active" : ""}" data-id="${ch.id}"> |
| <span class="history-item-icon">β</span> |
| <div class="history-item-content" data-action="load" data-id="${ch.id}"> |
| <div class="history-item-title">${escHtml(chatTitle(ch))}</div> |
| <div class="history-item-date">${fmtDate(ch.updatedAt)} Β· ${ch.messages.length} msgs</div> |
| </div> |
| <button class="history-item-delete" data-action="delete" data-id="${ch.id}" title="Delete chat">β</button> |
| </div> |
| `).join(""); |
| } |
|
|
| |
| |
| |
| function loadChat(id) { |
| const chat = getChat(id); |
| if (!chat) return; |
|
|
| currentChatId = id; |
| save(KEYS.ACTIVE, id); |
|
|
| chatArea.innerHTML = ""; |
|
|
| if (chat.messages.length === 0) { |
| |
| const w = document.createElement("div"); |
| w.className = "welcome"; |
| w.innerHTML = ` |
| <div class="pixel-art-display" id="pixelArtInner"></div> |
| <h2>WELCOME TO<br>PIXELAI CHAT</h2> |
| <p>Your retro-futuristic AI companion.<br>Open the <span class="highlight">β° MENU</span> to configure your API key.</p> |
| <div class="welcome-arrow">β Click the menu button</div> |
| `; |
| 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"); |
| } |
|
|
| |
| |
| |
| 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"; |
| } |
|
|
| |
| |
| |
| 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()); |
| } |
|
|
| |
| |
| |
| async function sendMessage() { |
| const text = chatInput.value.trim(); |
| if (!text || isProcessing) return; |
|
|
| isProcessing = true; |
| setUI("loading"); |
|
|
| |
| const w = chatArea.querySelector(".welcome"); |
| if (w) w.remove(); |
| quickActions.style.display = "none"; |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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(); |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| function toast(msg, type = "info") { |
| const t = document.createElement("div"); |
| t.className = `toast ${type}`; |
| t.textContent = msg; |
| toastContainer.appendChild(t); |
| setTimeout(() => t.remove(), 3200); |
| } |
|
|
| |
| |
| |
| 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); |
| }); |
| } |
|
|
| |
| |
| |
| function openSidebar() { |
| sidebar.classList.add("open"); |
| sidebarOverlay.classList.add("active"); |
| renderHistory(); |
| } |
|
|
| function closeSidebar() { |
| sidebar.classList.remove("open"); |
| sidebarOverlay.classList.remove("active"); |
| } |
|
|
| |
| |
| |
| 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"); |
| } |
|
|
| |
| |
| |
| 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); |
| }); |
| } |
|
|
| |
| |
| |
| 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")); |
|
|
| |
| |
| |
| 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); |
|
|
| |
| |
| |
| 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); |
|
|
| |
| |
| |
|
|
| |
| sendBtn.addEventListener("click", sendMessage); |
| chatInput.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } |
| }); |
|
|
| |
| quickActions.addEventListener("click", (e) => { |
| const btn = e.target.closest(".quick-btn"); |
| if (btn) { chatInput.value = btn.dataset.prompt; sendMessage(); } |
| }); |
|
|
| |
| menuBtn.addEventListener("click", openSidebar); |
| sidebarClose.addEventListener("click", closeSidebar); |
| sidebarOverlay.addEventListener("click", closeSidebar); |
|
|
| |
| newChatBtn.addEventListener("click", startNewChat); |
|
|
| |
| 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"); |
| } |
| } |
| }); |
|
|
| |
| exportBtn.addEventListener("click", exportChats); |
|
|
| |
| 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"); |
| } |
| }); |
|
|
| |
| apiProviderEl.addEventListener("change", () => updateDefaults(apiProviderEl.value)); |
|
|
| |
| toggleApiKeyBtn.addEventListener("click", () => { |
| const show = apiKeyEl.type === "password"; |
| apiKeyEl.type = show ? "text" : "password"; |
| toggleApiKeyBtn.textContent = show ? "β" : "β"; |
| }); |
|
|
| |
| 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"); |
| }); |
|
|
| |
| 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"); |
| } |
| } |
| }); |
|
|
| |
| |
| |
| 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(); |
| })(); |