diff --git a/game-buying-guide/.env.example b/game-buying-guide/.env.example new file mode 100644 index 00000000..697b3237 --- /dev/null +++ b/game-buying-guide/.env.example @@ -0,0 +1 @@ +MINO_API_KEY = YOUR_API_KEY diff --git a/game-buying-guide/.gitignore b/game-buying-guide/.gitignore new file mode 100644 index 00000000..f650315f --- /dev/null +++ b/game-buying-guide/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/game-buying-guide/README.md b/game-buying-guide/README.md new file mode 100644 index 00000000..d11a9e6a --- /dev/null +++ b/game-buying-guide/README.md @@ -0,0 +1,172 @@ +# GamePulse + +**Live:** [https://v0-game-buying-guide.vercel.app/](https://v0-game-buying-guide.vercel.app/) + +GamePulse helps users decide **whether to buy a video game now or wait for a better deal**. +It compares pricing, discounts, and store signals across **10 major gaming platforms in parallel** using **Mino autonomous browser agents**, then surfaces a clear recommendation for each store. + +Instead of relying on price-tracking APIs or scraped datasets, GamePulse launches real browser agents that visit each store, observe the live page, and return structured pricing analysis in real time. + +--- + +## Demo + +https://github.com/user-attachments/assets/61c22b80-2cfc-40a6-bc3a-7d5917cf71a9 + +--- + +## Mino API Usage + +GamePulse uses the **TinyFish SSE Browser Automation API** to analyze multiple game stores simultaneously. + +For each platform (Steam, Epic, PlayStation Store, etc.), the app launches a Mino agent that: +- Navigates to the store search page +- Locates the requested game +- Extracts pricing, discounts, and sale signals +- Returns a structured JSON recommendation + +### Example API Call + +```ts +const response = await fetch("https://mino.ai/v1/automation/run-sse", { + method: "POST", + headers: { + "X-API-Key": process.env.MINO_API_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url: platformSearchUrl, + goal: ` +You are analyzing a game store page to help a user decide +whether to buy "${gameTitle}" now or wait. + +Observe: +- Current price +- Sale or discount indicators +- User ratings and review signals + +Return a JSON object with pricing and a recommendation. +`, + timeout: 300000, + }), +}) +``` + +The response streams **Server-Sent Events (SSE)**, including: + +- `STREAMING_URL` → live browser preview of the agent + +- `STATUS` → navigation and extraction progress + +- `COMPLETE` → final structured pricing analysis JSON + +## How It Works + +1. User enters a game title (e.g., Elden Ring) + +2. Platform discovery generates search URLs from a curated list of 10 stores + +3. Parallel Mino agents launch (one per platform) + +4. Live browser previews stream into the UI + +5. Results aggregate into a buy / wait / consider recommendation dashboard + +## Supported Platforms + +GamePulse checks the following platforms for every search: + +- Steam + +- Epic Games Store + +- GOG + +- PlayStation Store + +- Xbox Store + +- Nintendo eShop + +- Humble Bundle + +- Green Man Gaming + +- Fanatical + +- CDKeys + +No external discovery APIs or LLMs are used — the platform list is curated and deterministic. + + +## How to Run +**Prerequisites** +- Node.js 18+ +- A Mino API key [get one here](https://mino.ai/api-keys) + +## Setup + +1. Install dependencies: +```bash +cd game-buying-guide +npm install +``` + + +2. Create a .env.local file: +```bash +MINO_API_KEY=your_mino_api_key_here +``` + +3. Start the dev server: +```bash +npm run dev +``` + +Open http://localhost:3000 + +## Architecture Diagram +```bash +┌─────────────────────────────────────────────────────────┐ +│ User (Browser) │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Next.js Frontend │ │ +│ │ │ │ +│ │ 1. Enter game title │ │ +│ │ 2. View 10 live agent cards │ │ +│ │ 3. See buy / wait recommendations │ │ +│ └──────────────────┬──────────────────────────────┘ │ +└─────────────────────┼───────────────────────────────────┘ + │ POST /api/analyze-platform (x10, parallel) + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Next.js API Routes │ +│ │ +│ - /api/discover-platforms │ +│ - /api/analyze-platform → Mino SSE proxy │ +└─────────────────────┬───────────────────────────────────┘ + │ POST /v1/automation/run-sse + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Mino API │ +│ │ +│ - Spins up autonomous browser agents │ +│ - Streams live previews and status │ +│ - Returns structured pricing JSON │ +└──────────┬──────────┬──────────┬──────────┬────────────┘ + ▼ ▼ ▼ ▼ + Steam Epic PlayStation Xbox ... (10 platforms) +``` + +## Environment Variables + +MINO_API_KEY - API key for Mino browser automation + + +## Notes + +- All platform analysis is performed via live browser automation + +- No price databases, scraping services, or AI discovery APIs are used + +- Results reflect real-time store state, not cached data diff --git a/game-buying-guide/app/api/analyze-platform/route.ts b/game-buying-guide/app/api/analyze-platform/route.ts new file mode 100644 index 00000000..f60a34c3 --- /dev/null +++ b/game-buying-guide/app/api/analyze-platform/route.ts @@ -0,0 +1,271 @@ +import { NextResponse } from 'next/server' + +// Allow streaming responses up to 300 seconds (requires Vercel Pro plan) +export const maxDuration = 300 + +const MINO_API_KEY = process.env.MINO_API_KEY + +export async function POST(request: Request) { + try { + const { platformName, url, gameTitle } = await request.json() + + if (!platformName || !url || !gameTitle) { + return NextResponse.json({ error: 'Platform name, URL, and game title are required' }, { status: 400 }) + } + + if (!MINO_API_KEY) { + return NextResponse.json({ error: 'Mino API key not configured' }, { status: 500 }) + } + + const currentDate = new Date().toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }) + + const goal = `You are analyzing a game store page to help a user decide whether to buy "${gameTitle}" now or wait. + +CURRENT DATE: ${currentDate} + +STEP 1 - NAVIGATE & OBSERVE: +Navigate to the store page and observe: +- Current price displayed +- Any sale/discount indicators +- Original price (if on sale) +- User ratings and review scores +- Any visible sale end dates or timers +- Bundle options or editions available + +STEP 2 - ANALYZE PURCHASE TIMING: +Consider these factors: +- Is there an active discount? How significant? +- Are there any visible sale patterns (seasonal sales, etc.)? +- What do user reviews say about the game's value? +- Are there any upcoming DLCs or editions that might affect price? + +STEP 3 - RETURN STRUCTURED ANALYSIS: +Return a JSON object with this exact format: +{ + "platform_name": "${platformName}", + "store_url": "${url}", + "current_price": "$XX.XX or regional equivalent", + "original_price": "$XX.XX if on sale, null otherwise", + "discount_percentage": "XX%" if on sale, null otherwise", + "is_on_sale": true/false, + "sale_ends": "Date/time if visible, null otherwise", + "user_rating": "Rating score if available (e.g., '9/10', '95%', '4.5/5')", + "review_count": "Number of reviews if visible", + "recommendation": "buy_now" | "wait" | "consider", + "reasoning": "2-3 sentence explanation of your recommendation", + "pros": ["Up to 3 reasons to buy from this platform"], + "cons": ["Up to 3 potential drawbacks or reasons to wait"] +} + +RECOMMENDATION GUIDELINES: +- "buy_now": Significant discount (30%+), historic low price, or sale ending soon +- "wait": Full price with known upcoming sales, or better deals elsewhere +- "consider": Moderate discount, decent value, user's preference matters + +Be accurate with prices and factual with observations. If you cannot find certain information, use null for that field.` + + // Create SSE stream + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + // Create abort controller for 300 second timeout + const abortController = new AbortController() + const timeoutId = setTimeout(() => { + console.log(`[v0] Timeout reached for ${platformName}, aborting...`) + abortController.abort() + }, 295000) // 295 seconds (leaving buffer for response) + + try { + console.log(`[v0] Starting Mino agent for ${platformName} at ${url}`) + + const response = await fetch('https://mino.ai/v1/automation/run-sse', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': MINO_API_KEY, + }, + body: JSON.stringify({ + url, + goal, + timeout: 300000, // 300 second timeout + }), + signal: abortController.signal, + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('Mino API error:', errorText) + controller.enqueue( + encoder.encode( + `data: ${JSON.stringify({ type: 'ERROR', error: 'Failed to start browser agent' })}\n\n` + ) + ) + controller.close() + return + } + + if (!response.body) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'No response body' })}\n\n`)) + controller.close() + return + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + let hasCompleted = false + let lastResult: unknown = null + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + + // Process complete lines + const lines = buffer.split('\n') + buffer = lines.pop() || '' + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const jsonStr = line.slice(6).trim() + if (!jsonStr || jsonStr === '[DONE]') continue + + const data = JSON.parse(jsonStr) + console.log(`[v0] ${platformName} event:`, JSON.stringify(data).slice(0, 200)) + + // Forward streaming URL - check multiple possible field names + const streamingUrl = data.streamingUrl || data.liveUrl || data.previewUrl || data.live_url || data.preview_url || data.browserUrl || data.browser_url + if (streamingUrl) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'STREAMING_URL', streamingUrl })}\n\n`) + ) + } + + // Forward status updates - check multiple possible formats + const statusMessage = data.message || data.status || data.action || data.step || data.event + if (statusMessage && typeof statusMessage === 'string' && !data.result && !data.output) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'STATUS', message: statusMessage })}\n\n`) + ) + } + + // Handle completion - check multiple possible field names + const resultData = data.result || data.resultJson || data.output || data.response || data.answer || data.data + if (data.type === 'COMPLETE' || data.type === 'complete' || data.type === 'done' || data.type === 'finished' || data.completed || data.done || (resultData && typeof resultData === 'object')) { + hasCompleted = true + let resultJson = resultData + + // Try to parse if it's a string + if (typeof resultJson === 'string') { + try { + const jsonMatch = resultJson.match(/\{[\s\S]*\}/) + if (jsonMatch) { + resultJson = JSON.parse(jsonMatch[0]) + } + } catch { + // Keep as string if parsing fails + } + } + + if (resultJson) { + lastResult = resultJson + console.log(`[v0] ${platformName} completed with result`) + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'COMPLETE', result: resultJson })}\n\n`) + ) + } + } + + // Handle errors + if (data.type === 'ERROR' || data.type === 'error' || data.error) { + hasCompleted = true + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: data.error || data.message || 'Unknown error' })}\n\n`) + ) + } + } catch (parseError) { + // Log non-JSON lines for debugging + console.log(`[v0] ${platformName} non-JSON line:`, line.slice(0, 100)) + } + } + } + } + + // Process remaining buffer + if (buffer.trim() && buffer.startsWith('data: ')) { + try { + const jsonStr = buffer.slice(6).trim() + if (jsonStr && jsonStr !== '[DONE]') { + const data = JSON.parse(jsonStr) + if (data.result || data.resultJson || data.output) { + hasCompleted = true + let resultJson = data.result || data.resultJson || data.output + if (typeof resultJson === 'string') { + const jsonMatch = resultJson.match(/\{[\s\S]*\}/) + if (jsonMatch) { + resultJson = JSON.parse(jsonMatch[0]) + } + } + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'COMPLETE', result: resultJson })}\n\n`) + ) + } + } + } catch { + // Ignore + } + } + + // Clear timeout since we completed normally + clearTimeout(timeoutId) + + console.log(`[v0] Stream ended for ${platformName}, hasCompleted: ${hasCompleted}`) + + // Ensure we send a completion event if stream ended without one + if (!hasCompleted) { + console.log(`[v0] No completion received for ${platformName}, sending error`) + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'Analysis ended without results - the agent may still be processing on the Mino dashboard' })}\n\n`) + ) + } + + controller.close() + } catch (error) { + clearTimeout(timeoutId) + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.error(`[v0] Stream error for ${platformName}:`, errorMessage) + + // Check if it was an abort + if (error instanceof Error && error.name === 'AbortError') { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'Analysis timed out' })}\n\n`) + ) + } else { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: `Stream processing failed: ${errorMessage}` })}\n\n`) + ) + } + controller.close() + } + }, + }) + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + }) + } catch (error) { + console.error('Error in analyze-platform:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/game-buying-guide/app/api/discover-platforms/route.ts b/game-buying-guide/app/api/discover-platforms/route.ts new file mode 100644 index 00000000..b4470ec0 --- /dev/null +++ b/game-buying-guide/app/api/discover-platforms/route.ts @@ -0,0 +1,76 @@ +import { NextResponse } from 'next/server' + +// Curated list of trusted gaming platforms with search URL patterns +const GAMING_PLATFORMS = [ + { + name: 'Steam', + searchUrl: (query: string) => `https://store.steampowered.com/search/?term=${encodeURIComponent(query)}`, + icon: 'steam', + }, + { + name: 'Epic Games Store', + searchUrl: (query: string) => `https://store.epicgames.com/en-US/browse?q=${encodeURIComponent(query)}`, + icon: 'epic', + }, + { + name: 'GOG', + searchUrl: (query: string) => `https://www.gog.com/en/games?query=${encodeURIComponent(query)}`, + icon: 'gog', + }, + { + name: 'PlayStation Store', + searchUrl: (query: string) => `https://store.playstation.com/en-us/search/${encodeURIComponent(query)}`, + icon: 'playstation', + }, + { + name: 'Xbox Store', + searchUrl: (query: string) => `https://www.xbox.com/en-US/search?q=${encodeURIComponent(query)}`, + icon: 'xbox', + }, + { + name: 'Nintendo eShop', + searchUrl: (query: string) => `https://www.nintendo.com/us/search/#q=${encodeURIComponent(query)}`, + icon: 'nintendo', + }, + { + name: 'Humble Bundle', + searchUrl: (query: string) => `https://www.humblebundle.com/store/search?search=${encodeURIComponent(query)}`, + icon: 'humble', + }, + { + name: 'Green Man Gaming', + searchUrl: (query: string) => `https://www.greenmangaming.com/search/?query=${encodeURIComponent(query)}`, + icon: 'gmg', + }, + { + name: 'Fanatical', + searchUrl: (query: string) => `https://www.fanatical.com/en/search?search=${encodeURIComponent(query)}`, + icon: 'fanatical', + }, + { + name: 'CDKeys', + searchUrl: (query: string) => `https://www.cdkeys.com/catalogsearch/result/?q=${encodeURIComponent(query)}`, + icon: 'cdkeys', + }, +] + +export async function POST(request: Request) { + try { + const { gameTitle } = await request.json() + + if (!gameTitle) { + return NextResponse.json({ error: 'Game title is required' }, { status: 400 }) + } + + // Generate platform URLs for the game + const platforms = GAMING_PLATFORMS.map((platform) => ({ + name: platform.name, + url: platform.searchUrl(gameTitle), + })) + + return NextResponse.json({ platforms }) + } catch (error) { + console.error('Error in discover-platforms:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/game-buying-guide/app/api/steamdb-price-history/route.ts b/game-buying-guide/app/api/steamdb-price-history/route.ts new file mode 100644 index 00000000..e7b61546 --- /dev/null +++ b/game-buying-guide/app/api/steamdb-price-history/route.ts @@ -0,0 +1,209 @@ +import { NextResponse } from 'next/server' + +// Allow streaming responses up to 300 seconds (requires Vercel Pro plan) +export const maxDuration = 300 + +const MINO_API_KEY = process.env.MINO_API_KEY + +export async function POST(request: Request) { + try { + const { gameTitle } = await request.json() + + if (!gameTitle) { + return NextResponse.json({ error: 'Game title is required' }, { status: 400 }) + } + + if (!MINO_API_KEY) { + return NextResponse.json({ error: 'Mino API key not configured' }, { status: 500 }) + } + + const url = `https://steamdb.info/search/?a=app&q=${encodeURIComponent(gameTitle)}` + + const goal = `You are analyzing SteamDB to find the historic lowest price for "${gameTitle}". + +STEP 1 - SEARCH & NAVIGATE: +1. You are on the SteamDB search page with results for "${gameTitle}" +2. Find the correct game in the search results (match the title as closely as possible) +3. Click on the game to go to its detail page + +STEP 2 - FIND PRICE HISTORY: +1. Look for the "Price History" section or chart on the game's page +2. Find the "Lowest recorded price" or historic low price information +3. Note the date when this historic low occurred +4. Note what discount percentage that was + +STEP 3 - COMPARE WITH CURRENT: +1. Find the current Steam price +2. Check if there's an active discount +3. Determine if the current price matches or is close to the historic low + +STEP 4 - RETURN STRUCTURED DATA: +Return a JSON object with this exact format: +{ + "game_name": "Full game name as shown on SteamDB", + "historic_lowest_price": "$XX.XX (the all-time lowest price)", + "historic_lowest_date": "Date when historic low occurred (e.g., 'June 2024')", + "historic_lowest_discount": "XX% (the discount when at historic low)", + "current_steam_price": "$XX.XX (current price on Steam)", + "current_discount": "XX% or null if no discount", + "is_current_historic_low": true/false, + "recommendation": "Brief recommendation based on price history (1-2 sentences)" +} + +Be accurate with the prices. If you cannot find certain information, use null for that field. +Focus on finding the LOWEST price the game has EVER been sold for on Steam.` + + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + const abortController = new AbortController() + const timeoutId = setTimeout(() => { + abortController.abort() + }, 295000) + + try { + const response = await fetch('https://mino.ai/v1/automation/run-sse', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': MINO_API_KEY, + }, + body: JSON.stringify({ + url, + goal, + timeout: 300000, + }), + signal: abortController.signal, + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('Mino API error:', errorText) + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'Failed to start SteamDB analysis' })}\n\n`) + ) + controller.close() + clearTimeout(timeoutId) + return + } + + if (!response.body) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'No response stream from Mino' })}\n\n`) + ) + controller.close() + clearTimeout(timeoutId) + return + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + let hasCompleted = false + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + + const lines = buffer.split('\n') + buffer = lines.pop() || '' + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const jsonStr = line.slice(6).trim() + if (!jsonStr || jsonStr === '[DONE]') continue + + const data = JSON.parse(jsonStr) + + const streamingUrl = data.streamingUrl || data.liveUrl || data.previewUrl || data.live_url || data.browser_url + if (streamingUrl) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'STREAMING_URL', streamingUrl })}\n\n`) + ) + } + + const statusMessage = data.message || data.status || data.action || data.step + if (statusMessage && typeof statusMessage === 'string' && !data.result && !data.output) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'STATUS', message: statusMessage })}\n\n`) + ) + } + + const resultData = data.result || data.resultJson || data.output || data.response || data.data + if (data.type === 'COMPLETE' || data.type === 'complete' || data.type === 'done' || (resultData && typeof resultData === 'object')) { + hasCompleted = true + let resultJson = resultData + + if (typeof resultJson === 'string') { + try { + const jsonMatch = resultJson.match(/\{[\s\S]*\}/) + if (jsonMatch) { + resultJson = JSON.parse(jsonMatch[0]) + } + } catch { + // Keep as string + } + } + + if (resultJson) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'COMPLETE', result: resultJson })}\n\n`) + ) + } + } + + if (data.type === 'ERROR' || data.type === 'error' || data.error) { + hasCompleted = true + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: data.error || data.message || 'Unknown error' })}\n\n`) + ) + } + } catch { + // Skip non-JSON lines + } + } + } + } + + clearTimeout(timeoutId) + + if (!hasCompleted) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'SteamDB analysis ended without results' })}\n\n`) + ) + } + + controller.close() + } catch (error) { + clearTimeout(timeoutId) + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + + if (error instanceof Error && error.name === 'AbortError') { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: 'SteamDB analysis timed out' })}\n\n`) + ) + } else { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'ERROR', error: `SteamDB analysis failed: ${errorMessage}` })}\n\n`) + ) + } + controller.close() + } + }, + }) + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + }) + } catch (error) { + console.error('Error in steamdb-price-history:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/game-buying-guide/app/globals.css b/game-buying-guide/app/globals.css new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/game-buying-guide/app/globals.css @@ -0,0 +1 @@ + diff --git a/game-buying-guide/app/layout.tsx b/game-buying-guide/app/layout.tsx new file mode 100644 index 00000000..4169f4b2 --- /dev/null +++ b/game-buying-guide/app/layout.tsx @@ -0,0 +1,46 @@ +import React from "react" +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const _geist = Geist({ subsets: ["latin"] }); +const _geistMono = Geist_Mono({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: 'GamePulse - Buy Now or Wait', + description: 'AI-powered game purchase decision tool. Analyze prices across platforms to decide the best time to buy.', + generator: 'v0.app', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/game-buying-guide/app/loading.tsx b/game-buying-guide/app/loading.tsx new file mode 100644 index 00000000..f15322a8 --- /dev/null +++ b/game-buying-guide/app/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null +} diff --git a/game-buying-guide/app/page.tsx b/game-buying-guide/app/page.tsx new file mode 100644 index 00000000..3d3cd3fe --- /dev/null +++ b/game-buying-guide/app/page.tsx @@ -0,0 +1,94 @@ +'use client' + +import { SearchForm } from '@/components/search-form' +import { AgentGrid } from '@/components/agent-grid' +import { ResultsSummary } from '@/components/results-summary' +import { SteamDBPriceCard } from '@/components/steamdb-price-card' +import { useGameSearch } from '@/hooks/use-game-search' +import { AlertCircle, RotateCcw } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Alert, AlertDescription } from '@/components/ui/alert' + +export default function HomePage() { + const { search, reset, isLoading, agents, error, gameName, steamDBAgent } = useGameSearch() + + const hasResults = agents.length > 0 + const hasCompleted = agents.some((a) => a.status === 'complete') + + return ( +
+ {/* Header Section */} +
+
+ +
+
+ + {/* Main Content */} +
+ {/* Error State */} + {error && ( + + + + {error} + + + + )} + + {/* SteamDB Historic Price - Always show at top when searching */} + {hasResults && steamDBAgent.status !== 'pending' && ( + + )} + + {/* Results Summary */} + {hasCompleted && } + + {/* Agent Grid */} + {hasResults && } + + {/* Empty State */} + {!hasResults && !isLoading && !error && ( +
+
+

Ready to find the best deal?

+

+ Enter a game title above and our AI agents will analyze prices across multiple platforms to help you + decide whether to buy now or wait for a better deal. +

+
+
+ )} + + {/* Loading State Info */} + {isLoading && agents.length === 0 && ( +
+
+
+
+

Discovering Platforms

+

+ Using AI to find where {gameName || 'your game'} is available... +

+
+
+
+ )} +
+ + {/* Footer */} +
+
+

