| | |
| | |
| | |
| | |
| |
|
| | (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(); |
| | })(); |