From 2160f20f9369be4257483e63b958085f0d9432a5 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 2 Jul 2025 11:36:14 +0000 Subject: [PATCH] AI Support bot created. --- backend/data/faqs.json | 14 +++ backend/server2.js | 22 ++++ backend/user_profile.db | 0 backend/utils/authenticateUser.js | 19 ++++ backend/utils/chatFreeEndpoint.js | 171 ++++++++++++++++++++++++++++++ backend/utils/seedFaq.js | 36 +++++++ backend/utils/vectorSearch.js | 38 +++++++ package-lock.json | 16 +++ package.json | 1 + src/App.js | 2 + src/components/CareerExplorer.js | 38 +++++++ src/components/ChatDrawer.js | 162 ++++++++++++++++++++++++++++ src/components/ui/sheet.js | 92 ++++++++++++++++ src/utils/usePageContext.js | 15 +++ tailwind.config.js | 8 ++ user_profile.db | Bin 159744 -> 188416 bytes 16 files changed, 634 insertions(+) create mode 100644 backend/data/faqs.json create mode 100644 backend/user_profile.db create mode 100644 backend/utils/authenticateUser.js create mode 100644 backend/utils/chatFreeEndpoint.js create mode 100644 backend/utils/seedFaq.js create mode 100644 backend/utils/vectorSearch.js create mode 100644 src/components/ChatDrawer.js create mode 100644 src/components/ui/sheet.js create mode 100644 src/utils/usePageContext.js diff --git a/backend/data/faqs.json b/backend/data/faqs.json new file mode 100644 index 0000000..2bec65f --- /dev/null +++ b/backend/data/faqs.json @@ -0,0 +1,14 @@ +[ + { + "q": "How do I reset my password?", + "a": "Click **Forgot Password** on the sign-in screen. Enter your e-mail and follow the link we send." + }, + { + "q": "Why can’t I see any careers in the list?", + "a": "Make sure you’ve finished the Interest Inventory and set at least one career-priority filter." + }, + { + "q": "What browsers are supported?", + "a": "Chrome, Edge, Safari, and Firefox (latest two versions). Mobile Safari is fully supported." + } +] diff --git a/backend/server2.js b/backend/server2.js index c02d699..0e85e34 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -14,6 +14,12 @@ import { open } from 'sqlite'; import sqlite3 from 'sqlite3'; import fs from 'fs'; import readline from 'readline'; +import chatFreeEndpoint from "./utils/chatFreeEndpoint.js"; +import { OpenAI } from 'openai'; +import rateLimit from 'express-rate-limit'; +import authenticateUser from './utils/authenticateUser.js'; +import { vectorSearch } from "./utils/vectorSearch.js"; + // --- Basic file init --- const __filename = fileURLToPath(import.meta.url); @@ -24,6 +30,14 @@ const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); dotenv.config({ path: envPath }); // Load .env +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +const chatLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 20, + keyGenerator: req => req.user?.id || req.ip +}); + // Whitelist CORS const allowedOrigins = [ 'http://localhost:3000', @@ -883,6 +897,7 @@ app.post('/api/ai-risk', async (req, res) => { jobDescription, tasks, riskLevel, + vectorSearch, reasoning, }); @@ -893,6 +908,13 @@ app.post('/api/ai-risk', async (req, res) => { } }); +chatFreeEndpoint(app, { + openai, + authenticateUser, // or omit if you don’t need auth yet + chatLimiter, + userProfileDb +}); + /************************************************** * Start the Express server **************************************************/ diff --git a/backend/user_profile.db b/backend/user_profile.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/authenticateUser.js b/backend/utils/authenticateUser.js new file mode 100644 index 0000000..ab4e370 --- /dev/null +++ b/backend/utils/authenticateUser.js @@ -0,0 +1,19 @@ +import jwt from "jsonwebtoken"; +const SECRET_KEY = process.env.SECRET_KEY || "supersecurekey"; + +/** + * Adds `req.user = { id: }` + * If no or bad token ➜ 401. + */ +export default function authenticateUser(req, res, next) { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) return res.status(401).json({ error: "Authorization token required" }); + + try { + const { id } = jwt.verify(token, SECRET_KEY); + req.user = { id }; // attach the id for downstream use + next(); + } catch (err) { + return res.status(401).json({ error: "Invalid or expired token" }); + } +} diff --git a/backend/utils/chatFreeEndpoint.js b/backend/utils/chatFreeEndpoint.js new file mode 100644 index 0000000..30205fe --- /dev/null +++ b/backend/utils/chatFreeEndpoint.js @@ -0,0 +1,171 @@ +// utils/chatFreeEndpoint.js +import path from "path"; +import { fileURLToPath } from "url"; +import { vectorSearch } from "./vectorSearch.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootPath = path.resolve(__dirname, "..", ".."); // backend/ +const FAQ_PATH = path.join(rootPath, "user_profile.db"); +const FAQ_THRESHOLD = 0.80; +const HELP_TRIGGERS = ["help","error","bug","issue","login","password","support"]; + +/* ---------- helpers ---------- */ +const classifyIntent = txt => + HELP_TRIGGERS.some(k => txt.toLowerCase().includes(k)) ? "support" : "guide"; + +const buildContext = (user = {}, page = "", intent = "guide") => { + const { firstname = "User", career_situation = "planning" } = user; + const mode = intent === "guide" ? "Aptiva Guide" : "Aptiva Support"; + return { + system: + `${mode} for page “${page}”. User: ${firstname}. Situation: ${career_situation}.` + + (intent === "support" + ? " Resolve issues quickly and end with: “Let me know if that fixed it.”" + : "") + }; +}; + +/* ---------------------------------------------------------------------------- + FACTORY: registers POST /api/chat/free on the passed-in Express app +----------------------------------------------------------------------------- */ +export default function chatFreeEndpoint( + app, + { + openai, + chatLimiter, + userProfileDb, + authenticateUser = (_req, _res, next) => next() + } +) { + /* -------- support-intent tool handlers now in scope -------- */ + const toolResolvers = { + async clearLocalCache() { return { status: "ok" }; }, + + async openTicket({ summary = "", user = {} }) { + try { + await userProfileDb.run( + `INSERT INTO support_tickets (user_id, summary, created_at) + VALUES (?,?,datetime('now'))`, + [user.id || 0, summary] + ); + return { ticket: "created" }; + } catch (err) { + console.error("[openTicket] DB error:", err); + return { ticket: "error" }; + } + }, + + async pingStatus() { + try { + await userProfileDb.get("SELECT 1"); + return { status: "healthy" }; + } catch { + return { status: "db_error" }; + } + } + }; + + const SUPPORT_TOOLS = [ + { + type: "function", + function: { + name: "clearLocalCache", + description: "Instruct front-end to purge its cache", + parameters: { type: "object", properties: {} } + } + }, + { + type: "function", + function: { + name: "openTicket", + description: "Create a support ticket", + parameters: { + type: "object", + properties: { summary: { type: "string" } }, + required: ["summary"] + } + } + }, + { + type: "function", + function: { + name: "pingStatus", + description: "Ping DB/health", + parameters: { type: "object", properties: {} } + } + } +]; + + /* ----------------------------- ROUTE ----------------------------- */ + app.post( + "/api/chat/free", + chatLimiter, + authenticateUser, + async (req, res) => { + try { + const { prompt = "", chatHistory = [], pageContext = "" } = req.body || {}; + if (!prompt.trim()) return res.status(400).json({ error: "Empty prompt" }); + + /* ---------- 0️⃣ FAQ fast-path ---------- */ + let faqHit = null; + try { + const { data } = await openai.embeddings.create({ + model: "text-embedding-3-small", + input: prompt + }); + const hits = await vectorSearch(FAQ_PATH, data[0].embedding, 1); + if (hits.length && hits[0].score >= FAQ_THRESHOLD) faqHit = hits[0]; + } catch { /* silently ignore if table/function missing */ } + + if (faqHit) return res.json({ answer: faqHit.answer }); + /* --------------------------------------- */ + + const intent = classifyIntent(prompt); + const { system } = buildContext(req.user || {}, pageContext, intent); + const tools = intent === "support" ? SUPPORT_TOOLS : []; + + const messages = [ + { role:"system", content: system }, + ...chatHistory, + { role:"user", content: prompt } + ]; + + const chatStream = await openai.chat.completions.create({ + model : "gpt-4o-mini", + stream : true, + messages, + tools, + tool_choice : tools.length ? "auto" : undefined + }); + + /* ---------- SSE headers ---------- */ + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + + for await (const part of chatStream) { + const delta = part.choices?.[0]?.delta || {}; + const chunk = delta.content || ""; + if (chunk) res.write(chunk); // ← plain text, no “data:” prefix + } + res.end(); + + /* ---------- tool calls ---------- */ + chatStream.on("tool", async call => { + const fn = toolResolvers[call.name]; + if (!fn) return; + try { + const args = JSON.parse(call.arguments || "{}"); + await fn({ ...args, user: req.user || {} }); + } catch (err) { + console.error("[tool resolver]", err); + } + }); + } catch (err) { + console.error("/api/chat/free error:", err); + res.status(500).json({ error: "Internal server error" }); + } + } + ); +} diff --git a/backend/utils/seedFaq.js b/backend/utils/seedFaq.js new file mode 100644 index 0000000..d560617 --- /dev/null +++ b/backend/utils/seedFaq.js @@ -0,0 +1,36 @@ +import { readFileSync } from "fs"; +import { OpenAI } from "openai"; +import sqlite3 from "sqlite3"; +import { open } from "sqlite"; +import dotenv from "dotenv"; +import { fileURLToPath } from 'url'; +import path from 'path'; + +// --- Basic file init --- +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootPath = path.resolve(__dirname, "../.."); +const env = process.env.NODE_ENV?.trim() || "development"; +dotenv.config({ path: path.resolve(rootPath, `.env.${env}`) }); + +const faqPath = path.resolve(rootPath, "backend", "data", "faqs.json"); +const faqs = JSON.parse(readFileSync(faqPath, "utf8")); + +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +const dbPath = path.resolve(rootPath, "user_profile.db"); +const db = await open({ filename: dbPath, driver: sqlite3.Database }); + +for (const { q, a } of faqs) { + const { data } = await openai.embeddings.create({ + model: "text-embedding-3-small", + input: q + }); + + const buf = Buffer.from(new Float32Array(data[0].embedding).buffer); + await db.run( + `INSERT INTO faq_embeddings (question,answer,embedding) VALUES (?,?,?)`, + [q, a, buf] + ); +} +console.log("Seeded FAQ embeddings"); diff --git a/backend/utils/vectorSearch.js b/backend/utils/vectorSearch.js new file mode 100644 index 0000000..f86983c --- /dev/null +++ b/backend/utils/vectorSearch.js @@ -0,0 +1,38 @@ +// utils/vectorSearch.js +import sqlite3 from "sqlite3"; +import { open } from "sqlite"; + +/* ---------- small helper ---------- */ +function cosineSim(a, b) { + let dot = 0, na = 0, nb = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; + na += a[i] * a[i]; + nb += b[i] * b[i]; + } + return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0; +} + +/** + * JS-only vector search against faq_embeddings + * @param {string} dbPath absolute path to user_profile.db + * @param {number[]} queryEmbedding the embedding array from OpenAI + * @param {number} topK how many rows to return + */ +export async function vectorSearch(dbPath, queryEmbedding, topK = 3) { + const db = await open({ filename: dbPath, driver: sqlite3.Database }); + const rows = await db.all(`SELECT id, question, answer, embedding FROM faq_embeddings`); + await db.close(); + + const scored = rows.map(r => { + // SQLite returns Buffer → turn it back into Float32Array + const vec = new Float32Array(r.embedding.buffer, + r.embedding.byteOffset, + r.embedding.length / 4); + return { ...r, score: cosineSim(queryEmbedding, vec) }; + }) + .sort((a, b) => b.score - a.score) // highest similarity first + .slice(0, topK); + + return scored; +} diff --git a/package-lock.json b/package-lock.json index e6b51e9..65a977b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "cra-template": "1.2.0", "docx": "^9.5.0", "dotenv": "^16.4.7", + "express-rate-limit": "^7.5.1", "file-saver": "^2.0.5", "fuse.js": "^7.1.0", "helmet": "^8.0.0", @@ -9112,6 +9113,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", diff --git a/package.json b/package.json index b2b6e2e..a85fa65 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "cra-template": "1.2.0", "docx": "^9.5.0", "dotenv": "^16.4.7", + "express-rate-limit": "^7.5.1", "file-saver": "^2.0.5", "fuse.js": "^7.1.0", "helmet": "^8.0.0", diff --git a/src/App.js b/src/App.js index a1c5e54..3e4f9ce 100644 --- a/src/App.js +++ b/src/App.js @@ -31,6 +31,7 @@ import OnboardingContainer from './components/PremiumOnboarding/OnboardingContai import RetirementPlanner from './components/RetirementPlanner.js'; import ResumeRewrite from './components/ResumeRewrite.js'; import LoanRepaymentPage from './components/LoanRepaymentPage.js'; +import usePageContext from './utils/usePageContext.js'; @@ -39,6 +40,7 @@ export const ProfileCtx = React.createContext(); function App() { const navigate = useNavigate(); const location = useLocation(); + usePageContext(); // Auth states const [isAuthenticated, setIsAuthenticated] = useState(false); diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index daabbdf..cd9349c 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -6,6 +6,7 @@ import CareerPrioritiesModal from './CareerPrioritiesModal.js'; import CareerModal from './CareerModal.js'; import InterestMeaningModal from './InterestMeaningModal.js'; import CareerSearch from './CareerSearch.js'; +import ChatDrawer from './ChatDrawer.js'; import { Button } from './ui/button.js'; import axios from 'axios'; @@ -784,6 +785,38 @@ const handleSelectForEducation = (career) => { ); }; + /* ---------- Chat-assistant snapshot ---------- */ +const explorerSnapshot = useMemo(() => { + // 1) RIASEC scores (if the interest inventory has been done) + const riasecScores = userProfile?.riasec_scores + ? JSON.parse(userProfile.riasec_scores) // stored as JSON in DB + : null; + + // 2) User-selected career-priority weights, normalised to 0-1 + const priorityWeights = priorities + ? { + stability : priorityWeight('stability' , priorities.stability) / 5, + growth : priorityWeight('growth' , priorities.growth) / 5, + balance : priorityWeight('balance' , priorities.balance) / 5, + recognition : priorityWeight('recognition', priorities.recognition)/ 5, + interests : priorityWeight('interests' , priorities.interests) / 5, + mission : priorityWeight('meaning' , priorities.meaning) / 5, + } + : null; + + // 3) Matrix of careers currently on-screen (or in the comparison list, pick one) + const careerMatrix = filteredCareers.map(c => ({ + socCode : c.code, + careerName : c.title, + // quick-and-dirty match score; replace with your own if you’ve already got one + matchScore : typeof c.matchScore === 'number' + ? c.matchScore + : (fitRatingMap[c.fit] || 0) * 20 // e.g. “Best” → 100, “Good” → 60 + })); + + return { riasecScores, priorityWeights, careerMatrix }; +}, [userProfile, priorities, filteredCareers]); + // ------------------------------------------------------ // Render // ------------------------------------------------------ @@ -984,6 +1017,11 @@ const handleSelectForEducation = (career) => { defaultMeaning={modalData.defaultMeaning} /> + + {selectedCareer && ( { + if (listRef.current) { + listRef.current.scrollTop = listRef.current.scrollHeight; + } + }, [messages]); + + // ─────────────────────────────────────────────── helper: append chunk + function appendAssistantChunk(chunk) { + setMessages((prev) => { + const last = prev[prev.length - 1]; + if (last && last.role === "assistant") { + // mutate copy – React will re‑render because we return new array + const updated = [...prev]; + updated[updated.length - 1] = { + ...last, + content: last.content + chunk, + }; + return updated; + } + // first assistant chunk → push new msg + return [...prev, { role: "assistant", content: chunk }]; + }); + } + + // ───────────────────────────────────────────────────────── send prompt + async function sendPrompt() { + const text = prompt.trim(); + if (!text) return; + + // optimistic user message + setMessages((m) => [...m, { role: "user", content: text }]); + setPrompt(""); + + const token = localStorage.getItem("token") || ""; + try { + const resp = await fetch("/api/chat/free", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "text/event-stream", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + prompt: text, + pageContext, + chatHistory: messages, + snapshot, + }), + }); + + if (!resp.ok || !resp.body) { + throw new Error(`Request failed ${resp.status}`); + } + + // stream the response + const reader = resp.body.getReader(); + const decoder = new TextDecoder(); + let done = false; + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + if (value) { + const chunk = decoder.decode(value); + appendAssistantChunk(chunk); + } + } + } catch (err) { + console.error("[ChatDrawer] error", err); + appendAssistantChunk("Sorry – something went wrong. Try again later."); + } + } + + // ────────────────────────────────────────────────────── key handler + function handleKeyDown(e) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendPrompt(); + } + } + + // ─────────────────────────────────────────────────────────── render + return ( + + {/* Floating action button */} + + + + + {/* Drawer */} + + {/* header */} +
+ {pageContext === "explorer" ? "Career Explorer Guide" : "Programs Guide"} +
+ + {/* messages list */} +
+ {messages.map((m, i) => ( +
+ {m.content} +
+ ))} +
+ + {/* input */} +
+
{ + e.preventDefault(); + sendPrompt(); + }} + className="flex gap-2" + > + setPrompt(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Ask me anything…" + className="flex-1" + /> + +
+
+
+
+ ); +} diff --git a/src/components/ui/sheet.js b/src/components/ui/sheet.js new file mode 100644 index 0000000..fea5e23 --- /dev/null +++ b/src/components/ui/sheet.js @@ -0,0 +1,92 @@ +import { createContext, useContext, useState } from "react"; +import { createPortal } from "react-dom"; + +/** + * Minimal implementation (slide‑in drawer) using plain React + Tailwind. + * + * Usage: + * + * + * + * + */ + +const SheetContext = createContext({ open: false, setOpen: () => {} }); + +export function Sheet({ open: controlled, onOpenChange, children }) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + const isControlled = controlled !== undefined; + const open = isControlled ? controlled : uncontrolledOpen; + const setOpen = isControlled ? onOpenChange : setUncontrolledOpen; + + return ( + + {children} + + ); +} + +export function SheetTrigger({ asChild, children, ...rest }) { + const { setOpen } = useContext(SheetContext); + const trigger = ( + + ); + + return asChild ? children : trigger; +} + +const sheetRoot = (() => { + let root = document.getElementById("__sheet_root"); + if (!root) { + root = document.createElement("div"); + root.id = "__sheet_root"; + document.body.appendChild(root); + } + return root; +})(); + +export function SheetContent({ side = "right", className = "", children }) { + const { open, setOpen } = useContext(SheetContext); + if (!open) return null; + + const translateMap = { + right: "translate-x-full", + left: "-translate-x-full", + top: "-translate-y-full", + bottom: "translate-y-full", + }; + + const positionMap = { + right: "right-0 top-0 h-full w-80 md:w-96", + left: "left-0 top-0 h-full w-80 md:w-96", + top: "top-0 left-0 w-full h-1/2", + bottom: "bottom-0 left-0 w-full h-1/2", + }; + + return createPortal( +
+ {/* backdrop */} +
setOpen(false)} + /> + + {/* panel */} +
+ {children} +
+
, + sheetRoot + ); +} diff --git a/src/utils/usePageContext.js b/src/utils/usePageContext.js new file mode 100644 index 0000000..b4a123b --- /dev/null +++ b/src/utils/usePageContext.js @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { sendSystemMessage } from '../utils/chatApi.js'; // you already call this for Jess + +export default function usePageContext () { + const { pathname } = useLocation(); + + useEffect(() => { + const page = pathname.split('/')[1] || 'Home'; + const featureFlags = window.__APTIVA_FLAGS__ || []; + + // invisible system message for the adaptive assistant + sendSystemMessage('page_context', { page, featureFlags }); + }, [pathname]); +} diff --git a/tailwind.config.js b/tailwind.config.js index 267991d..776552c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,14 @@ // tailwind.config.js module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], + theme: { + extend: { + keyframes: { + "slide-in": { from: { transform: "translateX(100%)" }, to: { transform: "translateX(0)" } }, + }, + animation: { "slide-in": "slide-in 0.25s ease-out forwards" }, + }, +}, theme: { extend: { /* brand colours */ diff --git a/user_profile.db b/user_profile.db index 1421d2680b86322c001bd8798bba425ebbdb8eb2..2fa80e7857dcb3bd8fefd960a4f9bab082155958 100644 GIT binary patch delta 21579 zcmeIaXH*qI(=|#)1OZ8A1w;h{ikZ{ZDk$bGMnpwLLsQV5z4xwnt^51_dRz-#o-i}rRkdsHT{FZ!wun7qxvW~nvjzqRp7irS zf5MkHY-V^iYs9w3R>n5x3~WBwJhC}wgEn-mi?GA#Iif9-$& z&%J$LKOcWTXMZ2x?taey`#op(^3Kkoqn-cf&+4{Y4C{`XI4L+fCNwfaz3T5b$iE?d zC@3O&N^sPFKKDPb{XZ8A&Rv}87{M{l z;nSQa1Vu+riHsWEwtZOWsPWGA>vxQd3W_apS+ zWwI8+=d?uE-gS8Bk927*9p-jpg*w zPr$uaPrO-EbC3q>kKhIm3)ywhT<~wy0~$|wh1XWaOUJhs@J;yhZ+X$;M70i5_rgrJ z?%$km-}en$nB9exmP7cEa%reHjmOx6d)PQ@xJl*VlUb-|@Rb>+Az2Uzb0 zNt{)^kGHQ(m22;M^8Oz0gx4u!e4y=5$Hpv7UDR(sj>y# z>)FbLD~G`9Or5fm?Y&b&hHNiFIsh0oN)c)EOVGC5Om?7tD-m1z8Cxtki!0|fXP>ry z5+{ZNUs8XKv>To(IxVisb*?pKXVWuC7pEI~!$_X}?8&rS1JAN?Ejh|WdpOtN1s0`T z#*JHB@ea@qvbNNfjtz~V&iz%eJ>nl;Y2TSuiS(xMoWhk09l_=5%Cb<=PRYp|z470Z zDl)%$KI%^_V#VVGdYM#{b`LT{e)HDyz?(z(;637xUgda$ea-k07hJ0J`HH5%x`#78evn0Lp3n>_x8|3)psf7{}8 z&rX+3ip?*)y_g>#f2i_ zLZJ9@rX@c+)qqiM$(5R=8EoG-h4o1XPA>@CMPbs_vO6AK?EtCQlfWjaM_J6e4l=>o z=5O)0LlbOuYa4n_^}ye24CSOT@v@Ueede?OA_O_yf#QgDJfo8puRiGx-rO`-olxZv zjvCdB=lwF|uH8?t%jJ_H!FqrU5PwkflySH)a6cR!P#p_be ^T`O7f6SeAaI%vU& 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 ON|r<3fqIm*(f