+ GamePulse uses AI-powered browser agents to analyze real-time pricing data. Prices and availability may + vary. +

+
+
+
+ ) +} diff --git a/game-buying-guide/components.json b/game-buying-guide/components.json new file mode 100644 index 00000000..4ee62ee1 --- /dev/null +++ b/game-buying-guide/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/game-buying-guide/components/agent-card.tsx b/game-buying-guide/components/agent-card.tsx new file mode 100644 index 00000000..fdd50d62 --- /dev/null +++ b/game-buying-guide/components/agent-card.tsx @@ -0,0 +1,232 @@ +'use client' + +import { useState } from 'react' +import { motion } from 'framer-motion' +import { + Monitor, + ExternalLink, + CheckCircle2, + AlertCircle, + Loader2, + Clock, + TrendingUp, + TrendingDown, + Minus, + Maximize2, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import type { AgentStatus } from '@/lib/types' +import { cn } from '@/lib/utils' + +interface AgentCardProps { + agent: AgentStatus + onExpandPreview?: (agent: AgentStatus) => void +} + +export function AgentCard({ agent, onExpandPreview }: AgentCardProps) { + const [imageError, setImageError] = useState(false) + + const getStatusIcon = () => { + switch (agent.status) { + case 'pending': + return + case 'running': + return + case 'complete': + return + case 'error': + return + } + } + + const getRecommendationBadge = () => { + if (!agent.result) return null + const rec = agent.result.recommendation + switch (rec) { + case 'buy_now': + return ( + + + Buy Now + + ) + case 'wait': + return ( + + + Wait + + ) + case 'consider': + return ( + + + Consider + + ) + } + } + + return ( + + + +
+
+ {getStatusIcon()} +

{agent.platformName}

+
+ {agent.status === 'complete' && getRecommendationBadge()} +
+ {agent.status === 'running' && agent.currentAction && ( +

{agent.currentAction}

+ )} +
+ + + {/* Live Preview - show when running */} + {agent.status === 'running' && ( +
agent.streamingUrl && onExpandPreview?.(agent)} + > +
+
+ + Live Preview + + + + +
+ {agent.streamingUrl && } +
+
+ {agent.streamingUrl && !imageError ? ( +