diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 19ef713..16f09d4 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -7,6 +7,11 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, + { + href: '/insights', + icon: icon.mdiRobot, + label: 'AI Insights', + }, { href: '/users/users-list', @@ -136,4 +141,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index d27d82e..2b29ba2 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,130 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import { getPexelsImage } from '../helpers/pexels'; +import * as icon from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; - -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('video'); - const [contentPosition, setContentPosition] = useState('left'); +export default function LandingPage() { + const [bgImage, setBgImage] = useState(''); const textColor = useAppSelector((state) => state.style.linkColor); - const title = 'App Draft' - - // Fetch Pexels image/video useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); + async function fetchBg() { + const image = await getPexelsImage('basketball court dark'); + if (image?.src?.large2x) { + setBgImage(image.src.large2x); + } } - fetchData(); + fetchBg(); }, []); - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - + return ( +
+ + {getPageTitle('SportScalp AI - Professional Sports Analytics')} + + + {/* Hero Section */} +
+ {/* Background Overlay */} +
+
+ +
+
+ + POWERED BY AI +
+

+ UNCOVER THE WINNING EDGE +

+

+ The world's most advanced sports data scalper. Ask questions, analyze betting lines, and get real-time insights for NBA, NFL, and NHL. +

+
+ + +
+
+ + {/* Features Section */} +
+
+
+
+ +
+

Natural Language Query

+

+ "Show me players with 70% 3PT accuracy in their last 15 games." Just ask, and our AI does the math. +

+
+ +
+
+ +
+

Multi-Source Scalping

+

+ Real-time data from StatMuse, NBA, NFL, and NHL official sources, and major betting platforms. +

+
+ +
+
+ +
+

Line Relevance Engine

+

+ Automatically cross-reference player performance with betting lines to find high-value opportunities. +

+
+
+
+ + {/* CTA Section */} +
+
+

READY TO DOMINATE THE DATA?

+ + GET STARTED NOW + +
+
+ + {/* Footer */} +
+

© 2026 SportScalp AI. All rights reserved.

+
+ Terms + Privacy +
+
); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; - - return ( -
- - {getPageTitle('Starter Page')} - - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
- -
- ); } -Starter.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - +LandingPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; \ No newline at end of file diff --git a/frontend/src/pages/insights.tsx b/frontend/src/pages/insights.tsx new file mode 100644 index 0000000..55e3d74 --- /dev/null +++ b/frontend/src/pages/insights.tsx @@ -0,0 +1,228 @@ +import * as icon from '@mdi/js'; +import Head from 'next/head'; +import React, { useState } from 'react'; +import type { ReactElement } from 'react'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import CardBox from '../components/CardBox'; +import BaseButton from '../components/BaseButton'; +import FormField from '../components/FormField'; +import { getPageTitle } from '../config'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { aiResponse } from '../stores/openAiSlice'; +import axios from 'axios'; +import BaseIcon from '../components/BaseIcon'; + +const InsightsPage = () => { + const dispatch = useAppDispatch(); + const { isAskingResponse } = useAppSelector((state) => state.openAi); + const [question, setQuestion] = useState(''); + const [chatHistory, setChatHistory] = useState>([]); + const [isExecutingSql, setIsExecutingSql] = useState(false); + + const extractText = (payload: any) => { + if (!payload || typeof payload !== 'object') return ''; + const data = payload.data || payload; + + if (Array.isArray(data.output)) { + let combined = ''; + for (const item of data.output) { + if (!item || !Array.isArray(item.content)) continue; + for (const block of item.content) { + if (block?.type === 'output_text' && typeof block.text === 'string') { + combined += block.text; + } + } + } + if (combined) return combined; + } + + if (data.choices?.[0]?.message?.content) { + return data.choices[0].message.content; + } + + return ''; + }; + + const handleAsk = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + if (!question.trim()) return; + + const userQuestion = question; + setQuestion(''); + setChatHistory(prev => [...prev, { role: 'user', content: userQuestion }]); + + try { + // Step 1: Generate SQL + const systemPrompt = `You are a sports data assistant. You have access to a PostgreSQL database with: +- players: id, name, position, teamId +- player_stats: id, playerId, game_date, points, three_pointers_made, three_point_attempts, rebounds, assists, minutes +- betting_lines: id, line_name, event_date, spread, moneyline_home, moneyline_away, total + +Translate the user question to a single SELECT SQL statement. +Use window functions for 'last N games' (e.g. ROW_NUMBER() OVER (PARTITION BY ps.playerId ORDER BY ps.game_date DESC)). +Return ONLY a JSON object: { "sql": "SELECT ..." }.`; + + const aiGenSqlResp = await dispatch(aiResponse({ + input: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userQuestion } + ] + })).unwrap(); + + const aiText = extractText(aiGenSqlResp); + let sql = ''; + try { + const cleanText = aiText.replace(/```json|```/g, '').trim(); + const parsed = JSON.parse(cleanText); + sql = parsed.sql; + } catch (err) { + const match = aiText.match(/SELECT[\s\S]+/i); + if (match) sql = match[0]; + } + + if (!sql) { + setChatHistory(prev => [...prev, { role: 'assistant', content: "I couldn't generate a valid query for that question. Could you try rephrasing it?" }]); + return; + } + + // Step 2: Execute SQL + setIsExecutingSql(true); + const sqlResult = await axios.post('/sql', { sql }); + setIsExecutingSql(false); + + const rows = sqlResult.data.rows; + + if (!rows || rows.length === 0) { + setChatHistory(prev => [...prev, { role: 'assistant', content: "I found no data matching your query." }]); + return; + } + + // Step 3: Summarize Results + const summaryPrompt = `Based on the following data from the database, answer the user's question: "${userQuestion}" + +Data: ${JSON.stringify(rows.slice(0, 10))} + +Provide a concise, professional summary. Mention specific names and numbers.`; + + const aiSummaryResp = await dispatch(aiResponse({ + input: [ + { role: 'system', content: 'You are a sports analyst.' }, + { role: 'user', content: summaryPrompt } + ] + })).unwrap(); + + const summary = extractText(aiSummaryResp); + + setChatHistory(prev => [...prev, { + role: 'assistant', + content: summary, + type: 'table', + data: rows + }]); + + } catch (error) { + console.error(error); + setIsExecutingSql(false); + setChatHistory(prev => [...prev, { role: 'assistant', content: "Sorry, I encountered an error while processing your request." }]); + } + }; + + return ( + <> + + {getPageTitle('AI Sports Insights')} + + + + setChatHistory([])} + /> + + + +
+ {chatHistory.length === 0 && ( +
+ +

Ask me anything about NBA, NFL, or NHL stats!

+

Example: "Who are the top 5 scorers in their last 10 games?"

+
+ )} + {chatHistory.map((msg, i) => ( +
+
+

{msg.content}

+ {msg.type === 'table' && msg.data && ( +
+ + + + {Object.keys(msg.data[0] || {}).map(k => ( + + ))} + + + + {msg.data.slice(0, 5).map((row, ri) => ( + + {Object.values(row).map((v: any, vi) => ( + + ))} + + ))} + +
{k}
{String(v)}
+ {msg.data.length > 5 &&

Showing 5 of {msg.data.length} results

} +
+ )} +
+
+ ))} + {(isAskingResponse || isExecutingSql) && ( +
+
+
+
+
+
+
+ )} +
+ +
+ + setQuestion(e.target.value)} + placeholder="e.g. "Show me players with 70% 3PT accuracy in last 15 games"" + className="w-full" + disabled={isAskingResponse || isExecutingSql} + /> + + + + + + + ); +}; + +InsightsPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default InsightsPage; \ No newline at end of file