From 189362e78dda54e83e2b53dcbef76e27bd152954 Mon Sep 17 00:00:00 2001 From: khondokartowsif171 Date: Sun, 24 May 2026 21:09:53 +0600 Subject: [PATCH] feat: add chatbox widget and dynamic nav links from dashboard API - Add ChatboxWidget component (src/chatbox.jsx) with business hours check - Update nav.jsx to fetch links dynamically from /api/public/links - Wire ChatboxWidget into app.jsx - Add chatbox.jsx script tag to index.html Co-Authored-By: Claude Sonnet 4.6 --- index.html | 1 + src/app.jsx | 1 + src/chatbox.jsx | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ src/nav.jsx | 41 +++++++-- 4 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 src/chatbox.jsx diff --git a/index.html b/index.html index d9420ec..6f2433a 100644 --- a/index.html +++ b/index.html @@ -204,6 +204,7 @@ + diff --git a/src/app.jsx b/src/app.jsx index 21925c9..f549da0 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -82,6 +82,7 @@ function App() { setPaletteOpen(false)} /> + diff --git a/src/chatbox.jsx b/src/chatbox.jsx new file mode 100644 index 0000000..029bcfe --- /dev/null +++ b/src/chatbox.jsx @@ -0,0 +1,220 @@ +// Aura Chatbox Widget +// Fetches config from dashboard API, routes messages through backend. + +const DASHBOARD = 'https://aura.auraajenticai.cloud' + +const ChatboxWidget = () => { + const [config, setConfig] = React.useState(null) + const [open, setOpen] = React.useState(false) + const [messages, setMessages] = React.useState([]) + const [input, setInput] = React.useState('') + const [loading, setLoading] = React.useState(false) + const [isAfterHours, setIsAfterHours] = React.useState(false) + const messagesEndRef = React.useRef(null) + + React.useEffect(() => { + fetch(`${DASHBOARD}/api/public/chatbox`) + .then(r => r.ok ? r.json() : null) + .then(cfg => { + if (!cfg) return + setConfig(cfg) + if (cfg.isEnabled) { + const afterHours = checkAfterHours(cfg.businessHours) + setIsAfterHours(afterHours) + setMessages([{ role: 'bot', text: cfg.welcomeMessage }]) + } + }) + .catch(() => {}) + }, []) + + React.useEffect(() => { + if (open && messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [messages, open]) + + function checkAfterHours(businessHours) { + if (!businessHours) return false + const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + const now = new Date() + const day = days[now.getDay()] + const hours = businessHours[day] + if (!hours || hours === 'closed') return true + const [open, close] = hours.split('-').map(t => { + const [h, m] = t.split(':').map(Number) + return h * 60 + (m || 0) + }) + // UTC+6 Bangladesh time + const bdMin = ((now.getUTCHours() + 6) % 24) * 60 + now.getUTCMinutes() + return bdMin < open || bdMin >= close + } + + async function sendMessage() { + if (!input.trim() || loading || isAfterHours) return + const text = input.trim() + setInput('') + setMessages(prev => [...prev, { role: 'user', text }]) + setLoading(true) + try { + const res = await fetch(`${DASHBOARD}/api/public/chatbox/chat`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ message: text }), + }) + const data = await res.json() + setMessages(prev => [...prev, { role: 'bot', text: data.reply || config?.fallbackMessage }]) + } catch { + setMessages(prev => [...prev, { role: 'bot', text: config?.fallbackMessage || "I'll get back to you soon." }]) + } finally { + setLoading(false) + } + } + + function onKey(e) { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() } + } + + if (!config?.isEnabled) return null + + return ( +
+ {/* Chat window */} + {open && ( +
+ {/* Header */} +
+
+
+
Aura AI
+
+ + {isAfterHours ? 'After hours' : 'Online'} +
+
+ +
+ + {/* Messages */} +
+ {messages.map((m, i) => ( +
+
{m.text}
+
+ ))} + {loading && ( +
+
•••
+
+ )} + {isAfterHours && ( +
+ {config.afterHoursMessage} +
+ )} +
+
+ + {/* Input */} +
+ setInput(e.target.value)} + onKeyDown={onKey} + disabled={isAfterHours || loading} + placeholder={isAfterHours ? 'Chat unavailable right now' : 'Ask me anything…'} + style={{ + flex: 1, padding: '8px 12px', + background: 'rgba(255,255,255,0.05)', + border: '1px solid rgba(255,255,255,0.1)', + borderRadius: 8, color: '#f4f5f7', + fontSize: 13, + outline: 'none', + }} + /> + +
+
+ )} + + {/* Toggle button */} + +
+ ) +} + +window.ChatboxWidget = ChatboxWidget diff --git a/src/nav.jsx b/src/nav.jsx index 57a9322..8b420bc 100644 --- a/src/nav.jsx +++ b/src/nav.jsx @@ -1,6 +1,16 @@ // Top nav — minimal, sticky, blurred +const DASHBOARD = 'https://aura.auraajenticai.cloud' +const NAV_FALLBACK = [ + { id: "work", label: "Services", url: "#work" }, + { id: "stack", label: "Stack", url: "#stack" }, + { id: "agents", label: "Agents", url: "#agents" }, + { id: "timeline", label: "Timeline", url: "#timeline" }, + { id: "contact", label: "Contact", url: "#contact" }, +] + const Nav = ({ onCmdK, theme, onToggleTheme, accent, lang, onToggleLang }) => { const [scrolled, setScrolled] = React.useState(false); + const [links, setLinks] = React.useState(NAV_FALLBACK); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); @@ -8,13 +18,28 @@ const Nav = ({ onCmdK, theme, onToggleTheme, accent, lang, onToggleLang }) => { return () => window.removeEventListener("scroll", onScroll); }, []); - const links = [ - { id: "work", label: "Services" }, - { id: "stack", label: "Stack" }, - { id: "agents", label: "Agents" }, - { id: "timeline", label: "Timeline" }, - { id: "contact", label: "Contact" }, - ]; + // Fetch nav links from Control Center API (stale-while-revalidate) + React.useEffect(() => { + const CACHE_KEY = 'aura_nav_links' + const CACHE_TTL = 5 * 60 * 1000 // 5 min + const cached = sessionStorage.getItem(CACHE_KEY) + if (cached) { + try { + const { data, ts } = JSON.parse(cached) + if (Date.now() - ts < CACHE_TTL) { setLinks(data.length ? data : NAV_FALLBACK); return } + } catch {} + } + fetch(`${DASHBOARD}/api/public/links?page=home`, { signal: AbortSignal.timeout(3000) }) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (Array.isArray(data) && data.length > 0) { + const mapped = data.map(l => ({ id: l.id, label: l.label, url: l.url })) + setLinks(mapped) + sessionStorage.setItem(CACHE_KEY, JSON.stringify({ data: mapped, ts: Date.now() })) + } + }) + .catch(() => {}) + }, []); return (