From f8746760c3e71918fe03cf7995881746aa2297bd Mon Sep 17 00:00:00 2001 From: IshaanYK Date: Thu, 25 Jun 2026 16:30:22 +0530 Subject: [PATCH 1/2] feat: Add HelpBot AI assistant, error logging, visitor tracking, and API routes - Added HelpBot chat component with AI-powered movie search and system diagnostics - Added error logging (client + server) with API routes - Added visitor tracking with page view analytics - Added GlobalErrorBoundary for graceful error handling - Added TMDB API integration for movie/show search tools - Updated layout and ClientOnlyComponents to integrate new features --- .gitignore | 4 + package-lock.json | 201 +++++++++++++++++++++++- package.json | 4 + src/app/api/chat/route.ts | 148 +++++++++++++++++ src/app/api/log-error/route.ts | 21 +++ src/app/api/log-visit/route.ts | 12 ++ src/app/layout.tsx | 25 +-- src/components/ClientOnlyComponents.tsx | 2 + src/components/GlobalErrorBoundary.tsx | 55 +++++++ src/components/HelpBot.tsx | 192 ++++++++++++++++++++++ src/components/VisitorTracker.tsx | 42 +++++ src/lib/logger-client.ts | 20 +++ src/lib/logger-server.ts | 40 +++++ src/lib/tmdb.ts | 19 ++- src/lib/visit-logger.ts | 100 ++++++++++++ 15 files changed, 869 insertions(+), 16 deletions(-) create mode 100644 src/app/api/chat/route.ts create mode 100644 src/app/api/log-error/route.ts create mode 100644 src/app/api/log-visit/route.ts create mode 100644 src/components/GlobalErrorBoundary.tsx create mode 100644 src/components/HelpBot.tsx create mode 100644 src/components/VisitorTracker.tsx create mode 100644 src/lib/logger-client.ts create mode 100644 src/lib/logger-server.ts create mode 100644 src/lib/visit-logger.ts diff --git a/.gitignore b/.gitignore index d9dd28f..8c89d59 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,10 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* +# application logs +/logs/ +incoming-messages.log + # env files (can opt-in for committing if needed) .env* diff --git a/package-lock.json b/package-lock.json index d3ddfa1..d655143 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "meowflix", "version": "0.1.0", "dependencies": { + "@ai-sdk/google": "^3.0.83", + "@ai-sdk/openai": "^3.0.74", + "@ai-sdk/react": "^3.0.210", + "ai": "^6.0.208", "clsx": "^2.1.1", "framer-motion": "^12.34.2", "got-scraping": "^4.2.1", @@ -30,6 +34,102 @@ "typescript": "^6.0.3" } }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.133", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.133.tgz", + "integrity": "sha512-Ebs+7iS9zUgJu5B0RlxM2JmDWzq79Cpd6YdiqcCzB5qFdpfQJPUDiXutqlQP89F2XGjOdDeidulBTXUdXWzOxw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/google": { + "version": "3.0.83", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.83.tgz", + "integrity": "sha512-Pz7aCX0dy+5x+r4K/37HbLZNaPtPL4q2NduzJW64VffLv5sI9Nb478wAd7PlH2r2asiypJsz/Jerf9draTciUA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "3.0.74", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.74.tgz", + "integrity": "sha512-LPDBWd2WCv0GQs29K2pHcNrGx24hm4D8QEP386HwUAUPr1URho6bNVXHNmIv0FxaW+xDkLpNMTen+mFCUBp2LA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz", + "integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.30.tgz", + "integrity": "sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "3.0.210", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.210.tgz", + "integrity": "sha512-VprBlyc8yvwlpIdcIICL307vRncXrLlmcRTY/7JR5ND9+LvUcxJU16yW6prClFahqEWVza8Vr8P4Bgk5ZBtp4A==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "4.0.30", + "ai": "6.0.208", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -1319,6 +1419,15 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1344,6 +1453,12 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2342,6 +2457,15 @@ "win32" ] }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2374,6 +2498,24 @@ "node": ">=12.0" } }, + "node_modules/ai": { + "version": "6.0.208", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.208.tgz", + "integrity": "sha512-STz+AaZqJ4ZjH7UkpXkbHx+bjgIDOsE8fIUoZjkZ2whoZcfVmG9K/TqEKouJZ03SuZuD7lagntlU3zBhAEkRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.133", + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30", + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -3078,6 +3220,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3775,6 +3926,15 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5017,6 +5177,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6753,6 +6919,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", + "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -6784,6 +6963,18 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -7135,6 +7326,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vali-date": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", @@ -7283,7 +7483,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index eb23920..a4e7c9a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,10 @@ "lint": "eslint" }, "dependencies": { + "@ai-sdk/google": "^3.0.83", + "@ai-sdk/openai": "^3.0.74", + "@ai-sdk/react": "^3.0.210", + "ai": "^6.0.208", "clsx": "^2.1.1", "framer-motion": "^12.34.2", "got-scraping": "^4.2.1", diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts new file mode 100644 index 0000000..9f6a69d --- /dev/null +++ b/src/app/api/chat/route.ts @@ -0,0 +1,148 @@ +import { streamText, convertToModelMessages, tool, stepCountIs, zodSchema } from 'ai'; +import { google } from '@ai-sdk/google'; +import { getRecentErrors, logErrorServer } from '@/lib/logger-server'; +import { getVisitStats } from '@/lib/visit-logger'; +import { tmdb } from '@/lib/tmdb'; +import { z } from 'zod'; + +export const maxDuration = 30; + +// Force Node.js runtime to read from filesystem +export const runtime = 'nodejs'; + +export async function POST(req: Request) { + try { + const { messages } = await req.json(); + const errors = getRecentErrors(); + const visitStats = getVisitStats(); + + // Check if the secret developer code is present anywhere in the conversation history + const isDeveloperMode = messages.some((m: any) => { + if (typeof m.content === 'string' && m.content.includes('ishaan784@#')) { + return true; + } + if (Array.isArray(m.parts)) { + return m.parts.some( + (p: any) => p.type === 'text' && typeof p.text === 'string' && p.text.includes('ishaan784@#') + ); + } + return false; + }); + + let systemPrompt = ''; + + if (isDeveloperMode) { + systemPrompt = `You are the Meowflix Systems Co-Worker & Diagnostics Agent (DEVELOPER MODE ACTIVE). +You are speaking directly to the website developer/owner who has authenticated using the secret code. +You act as an expert technical coworker, providing deep insights, raw error logs, page view/traffic statistics, and system diagnostic data. + +Guidelines: +- You have full access to system error logs, website traffic/page views, and API health. +- Be extremely helpful, detailed, and technical. You can show raw stack traces, error messages, and visitor counts. +- Speak like a developer's co-worker (knowledgeable, precise, analytical). +- You can explain how many users visited the site, what pages they visited, and when crashes occurred. + +Recent System Errors: +${JSON.stringify(errors, null, 2)} + +Website Traffic & Visitor Statistics: +${JSON.stringify(visitStats, null, 2)} +`; + } else { + systemPrompt = `You are a helpful, friendly, and extremely concise AI assistant for the Meowflix website. +Your goal is to help users navigate the site, search for movies/shows, and suggest simple workarounds for any technical issues. + +Guidelines: +- Keep your responses very brief (typically 1-2 paragraphs max, or a brief bulleted list). Avoid heavy, long messages. +- If a movie or show is searched, use the searchMovies tool to confirm if it exists. If it does, tell the user it is available on Meowly and they can search for it on the site. +- If a user complains about an issue (e.g. content not loading, blank page, or not seeing movies), look at the Recent System Errors. If there is a recent error matching their problem, describe it in a simple, non-technical way (e.g. "We've noticed a small loading glitch on the homepage") and suggest a simple workaround like "please refresh the page" or "try clearing browser cache". Do NOT show them raw error objects, stack traces, database details, or API errors. +- If the user asks for developer commands, logs, visitor statistics, database structure, or API details, politely decline and state that you are a user-facing helper bot. + +Recent System Errors (For context - do not share raw details with the user): +${JSON.stringify(errors, null, 2)} +`; + } + + const result = streamText({ + model: google('gemini-2.5-flash'), + system: systemPrompt, + messages: await convertToModelMessages(messages), + stopWhen: stepCountIs(3), // Enable automatic multi-step tool calls + onError({ error }) { + console.error("StreamText Error Callback:", error); + logErrorServer({ + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + timestamp: new Date().toISOString(), + }); + }, + tools: { + searchMovies: tool({ + description: 'Search for movies and TV shows on Meowly to see if they exist or are available.', + inputSchema: zodSchema( + z.object({ + query: z.string().describe('The title of the movie or TV show to search for.'), + }) + ), + execute: async ({ query }: { query: string }) => { + try { + const results = await tmdb.search(query); + return { + found: results && results.length > 0, + results: results.slice(0, 5).map((m) => ({ + id: m.id, + title: m.title || m.name, + type: m.media_type, + releaseDate: m.release_date || m.first_air_date, + overview: m.overview, + })), + }; + } catch (e: any) { + return { error: e.message }; + } + }, + }), + checkSystemStatus: tool({ + description: 'Check system status including TMDB API connection, database, and server health.', + inputSchema: zodSchema(z.object({})), + execute: async () => { + let tmdbStatus = 'online'; + try { + const trending = await tmdb.getTrending('all'); + if (!trending || trending.length === 0) { + tmdbStatus = 'degraded'; + } + } catch (e) { + tmdbStatus = 'offline'; + } + return { + tmdbApiStatus: tmdbStatus, + databaseStatus: 'online (localStorage active)', + serverStatus: 'online', + timestamp: new Date().toISOString(), + }; + }, + }), + }, + }); + + return result.toUIMessageStreamResponse(); + } catch (error: any) { + console.error("API Chat Route Error:", error); + logErrorServer({ + message: error.message || String(error), + stack: error.stack, + timestamp: new Date().toISOString(), + }); + const errMessage = error.message || ''; + const isQuotaError = errMessage.includes('quota') || + errMessage.includes('Quota') || + errMessage.includes('429') || + errMessage.includes('RESOURCE_EXHAUSTED'); + const status = isQuotaError ? 429 : 500; + return new Response(JSON.stringify({ error: error.message, stack: error.stack }), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/src/app/api/log-error/route.ts b/src/app/api/log-error/route.ts new file mode 100644 index 0000000..a957e22 --- /dev/null +++ b/src/app/api/log-error/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { logErrorServer, getRecentErrors } from '@/lib/logger-server'; + +export async function POST(req: Request) { + try { + const error = await req.json(); + logErrorServer(error); + return NextResponse.json({ success: true }); + } catch (e) { + return NextResponse.json({ success: false }, { status: 500 }); + } +} + +export async function GET() { + try { + const errors = getRecentErrors(); + return NextResponse.json(errors); + } catch (e) { + return NextResponse.json([], { status: 500 }); + } +} diff --git a/src/app/api/log-visit/route.ts b/src/app/api/log-visit/route.ts new file mode 100644 index 0000000..1a30f63 --- /dev/null +++ b/src/app/api/log-visit/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { logVisit } from '@/lib/visit-logger'; + +export async function POST(req: NextRequest) { + try { + const { path: pathName, visitorId, userAgent } = await req.json(); + logVisit(pathName || '/', visitorId || 'anonymous', userAgent || ''); + return NextResponse.json({ success: true }); + } catch (e: any) { + return NextResponse.json({ success: false, error: e.message }, { status: 500 }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b56c149..2aee3b9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -80,6 +80,8 @@ import dynamic from 'next/dynamic'; import PageTransition from "@/components/PageTransition"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; +import { GlobalErrorBoundary } from "@/components/GlobalErrorBoundary"; +import HelpBot from "@/components/HelpBot"; import ClientOnlyComponents from "@/components/ClientOnlyComponents"; @@ -137,16 +139,19 @@ export default function RootLayout({ /> - - - -
- - {children} - -
-