From cdd11385397bec2e7bfe0f01e55e0cbe4c3bb466 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 01:54:51 +0000 Subject: [PATCH 01/10] add new games --- public/books/script.js | 60 ++++------ public/pages/embed/index.html | 208 +++++++++++++++++++++++++--------- 2 files changed, 175 insertions(+), 93 deletions(-) diff --git a/public/books/script.js b/public/books/script.js index c1c85eb..a4bcb8b 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -1,18 +1,15 @@ import { SettingsManager } from "../assets/js/settings_manager.js"; -const BATCH = 60; -const BASE_PATH = '/books/files/k12learning/'; +const BASE_PATH = 'https://goshadow.net/books/files/k12learning/'; const FAV_KEY = 'arcade_favorites'; const settings = new SettingsManager(); let allGames = []; let visible = []; -let rendered = 0; let activeFilter = 'all'; let searchQuery = ''; let favorites = new Set(); -let imgObserver, scrollObserver; async function loadFavorites() { const stored = await settings.get(FAV_KEY); @@ -29,7 +26,8 @@ function isHot(game) { function openGameTab(game) { const gameUrl = BASE_PATH + game.url; - const embedUrl = `embed.html?url=${encodeURIComponent(gameUrl)}`; + const iconUrl = game.imageUrl ? BASE_PATH + game.imageUrl : ''; + const embedUrl = `embed.html?url=${encodeURIComponent(gameUrl)}&title=${encodeURIComponent(game.label || '')}&icon=${encodeURIComponent(iconUrl)}&src=books`; try { const tabsApi = window.parent?.tabs; @@ -43,26 +41,6 @@ function openGameTab(game) { const HEART_FILLED = ``; const HEART_EMPTY = ``; -function setupImgObserver() { - imgObserver = new IntersectionObserver(entries => { - entries.forEach(e => { - if (!e.isIntersecting) return; - const img = e.target; - img.src = img.dataset.src; - img.onload = () => { img.classList.add('loaded'); img.previousElementSibling?.classList.add('hidden'); }; - img.onerror = () => img.remove(); - imgObserver.unobserve(img); - }); - }, { rootMargin: '300px' }); -} - -function setupScrollObserver() { - scrollObserver = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && rendered < visible.length) renderBatch(); - }, { rootMargin: '400px' }); - scrollObserver.observe(document.getElementById('sentinel')); -} - function buildCard(game) { const a = document.createElement('a'); a.className = 'card'; @@ -79,11 +57,20 @@ function buildCard(game) { if (game.imageUrl) { const img = document.createElement('img'); - img.dataset.src = BASE_PATH + game.imageUrl; + img.src = BASE_PATH + game.imageUrl; img.alt = game.label; + img.loading = 'lazy'; img.decoding = 'async'; + img.onload = () => { + if (!img.isConnected) return; + img.classList.add('loaded'); + img.previousElementSibling?.classList.add('hidden'); + }; + img.onerror = () => { + if (!img.isConnected) return; + img.remove(); + }; a.appendChild(img); - imgObserver.observe(img); } if (isHot(game)) { @@ -133,23 +120,21 @@ function buildCard(game) { return a; } -function renderBatch() { +function renderVisible() { const grid = document.getElementById('grid'); - const end = Math.min(rendered + BATCH, visible.length); const frag = document.createDocumentFragment(); - for (let i = rendered; i < end; i++) frag.appendChild(buildCard(visible[i])); + for (let i = 0; i < visible.length; i++) frag.appendChild(buildCard(visible[i])); grid.appendChild(frag); - rendered = end; } function applyFilters() { const q = searchQuery.toLowerCase(); let filtered = allGames.filter(g => { - if (activeFilter === 'favorites') return favorites.has(g.label); + const searchOk = !q || g.label.toLowerCase().includes(q); + if (activeFilter === 'favorites') return favorites.has(g.label) && searchOk; const catOk = activeFilter === 'all' || (g.categories || []).some(c => c.toLowerCase() === activeFilter); - const searchOk = !q || g.label.toLowerCase().includes(q); return catOk && searchOk; }); @@ -161,8 +146,11 @@ function applyFilters() { visible = filtered; } - rendered = 0; const grid = document.getElementById('grid'); + grid.querySelectorAll('img').forEach(img => { + img.onload = null; + img.onerror = null; + }); grid.innerHTML = ''; document.getElementById('countDisplay').textContent = @@ -173,7 +161,7 @@ function applyFilters() { return; } - renderBatch(); + renderVisible(); } function buildFilters(games) { @@ -208,8 +196,6 @@ function debounce(fn, ms) { } async function init() { - setupImgObserver(); - setupScrollObserver(); await loadFavorites(); try { const data = await fetch('learningcourses.json').then(r => r.json()); diff --git a/public/pages/embed/index.html b/public/pages/embed/index.html index 2376f3e..9f6bccc 100644 --- a/public/pages/embed/index.html +++ b/public/pages/embed/index.html @@ -16,67 +16,163 @@ From 4f15e22151d0e77e397d4b49361c9971eb256149 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 02:26:32 +0000 Subject: [PATCH 02/10] Fixed hardcoded goshadow.net into games --- public/books/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/books/script.js b/public/books/script.js index a4bcb8b..1077931 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -1,6 +1,6 @@ import { SettingsManager } from "../assets/js/settings_manager.js"; -const BASE_PATH = 'https://goshadow.net/books/files/k12learning/'; +const BASE_PATH = 'books/files/k12learning/'; const FAV_KEY = 'arcade_favorites'; const settings = new SettingsManager(); From deee97b619187f367220b0cbdd4d461e4cc8e506 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 02:27:10 +0000 Subject: [PATCH 03/10] frgt a / --- public/books/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/books/script.js b/public/books/script.js index 1077931..ffd16e6 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -1,6 +1,6 @@ import { SettingsManager } from "../assets/js/settings_manager.js"; -const BASE_PATH = 'books/files/k12learning/'; +const BASE_PATH = '/books/files/k12learning/'; const FAV_KEY = 'arcade_favorites'; const settings = new SettingsManager(); From f0ae3bf8e2b7bd87f1293455dbb26cfb67cbd296 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 02:41:04 +0000 Subject: [PATCH 04/10] sa --- public/books/script.js | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/public/books/script.js b/public/books/script.js index ffd16e6..f800673 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -45,6 +45,7 @@ function buildCard(game) { const a = document.createElement('a'); a.className = 'card'; a.href = '#'; + a.dataset.gameLabel = game.label; a.addEventListener('click', e => { e.preventDefault(); openGameTab(game); @@ -127,6 +128,29 @@ function renderVisible() { grid.appendChild(frag); } +function updateDisplay() { + const grid = document.getElementById('grid'); + const visibleLabels = new Set(visible.map(g => g.label)); + + grid.querySelectorAll('.card').forEach(card => { + const label = card.dataset.gameLabel; + card.style.display = visibleLabels.has(label) ? '' : 'none'; + }); + + let emptyMsg = grid.querySelector('.empty'); + if (visible.length === 0) { + if (!emptyMsg) { + emptyMsg = document.createElement('div'); + emptyMsg.className = 'empty'; + emptyMsg.innerHTML = 'NO RESULTSTry a different search or filter'; + grid.appendChild(emptyMsg); + } + emptyMsg.style.display = ''; + } else if (emptyMsg) { + emptyMsg.style.display = 'none'; + } +} + function applyFilters() { const q = searchQuery.toLowerCase(); @@ -146,22 +170,10 @@ function applyFilters() { visible = filtered; } - const grid = document.getElementById('grid'); - grid.querySelectorAll('img').forEach(img => { - img.onload = null; - img.onerror = null; - }); - grid.innerHTML = ''; - document.getElementById('countDisplay').textContent = `${visible.length} game${visible.length !== 1 ? 's' : ''}`; - if (visible.length === 0) { - grid.innerHTML = '
NO RESULTSTry a different search or filter
'; - return; - } - - renderVisible(); + updateDisplay(); } function buildFilters(games) { @@ -203,6 +215,8 @@ async function init() { document.getElementById('loading').style.display = 'none'; document.getElementById('grid').style.display = 'grid'; buildFilters(allGames); + visible = allGames; + renderVisible(); applyFilters(); document.getElementById('searchInput').addEventListener('input', debounce(e => { searchQuery = e.target.value; applyFilters(); }, 150)); From 788e295e788b39e64f39ab9464c81cea7606da63 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 02:42:30 +0000 Subject: [PATCH 05/10] r --- public/books/script.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/public/books/script.js b/public/books/script.js index f800673..5948f1c 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -7,6 +7,7 @@ const settings = new SettingsManager(); let allGames = []; let visible = []; +let previousVisible = new Set(); let activeFilter = 'all'; let searchQuery = ''; let favorites = new Set(); @@ -134,8 +135,16 @@ function updateDisplay() { grid.querySelectorAll('.card').forEach(card => { const label = card.dataset.gameLabel; - card.style.display = visibleLabels.has(label) ? '' : 'none'; + const shouldBeVisible = visibleLabels.has(label); + const wasVisible = previousVisible.has(label); + + // Only update if visibility changed + if (shouldBeVisible !== wasVisible) { + card.style.display = shouldBeVisible ? '' : 'none'; + } }); + + previousVisible = visibleLabels; let emptyMsg = grid.querySelector('.empty'); if (visible.length === 0) { @@ -216,6 +225,7 @@ async function init() { document.getElementById('grid').style.display = 'grid'; buildFilters(allGames); visible = allGames; + previousVisible = new Set(allGames.map(g => g.label)); renderVisible(); applyFilters(); document.getElementById('searchInput').addEventListener('input', From fbcaba915e48117f5016c58ff961b8e66469b975 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 16:20:11 +0000 Subject: [PATCH 06/10] Fixed Ai Model Bug, added claude. --- public/ai.html | 3 ++- public/books/script.js | 15 +++++++++++++++ public/css/ai.css | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/public/ai.html b/public/ai.html index ad9849c..7c20def 100644 --- a/public/ai.html +++ b/public/ai.html @@ -43,7 +43,8 @@

ShadowAssistant

- + + diff --git a/public/books/script.js b/public/books/script.js index 5948f1c..6ee10d9 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -132,6 +132,21 @@ function renderVisible() { function updateDisplay() { const grid = document.getElementById('grid'); const visibleLabels = new Set(visible.map(g => g.label)); + const cardsByLabel = new Map(); + + grid.querySelectorAll('.card').forEach(card => { + cardsByLabel.set(card.dataset.gameLabel, card); + }); + + const orderedCards = []; + for (let i = 0; i < visible.length; i++) { + const card = cardsByLabel.get(visible[i].label); + if (card) orderedCards.push(card); + } + + for (let i = 0; i < orderedCards.length; i++) { + grid.appendChild(orderedCards[i]); + } grid.querySelectorAll('.card').forEach(card => { const label = card.dataset.gameLabel; diff --git a/public/css/ai.css b/public/css/ai.css index d557e98..8cea65d 100644 --- a/public/css/ai.css +++ b/public/css/ai.css @@ -175,6 +175,15 @@ body { padding-right: 36px; } +#model-selector option { + background: var(--primary); + color: var(--accent-light); + outline:none; + border:none; + padding: 10px 12px; +} + + #model-selector:focus { outline: 2px solid var(--accent); } From 211c3bfe3b21a6ac8cb8a61210f8cf06a384a10a Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 16:20:23 +0000 Subject: [PATCH 07/10] Removed duped retro bowl --- public/books/learningcourses.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/public/books/learningcourses.json b/public/books/learningcourses.json index 38aaf97..a63ac91 100644 --- a/public/books/learningcourses.json +++ b/public/books/learningcourses.json @@ -9,14 +9,6 @@ "Shooting" ] }, - { - "label": "Retro Bowl", - "imageUrl": "images/main4/3_33.png", - "url": "gn/33.html", - "categories": [ - "Sports" - ] - }, { "label": "Sky Riders", "url": "arsenic/sky-riders/index.html", From fed9f089b7293d5ab0e8b85cb999d9a16051fa52 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Mon, 23 Mar 2026 16:45:45 +0000 Subject: [PATCH 08/10] Pinned hot games to the top --- public/books/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/books/script.js b/public/books/script.js index 6ee10d9..70f86bb 100644 --- a/public/books/script.js +++ b/public/books/script.js @@ -178,7 +178,7 @@ function updateDisplay() { function applyFilters() { const q = searchQuery.toLowerCase(); - let filtered = allGames.filter(g => { + const filtered = allGames.filter(g => { const searchOk = !q || g.label.toLowerCase().includes(q); if (activeFilter === 'favorites') return favorites.has(g.label) && searchOk; const catOk = activeFilter === 'all' || @@ -186,7 +186,7 @@ function applyFilters() { return catOk && searchOk; }); - if (activeFilter !== 'favorites' && !q) { + if (activeFilter !== 'favorites') { const hot = filtered.filter(g => isHot(g)); const rest = filtered.filter(g => !isHot(g)); visible = [...hot, ...rest]; From 9b4659f96a3922c849857b51797e2dc1bd05c322 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Fri, 22 May 2026 19:55:09 +0000 Subject: [PATCH 09/10] new ai! --- index.js | 191 ++++++++++-- package-lock.json | 11 +- public/ai.html | 171 +++++++---- public/assets/js/ai.js | 342 +++++++++++++++++++++- public/css/ai.css | 646 ++++++++++++++++++++++++++++++----------- 5 files changed, 1092 insertions(+), 269 deletions(-) diff --git a/index.js b/index.js index 1c2b119..11a1361 100644 --- a/index.js +++ b/index.js @@ -168,6 +168,143 @@ app.get("/csrf-token", (req, res) => { res.json({ csrfToken: generateToken(req, res) }); }); +const AI_MINUTE_LIMIT = 10; +const AI_DAY_LIMIT = 250; +const AI_MINUTE_WINDOW_MS = 60 * 1000; +const DEFAULT_AI_MODEL = "gpt-5.4-mini"; +const AI_MODELS = new Set([ + DEFAULT_AI_MODEL, + "gpt-5.4", + "claude-sonnet-4.6", + "gemini-3-flash-preview", +]); +const aiRequestLimits = new Map(); + +function getDayKey(timestamp) { + return new Date(timestamp).toISOString().slice(0, 10); +} + +function getRateLimitKey(req) { + return ( + req.get("x-shadow-fingerprint")?.trim() || req.sessionID || req.ip || "anonymous" + ); +} + +function readMessageText(content) { + if (typeof content === "string") { + return content.trim(); + } + + if (Array.isArray(content)) { + return content + .map((part) => (part?.type === "text" ? String(part.text || "") : "")) + .join(" ") + .replace(/\s+/g, " ") + .trim(); + } + + return ""; +} + +function getModelName(model) { + if (typeof model !== "string") return DEFAULT_AI_MODEL; + + const trimmed = model.trim(); + return trimmed && AI_MODELS.has(trimmed) ? trimmed : DEFAULT_AI_MODEL; +} + +async function postAiRequest(messages, model) { + const response = await fetch("https://api.navy/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.NAVY_API_KEY}`, + }, + body: JSON.stringify({ messages, model }), + }); + + const data = await response.json(); + + if (!response.ok) { + const errorMessage = data?.error?.message || data?.error || "AI provider request failed"; + return { ok: false, status: response.status, error: errorMessage }; + } + + const reply = data?.choices?.[0]?.message?.content; + if (!reply) { + return { ok: false, status: 500, error: "Unexpected response from AI API" }; + } + + return { ok: true, reply }; +} + +function getAiLimitState(limitKey, now) { + const currentDay = getDayKey(now); + const existingState = aiRequestLimits.get(limitKey); + + if (!existingState) { + const freshState = { + dayKey: currentDay, + dayCount: 0, + minuteHits: [], + }; + aiRequestLimits.set(limitKey, freshState); + return freshState; + } + + existingState.minuteHits = existingState.minuteHits.filter( + (hitTime) => now - hitTime < AI_MINUTE_WINDOW_MS, + ); + + if (existingState.dayKey !== currentDay) { + existingState.dayKey = currentDay; + existingState.dayCount = 0; + } + + return existingState; +} + +function checkAiRateLimit(req) { + const now = Date.now(); + const limitKey = getRateLimitKey(req); + const state = getAiLimitState(limitKey, now); + + if (state.minuteHits.length >= AI_MINUTE_LIMIT) { + const oldestHit = state.minuteHits[0]; + const retryAfterSeconds = Math.max( + 1, + Math.ceil((AI_MINUTE_WINDOW_MS - (now - oldestHit)) / 1000), + ); + return { + allowed: false, + status: 429, + error: "Rate limit reached. Try again in a minute.", + retryAfterSeconds, + }; + } + + if (state.dayCount >= AI_DAY_LIMIT) { + const nextReset = new Date(now); + nextReset.setUTCHours(24, 0, 0, 0); + const retryAfterSeconds = Math.max( + 1, + Math.ceil((nextReset.getTime() - now) / 1000), + ); + return { + allowed: false, + status: 429, + error: "Daily rate limit reached. Try again tomorrow.", + retryAfterSeconds, + }; + } + + state.minuteHits.push(now); + state.dayCount += 1; + aiRequestLimits.set(limitKey, state); + + return { allowed: true }; +} + app.post("/ask", express.json({ limit: "12mb" }), requireSession, csrfProtection, async (req, res) => { const { messages, model } = req.body; @@ -177,38 +314,38 @@ app.post("/ask", express.json({ limit: "12mb" }), requireSession, csrfProtection .json({ error: "msgs need to be in an array format." }); } - const requestedModel = - typeof model === "string" && model.trim() ? model.trim() : "shuttleai/auto"; - - try { - const response = await fetch( - "https://api.shuttleai.com/v1/chat/completions", - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.SHUTTLEAI_API_KEY}`, - }, - body: JSON.stringify({ - model: requestedModel, - messages, - }), - }, - ); - - const data = await response.json(); - - if (!response.ok) { - const apiMessage = - data?.error?.message || data?.error || "AI provider request failed"; - return res.status(response.status).json({ error: apiMessage }); + const rateLimitCheck = checkAiRateLimit(req); + if (!rateLimitCheck.allowed) { + if (typeof rateLimitCheck.retryAfterSeconds === "number") { + res.setHeader("Retry-After", String(rateLimitCheck.retryAfterSeconds)); } + return res.status(rateLimitCheck.status).json({ error: rateLimitCheck.error }); + } + const requestedModel = + typeof model === "string" && model.trim() && allowedAiModels.has(model.trim()) + ? model.trim() + : "gpt-5.4-mini"; + + let requestMessages = messages; + + if (webSearch) { + const searchQuery = getWebSearchQuery(messages); + if (searchQuery) { + const searchContext = await fetchWebSearchContext(searchQuery); + if (searchContext) { + const selectedModel = getModelName(model); + searchContext, + }, + const aiResult = await postAiRequest(messages, selectedModel); + + if (!aiResult.ok) { + return res.status(aiResult.status).json({ error: aiResult.error }); if (!data?.choices?.[0]?.message?.content) { console.error("Unexpected response:", data); return res.status(500).json({ error: "Unexpected response from AI API" }); - } - + model: selectedModel, + message: aiResult.reply, res.json({ model: requestedModel, message: data.choices[0].message.content, diff --git a/package-lock.json b/package-lock.json index e47b214..31f98fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1650,12 +1650,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/prettier": { diff --git a/public/ai.html b/public/ai.html index 7c20def..292547d 100644 --- a/public/ai.html +++ b/public/ai.html @@ -4,79 +4,148 @@ ShadowAssistant + - - - - + +
+ + +
-
-

ShadowAssistant

-

- powered and sponsored by - ShuttleAI -

+ +
+
+

New chat

+

powered by Navy AI

+
+
+ + +
-
- - - Not sharing + +
+
+

What can I help
you with today?

+
+ + + + + + + + + +
+
-
-
- - + + +
+ + +
+ + +
+ + + + +
+
+ +

Shadow AI can make mistakes. Please verify important info.

+
- + + - + \ No newline at end of file diff --git a/public/assets/js/ai.js b/public/assets/js/ai.js index eceb36a..1fcdfe0 100644 --- a/public/assets/js/ai.js +++ b/public/assets/js/ai.js @@ -8,19 +8,30 @@ const screenShareBtn = document.getElementById("screen-share-btn"); const screenShareStatus = document.getElementById("screen-share-status"); const chatList = document.getElementById("chat-list"); const newChatBtn = document.getElementById("new-chat-btn"); +const chatSearchInput = document.getElementById("chat-search-input"); +const welcomePanel = document.getElementById("welcome-panel"); +const imagePicker = document.getElementById("image-picker"); +const uploadImageBtn = document.getElementById("upload-image-btn"); +const attachmentTray = document.getElementById("attachment-tray"); +const quickPromptCards = document.querySelectorAll("[data-prompt]"); const USER = "user"; const ASSISTANT = "assistant"; const STORAGE_KEY = "shadow-ai-sessions-v1"; +const FINGERPRINT_STORAGE_KEY = "shadow-client-fingerprint-v1"; const CONTEXT_LIMIT = 12; +const THUMBMARK_MODULE_URL = + "https://cdn.jsdelivr.net/npm/@thumbmarkjs/thumbmarkjs/dist/thumbmark.esm.js"; let csrfToken = null; let sessions = []; let activeSessionId = null; +let fingerprintPromise = null; let screenStream = null; let screenVideoTrack = null; let captureVideo = null; +let draftAttachments = []; let assistantSession = null; const assistantState = { @@ -106,10 +117,25 @@ function formatDate(value) { function renderChatList() { chatList.innerHTML = ""; + const filterValue = String(chatSearchInput?.value || "") + .trim() + .toLowerCase(); + const sorted = [...sessions].sort( (a, b) => (b.updatedAt || 0) - (a.updatedAt || 0), ); for (const session of sorted) { + const searchableText = [ + session.title, + ...session.messages.map((message) => extractTextFromContent(message.content)), + ] + .join(" ") + .toLowerCase(); + + if (filterValue && !searchableText.includes(filterValue)) { + continue; + } + const item = document.createElement("div"); item.className = "chat-item"; if (session.id === activeSessionId) item.classList.add("active"); @@ -152,16 +178,22 @@ function renderMessages() { const session = getActiveSession(); messagesContainer.innerHTML = ""; if (!session) { + if (welcomePanel) welcomePanel.hidden = false; return; } if (session.messages.length === 0) { - addMsg("**Hello, how may I help you?**", ASSISTANT); + if (welcomePanel) welcomePanel.hidden = false; return; } + if (welcomePanel) welcomePanel.hidden = true; + session.messages.forEach((message) => { - addMsg(message.content, message.role); + const messageText = + extractTextFromContent(message.content) || + createDraftMessageLabel("", message.attachments || []); + addMsg(messageText, message.role, false, message.attachments || []); }); } @@ -211,6 +243,209 @@ function getThemeToken(name, fallback) { return value || fallback; } +function extractTextFromContent(content) { + if (typeof content === "string") { + return content.trim(); + } + + if (Array.isArray(content)) { + return content + .map((part) => (part?.type === "text" ? String(part.text || "") : "")) + .join(" ") + .replace(/\s+/g, " ") + .trim(); + } + + return ""; +} + +function renderAttachmentTray() { + if (!attachmentTray) return; + + attachmentTray.innerHTML = ""; + + if (draftAttachments.length === 0) { + attachmentTray.hidden = true; + return; + } + + attachmentTray.hidden = false; + + for (const attachment of draftAttachments) { + const tile = document.createElement("div"); + tile.className = "attachment-tile"; + + const img = document.createElement("img"); + img.src = attachment.previewUrl; + img.alt = attachment.name; + + const meta = document.createElement("div"); + meta.className = "attachment-meta"; + + const title = document.createElement("span"); + title.className = "attachment-name"; + title.textContent = attachment.name; + + const remove = document.createElement("button"); + remove.type = "button"; + remove.className = "attachment-remove"; + remove.textContent = "Remove"; + remove.addEventListener("click", () => { + removeDraftAttachment(attachment.id); + }); + + meta.appendChild(title); + meta.appendChild(remove); + tile.appendChild(img); + tile.appendChild(meta); + attachmentTray.appendChild(tile); + } +} + +function removeDraftAttachment(attachmentId) { + const next = draftAttachments.filter((item) => item.id !== attachmentId); + const removed = draftAttachments.find((item) => item.id === attachmentId); + if (removed?.previewUrl) { + URL.revokeObjectURL(removed.previewUrl); + } + draftAttachments = next; + renderAttachmentTray(); +} + +function clearDraftAttachments() { + draftAttachments.forEach((attachment) => { + if (attachment?.previewUrl) { + URL.revokeObjectURL(attachment.previewUrl); + } + }); + draftAttachments = []; + renderAttachmentTray(); +} + +function readFileAsDataUrl(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result || "")); + reader.onerror = () => reject(reader.error || new Error("Failed to read file")); + reader.readAsDataURL(file); + }); +} + +async function addDraftAttachments(files) { + const acceptedFiles = [...files].filter((file) => file.type.startsWith("image/")); + if (acceptedFiles.length === 0) return; + + const nextAttachments = []; + + for (const file of acceptedFiles) { + const previewUrl = URL.createObjectURL(file); + const dataUrl = await readFileAsDataUrl(file); + nextAttachments.push({ + id: createId(), + name: file.name, + type: file.type, + previewUrl, + dataUrl, + }); + } + + draftAttachments = [...draftAttachments, ...nextAttachments]; + renderAttachmentTray(); +} + +function getDraftUserContent(text, attachments) { + const trimmedText = String(text || "").trim(); + const imageParts = attachments.map((attachment) => ({ + type: "image_url", + image_url: { url: attachment.dataUrl, detail: "auto" }, + })); + + if (trimmedText && imageParts.length === 0) { + return trimmedText; + } + + if (!trimmedText && imageParts.length === 1) { + return imageParts; + } + + const parts = []; + if (trimmedText) { + parts.push({ type: "text", text: trimmedText }); + } + parts.push(...imageParts); + return parts; +} + +function createDraftMessageLabel(text, attachments) { + const trimmedText = String(text || "").trim(); + if (trimmedText) return trimmedText; + if (attachments.length === 1) return "Image attachment"; + return `${attachments.length} image attachments`; +} + +function extractFingerprintValue(result) { + if (!result) return ""; + if (typeof result === "string") return result.trim(); + if (typeof result !== "object") return String(result).trim(); + + const candidateKeys = [ + "thumbmark", + "fingerprint", + "visitorId", + "visitor_id", + "id", + ]; + + for (const key of candidateKeys) { + const value = result[key]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + + try { + return JSON.stringify(result); + } catch { + return ""; + } +} + +async function loadClientFingerprint() { + try { + const cachedValue = localStorage.getItem(FINGERPRINT_STORAGE_KEY); + if (cachedValue) return cachedValue; + + const thumbmarkModule = await import(THUMBMARK_MODULE_URL); + const ThumbmarkClass = + thumbmarkModule.Thumbmark || + thumbmarkModule.default?.Thumbmark || + thumbmarkModule.default; + + if (typeof ThumbmarkClass !== "function") { + return ""; + } + + const thumbmark = new ThumbmarkClass(); + const result = await thumbmark.get(); + const fingerprint = extractFingerprintValue(result); + + if (fingerprint) { + localStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprint); + } + + return fingerprint; + } catch { + return localStorage.getItem(FINGERPRINT_STORAGE_KEY) || ""; + } +} + +function getClientFingerprint() { + if (!fingerprintPromise) { + fingerprintPromise = loadClientFingerprint(); + } + return fingerprintPromise; +} + function getAssistantTheme() { return { background: getThemeToken("--background", "#0f1117"), @@ -636,7 +871,10 @@ function getContextMessages(sessionId = activeSessionId) { if (!session) return []; return session.messages.slice(-CONTEXT_LIMIT).map((item) => ({ role: item.role, - content: item.content, + content: + Array.isArray(item.content) || typeof item.content === "string" + ? item.content + : getDraftUserContent(item.content, item.attachments || []), })); } @@ -655,19 +893,26 @@ async function buildOutboundMessages( const lastIndex = contextMessages.length - 1; const latest = contextMessages[lastIndex]; - if (!latest || latest.role !== USER || typeof latest.content !== "string") { + if (!latest || latest.role !== USER) { return contextMessages; } const imageDataUrl = await captureScreenFrameDataUrl(); return contextMessages.map((entry, index) => { if (index !== lastIndex) return entry; + + const content = Array.isArray(entry.content) + ? [...entry.content] + : [{ type: "text", text: String(entry.content || "") }]; + + content.push({ + type: "image_url", + image_url: { url: imageDataUrl, detail: "auto" }, + }); + return { role: USER, - content: [ - { type: "text", text: latest.content }, - { type: "image_url", image_url: { url: imageDataUrl, detail: "auto" } }, - ], + content, }; }); } @@ -683,11 +928,13 @@ async function getCsrfToken() { } async function requestAssistantResponse(messages, model) { + const fingerprint = await getClientFingerprint(); const response = await fetch("/ask", { method: "POST", headers: { "Content-Type": "application/json", "x-csrf-token": csrfToken, + ...(fingerprint ? { "x-shadow-fingerprint": fingerprint } : {}), }, credentials: "include", body: JSON.stringify({ messages, model }), @@ -745,7 +992,10 @@ async function askFromAssistant(prompt) { }, ]; - const aiMsg = await requestAssistantResponse(messages, modelSelector.value); + const aiMsg = await requestAssistantResponse( + messages, + modelSelector.value, + ); assistantState.output = aiMsg; assistantState.error = ""; } catch (error) { @@ -761,7 +1011,7 @@ async function askFromAssistant(prompt) { } } -function addMsg(content, role, isLoading = false) { +function addMsg(content, role, isLoading = false, attachments = []) { const msgElement = document.createElement("div"); msgElement.classList.add("message", role); @@ -772,6 +1022,20 @@ function addMsg(content, role, isLoading = false) { msgElement.innerHTML = marked(content); } + if (attachments.length > 0) { + const attachmentWrap = document.createElement("div"); + attachmentWrap.className = "message-attachments"; + + for (const attachment of attachments) { + const image = document.createElement("img"); + image.src = attachment.dataUrl || attachment.previewUrl; + image.alt = attachment.name || "Uploaded image"; + attachmentWrap.appendChild(image); + } + + msgElement.appendChild(attachmentWrap); + } + messagesContainer.appendChild(msgElement); messagesContainer.scrollTop = messagesContainer.scrollHeight; @@ -786,12 +1050,22 @@ function addMessageToSession(role, content, sessionId = activeSessionId) { const session = getSessionById(sessionId); if (!session) return; - session.messages.push({ role, content, createdAt: Date.now() }); + let attachments = []; + let messageContent = content; + + if (typeof content === "object" && content !== null && !Array.isArray(content)) { + attachments = Array.isArray(content.attachments) ? content.attachments : []; + messageContent = content.content; + } + + session.messages.push({ role, content: messageContent, attachments, createdAt: Date.now() }); if ( role === USER && session.messages.filter((msg) => msg.role === USER).length === 1 ) { - session.title = compactTitle(content); + session.title = compactTitle( + extractTextFromContent(messageContent) || "Image attachment", + ); } touchSession(session); @@ -799,14 +1073,28 @@ function addMessageToSession(role, content, sessionId = activeSessionId) { async function sendMsg() { const message = textarea.value.trim(); - if (!message) return; + if (!message && draftAttachments.length === 0) return; const requestSessionId = activeSessionId; if (!requestSessionId) return; - addMessageToSession(USER, message, requestSessionId); - addMsg(message, USER); + const outboundAttachments = draftAttachments.map((attachment) => ({ + ...attachment, + })); + const displayMessage = createDraftMessageLabel(message, outboundAttachments); + const content = getDraftUserContent(message, outboundAttachments); + + addMessageToSession( + USER, + { + content, + attachments: outboundAttachments, + }, + requestSessionId, + ); + addMsg(displayMessage, USER, false, outboundAttachments); textarea.value = ""; + clearDraftAttachments(); let outboundMessages; try { @@ -852,6 +1140,7 @@ async function sendMsg() { async function init() { csrfToken = await getCsrfToken(); + void getClientFingerprint(); sessions = loadSessions(); if (sessions.length === 0) { @@ -861,12 +1150,35 @@ async function init() { renderChatList(); renderMessages(); + renderAttachmentTray(); updateShareUi(); sendBtn.addEventListener("click", async () => { await sendMsg(); }); + uploadImageBtn.addEventListener("click", () => { + imagePicker.click(); + }); + + imagePicker.addEventListener("change", async () => { + if (imagePicker.files) { + await addDraftAttachments(imagePicker.files); + imagePicker.value = ""; + } + }); + + chatSearchInput.addEventListener("input", () => { + renderChatList(); + }); + + quickPromptCards.forEach((card) => { + card.addEventListener("click", () => { + textarea.value = card.dataset.prompt || ""; + textarea.focus(); + }); + }); + screenShareBtn.addEventListener("click", async () => { if (screenStream) { stopScreenShare(); diff --git a/public/css/ai.css b/public/css/ai.css index 8cea65d..6f7d9ac 100644 --- a/public/css/ai.css +++ b/public/css/ai.css @@ -1,5 +1,8 @@ +/* ── Reset ─────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +/* ── Base ──────────────────────────────────────────────────── */ body { - margin: 0; font-family: "Poppins", sans-serif; background: var(--background); color: var(--text); @@ -7,76 +10,155 @@ body { overflow: hidden; } +/* ── Layout ────────────────────────────────────────────────── */ .ai-layout { display: grid; - grid-template-columns: 280px 1fr; + grid-template-columns: 302px minmax(0, 1fr); height: 100vh; } +/* ── Sidebar ───────────────────────────────────────────────── */ .chat-sidebar { - border-right: 1px solid var(--border); - background: linear-gradient(180deg, var(--secondary), var(--background)); + border-right: 1px solid rgba(255, 255, 255, 0.07); + background: rgba(7, 7, 10, 0.92); + backdrop-filter: blur(14px); display: flex; flex-direction: column; min-height: 0; + padding: 14px 12px 12px; } .sidebar-header { display: flex; align-items: center; justify-content: space-between; - padding: 14px 12px; - border-bottom: 1px solid var(--border); + padding: 4px 4px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.brand-lockup { + display: flex; + align-items: center; + gap: 10px; +} + +.brand-mark { + width: 28px; + height: 28px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(145deg, rgba(255,255,255,0.16), rgba(255,255,255,0.04)); + border: 1px solid rgba(255,255,255,0.08); + color: var(--text); + font-size: 0.9rem; + flex-shrink: 0; } .sidebar-header h2 { margin: 0; - font-size: 1rem; - letter-spacing: 0.02em; + font-size: 0.92rem; + font-weight: 600; + letter-spacing: 0.01em; +} + +.sidebar-header p { + margin: 2px 0 0; + font-size: 0.72rem; + opacity: 0.55; } .new-chat-btn { - border: 1px solid var(--border); - background: var(--secondary); + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.08); color: var(--text); - border-radius: 8px; - padding: 7px 10px; + border-radius: 10px; + padding: 7px 9px; cursor: pointer; - font-size: 0.82rem; - font-family: "Poppins", sans-serif; + display: flex; + align-items: center; + justify-content: center; + transition: border-color 0.15s, background 0.15s; + flex-shrink: 0; } .new-chat-btn:hover { border-color: var(--accent); + background: var(--accent-glow); +} + +/* Search */ +.chat-search { + display: flex; + align-items: center; + gap: 10px; + margin: 12px 4px 10px; + padding: 10px 12px; + border: 1px solid rgba(255,255,255,0.06); + border-radius: 12px; + background: rgba(255,255,255,0.03); +} + +.chat-search .search-icon { + opacity: 0.45; + flex-shrink: 0; +} + +.chat-search input { + border: 0; + outline: none; + background: transparent; + color: var(--text); + width: 100%; + font-family: inherit; + font-size: 0.84rem; +} + +.chat-search input::placeholder { + color: rgba(255,255,255,0.38); } +/* Section label */ +.sidebar-section-label { + margin: 12px 6px 8px; + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; + opacity: 0.42; +} + +/* Chat list */ .chat-list { - padding: 10px; + padding: 2px 4px 10px; + flex: 1; overflow-y: auto; display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } +.chat-list::-webkit-scrollbar { width: 4px; } +.chat-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 99px; } + .chat-item { - border: 1px solid var(--border); - background: var(--secondary); - border-radius: 10px; - padding: 10px; + border: 1px solid rgba(255,255,255,0.05); + background: rgba(255,255,255,0.03); + border-radius: 12px; + padding: 10px 12px; cursor: pointer; - transition: - border-color 0.2s ease, - transform 0.2s ease; + transition: border-color 0.15s, background 0.15s, transform 0.15s; } .chat-item:hover { + border-color: rgba(255,255,255,0.18); + background: rgba(255,255,255,0.06); transform: translateY(-1px); - border-color: var(--accent); } .chat-item.active { border-color: var(--accent); - box-shadow: 0 0 0 1px var(--accent) inset; + background: var(--accent-glow); } .chat-item-top { @@ -87,7 +169,8 @@ body { .chat-item-title { margin: 0; - font-size: 0.88rem; + font-size: 0.86rem; + font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -98,109 +181,130 @@ body { border: 0; background: transparent; color: var(--text); - opacity: 0.65; + opacity: 0.45; cursor: pointer; - font-size: 0.95rem; + font-size: 0.9rem; line-height: 1; + padding: 2px 4px; + border-radius: 4px; + transition: opacity 0.12s, color 0.12s; } .chat-item-delete:hover { opacity: 1; + color: var(--hover-danger); } .chat-item-meta { font-size: 0.72rem; - opacity: 0.7; + opacity: 0.5; margin-top: 4px; } +/* Sidebar footer */ +.sidebar-footer { + margin-top: auto; + padding: 12px 4px 4px; + border-top: 1px solid rgba(255,255,255,0.06); +} + +.model-label { + display: block; + margin-bottom: 8px; + font-size: 0.72rem; + opacity: 0.62; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +#model-selector { + width: 100%; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.04); + color: var(--text); + font-family: "Poppins", sans-serif; + font-size: 0.88rem; + cursor: pointer; + appearance: none; + outline: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.4)' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 30px; + transition: border-color 0.15s; +} + +#model-selector:focus { border-color: var(--accent); } + +#model-selector option { + background: var(--primary); + color: var(--accent-light); +} + +/* ── Main ──────────────────────────────────────────────────── */ .chat-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; - padding: 14px 16px 16px; + padding: 18px 18px 14px; + gap: 10px; } -.header { +/* Topbar */ +.topbar { display: flex; - justify-content: space-between; align-items: center; + justify-content: space-between; width: 100%; - max-width: 1200px; - margin-bottom: 8px; - flex-wrap: wrap; + max-width: 960px; + margin: 0 auto; + padding-bottom: 8px; } -.header h1 { - margin: 0; - font-size: 2.2rem; - background: linear-gradient(90deg, var(--accent), var(--text)); - background-size: 200% 100%; - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; +.topbar-copy h1 { + font-size: 1.1rem; + font-weight: 600; } -.header p { - margin: 0; +.topbar-copy p { + margin: 4px 0 0; + font-size: 0.82rem; + opacity: 0.56; } -.header p a { - color: var(--accent); +.topbar-copy p a { + color: var(--text); text-decoration: none; - font-size: 1rem; - font-weight: 600; + border-bottom: 1px solid rgba(255,255,255,0.22); + transition: border-color 0.15s; } -.ai-controls { - width: 100%; - max-width: 1200px; +.topbar-copy p a:hover { border-color: var(--accent); color: var(--accent-light); } + +.topbar-actions { display: flex; align-items: center; gap: 10px; - margin: 0 0 8px; -} - -#model-selector { - padding: 8px 14px; - border-radius: 8px; - border: 1px solid var(--border); - background: linear-gradient(145deg, var(--secondary), var(--background)); - color: var(--text); - font-family: "Poppins", sans-serif; - font-size: 0.95rem; - cursor: pointer; - appearance: none; - padding-right: 36px; -} - -#model-selector option { - background: var(--primary); - color: var(--accent-light); - outline:none; - border:none; - padding: 10px 12px; -} - - -#model-selector:focus { - outline: 2px solid var(--accent); } .screen-share-btn { - padding: 8px 12px; - border-radius: 8px; - border: 1px solid var(--border); - background: var(--secondary); + padding: 9px 14px; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.03); color: var(--text); font-family: "Poppins", sans-serif; - font-size: 0.9rem; + font-size: 0.84rem; cursor: pointer; + transition: border-color 0.15s, background 0.15s; } .screen-share-btn:hover { border-color: var(--accent); + background: var(--accent-glow); } .screen-share-btn.active { @@ -210,68 +314,248 @@ body { } .screen-share-status { - font-size: 0.85rem; - opacity: 0.75; + font-size: 0.82rem; + opacity: 0.55; } +/* ── Messages ──────────────────────────────────────────────── */ .messages { width: 100%; - max-width: 1200px; - margin: 6px 0; + max-width: 960px; + margin: 0 auto; flex: 1; overflow-y: auto; - padding-right: 4px; + padding: 2px 4px 0; } -.messages::-webkit-scrollbar, -.chat-list::-webkit-scrollbar { - width: 8px; +.messages::-webkit-scrollbar { width: 5px; } +.messages::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 99px; } + +/* Welcome panel */ +.welcome-panel { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 22px; + min-height: 100%; + padding: 20px 10px 10px; } -.messages::-webkit-scrollbar-thumb, -.chat-list::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 20px; +.welcome-panel h2 { + margin: 0; + max-width: 540px; + text-align: center; + font-size: clamp(1.9rem, 3vw, 3rem); + line-height: 1.1; + letter-spacing: -0.03em; } -.message { - padding: 3px 16px; - margin: 10px 0; +.welcome-grid { + width: min(760px, 100%); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.welcome-card { + text-align: left; border-radius: 14px; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.03); + color: var(--text); + padding: 16px; + min-height: 84px; + cursor: pointer; + font-family: inherit; + display: flex; + flex-direction: column; + gap: 5px; + transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; +} + +.welcome-card:hover { + border-color: var(--accent); + background: var(--accent-glow); + box-shadow: 0 0 18px var(--accent-glow); +} + +.welcome-card-title { + font-size: 0.96rem; + font-weight: 600; +} + +.welcome-card-subtitle { + font-size: 0.82rem; + opacity: 0.55; +} + +/* Messages */ +.message { + padding: 14px 16px; + margin: 12px 0; + border-radius: 18px; + max-width: min(84%, 820px); + line-height: 1.58; opacity: 0; - animation: fadeIn 0.35s forwards; - max-width: 76%; + transform: translateY(5px); + transition: opacity 0.25s ease, transform 0.25s ease; } +.message.show { opacity: 1; transform: translateY(0); } + .message.assistant { - background: var(--active-tab); - margin-right: auto; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.06); border-radius: 18px 18px 18px 4px; + margin-right: auto; } .message.user { - background: linear-gradient(145deg, var(--accent), var(--border)); - margin-left: auto; + background: linear-gradient(145deg, rgba(255,255,255,0.12), rgba(255,255,255,0.05)); + border: 1px solid rgba(255,255,255,0.1); border-radius: 18px 18px 4px 18px; + margin-left: auto; } .message.loading { display: flex; align-items: center; + gap: 10px; width: fit-content; - padding: 12px; + color: rgba(255,255,255,0.5); } -.input-container { +.message pre { + background: rgba(255,255,255,0.05); + color: var(--text); + padding: 10px 12px; + border-radius: 8px; + overflow-x: auto; + white-space: pre-wrap; + margin: 8px 0; + border: 1px solid rgba(255,255,255,0.06); +} + +.message code { + background: rgba(255,255,255,0.08); + padding: 2px 5px; + border-radius: 5px; + font-size: 0.9em; +} + +.message a { + color: var(--accent-light); + text-decoration: none; + border-bottom: 1px solid var(--border); +} + +/* Message attachments */ +.message-attachments { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 240px)); + gap: 10px; + margin-top: 10px; +} + +.message-attachments img { + width: 100%; + max-height: 220px; + object-fit: cover; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.08); +} + +/* Spinner */ +.spinner { + border: 3px solid rgba(255,255,255,0.08); + border-top-color: var(--accent); + border-radius: 50%; + width: 18px; + height: 18px; + animation: spin 0.8s linear infinite; + flex-shrink: 0; +} + +/* ── Composer ──────────────────────────────────────────────── */ +.composer-shell { + width: 100%; + max-width: 960px; + margin: 0 auto; + padding-top: 6px; +} + +/* Attachment tray */ +.attachment-tray { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 10px; + margin-bottom: 10px; +} + +.attachment-tile { + border: 1px solid rgba(255,255,255,0.08); + border-radius: 16px; + overflow: hidden; + background: rgba(255,255,255,0.03); +} + +.attachment-tile img { + display: block; + width: 100%; + height: 120px; + object-fit: cover; +} + +.attachment-meta { display: flex; align-items: center; - background-color: var(--secondary); - padding: 10px; - border-radius: 10px; - width: 100%; - max-width: 1200px; + justify-content: space-between; + gap: 10px; + padding: 8px 12px; +} + +.attachment-name { + font-size: 0.8rem; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.attachment-remove { + border: 0; + background: transparent; + color: var(--text); + opacity: 0.55; + cursor: pointer; + font-family: inherit; + font-size: 0.8rem; + transition: opacity 0.12s, color 0.12s; +} + +.attachment-remove:hover { opacity: 1; color: var(--hover-danger); } + +/* Input box */ +.input-container { + display: flex; + align-items: flex-end; + gap: 0; + background: rgba(255,255,255,0.04); + padding: 12px 12px 12px 16px; + border-radius: 20px; + border: 1px solid rgba(255,255,255,0.08); + box-shadow: 0 14px 40px rgba(0,0,0,0.3); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.input-container:focus-within { + border-color: var(--border); + box-shadow: 0 0 0 3px var(--accent-glow), 0 14px 40px rgba(0,0,0,0.3); } +/* Textarea — no scrollbar, auto-grows via JS */ textarea { flex: 1; background: transparent; @@ -282,76 +566,94 @@ textarea { outline: none; resize: none; min-height: 28px; - max-height: 140px; + max-height: 160px; + line-height: 1.5; + overflow-y: hidden; /* hide scrollbar */ + padding-right: 10px; + align-self: center; } -textarea::placeholder { - color: rgba(255, 255, 255, 0.7); +textarea::placeholder { color: rgba(255,255,255,0.38); } + +/* hide scrollbar across browsers */ +textarea::-webkit-scrollbar { display: none; } +textarea { scrollbar-width: none; -ms-overflow-style: none; } + +/* Icon row inside composer */ +.composer-icon-row { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; } -.send-btn { - background-color: var(--accent); +.composer-icon-btn { + background: none; border: none; - padding: 12px; - border-radius: 8px; cursor: pointer; + color: rgba(255,255,255,0.4); + width: 34px; + height: 34px; + border-radius: 10px; display: flex; align-items: center; justify-content: center; + transition: color 0.15s, background 0.15s; } -.send-btn svg { - width: 20px; - height: 20px; - fill: #fff; +.composer-icon-btn:hover { + color: var(--text); + background: rgba(255,255,255,0.06); } -.spinner { - border: 4px solid var(--secondary); - border-top: 4px solid var(--accent); - border-radius: 50%; - width: 20px; - height: 20px; - animation: spin 1s linear infinite; - margin-right: 6px; -} +.composer-icon-btn.on { color: var(--accent); } -.message pre { - background-color: var(--secondary); - color: var(--text); - padding: 10px; - border-radius: 6px; - overflow-x: auto; - white-space: pre-wrap; -} +/* hidden file input & checkbox */ +#image-picker, +#web-search-toggle { display: none; } -.message code { - background-color: var(--secondary); - padding: 2px 4px; - border-radius: 4px; +/* Send button */ +.send-btn { + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 12px; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s, box-shadow 0.15s; + flex-shrink: 0; + margin-left: 2px; } -.message a { - color: var(--accent); - text-decoration: none; - border-bottom: 1px solid var(--accent); +.send-btn:hover { + background: var(--accent); + box-shadow: 0 0 14px var(--accent-glow); + border-color: transparent; } -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } +.send-btn svg { + width: 16px; + height: 16px; + fill: #fff; } -@keyframes fadeIn { - to { - opacity: 1; - } +/* Composer note */ +.composer-note { + margin: 10px 0 0; + text-align: center; + font-size: 0.78rem; + opacity: 0.38; } +/* ── Animations ────────────────────────────────────────────── */ +@keyframes spin { to { transform: rotate(360deg); } } + +@keyframes fadeIn { to { opacity: 1; } } + +/* ── Responsive ────────────────────────────────────────────── */ @media (max-width: 980px) { .ai-layout { grid-template-columns: 1fr; @@ -359,8 +661,8 @@ textarea::placeholder { .chat-sidebar { border-right: 0; - border-bottom: 1px solid var(--border); - max-height: 170px; + border-bottom: 1px solid rgba(255,255,255,0.07); + max-height: 260px; } .chat-list { @@ -368,12 +670,12 @@ textarea::placeholder { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .chat-main { - padding-top: 10px; + .welcome-grid { + grid-template-columns: 1fr; } - .header h1 { - font-size: 1.8rem; + .message { + max-width: 94%; } } @@ -382,15 +684,17 @@ textarea::placeholder { grid-template-columns: 1fr; } - .ai-controls { - flex-wrap: wrap; + .topbar { + flex-direction: column; + align-items: flex-start; + gap: 8px; } .message { - max-width: 94%; + max-width: 100%; } - .header h1 { - font-size: 1.45rem; + .welcome-panel h2 { + font-size: 1.8rem; } -} +} \ No newline at end of file From ec2019d205ba0ae679da7a1133485e4c06695fc4 Mon Sep 17 00:00:00 2001 From: NCCoder2 Date: Thu, 28 May 2026 20:54:05 +0000 Subject: [PATCH 10/10] 1 --- index.js | 41 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/index.js b/index.js index 11a1361..a2aeff4 100644 --- a/index.js +++ b/index.js @@ -322,38 +322,17 @@ app.post("/ask", express.json({ limit: "12mb" }), requireSession, csrfProtection return res.status(rateLimitCheck.status).json({ error: rateLimitCheck.error }); } - const requestedModel = - typeof model === "string" && model.trim() && allowedAiModels.has(model.trim()) - ? model.trim() - : "gpt-5.4-mini"; - - let requestMessages = messages; - - if (webSearch) { - const searchQuery = getWebSearchQuery(messages); - if (searchQuery) { - const searchContext = await fetchWebSearchContext(searchQuery); - if (searchContext) { - const selectedModel = getModelName(model); - searchContext, - }, - const aiResult = await postAiRequest(messages, selectedModel); - - if (!aiResult.ok) { - return res.status(aiResult.status).json({ error: aiResult.error }); - if (!data?.choices?.[0]?.message?.content) { - console.error("Unexpected response:", data); - return res.status(500).json({ error: "Unexpected response from AI API" }); - model: selectedModel, - message: aiResult.reply, - res.json({ - model: requestedModel, - message: data.choices[0].message.content, - }); - } catch (error) { - console.error(error); - res.status(500).json({ error: "Failed to Retrieve Request" }); + const selectedModel = getModelName(model); + const aiResult = await postAiRequest(messages, selectedModel); + + if (!aiResult.ok) { + return res.status(aiResult.status).json({ error: aiResult.error }); } + + res.json({ + model: selectedModel, + message: aiResult.reply, + }); }); app.get("/v1/api/user-agents", async (req, res) => {