Live Oak Technologies Self-Hosted AI
Live Oak Technologies (via Ollama + IIS/NGINX)
Connected to → Lewis
Send
.
.
.
Running locally on Jet Fuel — Model:
gemma3:4b
", "function ", "const ", "let ", "class ", "import ", "export ", "def ", "from ", "using ", "#include", "public ", "private ", "protected ", "return ", "if (", "for (", "while (", "=>" ]; if (codeHints.some(h => t.includes(h))) return true; const words = t.split(/\s+/); if (words.length > 80 || t.length > 1000) { // Big blob → probably code/file, not “what time is it” return true; } return false; } // ======================== // REFERENCE DATA // ======================== let htmlReference = null; let cssReference = null; let phpReference = null; let jsReference = null; let pythonReference = null; let referencesLoaded = false; async function loadReferences() { try { const [htmlResp, cssResp, phpResp, jsResp, pyResp] = await Promise.all([ fetch(HTML_REF_URL), fetch(CSS_REF_URL), fetch(PHP_REF_URL), fetch(JS_REF_URL), fetch(PY_REF_URL) ]); if (htmlResp.ok) { const h = await htmlResp.json(); if (h && h.success && h.data) htmlReference = h.data; } if (cssResp.ok) { const c = await cssResp.json(); if (c && c.success && c.data) cssReference = c.data; } if (phpResp.ok) { const p = await phpResp.json(); if (p && p.success && p.data) phpReference = p.data; } if (jsResp.ok) { const j = await jsResp.json(); if (j && j.success && j.data) jsReference = j.data; } if (pyResp.ok) { const py = await pyResp.json(); if (py && py.success && py.data) pythonReference = py.data; } } catch (err) { console.error("Error loading references:", err); } finally { referencesLoaded = true; } } // ======================== // MEMORY HELPERS (memory.php) // ======================== let memoryLoaded = false; let memoryProfile = null; // object or [] let memoryFacts = []; // [{fact, created_at}, ...] or string[] let memoryMessages = []; // [{time, role, content}, ...] async function memoryLoad(userId) { try { const resp = await fetch( `${MEMORY_API_URL}?action=load&user_id=${encodeURIComponent(userId)}` ); if (!resp.ok) { console.error("memoryLoad HTTP error:", resp.status); return null; } const data = await resp.json(); if (!data.success) { console.warn("memoryLoad error:", data.error); return null; } return data; // {success, profile, messages, facts} } catch (e) { console.error("memoryLoad network error:", e); return null; } } async function memorySaveMessage(userId, role, content) { try { const resp = await fetch(MEMORY_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "save_message", // IMPORTANT: fixes "Missing action" user_id: userId, role, content }) }); if (!resp.ok) { console.error("memorySaveMessage HTTP error:", resp.status); return; } const data = await resp.json(); if (!data.success) { console.warn("memorySaveMessage error:", data.error); } } catch (e) { console.error("memorySaveMessage network error:", e); } } // ======================== // CHAT STATE & DOM // ======================== const chatEl = document.getElementById("chat"); const inputEl = document.getElementById("input"); const sendBtn = document.getElementById("sendBtn"); const spinner = document.getElementById("spinner"); const statusEl = document.getElementById("status"); const historyListEl = document.getElementById("historyList"); const newChatBtn = document.getElementById("newChatBtn"); let chats = []; // [{id, title, createdAt, messages:[{role,content}]}] let activeChatId = null; let visibleMessages = []; // ======================== // STORAGE HELPERS // ======================== function loadChatsFromStorage() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed; } catch (e) { console.error("Failed to load chats from storage:", e); return []; } } function saveChatsToStorage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(chats)); } catch (e) { console.error("Failed to save chats to storage:", e); } } function createNewChat() { const id = Date.now().toString(); const now = new Date().toISOString(); const chat = { id, title: "New chat", createdAt: now, messages: [] }; chats.unshift(chat); activeChatId = id; visibleMessages = chat.messages; saveChatsToStorage(); renderHistory(); renderChatMessages(); } function getActiveChat() { return chats.find(c => c.id === activeChatId) || null; } function deleteChat(chatId) { const idx = chats.findIndex(c => c.id === chatId); if (idx === -1) return; chats.splice(idx, 1); if (activeChatId === chatId) { if (chats.length > 0) { activeChatId = chats[0].id; visibleMessages = chats[0].messages; } else { activeChatId = null; visibleMessages = []; createNewChat(); return; } } saveChatsToStorage(); renderHistory(); renderChatMessages(); } // ======================== // UI HELPERS // ======================== function addMessage(role, text) { const row = document.createElement("div"); row.className = `msg-row ${role}`; const bubble = document.createElement("div"); bubble.className = "msg-bubble"; const label = document.createElement("div"); label.className = "msg-label"; label.textContent = role === "user" ? "You" : "Lewis"; const body = document.createElement("div"); body.textContent = text; bubble.appendChild(label); bubble.appendChild(body); row.appendChild(bubble); chatEl.appendChild(row); chatEl.scrollTop = chatEl.scrollHeight; } function renderChatMessages() { chatEl.innerHTML = ""; if (!visibleMessages) return; for (const msg of visibleMessages) { const roleClass = msg.role === "user" ? "user" : "assistant"; addMessage(roleClass, msg.content); } } function renderHistory() { historyListEl.innerHTML = ""; if (!chats.length) { const empty = document.createElement("div"); empty.style.fontSize = "0.8rem"; empty.style.color = "#9ca3af"; empty.textContent = "No chats yet. Start one!"; historyListEl.appendChild(empty); return; } chats.forEach(chat => { const item = document.createElement("div"); item.className = "history-item" + (chat.id === activeChatId ? " active" : ""); const main = document.createElement("div"); main.className = "history-item-main"; const titleEl = document.createElement("div"); titleEl.className = "history-item-title"; titleEl.textContent = chat.title || "Untitled chat"; const timeEl = document.createElement("div"); timeEl.className = "history-item-time"; const dt = new Date(chat.createdAt); timeEl.textContent = dt.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); main.appendChild(titleEl); main.appendChild(timeEl); const deleteBtn = document.createElement("button"); deleteBtn.className = "history-item-delete"; deleteBtn.textContent = "🗑"; deleteBtn.title = "Delete chat"; deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); if (confirm("Delete this conversation?")) { deleteChat(chat.id); } }); item.addEventListener("click", () => { activeChatId = chat.id; visibleMessages = chat.messages; renderHistory(); renderChatMessages(); }); item.appendChild(main); item.appendChild(deleteBtn); historyListEl.appendChild(item); }); } function setLoading(isLoading) { sendBtn.disabled = isLoading; spinner.style.display = isLoading ? "inline-flex" : "none"; statusEl.textContent = isLoading ? "Lewis is thinking..." : "Connected to ai.fairhopemail.com → Lewis"; statusEl.classList.remove("error"); } function setError(msg) { statusEl.textContent = msg; statusEl.classList.add("error"); } // ======================== // LOCAL TIME / DATE HELPERS // ======================== function getLocalTime12hInZone(timeZone) { const now = new Date(); const formatter = new Intl.DateTimeFormat("en-US", { timeZone, hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true }); return formatter.format(now); } function getLocalTime12h() { return getLocalTime12hInZone(USER_TIMEZONE); } function getLocalDate() { const now = new Date(); const formatter = new Intl.DateTimeFormat("en-US", { timeZone: USER_TIMEZONE, year: "numeric", month: "2-digit", day: "2-digit" }); return formatter.format(now); } // ======================== // QUESTION TYPE DETECTION // ======================== function isTimeQuestion(text) { const t = text.toLowerCase().trim(); if (!t.includes("time")) return false; if ( t.includes("what time is it") || t.includes("what's the time") || t.includes("whats the time") || t.includes("what is the time") || t.includes("current time") || t.includes("local time") || t.includes("tell me the time") || t.includes("tell me what time") || t.includes("can you tell me the time") || t.includes("time here") ) { return true; } if (t.endsWith("time") || t.endsWith("time?")) return true; return false; } function isDateQuestion(text) { const t = text.toLowerCase().trim(); if (!t.includes("date") && !t.includes("day")) return false; if ( t.includes("what's the date") || t.includes("whats the date") || t.includes("today's date") || t.includes("todays date") || t.includes("what day is it") ) { return true; } if (t.endsWith("date") || t.endsWith("date?")) return true; return false; } // ======================== // LOCATION EXTRACTION FOR TIME ("time in X") // ======================== function extractTimeLocation(text) { const t = text.toLowerCase().trim(); let loc = null; const patterns = [ "time is it in ", "current time in ", "time in " ]; for (const pat of patterns) { const idx = t.indexOf(pat); if (idx !== -1) { loc = t.slice(idx + pat.length); break; } } if (!loc) return null; loc = loc.replace(/\?+$/, "").trim(); loc = loc.replace(/\s+right now$/, "").trim(); loc = loc.replace(/\s+please$/, "").trim(); if (!loc) return null; return loc; } // ======================== // FOLLOW-UP LOCATION ("what about Dallas") // ======================== function inferFollowupTimeLocation(lowerText, chat) { const t = lowerText.trim(); if (!chat || !chat.messages || chat.messages.length < 2) return null; let lastAssistant = null; for (let i = chat.messages.length - 1; i >= 0; i--) { if (chat.messages[i].role === "assistant") { lastAssistant = chat.messages[i]; break; } } if (!lastAssistant) return null; const last = lastAssistant.content.toLowerCase(); let lastWasTime = last.includes("time zone:") || last.includes("it’s currently") || last.includes("it's currently") || last.includes("current time") || /\d{4}-\d{2}-\d{2} .* in .* \(time zone:/.test(last); const clockPattern = /\b\d{1,2}:\d{2}(:\d{2})?\s?(am|pm)?\b/i; if (!lastWasTime && clockPattern.test(lastAssistant.content)) { lastWasTime = true; } if (!lastWasTime) return null; const m = t.match(/^(what about|how about|and what about|and how about)\s+(.+)$/); let loc = null; if (m) { loc = m[2]; } else { const wordCount = t.split(/\s+/).length; if (wordCount <= 4) { loc = t; } } if (!loc) return null; loc = loc.replace(/\?+$/, "").trim(); if (!loc) return null; return loc; } // ======================== // time.php FETCH HELPER // ======================== async function fetchTimeForPlace(place) { try { const resp = await fetch( `${TIME_API_URL}?api=1&place=${encodeURIComponent(place)}` ); if (!resp.ok) { return { success: false, error: `HTTP ${resp.status}` }; } const data = await resp.json(); return data; } catch (err) { console.error("Time API error:", err); return { success: false, error: err?.message || "Network error" }; } } // ======================== // SIMPLE FACT-BASED ANSWERS // ======================== function tryAnswerFromFacts(lowerText) { if (!Array.isArray(memoryFacts) || memoryFacts.length === 0) return null; // Example: SMTP override from facts if (lowerText.includes("smtp") && lowerText.includes("screenrooms.tech")) { for (const f of memoryFacts) { const factText = typeof f === "string" ? f : (f.fact || ""); if (factText && factText.toLowerCase().includes("smtp server for screenrooms.tech")) { return `From what you've told me before: ${factText}`; } } // If any SMTP-related fact exists, still use it for (const f of memoryFacts) { const factText = typeof f === "string" ? f : (f.fact || ""); if (factText && factText.toLowerCase().includes("smtp")) { return `From what you've told me before: ${factText}`; } } } return null; } // ======================== // BUILD SYSTEM MESSAGE FOR OLLAMA // ======================== function buildSystemMessage() { let content = `You are Lewis, an AI assistant for Live Oak Technologies.\n` + `You are running locally behind Ollama via an IIS/NGINX proxy.\n` + `The current user has a stable anonymous ID: ${USER_ID}.\n\n` + `STYLE & BEHAVIOR RULES (VERY IMPORTANT):\n` + `- Always respond like a helpful senior engineer / tutor.\n` + `- Do NOT just spit out raw code or raw data.\n` + `- First, briefly explain what you are going to do or what you did.\n` + `- Then show code or output (if needed), formatted in proper Markdown code fences.\n` + `- After the code or answer, add a short follow-up section like:\n` + ` "How to use this", "What to change next", or "Suggestions / next steps".\n` + `- If the user seems unsure or is making a heavy decision, offer a couple of options.\n` + `- If something is ambiguous, make a reasonable assumption and SAY what you assumed.\n` + `- Keep the tone friendly, practical, and direct (no fluff).\n` + `- When the user asks for code, you may still give full working code, but always with explanation + suggestions.\n\n` + `LANGUAGE & REFERENCE RULES:\n` + `- When the user asks for HTML or CSS code:\n` + ` * Use clean, semantic HTML5.\n` + ` * Do NOT use deprecated tags like
or
unless explicitly asked.\n` + ` * Prefer tags and properties from the provided HTML_REFERENCE and CSS_REFERENCE.\n` + ` * You may combine them as needed to produce working code.\n` + `- When the user asks for PHP, JavaScript, or Python code:\n` + ` * Prefer syntax, functions, and patterns from PHP_REFERENCE, JAVASCRIPT_REFERENCE, and PYTHON_REFERENCE.\n` + ` * Stay within those language features unless the user explicitly asks for external libraries or frameworks.\n` + ` * If you use something not in the reference, briefly justify why and explain what it does.\n\n` + `USER MEMORY RULES:\n` + `- The frontend may provide USER_PROFILE, USER_FACTS, and USER_PAST_MESSAGES below.\n` + `- Treat USER_PROFILE as stable information about the user.\n` + `- Treat USER_FACTS as things the user has explicitly or implicitly told you to remember.\n` + `- USER_PAST_MESSAGES is a log of the user's recent conversations with you.\n` + `- When the user asks about something they've said before (e.g. "what is my name"), prefer these sources over guessing.\n\n` + `OUTPUT FORMAT GUIDELINES:\n` + `- Use Markdown.\n` + `- For code, ALWAYS use fenced code blocks like:\n` + ` \`\`\`php\n // code here\n \`\`\`\n` + `- When returning multiple files, label each clearly.\n` + `- Keep explanations short but clear unless the user asks for deep detail.\n\n`; // Attach structured references (for the model) if (htmlReference) { content += "HTML_REFERENCE:\n" + JSON.stringify(htmlReference) + "\n\n"; } else { content += "HTML_REFERENCE: [unavailable]\n\n"; } if (cssReference) { content += "CSS_REFERENCE:\n" + JSON.stringify(cssReference) + "\n\n"; } else { content += "CSS_REFERENCE: [unavailable]\n\n"; } if (phpReference) { content += "PHP_REFERENCE:\n" + JSON.stringify(phpReference) + "\n\n"; } else { content += "PHP_REFERENCE: [unavailable]\n\n"; } if (jsReference) { content += "JAVASCRIPT_REFERENCE:\n" + JSON.stringify(jsReference) + "\n\n"; } else { content += "JAVASCRIPT_REFERENCE: [unavailable]\n\n"; } if (pythonReference) { content += "PYTHON_REFERENCE:\n" + JSON.stringify(pythonReference) + "\n\n"; } else { content += "PYTHON_REFERENCE: [unavailable]\n\n"; } // Attach raw memory blobs if (memoryProfile && Object.keys(memoryProfile).length > 0) { content += "USER_PROFILE:\n" + JSON.stringify(memoryProfile) + "\n\n"; } else { content += "USER_PROFILE: {}\n\n"; } if (Array.isArray(memoryFacts) && memoryFacts.length > 0) { content += "USER_FACTS:\n" + JSON.stringify(memoryFacts) + "\n\n"; } else { content += "USER_FACTS: []\n\n"; } if (Array.isArray(memoryMessages) && memoryMessages.length > 0) { content += "USER_PAST_MESSAGES:\n" + JSON.stringify(memoryMessages) + "\n\n"; } else { content += "USER_PAST_MESSAGES: []\n\n"; } // VERY IMPORTANT: human-readable facts so Lewis actually uses them content += "\nKNOWN USER FACTS (EXTREMELY IMPORTANT):\n"; if (Array.isArray(memoryFacts) && memoryFacts.length > 0) { for (const f of memoryFacts) { const factText = typeof f === "string" ? f : (f.fact || ""); if (factText) { content += `- ${factText}\n`; } } } else { content += "- (none)\n"; } content += "\nWhen the user asks about any of these topics, ALWAYS answer using these facts exactly as written.\n"; return { role: "system", content }; } // ======================== // MAIN SEND LOGIC // ======================== async function sendMessage() { const text = inputEl.value.trim(); if (!text) return; // Ensure chat if (!activeChatId) { createNewChat(); } const chat = getActiveChat(); if (!chat) { console.error("No active chat found"); return; } // Add user message to state & UI const userMsg = { role: "user", content: text }; chat.messages.push(userMsg); visibleMessages = chat.messages; addMessage("user", text); // Save user message into server-side memory memorySaveMessage(USER_ID, "user", text); // Set chat title from first user message if (chat.title === "New chat") { const maxLen = 40; const clean = text.replace(/\s+/g, " ").trim(); chat.title = clean.length > maxLen ? clean.slice(0, maxLen) + "…" : clean || "New chat"; } inputEl.value = ""; setLoading(true); saveChatsToStorage(); renderHistory(); const lowerText = text.toLowerCase().trim(); const isCodeLike = looksLikeCode(text); // --- 0) Try answering directly from known facts (e.g. SMTP) --- if (!isCodeLike) { const factAnswer = tryAnswerFromFacts(lowerText); if (factAnswer) { const assistantMsg = { role: "assistant", content: factAnswer }; chat.messages.push(assistantMsg); visibleMessages = chat.messages; addMessage("assistant", factAnswer); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", factAnswer); setLoading(false); return; } } // --- 1) Explicit city/place time (only if NOT code) --- if (!isCodeLike) { let locRaw = extractTimeLocation(lowerText); if (locRaw) { const apiResult = await fetchTimeForPlace(locRaw); if (apiResult && apiResult.success && apiResult.data) { const d = apiResult.data; // Adjust to your time.php field names const fullDateTime = d.fullDateTime || d.full_datetime || d.full || d.dateTime || ""; const resolvedName = d.resolvedName || d.resolved_name || d.name || locRaw; const timeZone = d.timeZone || d.timezone || "Unknown"; const answer = `${fullDateTime} in ${resolvedName} (Time zone: ${timeZone})`; const assistantMsg = { role: "assistant", content: answer }; chat.messages.push(assistantMsg); visibleMessages = chat.messages; addMessage("assistant", answer); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", answer); setLoading(false); return; } else { const errMsg = apiResult && apiResult.error ? `I tried looking up "${locRaw}" but the time service replied: ${apiResult.error}` : `I couldn't determine the time for "${locRaw}". Try a different spelling or add the country or state.`; const assistantMsg = { role: "assistant", content: errMsg }; chat.messages.push(assistantMsg); visibleMessages = chat.messages; addMessage("assistant", errMsg); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", errMsg); setLoading(false); return; } } } // --- 2) Local time question (only if NOT code) --- if (!isCodeLike && isTimeQuestion(lowerText)) { const timeStr = getLocalTime12h(); const assistantMsg = { role: "assistant", content: timeStr }; chat.messages.push(assistantMsg); visibleMessages = chat.messages; addMessage("assistant", timeStr); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", timeStr); setLoading(false); return; } // --- 3) Follow-up time location (NOT code) --- if (!isCodeLike) { const locRaw2 = inferFollowupTimeLocation(lowerText, chat); if (locRaw2) { const apiResult2 = await fetchTimeForPlace(locRaw2); if (apiResult2 && apiResult2.success && apiResult2.data) { const d = apiResult2.data; const fullDateTime = d.fullDateTime || d.full_datetime || d.full || d.dateTime || ""; const resolvedName = d.resolvedName || d.resolved_name || d.name || locRaw2; const timeZone = d.timeZone || d.timezone || "Unknown"; const answer2 = `${fullDateTime} in ${resolvedName} (Time zone: ${timeZone})`; const assistantMsg2 = { role: "assistant", content: answer2 }; chat.messages.push(assistantMsg2); visibleMessages = chat.messages; addMessage("assistant", answer2); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", answer2); setLoading(false); return; } else { const errMsg2 = apiResult2 && apiResult2.error ? `I tried looking up "${locRaw2}" but the time service replied: ${apiResult2.error}` : `I couldn't determine the time for "${locRaw2}". Try a different spelling or add the country or state.`; const assistantMsg2 = { role: "assistant", content: errMsg2 }; chat.messages.push(assistantMsg2); visibleMessages = chat.messages; addMessage("assistant", errMsg2); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", errMsg2); setLoading(false); return; } } } // --- 4) Local date (NOT code) --- if (!isCodeLike && isDateQuestion(lowerText)) { const dateStr = getLocalDate(); const assistantMsg = { role: "assistant", content: dateStr }; chat.messages.push(assistantMsg); visibleMessages = chat.messages; addMessage("assistant", dateStr); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", dateStr); setLoading(false); return; } // --- 5) Otherwise: normal Ollama call --- if (!referencesLoaded) { try { await loadReferences(); } catch (e) { console.error("Error loading references before chat:", e); } } const systemMessage = buildSystemMessage(); const messages = [systemMessage, ...chat.messages]; const payload = { model: MODEL_ID, messages, stream: false, temperature: 0.4 }; try { const response = await fetch(`${API_BASE}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) { const err = await response.text(); console.error("Ollama error:", err); setError("Error contacting Lewis via Ollama"); setLoading(false); return; } const data = await response.json(); const content = data?.message?.content || "[No response]"; const assistantMsg = { role: "assistant", content }; chat.messages.push(assistantMsg); visibleMessages = chat.messages; addMessage("assistant", content); saveChatsToStorage(); memorySaveMessage(USER_ID, "assistant", content); } catch (err) { console.error(err); setError("Network error talking to Lewis (Ollama)"); } finally { setLoading(false); } } // ======================== // EVENT HANDLERS // ======================== sendBtn.addEventListener("click", sendMessage); inputEl.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); newChatBtn.addEventListener("click", () => { createNewChat(); }); // ======================== // INIT // ======================== (function init() { // Fire off reference loading loadReferences(); // Fire off memory loading memoryLoad(USER_ID).then(mem => { if (mem) { memoryProfile = mem.profile || {}; memoryFacts = Array.isArray(mem.facts) ? mem.facts : []; memoryMessages = Array.isArray(mem.messages) ? mem.messages : []; memoryLoaded = true; console.log("Loaded memory for user", USER_ID, mem); } }).catch(err => { console.error("Error loading memory:", err); }); chats = loadChatsFromStorage(); if (chats.length) { activeChatId = chats[0].id; visibleMessages = chats[0].messages; } else { createNewChat(); } renderHistory(); renderChatMessages(); const active = getActiveChat(); if (active && active.messages.length === 0) { const greeting = "Hi everyone 👋 I'm Lewis. What would you like to do?"; const msg = { role: "assistant", content: greeting }; active.messages.push(msg); visibleMessages = active.messages; saveChatsToStorage(); renderChatMessages(); } })();