fullstuckdev commited on
Commit
2341446
·
verified ·
1 Parent(s): 5a9ed4f

Upload 25 files

Browse files
next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rag-product-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@emotion/react": "^11.13.5",
13
+ "@emotion/styled": "^11.13.5",
14
+ "@huggingface/inference": "^2.8.1",
15
+ "@langchain/community": "^0.3.16",
16
+ "@langchain/openai": "^0.3.14",
17
+ "@mui/icons-material": "^6.1.8",
18
+ "@mui/lab": "^6.0.0-beta.16",
19
+ "@mui/material": "^6.1.8",
20
+ "@types/pdf-parse": "^1.1.4",
21
+ "docx": "^9.0.3",
22
+ "docx-loader": "^1.0.4",
23
+ "framer-motion": "^11.11.17",
24
+ "langchain": "^0.3.6",
25
+ "next": "15.0.3",
26
+ "pdf-parse": "^1.1.1",
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20",
32
+ "@types/react": "^18",
33
+ "@types/react-dom": "^18",
34
+ "eslint": "^8",
35
+ "eslint-config-next": "15.0.3",
36
+ "postcss": "^8",
37
+ "tailwindcss": "^3.4.1",
38
+ "typescript": "^5"
39
+ }
40
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/app/api/qa/route.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { HfInference } from '@huggingface/inference'
3
+
4
+ const hf = new HfInference(process.env.HUGGINGFACE_API_KEY)
5
+
6
+ interface RequestBody {
7
+ query: string
8
+ documents: Array<{ text: string }>
9
+ }
10
+
11
+ export async function POST(request: NextRequest) {
12
+ try {
13
+ const body: RequestBody = await request.json()
14
+ const { query, documents } = body
15
+
16
+ // If query is empty, return early
17
+ if (!query.trim()) {
18
+ return NextResponse.json({
19
+ answer: 'Please enter a question.'
20
+ })
21
+ }
22
+
23
+ // Clean and format the document texts
24
+ const context = documents
25
+ .map(doc => {
26
+ // Clean up the text and ensure proper spacing
27
+ return doc.text
28
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
29
+ .trim() // Remove leading/trailing whitespace
30
+ })
31
+ .filter(text => text.length > 0)
32
+ .join('\n\n') // Add clear separation between documents
33
+
34
+ // Create a prompt following Llama's instruction format
35
+ const prompt = `[INST] You are a helpful AI assistant. Please answer the following question based on the provided context. If the answer cannot be found in the context, say "I cannot find the answer in the provided documents." Answer in the same language as the question. Do not include any instruction tags in your response.
36
+
37
+ Context:
38
+ ${context}
39
+
40
+ Question:
41
+ ${query} [/INST]`
42
+
43
+ const response = await hf.textGeneration({
44
+ model: 'nvidia/Llama-3.1-Nemotron-70B-Instruct-HF',
45
+ inputs: prompt,
46
+ parameters: {
47
+ max_new_tokens: 512,
48
+ temperature: 0.7,
49
+ top_p: 0.95,
50
+ repetition_penalty: 1.15,
51
+ return_full_text: false // Only return the generated response
52
+ }
53
+ })
54
+
55
+ const answer = response.generated_text?.trim()
56
+ .replace(/\[INST\]/g, '') // Remove [INST] tags
57
+ .replace(/\[\/INST\]/g, '') // Remove [/INST] tags
58
+ .trim() || 'Failed to generate an answer'
59
+
60
+ return NextResponse.json({ answer })
61
+ } catch (error) {
62
+ console.error('Error processing QA request:', error)
63
+ return NextResponse.json(
64
+ { error: 'Failed to process request' },
65
+ { status: 500 }
66
+ )
67
+ }
68
+ }
src/app/api/upload/route.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf"
2
+ import { DocxLoader } from "@langchain/community/document_loaders/fs/docx"
3
+ import { NextResponse } from 'next/server'
4
+ import { writeFile } from 'fs/promises'
5
+ import { join } from 'path'
6
+ import { Document } from '@langchain/core/documents'
7
+ import { mkdir } from 'fs/promises'
8
+
9
+ export async function POST(request: Request) {
10
+ try {
11
+ const formData = await request.formData()
12
+ const files = formData.getAll('files') as File[]
13
+
14
+ if (!files?.length) {
15
+ return NextResponse.json(
16
+ { error: 'No files uploaded' },
17
+ { status: 400 }
18
+ )
19
+ }
20
+
21
+ // Ensure temp directory exists
22
+ const tempDir = '/tmp'
23
+ try {
24
+ await mkdir(tempDir, { recursive: true })
25
+ } catch (err) {
26
+ console.error('Error creating temp directory:', err)
27
+ }
28
+
29
+ const documents: { text: string }[] = []
30
+
31
+ for (const file of files) {
32
+ try {
33
+ const bytes = await file.arrayBuffer()
34
+ const buffer = Buffer.from(bytes)
35
+ const tempPath = join(tempDir, `${Date.now()}-${file.name}`)
36
+
37
+ await writeFile(tempPath, buffer)
38
+ console.log('File written to:', tempPath)
39
+
40
+ let docs: Document[] = []
41
+ if (file.name.toLowerCase().endsWith('.pdf')) {
42
+ const loader = new PDFLoader(tempPath)
43
+ docs = await loader.load()
44
+ } else if (file.name.toLowerCase().endsWith('.docx')) {
45
+ const loader = new DocxLoader(tempPath)
46
+ docs = await loader.load()
47
+ }
48
+
49
+ documents.push(...docs.map(doc => ({
50
+ text: doc.pageContent
51
+ .replace(/\s+/g, ' ')
52
+ .trim()
53
+ })))
54
+ } catch (err) {
55
+ console.error(`Error processing file ${file.name}:`, err)
56
+ }
57
+ }
58
+
59
+ if (documents.length === 0) {
60
+ return NextResponse.json(
61
+ { error: 'No documents could be processed' },
62
+ { status: 400 }
63
+ )
64
+ }
65
+
66
+ return NextResponse.json({ documents })
67
+ } catch (error) {
68
+ console.error('Upload error:', error)
69
+ return NextResponse.json(
70
+ { error: 'Failed to process files' },
71
+ { status: 500 }
72
+ )
73
+ }
74
+ }
src/app/favicon.ico ADDED
src/app/fonts/GeistMonoVF.woff ADDED
Binary file (67.9 kB). View file
 
