Spaces:
Sleeping
Sleeping
Upload 25 files
Browse files- next-env.d.ts +5 -0
- next.config.ts +7 -0
- package-lock.json +0 -0
- package.json +40 -0
- postcss.config.mjs +8 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- public/window.svg +1 -0
- src/app/api/qa/route.ts +68 -0
- src/app/api/upload/route.ts +74 -0
- src/app/favicon.ico +0 -0
- src/app/fonts/GeistMonoVF.woff +0 -0
- src/app/fonts/GeistVF.woff +0 -0
- src/app/globals.css +11 -0
- src/app/layout.tsx +34 -0
- src/app/page.tsx +9 -0
- src/components/FileUpload.tsx +131 -0
- src/components/Providers.tsx +31 -0
- src/components/QASystem.tsx +221 -0
- src/lib/createEmotionCache.ts +20 -0
- src/theme.ts +78 -0
- tailwind.config.ts +18 -0
- tsconfig.json +27 -0
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 |
+
}
|