src/app/fonts/GeistVF.woff ADDED
Binary file (66.3 kB). View file
 
src/app/globals.css ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --background: #ffffff;
3
+ --foreground: #171717;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ padding: 0;
9
+ color: var(--foreground);
10
+ background: var(--background);
11
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import localFont from "next/font/local";
3
+ import "./globals.css";
4
+ import Providers from '@/components/Providers'
5
+
6
+ const geistSans = localFont({
7
+ src: "./fonts/GeistVF.woff",
8
+ variable: "--font-geist-sans",
9
+ weight: "100 900",
10
+ });
11
+ const geistMono = localFont({
12
+ src: "./fonts/GeistMonoVF.woff",
13
+ variable: "--font-geist-mono",
14
+ weight: "100 900",
15
+ });
16
+
17
+ export const metadata: Metadata = {
18
+ title: "AI Document Assistant",
19
+ description: "Upload documents and ask questions",
20
+ };
21
+
22
+ export default function RootLayout({
23
+ children,
24
+ }: {
25
+ children: React.ReactNode
26
+ }) {
27
+ return (
28
+ <html lang="en">
29
+ <body className={`${geistSans.variable} ${geistMono.variable}`} suppressHydrationWarning>
30
+ <Providers>{children}</Providers>
31
+ </body>
32
+ </html>
33
+ );
34
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import QASystem from '@/components/QASystem'
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main>
6
+ <QASystem />
7
+ </main>
8
+ )
9
+ }
src/components/FileUpload.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useRef } from 'react'
4
+ import { Box, Typography, Button, CircularProgress, Chip, Stack } from '@mui/material'
5
+ import UploadFileIcon from '@mui/icons-material/UploadFile'
6
+ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
7
+
8
+ interface FileUploadProps {
9
+ onDocumentsLoaded: (documents: { text: string }[]) => void;
10
+ }
11
+
12
+ export default function FileUpload({ onDocumentsLoaded }: FileUploadProps) {
13
+ const [loading, setLoading] = useState(false)
14
+ const [error, setError] = useState<string | null>(null)
15
+ const [uploadedFiles, setUploadedFiles] = useState<string[]>([])
16
+ const fileInputRef = useRef<HTMLInputElement>(null)
17
+
18
+ const handleButtonClick = () => {
19
+ fileInputRef.current?.click()
20
+ }
21
+
22
+ const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
23
+ const files = e.target.files
24
+ if (!files?.length) return
25
+
26
+ setLoading(true)
27
+ setError(null)
28
+
29
+ try {
30
+ const formData = new FormData()
31
+ const fileNames: string[] = []
32
+
33
+ Array.from(files).forEach(file => {
34
+ formData.append('files', file)
35
+ fileNames.push(file.name)
36
+ })
37
+
38
+ const response = await fetch('/api/upload', {
39
+ method: 'POST',
40
+ body: formData,
41
+ })
42
+
43
+ if (!response.ok) {
44
+ const errorData = await response.json()
45
+ throw new Error(errorData.error || 'Upload failed')
46
+ }
47
+
48
+ const data = await response.json()
49
+ setUploadedFiles(prev => [...prev, ...fileNames])
50
+ onDocumentsLoaded(data.documents)
51
+ } catch (err) {
52
+ console.error('Upload error:', err)
53
+ setError(err instanceof Error ? err.message : 'Failed to process files. Please try again.')
54
+ } finally {
55
+ setLoading(false)
56
+ if (fileInputRef.current) {
57
+ fileInputRef.current.value = ''
58
+ }
59
+ }
60
+ }
61
+
62
+ return (
63
+ <Box>
64
+ <Typography variant="subtitle1" sx={{ mb: 1, color: 'text.primary' }}>
65
+ Upload Documents
66
+ </Typography>
67
+ <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
68
+ You can upload multiple PDF and Word documents at once
69
+ </Typography>
70
+
71
+ {uploadedFiles.length > 0 && (
72
+ <Box sx={{ mb: 3 }}>
73
+ <Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
74
+ {uploadedFiles.length} {uploadedFiles.length === 1 ? 'file' : 'files'} uploaded
75
+ </Typography>
76
+ <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', gap: 1 }}>
77
+ {uploadedFiles.map((fileName, index) => (
78
+ <Chip
79
+ key={index}
80
+ icon={<InsertDriveFileIcon />}
81
+ label={fileName}
82
+ variant="outlined"
83
+ sx={{
84
+ backgroundColor: 'rgba(37, 99, 235, 0.1)',
85
+ borderColor: 'rgba(37, 99, 235, 0.2)',
86
+ '& .MuiChip-icon': {
87
+ color: 'primary.main',
88
+ }
89
+ }}
90
+ />
91
+ ))}
92
+ </Stack>
93
+ </Box>
94
+ )}
95
+
96
+ <input
97
+ ref={fileInputRef}
98
+ type="file"
99
+ onChange={handleFileUpload}
100
+ accept=".pdf,.doc,.docx"
101
+ multiple
102
+ style={{ display: 'none' }}
103
+ disabled={loading}
104
+ />
105
+ <Button
106
+ variant="outlined"
107
+ fullWidth
108
+ onClick={handleButtonClick}
109
+ disabled={loading}
110
+ startIcon={loading ? <CircularProgress size={20} /> : <UploadFileIcon />}
111
+ sx={{
112
+ py: 3,
113
+ borderStyle: 'dashed',
114
+ borderWidth: 2,
115
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
116
+ '&:hover': {
117
+ borderColor: 'primary.main',
118
+ backgroundColor: 'rgba(37, 99, 235, 0.04)',
119
+ }
120
+ }}
121
+ >
122
+ {loading ? 'Processing...' : 'Click to upload PDF or Word documents'}
123
+ </Button>
124
+ {error && (
125
+ <Typography color="error" sx={{ mt: 2, fontSize: '0.875rem' }}>
126
+ {error}
127
+ </Typography>
128
+ )}
129
+ </Box>
130
+ )
131
+ }
src/components/Providers.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { ThemeProvider } from '@mui/material/styles'
5
+ import CssBaseline from '@mui/material/CssBaseline'
6
+ import { CacheProvider } from '@emotion/react'
7
+ import createEmotionCache from '@/lib/createEmotionCache'
8
+ import { theme } from '@/theme'
9
+
10
+ const clientSideEmotionCache = createEmotionCache()
11
+
12
+ export default function Providers({ children }: { children: React.ReactNode }) {
13
+ const [mounted, setMounted] = useState(false)
14
+
15
+ useEffect(() => {
16
+ setMounted(true)
17
+ }, [])
18
+
19
+ if (!mounted) {
20
+ return null
21
+ }
22
+
23
+ return (
24
+ <CacheProvider value={clientSideEmotionCache}>
25
+ <ThemeProvider theme={theme}>
26
+ <CssBaseline />
27
+ {children}
28
+ </ThemeProvider>
29
+ </CacheProvider>
30
+ )
31
+ }
src/components/QASystem.tsx ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, ComponentType } from 'react'
4
+ import FileUpload from './FileUpload'
5
+ import {
6
+ Box,
7
+ Container,
8
+ Typography,
9
+ TextField,
10
+ Button,
11
+ CircularProgress,
12
+ Paper,
13
+ Fade,
14
+ } from '@mui/material'
15
+ import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer'
16
+ import { motion } from 'framer-motion'
17
+
18
+ interface Document {
19
+ text: string
20
+ }
21
+
22
+ const MotionContainer = motion(Container as any)
23
+ const MotionPaper = motion(Paper as any)
24
+
25
+ export default function QASystem() {
26
+ const [query, setQuery] = useState('')
27
+ const [answer, setAnswer] = useState('')
28
+ const [loading, setLoading] = useState(false)
29
+ const [documents, setDocuments] = useState<Document[]>([])
30
+
31
+ const handleDocumentsLoaded = (newDocuments: Document[]) => {
32
+ setDocuments(newDocuments)
33
+ }
34
+
35
+ const handleSubmit = async (e: React.FormEvent) => {
36
+ e.preventDefault()
37
+ if (documents.length === 0) {
38
+ alert('Please upload some documents first')
39
+ return
40
+ }
41
+
42
+ setLoading(true)
43
+
44
+ try {
45
+ const response = await fetch('/api/qa', {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ },
50
+ body: JSON.stringify({ query, documents }),
51
+ })
52
+
53
+ const data = await response.json()
54
+ setAnswer(data.answer)
55
+ } catch (error) {
56
+ console.error('Error:', error)
57
+ setAnswer('Failed to get answer. Please try again.')
58
+ } finally {
59
+ setLoading(false)
60
+ }
61
+ }
62
+
63
+ return (
64
+ <Box
65
+ sx={{
66
+ minHeight: '100vh',
67
+ background: 'linear-gradient(135deg, #f6f8fc 0%, #f0f4f8 100%)',
68
+ py: 8
69
+ }}
70
+ >
71
+ <MotionContainer
72
+ maxWidth="lg"
73
+ initial={{ opacity: 0, y: 20 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ transition={{ duration: 0.5 }}
76
+ >
77
+ <Box sx={{ textAlign: 'center', mb: 8 }}>
78
+ <Typography
79
+ variant="h1"
80
+ component="h1"
81
+ sx={{
82
+ mb: 3,
83
+ fontSize: { xs: '2rem', md: '3rem' },
84
+ fontWeight: 800,
85
+ background: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)',
86
+ backgroundClip: 'text',
87
+ WebkitBackgroundClip: 'text',
88
+ color: 'transparent',
89
+ textShadow: '0 2px 10px rgba(37, 99, 235, 0.1)',
90
+ }}
91
+ >
92
+ AI Document Assistant
93
+ </Typography>
94
+ <Typography
95
+ variant="h6"
96
+ sx={{
97
+ color: 'text.secondary',
98
+ maxWidth: '600px',
99
+ mx: 'auto',
100
+ lineHeight: 1.6
101
+ }}
102
+ >
103
+ Upload your documents and get instant, AI-powered answers to your questions
104
+ </Typography>
105
+ </Box>
106
+
107
+ <MotionPaper
108
+ elevation={0}
109
+ initial={{ opacity: 0, y: 20 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ transition={{ duration: 0.5, delay: 0.2 }}
112
+ sx={{
113
+ p: { xs: 3, md: 5 },
114
+ mb: 4,
115
+ border: '1px solid',
116
+ borderColor: 'grey.100',
117
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
118
+ backdropFilter: 'blur(10px)',
119
+ borderRadius: 3,
120
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.05)',
121
+ }}
122
+ >
123
+ <FileUpload onDocumentsLoaded={handleDocumentsLoaded} />
124
+
125
+ <Box component="form" onSubmit={handleSubmit} sx={{ mt: 4 }}>
126
+ <TextField
127
+ fullWidth
128
+ multiline
129
+ rows={3}
130
+ label="What would you like to know?"
131
+ value={query}
132
+ onChange={(e) => setQuery(e.target.value)}
133
+ sx={{
134
+ mb: 3,
135
+ '& .MuiOutlinedInput-root': {
136
+ backgroundColor: 'white',
137
+ borderRadius: 2,
138
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)',
139
+ }
140
+ }}
141
+ variant="outlined"
142
+ />
143
+
144
+ <Button
145
+ fullWidth
146
+ type="submit"
147
+ disabled={loading || documents.length === 0}
148
+ variant="contained"
149
+ size="large"
150
+ sx={{
151
+ py: 2,
152
+ background: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)',
153
+ borderRadius: 2,
154
+ transition: 'all 0.3s ease',
155
+ '&:hover': {
156
+ transform: 'translateY(-2px)',
157
+ boxShadow: '0 6px 20px rgba(37, 99, 235, 0.2)',
158
+ background: 'linear-gradient(135deg, #1d4ed8 0%, #6d28d9 100%)',
159
+ },
160
+ '&:disabled': {
161
+ background: '#e2e8f0',
162
+ color: '#94a3b8',
163
+ }
164
+ }}
165
+ startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <QuestionAnswerIcon />}
166
+ >
167
+ {loading ? 'Processing...' : 'Ask Question'}
168
+ </Button>
169
+ </Box>
170
+ </MotionPaper>
171
+
172
+ <Fade in={!!answer}>
173
+ <MotionPaper
174
+ elevation={0}
175
+ initial={{ opacity: 0, y: 20 }}
176
+ animate={{ opacity: 1, y: 0 }}
177
+ transition={{ duration: 0.5 }}
178
+ sx={{
179
+ p: { xs: 3, md: 5 },
180
+ border: '1px solid',
181
+ borderColor: 'grey.100',
182
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
183
+ backdropFilter: 'blur(10px)',
184
+ borderRadius: 3,
185
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.05)',
186
+ }}
187
+ >
188
+ <Typography
189
+ variant="h6"
190
+ sx={{
191
+ mb: 3,
192
+ fontWeight: 700,
193
+ color: 'text.primary',
194
+ fontSize: '1.25rem',
195
+ }}
196
+ >
197
+ Answer
198
+ </Typography>
199
+ <Box
200
+ sx={{
201
+ p: 4,
202
+ backgroundColor: '#f8fafc',
203
+ borderRadius: 2,
204
+ border: '1px solid',
205
+ borderColor: 'grey.100',
206
+ }}
207
+ >
208
+ <Typography sx={{
209
+ color: 'text.secondary',
210
+ lineHeight: 1.8,
211
+ fontSize: '1.1rem'
212
+ }}>
213
+ {answer}
214
+ </Typography>
215
+ </Box>
216
+ </MotionPaper>
217
+ </Fade>
218
+ </MotionContainer>
219
+ </Box>
220
+ )
221
+ }
src/lib/createEmotionCache.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import createCache from '@emotion/cache'
2
+
3
+ const isBrowser = typeof document !== 'undefined'
4
+
5
+ export default function createEmotionCache() {
6
+ let insertionPoint
7
+
8
+ if (isBrowser) {
9
+ const emotionInsertionPoint = document.querySelector<HTMLMetaElement>(
10
+ 'meta[name="emotion-insertion-point"]'
11
+ )
12
+ insertionPoint = emotionInsertionPoint ?? undefined
13
+ }
14
+
15
+ return createCache({
16
+ key: 'mui-style',
17
+ insertionPoint,
18
+ prepend: true
19
+ })
20
+ }
src/theme.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createTheme } from '@mui/material'
2
+
3
+ export const theme = createTheme({
4
+ palette: {
5
+ mode: 'light',
6
+ primary: {
7
+ main: '#2563eb',
8
+ light: '#3b82f6',
9
+ dark: '#1d4ed8',
10
+ },
11
+ secondary: {
12
+ main: '#7c3aed',
13
+ light: '#8b5cf6',
14
+ dark: '#6d28d9',
15
+ },
16
+ background: {
17
+ default: '#f8fafc',
18
+ paper: '#ffffff',
19
+ },
20
+ text: {
21
+ primary: '#1e293b',
22
+ secondary: '#475569',
23
+ },
24
+ grey: {
25
+ 50: '#f8fafc',
26
+ 100: '#f1f5f9',
27
+ 200: '#e2e8f0',
28
+ 300: '#cbd5e1',
29
+ 800: '#1e293b',
30
+ }
31
+ },
32
+ shape: {
33
+ borderRadius: 12,
34
+ },
35
+ typography: {
36
+ fontFamily: 'var(--font-geist-sans)',
37
+ h1: {
38
+ fontSize: '2.5rem',
39
+ fontWeight: 700,
40
+ color: '#1e293b',
41
+ },
42
+ h6: {
43
+ fontSize: '1.125rem',
44
+ fontWeight: 500,
45
+ color: '#475569',
46
+ }
47
+ },
48
+ components: {
49
+ MuiButton: {
50
+ styleOverrides: {
51
+ root: {
52
+ textTransform: 'none',
53
+ padding: '12px 24px',
54
+ boxShadow: 'none',
55
+ '&:hover': {
56
+ boxShadow: 'none',
57
+ }
58
+ },
59
+ },
60
+ },
61
+ MuiTextField: {
62
+ styleOverrides: {
63
+ root: {
64
+ '& .MuiOutlinedInput-root': {
65
+ backgroundColor: '#f8fafc',
66
+ }
67
+ }
68
+ }
69
+ },
70
+ MuiPaper: {
71
+ styleOverrides: {
72
+ root: {
73
+ boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
74
+ }
75
+ }
76
+ }
77
+ },
78
+ })
tailwind.config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ export default {
4
+ content: [
5
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ colors: {
12
+ background: "var(--background)",
13
+ foreground: "var(--foreground)",
14
+ },
15
+ },
16
+ },
17
+ plugins: [],
18
+ } satisfies Config;
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }