├── .env.example ├── .gitignore ├── README.md ├── app screenshots ├── Final output with report.png └── Researching.png ├── components.json ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── background.jpg ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── api │ │ ├── deep-research │ │ │ ├── activity-tracker.ts │ │ │ ├── constants.ts │ │ │ ├── main.ts │ │ │ ├── model-caller.ts │ │ │ ├── prompts.ts │ │ │ ├── research-functions.ts │ │ │ ├── route.ts │ │ │ ├── services.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ └── generate-questions │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ └── ui │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── collapsible.tsx │ │ ├── deep-research │ │ ├── CompletedQuestions.tsx │ │ ├── QnA.tsx │ │ ├── QuestionForm.tsx │ │ ├── ResearchActivities.tsx │ │ ├── ResearchReport.tsx │ │ ├── ResearchTimer.tsx │ │ └── UserInput.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── lib │ └── utils.ts └── store │ └── deepResearch.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | OPENROUTER_API_KEY='add your openrouter api key here' 2 | EXA_SEARCH_API_KEY='add your exa search api key here' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env.local 35 | .env.development 36 | .env.production 37 | .env 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | next-env.d.ts 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Deep Research AI Agent with Next.js, Vercel AI SDk and LLMs like Gemini, Deepseek & Gpt-4o 2 | 3 | A powerful Deep Research AI agent like Gemini or ChatGPT. Using Next.js, Vercel AI SDK, and Exa Search API, An intelligent system that generates follow-up questions, crafts optimal search queries, and compiles comprehensive research reports. 4 | 5 | ![GitHub stars](https://img.shields.io/github/stars/codebucks27/Deep-Research-AI-Agent?style=social&logo=ApacheSpark&label=Stars)   6 | ![GitHub forks](https://img.shields.io/github/forks/codebucks27/Deep-Research-AI-Agent?style=social&logo=KashFlow&maxAge=3600)   7 | ![Github Followers](https://img.shields.io/github/followers/codebucks27.svg?style=social&label=Follow)  
8 | 9 | If you want to learn how to create it please follow below tutorial👇:
10 | ➡ Tutorial Link 💚: [Deep research ai agent](https://youtu.be/zKN18GQBxCM)
11 | 12 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/zKN18GQBxCM/0.jpg)](https://www.youtube.com/watch?v=zKN18GQBxCM) 13 | 14 | 🎯 For customised solutions or deployment please contact: https://tally.so/r/wdlj0N 15 | 16 | #### ⭐DO NOT FORGET TO STAR THIS REPO⭐ 17 | 18 | ![deep research Ai Agent](https://github.com/codebucks27/Deep-Research-AI-Agent/blob/main/app%20screenshots/Final%20output%20with%20report.png) 19 | 20 | ## 🚀 Key Features 21 | 22 | - 🔧 Fully Customizable Research Flow 23 | - 🔍 Adaptive Search Queries 24 | - ⚙️ Seamless LLM Integration 25 | - 💼 Modular Components 26 | - 🌐 Next.js & Vercel AI SDK 27 | - ♻️ Iterative Research Loop 28 | 29 | ## 🛠️ Tech Stack 30 | 31 | - **Framework:** Next.js 15 (App Router) 32 | - **Styling:** Tailwind CSS, Shadcn UI 33 | - **AI Integration:** Vercel AI SDK 34 | - **LLMs:** GPT-4o, Gemini, Deepseel using OpenRouter 35 | - **Web Search:** Exa Search API 36 | - **UI Components:** Shadcn 37 | - **Language:** TypeScript 38 | 39 | ## ⚡ Prerequisites 40 | 41 | Before you begin, ensure you have: 42 | 43 | - OpenRouter API key (or you can use grok or any other LLM providers) 44 | - Exa search API key 45 | 46 | ## 🚀 Setup Instructions 47 | 48 | ### 1. Clone the Repository 49 | 50 | ```bash 51 | git clone [repo-url] 52 | cd Deep-Research-AI-Agent 53 | ``` 54 | 55 | ### 2. Install Dependencies 56 | 57 | > **NOTE:** When installing the required dependencies, use the `--legacy-peer-deps` flag if you encounter any issues with inter-dependent dependencies. 58 | 59 | ```bash 60 | npm install 61 | # or 62 | yarn install 63 | # or 64 | pnpm install 65 | ``` 66 | 67 | ### 3. Environment Variables 68 | 69 | Create a `.env.local` file in the root directory. Check `.env.example` for required variables. 70 | 71 | ### 4. Start Development Server 72 | 73 | ```bash 74 | npm run dev 75 | # or 76 | yarn dev 77 | # or 78 | pnpm dev 79 | ``` 80 | 81 | Visit `http://localhost:3000` to see your app. 82 | 83 | ## 🌟 Show Your Support 84 | 85 | Give a ⭐️ if this project helped you! 86 | 87 | If you have any question or want a custom build for your business, you can reach out to me via: 88 | 89 | - E-mail : codebucks27@gmail.com 90 | - Twitter: https://twitter.com/code_bucks 91 | - Instagram: https://www.instagram.com/code.bucks/ 92 | 93 | MyChannel: https://www.youtube.com/codebucks 94 | My Website: https://devdreaming.com/ 95 | -------------------------------------------------------------------------------- /app screenshots/Final output with report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codebucks27/Deep-Research-AI-Agent/274eff7e2c22b10b28a9f9fcbfe6db54e2d991f6/app screenshots/Final output with report.png -------------------------------------------------------------------------------- /app screenshots/Researching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codebucks27/Deep-Research-AI-Agent/274eff7e2c22b10b28a9f9fcbfe6db54e2d991f6/app screenshots/Researching.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | compiler:{ 6 | removeConsole: process.env.NODE_ENV === "production" ? true : false, 7 | } 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-research-ai-agent", 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 | "@ai-sdk/react": "^1.1.21", 13 | "@hookform/resolvers": "^4.1.3", 14 | "@openrouter/ai-sdk-provider": "^0.4.3", 15 | "@opentelemetry/api": "^1.9.0", 16 | "@radix-ui/react-accordion": "^1.2.3", 17 | "@radix-ui/react-collapsible": "^1.1.3", 18 | "@radix-ui/react-label": "^2.1.2", 19 | "@radix-ui/react-progress": "^1.1.2", 20 | "@radix-ui/react-radio-group": "^1.2.3", 21 | "@radix-ui/react-select": "^2.1.6", 22 | "@radix-ui/react-slot": "^1.1.2", 23 | "@radix-ui/react-tabs": "^1.1.3", 24 | "@radix-ui/react-tooltip": "^1.1.8", 25 | "ai": "^4.1.54", 26 | "class-variance-authority": "^0.7.1", 27 | "clsx": "^2.1.1", 28 | "date-fns": "^4.1.0", 29 | "exa-js": "^1.5.11", 30 | "lucide-react": "^0.477.0", 31 | "next": "15.2.4", 32 | "next-themes": "^0.4.4", 33 | "react": "^19.0.0", 34 | "react-dom": "^19.0.0", 35 | "react-hook-form": "^7.54.2", 36 | "react-markdown": "^10.1.0", 37 | "react-syntax-highlighter": "^15.6.1", 38 | "remark-gfm": "^4.0.1", 39 | "sonner": "^2.0.1", 40 | "tailwind-merge": "^3.0.2", 41 | "tailwindcss-animate": "^1.0.7", 42 | "zod": "^3.24.2", 43 | "zustand": "^5.0.3" 44 | }, 45 | "devDependencies": { 46 | "@eslint/eslintrc": "^3", 47 | "@tailwindcss/postcss": "^4", 48 | "@tailwindcss/typography": "^0.5.16", 49 | "@types/node": "^20", 50 | "@types/react": "^19", 51 | "@types/react-dom": "^19", 52 | "@types/react-syntax-highlighter": "^15.5.13", 53 | "eslint": "^9", 54 | "eslint-config-next": "15.2.1", 55 | "tailwindcss": "^4", 56 | "typescript": "^5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codebucks27/Deep-Research-AI-Agent/274eff7e2c22b10b28a9f9fcbfe6db54e2d991f6/public/background.jpg -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/deep-research/activity-tracker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Activity, ResearchState } from './types'; 3 | 4 | 5 | export const createActivityTracker = (dataStream: any, researchState: ResearchState) => { 6 | 7 | return { 8 | add: (type: Activity['type'], status: Activity['status'], message: Activity['message'] ) => { 9 | dataStream.writeData({ 10 | type: "activity", 11 | content:{ 12 | type, 13 | status, 14 | message, 15 | timestamp: Date.now(), 16 | completedSteps: researchState.completedSteps, 17 | tokenUsed: researchState.tokenUsed 18 | } 19 | }) 20 | } 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/app/api/deep-research/constants.ts: -------------------------------------------------------------------------------- 1 | // Research constants 2 | export const MAX_ITERATIONS = 3; // Maximum number of iterations 3 | export const MAX_SEARCH_RESULTS = 5; // Maximum number of search results 4 | export const MAX_CONTENT_CHARS = 20000; // Maximum number of characters in the content 5 | export const MAX_RETRY_ATTEMPTS = 3; // It is the number of times the model will try to call LLMs if it fails 6 | export const RETRY_DELAY_MS = 1000; // It is the delay in milliseconds between retries for the model to call LLMs 7 | 8 | // Model names 9 | export const MODELS = { 10 | PLANNING: "openai/gpt-4o", 11 | EXTRACTION: "openai/gpt-4o-mini", 12 | ANALYSIS: "openai/gpt-4o", 13 | REPORT: "google/gemini-2.0-flash-thinking-exp:free" 14 | // REPORT: "anthropic/claude-3.7-sonnet:thinking", 15 | }; -------------------------------------------------------------------------------- /src/app/api/deep-research/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { createActivityTracker } from "./activity-tracker"; 3 | import { MAX_ITERATIONS } from "./constants"; 4 | import { analyzeFindings, generateReport, generateSearchQueries, processSearchResults, search } from "./research-functions"; 5 | import { ResearchState } from "./types"; 6 | 7 | 8 | export async function deepResearch(researchState: ResearchState, dataStream: any){ 9 | 10 | let iteration = 0; 11 | 12 | const activityTracker = createActivityTracker(dataStream, researchState); 13 | 14 | const initialQueries = await generateSearchQueries(researchState, activityTracker) 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | let currentQueries = (initialQueries as any).searchQueries 17 | while(currentQueries && currentQueries.length > 0 && iteration <= MAX_ITERATIONS){ 18 | iteration++; 19 | 20 | console.log("We are running on the itration number: ", iteration); 21 | 22 | const searchResults = currentQueries.map((query: string) => search(query, researchState, activityTracker)); 23 | const searchResultsResponses = await Promise.allSettled(searchResults) 24 | 25 | const allSearchResults = searchResultsResponses.filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled' && result.value.length > 0).map(result => result.value).flat() 26 | 27 | console.log(`We got ${allSearchResults.length} search results!`) 28 | 29 | const newFindings = await processSearchResults( 30 | allSearchResults, researchState, activityTracker 31 | ) 32 | 33 | console.log("Results are processed!") 34 | 35 | researchState.findings = [...researchState.findings, ...newFindings] 36 | 37 | const analysis = await analyzeFindings( 38 | researchState, 39 | currentQueries, 40 | iteration, 41 | activityTracker 42 | ) 43 | 44 | console.log("Analysis: ", analysis) 45 | 46 | if((analysis as any).sufficient){ 47 | break; 48 | } 49 | 50 | 51 | currentQueries = ((analysis as any).queries || []).filter((query:string) => !currentQueries.includes(query)); 52 | } 53 | 54 | console.log("We are outside of the loop with total iterations: ", iteration) 55 | 56 | const report = await generateReport(researchState, activityTracker); 57 | 58 | dataStream.writeData({ 59 | type: "report", 60 | content: report 61 | }) 62 | 63 | // console.log("REPORT: ", report) 64 | 65 | return initialQueries; 66 | 67 | } -------------------------------------------------------------------------------- /src/app/api/deep-research/model-caller.ts: -------------------------------------------------------------------------------- 1 | import { generateObject, generateText } from "ai"; 2 | import { openrouter } from "./services"; 3 | import { ActivityTracker, ModelCallOptions, ResearchState } from "./types"; 4 | import { MAX_RETRY_ATTEMPTS, RETRY_DELAY_MS } from "./constants"; 5 | import { delay } from "./utils"; 6 | 7 | 8 | export async function callModel({ 9 | model, prompt, system, schema, activityType = "generate" 10 | }: ModelCallOptions, 11 | researchState: ResearchState,activityTracker: ActivityTracker ): Promise{ 12 | 13 | let attempts = 0; 14 | let lastError: Error | null = null; 15 | 16 | while(attempts < MAX_RETRY_ATTEMPTS){ 17 | try{ 18 | if(schema){ 19 | 20 | const { object, usage } = await generateObject({ 21 | model: openrouter(model), 22 | prompt, 23 | system, 24 | schema: schema 25 | }); 26 | 27 | researchState.tokenUsed += usage.totalTokens; 28 | researchState.completedSteps++ 29 | 30 | return object; 31 | }else{ 32 | 33 | const { text, usage } = await generateText({ 34 | model: openrouter(model), 35 | prompt, 36 | system, 37 | }); 38 | 39 | researchState.tokenUsed += usage.totalTokens; 40 | researchState.completedSteps++ 41 | 42 | return text; 43 | } 44 | }catch(error){ 45 | attempts++; 46 | lastError = error instanceof Error ? error : new Error('Unknown error'); 47 | 48 | if(attempts < MAX_RETRY_ATTEMPTS){ 49 | activityTracker.add(activityType, 'warning', `Model call failed, attempt ${attempts}/${MAX_RETRY_ATTEMPTS}. Retrying...`) 50 | } 51 | await delay(RETRY_DELAY_MS*attempts) 52 | } 53 | } 54 | 55 | throw lastError || new Error(`Failed after ${MAX_RETRY_ATTEMPTS} attempst!`) 56 | } -------------------------------------------------------------------------------- /src/app/api/deep-research/prompts.ts: -------------------------------------------------------------------------------- 1 | export const EXTRACTION_SYSTEM_PROMPT = ` 2 | You are a senior technical documentation writer working in R&D department of a company. 3 | 4 | Your team needs a clear, actionable summary of the content to share with the other departments. The summary will be used to guide the comprehensive research on the topic. 5 | 6 | Create a comprehensive technical summary of the given content that can be used to guide the comprehensive research on the given topic and clarifications. 7 | 8 | Content is relevant if it directly addresses aspects of the main topic and clarifications, contains factual information rather than opinions, and provides depth on the subject matter. 9 | 10 | Maintain technical accuracy while making it accessible to the other departments. Include specific examples, code snippets, and other details mentioned in the content to illustrate key points. Provde response in JSON format. 11 | 12 | Format the summary in markdown using: 13 | - Main title as H1 (#) 14 | - Major sections as H2 (##) 15 | - Subsections as H3 (###) 16 | - Bullet points for lists 17 | - Bold for key terms and concepts 18 | - Code blocks for any technical examples 19 | - Block quotes for direct quotations`; 20 | 21 | export const getExtractionPrompt = (content: string, topic: string, clarificationsText: string) => 22 | `Here is the content: ${content} and here is the topic: ${topic}, 23 | ${clarificationsText} 24 | `; 25 | 26 | 27 | export const ANALYSIS_SYSTEM_PROMPT = `You are an expert research analyst. Your task is to analyze the provided content and determine if it contains enough substantive information to create a comprehensive report on the given topic. 28 | 29 | Remember the current year is ${new Date().getFullYear()}. 30 | 31 | Sufficient content must: 32 | - Cover the core aspects of the topic 33 | - Provide factual information from credible sources 34 | - Include enough detail to support a comprehensive report 35 | - Address the key points mentioned in the topic clarifications 36 | 37 | Your assessment should be PRACTICAL and REALISTIC. If there is enough information to write a useful report, even if not perfect, consider it sufficient. Remember: collecting more information has diminishing returns after a certain point. 38 | 39 | In later iterations, be more lenient in your assessment as we approach the maximum iteration limit. 40 | 41 | If the content is sufficient (output format): 42 | { 43 | "sufficient": true, 44 | "gaps": ["List any minor gaps that exist but don't require additional searches"], 45 | "queries": [] 46 | } 47 | 48 | If the content is not sufficient (output format): 49 | { 50 | "sufficient": false, 51 | "gaps": ["List specific information missing from the content"], 52 | "queries": ["1-3 highly targeted search queries to fill the identified gaps"] 53 | } 54 | 55 | On iteration MAX_ITERATIONS-1 or later, strongly consider marking as sufficient unless critical information is completely missing.`; 56 | 57 | export const getAnalysisPrompt = (contentText: string, topic: string, clarificationsText: string, currentQueries: string[], currentIteration: number, maxIterations: number, findingsLength: number) => 58 | `Analyze the following content and determine if it's sufficient for a comprehensive report: 59 | 60 | Topic: ${topic} 61 | 62 | Topic Clarifications: 63 | ${clarificationsText} 64 | 65 | Content: 66 | ${contentText} 67 | 68 | Previous queries: 69 | ${currentQueries.join(", ")} 70 | 71 | Current Research State: 72 | - This is iteration ${currentIteration} of a maximum ${maxIterations} iterations 73 | - We have collected ${findingsLength} distinct findings so far 74 | - Previous attempts at information gathering have yielded ${contentText.length} characters of content`; 75 | 76 | 77 | 78 | 79 | export const PLANNING_SYSTEM_PROMPT = ` 80 | You are a senior project manager. You are responsible for the research on the topic. 81 | 82 | Remember the current year is ${new Date().getFullYear()}. 83 | 84 | You need to find the most relevant content on the given topic. Based on the given topic and clarifications you need to generate the right search queries that can be used to cover the topic and find the most relevant content which can be used to write the comprehensive report. Create diverse queries that target different aspects of the topic. 85 | 86 | You need to generate the search queries in a way that can be used to find the most relevant content which can be used to write the comprehensive report. 87 | `; 88 | export const getPlanningPrompt = (topic: string, clarificationsText: string) => 89 | `Here is the topic: ${topic} and 90 | Here is the topic clarifications: 91 | ${clarificationsText}`; 92 | 93 | 94 | 95 | 96 | export const REPORT_SYSTEM_PROMPT = ` 97 | You are a senior technical documentation writer with deep expertise across many technical domains. 98 | 99 | Your goal is to create a comprehensive, authoritative report on the provided topic that combines: 100 | 1. The provided research findings when they are relevant and accurate 101 | 2. Your own domain expertise and general knowledge to: 102 | - Fill in any gaps in the research 103 | - Provide additional context, explanations, or examples 104 | - Correct any outdated or inaccurate information in the findings (only if you are sure) 105 | - Ensure complete coverage of all important aspects of the topic 106 | 107 | The report should be comprehensive even if the provided research findings are minimal or incomplete. 108 | 109 | Important: You should prioritize being helpful, accurate and thorough over strictly limiting yourself to only the provided content. If the research findings don't adequately cover important aspects of the topic, use your knowledge to fill these gaps. 110 | 111 | Format the report in markdown using: 112 | - Main title as H1 (#) 113 | - Major sections as H2 (##) 114 | - Subsections as H3 (###) 115 | - Bullet points for lists 116 | - Bold for key terms and concepts 117 | - Code blocks for any technical examples with language name 118 | - Block quotes for direct quotations 119 | 120 | At the end include: 121 | 1. A "Sources" section listing references from the provided findings as links (if any, if not then don't include it) 122 | 2. A "Further Reading" section with additional resources you recommend as links (if any, if not then don't include it) 123 | 124 | Remember the current year is ${new Date().getFullYear()}. 125 | 126 | You must provide the report in markdown format. Enclose the report in tags.`; 127 | 128 | 129 | export const getReportPrompt = (contentText: string, topic: string, clarificationsText: string) => 130 | `Please generate the comprehensive report using the content. 131 | Here is the topic: ${topic} 132 | Here is the topic clarifications: 133 | ${clarificationsText} 134 | I've gathered the following research findings to help with this report: 135 | ${contentText}`; -------------------------------------------------------------------------------- /src/app/api/deep-research/research-functions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | ActivityTracker, 4 | ResearchFindings, 5 | ResearchState, 6 | SearchResult, 7 | } from "./types"; 8 | import { z } from "zod"; 9 | import { 10 | ANALYSIS_SYSTEM_PROMPT, 11 | EXTRACTION_SYSTEM_PROMPT, 12 | getAnalysisPrompt, 13 | getExtractionPrompt, 14 | getPlanningPrompt, 15 | getReportPrompt, 16 | PLANNING_SYSTEM_PROMPT, 17 | REPORT_SYSTEM_PROMPT, 18 | } from "./prompts"; 19 | import { callModel } from "./model-caller"; 20 | import { exa } from "./services"; 21 | import { combineFindings, handleError } from "./utils"; 22 | import { 23 | MAX_CONTENT_CHARS, 24 | MAX_ITERATIONS, 25 | MAX_SEARCH_RESULTS, 26 | MODELS, 27 | } from "./constants"; 28 | 29 | export async function generateSearchQueries( 30 | researchState: ResearchState, 31 | activityTracker: ActivityTracker 32 | ) { 33 | try{ 34 | activityTracker.add("planning","pending","Planning the research"); 35 | 36 | const result = await callModel( 37 | { 38 | model: MODELS.PLANNING, 39 | prompt: getPlanningPrompt( 40 | researchState.topic, 41 | researchState.clerificationsText 42 | ), 43 | system: PLANNING_SYSTEM_PROMPT, 44 | schema: z.object({ 45 | searchQueries: z 46 | .array(z.string()) 47 | .describe( 48 | "The search queries that can be used to find the most relevant content which can be used to write the comprehensive report on the given topic. (max 3 queries)" 49 | ), 50 | }), 51 | activityType: "planning" 52 | }, 53 | researchState, activityTracker 54 | ); 55 | 56 | activityTracker.add("planning", "complete", "Crafted the research plan"); 57 | 58 | return result; 59 | }catch(error){ 60 | return handleError(error, `Research planning`, activityTracker, "planning", { 61 | searchQueries: [`${researchState.topic} best practices`,`${researchState.topic} guidelines`, `${researchState.topic} examples` ] 62 | }) 63 | 64 | } 65 | } 66 | 67 | export async function search( 68 | query: string, 69 | researchState: ResearchState, 70 | activityTracker: ActivityTracker 71 | ): Promise { 72 | 73 | activityTracker.add("search","pending",`Searching for ${query}`); 74 | 75 | try { 76 | const searchResult = await exa.searchAndContents(query, { 77 | type: "keyword", 78 | numResults: MAX_SEARCH_RESULTS, 79 | startPublishedDate: new Date( 80 | Date.now() - 365 * 24 * 60 * 60 * 1000 81 | ).toISOString(), 82 | endPublishedDate: new Date().toISOString(), 83 | startCrawlDate: new Date( 84 | Date.now() - 365 * 24 * 60 * 60 * 1000 85 | ).toISOString(), 86 | endCrawlDate: new Date().toISOString(), 87 | excludeDomains: ["https://youtube.com"], 88 | text: { 89 | maxCharacters: MAX_CONTENT_CHARS, 90 | }, 91 | }); 92 | 93 | const filteredResults = searchResult.results 94 | .filter((r) => r.title && r.text !== undefined) 95 | .map((r) => ({ 96 | title: r.title || "", 97 | url: r.url, 98 | content: r.text || "", 99 | })); 100 | 101 | researchState.completedSteps++; 102 | 103 | activityTracker.add("search","complete",`Found ${filteredResults.length} results for ${query}`); 104 | 105 | 106 | return filteredResults; 107 | } catch (error) { 108 | console.log("error: ", error); 109 | return handleError(error, `Searching for ${query}`, activityTracker, "search", []) || [] 110 | } 111 | } 112 | 113 | export async function extractContent( 114 | content: string, 115 | url: string, 116 | researchState: ResearchState, 117 | activityTracker: ActivityTracker 118 | ) { 119 | 120 | try{ 121 | activityTracker.add("extract","pending",`Extracting content from ${url}`); 122 | 123 | const result = await callModel( 124 | { 125 | model: MODELS.EXTRACTION, 126 | prompt: getExtractionPrompt( 127 | content, 128 | researchState.topic, 129 | researchState.clerificationsText 130 | ), 131 | system: EXTRACTION_SYSTEM_PROMPT, 132 | schema: z.object({ 133 | summary: z.string().describe("A comprehensive summary of the content"), 134 | }), 135 | activityType: "extract" 136 | }, 137 | researchState, activityTracker 138 | ); 139 | 140 | activityTracker.add("extract","complete",`Extracted content from ${url}`); 141 | 142 | return { 143 | url, 144 | summary: (result as any).summary, 145 | }; 146 | }catch(error){ 147 | return handleError(error, `Content extraction from ${url}`, activityTracker, "extract", null) || null 148 | } 149 | } 150 | 151 | export async function processSearchResults( 152 | searchResults: SearchResult[], 153 | researchState: ResearchState, 154 | activityTracker: ActivityTracker 155 | ): Promise { 156 | const extractionPromises = searchResults.map((result) => 157 | extractContent(result.content, result.url, researchState, activityTracker) 158 | ); 159 | const extractionResults = await Promise.allSettled(extractionPromises); 160 | 161 | type ExtractionResult = { url: string; summary: string }; 162 | 163 | const newFindings = extractionResults 164 | .filter( 165 | (result): result is PromiseFulfilledResult => 166 | result.status === "fulfilled" && 167 | result.value !== null && 168 | result.value !== undefined 169 | ) 170 | .map((result) => { 171 | const { summary, url } = result.value; 172 | return { 173 | summary, 174 | source: url, 175 | }; 176 | }); 177 | 178 | return newFindings; 179 | } 180 | 181 | export async function analyzeFindings( 182 | researchState: ResearchState, 183 | currentQueries: string[], 184 | currentIteration: number, 185 | activityTracker: ActivityTracker 186 | ) { 187 | try { 188 | activityTracker.add("analyze","pending",`Analyzing research findings (iteration ${currentIteration}) of ${MAX_ITERATIONS}`); 189 | const contentText = combineFindings(researchState.findings); 190 | 191 | const result = await callModel( 192 | { 193 | model: MODELS.ANALYSIS, 194 | prompt: getAnalysisPrompt( 195 | contentText, 196 | researchState.topic, 197 | researchState.clerificationsText, 198 | currentQueries, 199 | currentIteration, 200 | MAX_ITERATIONS, 201 | contentText.length 202 | ), 203 | system: ANALYSIS_SYSTEM_PROMPT, 204 | schema: z.object({ 205 | sufficient: z 206 | .boolean() 207 | .describe( 208 | "Whether the collected content is sufficient for a useful report" 209 | ), 210 | gaps: z.array(z.string()).describe("Identified gaps in the content"), 211 | queries: z 212 | .array(z.string()) 213 | .describe( 214 | "Search queries for missing informationo. Max 3 queries." 215 | ), 216 | }), 217 | activityType: "analyze" 218 | }, 219 | researchState, activityTracker 220 | ); 221 | 222 | const isContentSufficient = typeof result !== 'string' && result.sufficient; 223 | 224 | activityTracker.add("analyze","complete",`Analyzed collected research findings: ${isContentSufficient ? 'Content is sufficient' : 'More research is needed!'}`); 225 | 226 | return result; 227 | } catch (error) { 228 | return handleError(error, `Content analysis`, activityTracker, "analyze", { 229 | sufficient: false, 230 | gaps: ["Unable to analyz content"], 231 | queries: ["Please try a different search query"] 232 | }) 233 | } 234 | } 235 | 236 | export async function generateReport(researchState: ResearchState, activityTracker: ActivityTracker) { 237 | try { 238 | activityTracker.add("generate","pending",`Geneating comprehensive report!`); 239 | 240 | const contentText = combineFindings(researchState.findings); 241 | 242 | const report = await callModel( 243 | { 244 | model: MODELS.REPORT, 245 | prompt: getReportPrompt( 246 | contentText, 247 | researchState.topic, 248 | researchState.clerificationsText 249 | ), 250 | system: REPORT_SYSTEM_PROMPT, 251 | activityType: "generate" 252 | }, 253 | researchState, activityTracker 254 | ); 255 | 256 | activityTracker.add("generate","complete",`Generated comprehensive report, Total tokens used: ${researchState.tokenUsed}. Research completed in ${researchState.completedSteps} steps.`); 257 | 258 | return report; 259 | } catch (error) { 260 | console.log(error); 261 | return handleError(error, `Report Generation`, activityTracker, "generate", "Error generating report. Please try again. ") 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/app/api/deep-research/route.ts: -------------------------------------------------------------------------------- 1 | import { createDataStreamResponse } from "ai"; 2 | import { ResearchState } from "./types"; 3 | import { deepResearch } from "./main"; 4 | 5 | export async function POST(req: Request) { 6 | try { 7 | const {messages } = await req.json(); 8 | 9 | const lastMessageContent = messages[messages.length - 1].content; 10 | 11 | const parsed = JSON.parse(lastMessageContent); 12 | 13 | const topic = parsed.topic; 14 | const clerifications = parsed.clerifications; 15 | 16 | 17 | return createDataStreamResponse({ 18 | execute: async (dataStream) => { 19 | // Write data 20 | // dataStream.writeData({ value: 'Hello' }); 21 | 22 | const researchState: ResearchState = { 23 | topic: topic, 24 | completedSteps: 0, 25 | tokenUsed: 0, 26 | findings: [], 27 | processedUrl: new Set(), 28 | clerificationsText: JSON.stringify(clerifications) 29 | } 30 | await deepResearch(researchState, dataStream) 31 | 32 | 33 | }, 34 | // onError: error => `Custom error: ${error.message}`, 35 | }); 36 | } catch (error) { 37 | 38 | return new Response( 39 | JSON.stringify({ 40 | success: false, 41 | error: error instanceof Error ? error.message: "Invalid message format!" 42 | }), 43 | { status: 200 } 44 | ); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/api/deep-research/services.ts: -------------------------------------------------------------------------------- 1 | import { createOpenRouter } from '@openrouter/ai-sdk-provider'; 2 | import Exa from "exa-js" 3 | 4 | export const exa = new Exa(process.env.EXA_SEARCH_API_KEY || ""); 5 | 6 | export const openrouter = createOpenRouter({ 7 | apiKey: process.env.OPENROUTER_API_KEY || "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/api/deep-research/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | 4 | export interface ResearchFindings { 5 | summary: string, 6 | source: string 7 | } 8 | 9 | export interface ResearchState { 10 | topic: string, 11 | completedSteps: number, 12 | tokenUsed: number, 13 | findings: ResearchFindings[], 14 | processedUrl: Set, 15 | clerificationsText: string 16 | } 17 | 18 | export interface ModelCallOptions{ 19 | model: string; 20 | prompt: string; 21 | system: string; 22 | schema?: z.ZodType; 23 | activityType?: Activity["type"] 24 | } 25 | 26 | export interface SearchResult{ 27 | title: string; 28 | url: string; 29 | content: string 30 | } 31 | 32 | export interface Activity{ 33 | type: 'search' | 'extract' | 'analyze' | 'generate' | 'planning'; 34 | status: 'pending' | 'complete' | 'warning' | 'error'; 35 | message: string; 36 | timestamp?: number; 37 | } 38 | 39 | export type ActivityTracker = { 40 | add: (type: Activity['type'], status: Activity['status'], message: Activity['message']) => void; 41 | } 42 | 43 | 44 | export interface Source { 45 | url: string; 46 | title: string 47 | } -------------------------------------------------------------------------------- /src/app/api/deep-research/utils.ts: -------------------------------------------------------------------------------- 1 | import { Activity, ActivityTracker, ResearchFindings } from "./types"; 2 | 3 | export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 4 | 5 | export const combineFindings = (findings: ResearchFindings[]) : string => { 6 | return findings.map(finding => `${finding.summary}\n\n Source: ${finding.source}`).join('\n\n---\n\n') 7 | } 8 | 9 | export const handleError = (error: unknown, context: string,activityTracker?:ActivityTracker, activityType?: Activity["type"], fallbackReturn?: T) =>{ 10 | 11 | const errorMessage = error instanceof Error ? error.message : 'Unkown error'; 12 | 13 | if(activityTracker && activityType){ 14 | activityTracker.add(activityType, "error", `${context} failed" ${errorMessage}`) 15 | } 16 | return fallbackReturn 17 | } -------------------------------------------------------------------------------- /src/app/api/generate-questions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { generateObject } from 'ai'; 3 | import { createOpenRouter } from '@openrouter/ai-sdk-provider'; 4 | import { z } from "zod"; 5 | 6 | 7 | const openrouter = createOpenRouter({ 8 | apiKey: process.env.OPENROUTER_API_KEY || "", 9 | }); 10 | 11 | const clarifyResearchGoals = async (topic: string) => { 12 | 13 | const prompt = ` 14 | Given the research topic ${topic}, generate2-4 clarifying questions to help narrow down the research scope. Focus on identifying: 15 | - Specifi aspects of interest 16 | - Required depth/complexity level 17 | - Any particular perspective or excluded sources 18 | ` 19 | 20 | try{ 21 | const { object } = await generateObject({ 22 | model: openrouter("meta-llama/llama-3.3-70b-instruct"), 23 | prompt, 24 | schema: z.object({ 25 | questions: z.array(z.string()) 26 | }) 27 | }); 28 | 29 | return object.questions; 30 | }catch(error){ 31 | console.log("Error while generating questions: ", error) 32 | 33 | } 34 | } 35 | 36 | 37 | export async function POST(req: Request){ 38 | 39 | const {topic} = await req.json(); 40 | console.log("Topic: ", topic); 41 | 42 | try{ 43 | const questions = await clarifyResearchGoals(topic); 44 | console.log("Questions: ", questions) 45 | 46 | return NextResponse.json(questions) 47 | }catch(error){ 48 | 49 | console.error("Error while generating questions: ", error) 50 | return NextResponse.json({ 51 | success: false, error: "Failed to generate questions" 52 | }, {status: 500}) 53 | 54 | } 55 | 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codebucks27/Deep-Research-AI-Agent/274eff7e2c22b10b28a9f9fcbfe6db54e2d991f6/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "tailwindcss-animate"; 4 | @plugin "@tailwindcss/typography"; 5 | 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | @theme inline { 9 | 10 | --font-dancing-script: var(--font-dancing-script); 11 | --font-inter: var(--font-inter); 12 | 13 | 14 | 15 | --color-background: var(--background); 16 | --color-foreground: var(--foreground); 17 | --font-sans: var(--font-geist-sans); 18 | --font-mono: var(--font-geist-mono); 19 | --color-sidebar-ring: var(--sidebar-ring); 20 | --color-sidebar-border: var(--sidebar-border); 21 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 22 | --color-sidebar-accent: var(--sidebar-accent); 23 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 24 | --color-sidebar-primary: var(--sidebar-primary); 25 | --color-sidebar-foreground: var(--sidebar-foreground); 26 | --color-sidebar: var(--sidebar); 27 | --color-chart-5: var(--chart-5); 28 | --color-chart-4: var(--chart-4); 29 | --color-chart-3: var(--chart-3); 30 | --color-chart-2: var(--chart-2); 31 | --color-chart-1: var(--chart-1); 32 | --color-ring: var(--ring); 33 | --color-input: var(--input); 34 | --color-border: var(--border); 35 | --color-destructive-foreground: var(--destructive-foreground); 36 | --color-destructive: var(--destructive); 37 | --color-accent-foreground: var(--accent-foreground); 38 | --color-accent: var(--accent); 39 | --color-muted-foreground: var(--muted-foreground); 40 | --color-muted: var(--muted); 41 | --color-secondary-foreground: var(--secondary-foreground); 42 | --color-secondary: var(--secondary); 43 | --color-primary-foreground: var(--primary-foreground); 44 | --color-primary: var(--primary); 45 | --color-popover-foreground: var(--popover-foreground); 46 | --color-popover: var(--popover); 47 | --color-card-foreground: var(--card-foreground); 48 | --color-card: var(--card); 49 | --radius-sm: calc(var(--radius) - 4px); 50 | --radius-md: calc(var(--radius) - 2px); 51 | --radius-lg: var(--radius); 52 | --radius-xl: calc(var(--radius) + 4px); 53 | } 54 | 55 | :root { 56 | --background: oklch(1 0 0); 57 | --foreground: oklch(0.145 0 0); 58 | --card: oklch(1 0 0); 59 | --card-foreground: oklch(0.145 0 0); 60 | --popover: oklch(1 0 0); 61 | --popover-foreground: oklch(0.145 0 0); 62 | --primary: oklch(0.205 0 0); 63 | --primary-foreground: oklch(0.985 0 0); 64 | --secondary: oklch(0.97 0 0); 65 | --secondary-foreground: oklch(0.205 0 0); 66 | --muted: oklch(0.97 0 0); 67 | --muted-foreground: oklch(0.556 0 0); 68 | --accent: oklch(0.97 0 0); 69 | --accent-foreground: oklch(0.205 0 0); 70 | --destructive: oklch(0.577 0.245 27.325); 71 | --destructive-foreground: oklch(0.577 0.245 27.325); 72 | --border: oklch(0.922 0 0); 73 | --input: oklch(0.922 0 0); 74 | --ring: oklch(0.708 0 0); 75 | --chart-1: oklch(0.646 0.222 41.116); 76 | --chart-2: oklch(0.6 0.118 184.704); 77 | --chart-3: oklch(0.398 0.07 227.392); 78 | --chart-4: oklch(0.828 0.189 84.429); 79 | --chart-5: oklch(0.769 0.188 70.08); 80 | --radius: 0.625rem; 81 | --sidebar: oklch(0.985 0 0); 82 | --sidebar-foreground: oklch(0.145 0 0); 83 | --sidebar-primary: oklch(0.205 0 0); 84 | --sidebar-primary-foreground: oklch(0.985 0 0); 85 | --sidebar-accent: oklch(0.97 0 0); 86 | --sidebar-accent-foreground: oklch(0.205 0 0); 87 | --sidebar-border: oklch(0.922 0 0); 88 | --sidebar-ring: oklch(0.708 0 0); 89 | } 90 | 91 | .dark { 92 | --background: oklch(0.145 0 0); 93 | --foreground: oklch(0.985 0 0); 94 | --card: oklch(0.145 0 0); 95 | --card-foreground: oklch(0.985 0 0); 96 | --popover: oklch(0.145 0 0); 97 | --popover-foreground: oklch(0.985 0 0); 98 | --primary: oklch(0.985 0 0); 99 | --primary-foreground: oklch(0.205 0 0); 100 | --secondary: oklch(0.269 0 0); 101 | --secondary-foreground: oklch(0.985 0 0); 102 | --muted: oklch(0.269 0 0); 103 | --muted-foreground: oklch(0.708 0 0); 104 | --accent: oklch(0.269 0 0); 105 | --accent-foreground: oklch(0.985 0 0); 106 | --destructive: oklch(0.396 0.141 25.723); 107 | --destructive-foreground: oklch(0.637 0.237 25.331); 108 | --border: oklch(0.269 0 0); 109 | --input: oklch(0.269 0 0); 110 | --ring: oklch(0.439 0 0); 111 | --chart-1: oklch(0.488 0.243 264.376); 112 | --chart-2: oklch(0.696 0.17 162.48); 113 | --chart-3: oklch(0.769 0.188 70.08); 114 | --chart-4: oklch(0.627 0.265 303.9); 115 | --chart-5: oklch(0.645 0.246 16.439); 116 | --sidebar: oklch(0.205 0 0); 117 | --sidebar-foreground: oklch(0.985 0 0); 118 | --sidebar-primary: oklch(0.488 0.243 264.376); 119 | --sidebar-primary-foreground: oklch(0.985 0 0); 120 | --sidebar-accent: oklch(0.269 0 0); 121 | --sidebar-accent-foreground: oklch(0.985 0 0); 122 | --sidebar-border: oklch(0.269 0 0); 123 | --sidebar-ring: oklch(0.439 0 0); 124 | } 125 | 126 | @layer base { 127 | * { 128 | @apply border-border outline-ring/50; 129 | } 130 | body { 131 | @apply bg-background text-foreground; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Dancing_Script, Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ 6 | variable: "--font-inter", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const dancingScript = Dancing_Script({ 11 | variable: "--font-dancing-script", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import QnA from "@/components/ui/deep-research/QnA"; 2 | import UserInput from "@/components/ui/deep-research/UserInput"; 3 | import Image from "next/image"; 4 | import bg from "../../public/background.jpg" 5 | 6 | export default function Home() { 7 | return ( 8 |
9 | 10 |
11 | Deep Research AI Agent 12 |
13 | 14 |
15 |

16 | Deep Research 17 |

18 |

19 | Enter a topic and answer a few questions to generate a comprehensive research report. 20 |

21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Accordion({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AccordionItem({ 16 | className, 17 | ...props 18 | }: React.ComponentProps) { 19 | return ( 20 | 25 | ) 26 | } 27 | 28 | function AccordionTrigger({ 29 | className, 30 | children, 31 | ...props 32 | }: React.ComponentProps) { 33 | return ( 34 | 35 | svg]:rotate-180", 39 | className 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | function AccordionContent({ 51 | className, 52 | children, 53 | ...props 54 | }: React.ComponentProps) { 55 | return ( 56 | 61 |
{children}
62 |
63 | ) 64 | } 65 | 66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 67 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<"button"> & 44 | VariantProps & { 45 | asChild?: boolean 46 | }) { 47 | const Comp = asChild ? Slot : "button" 48 | 49 | return ( 50 | 55 | ) 56 | } 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
25 | ) 26 | } 27 | 28 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 29 | return ( 30 |
35 | ) 36 | } 37 | 38 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 39 | return ( 40 |
45 | ) 46 | } 47 | 48 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 49 | return ( 50 |
55 | ) 56 | } 57 | 58 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 59 | return ( 60 |
65 | ) 66 | } 67 | 68 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 69 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | function Collapsible({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return 9 | } 10 | 11 | function CollapsibleTrigger({ 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 19 | ) 20 | } 21 | 22 | function CollapsibleContent({ 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 30 | ) 31 | } 32 | 33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 34 | -------------------------------------------------------------------------------- /src/components/ui/deep-research/CompletedQuestions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useDeepResearchStore } from '@/store/deepResearch' 3 | import React from 'react' 4 | import { 5 | Accordion, 6 | AccordionContent, 7 | AccordionItem, 8 | AccordionTrigger, 9 | } from "@/components/ui/accordion" 10 | 11 | const CompletedQuestions = () => { 12 | const {questions, answers, isCompleted} = useDeepResearchStore(); 13 | 14 | if(!isCompleted || questions.length === 0) return null; 15 | return ( 16 | 17 | 18 | 19 | Questions and Answers 20 | 21 | 22 |
23 | 24 | {questions.map((question, index) => ( 25 | 26 | 27 | 28 | Question {index + 1}: {question} 29 | 30 | 31 | 32 |

{answers[index]}

33 |
34 |
35 | ))} 36 |
37 |
38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | export default CompletedQuestions -------------------------------------------------------------------------------- /src/components/ui/deep-research/QnA.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | "use client"; 3 | import { useDeepResearchStore } from "@/store/deepResearch"; 4 | import React, { useEffect } from "react"; 5 | import QuestionForm from "./QuestionForm"; 6 | import { useChat } from "@ai-sdk/react"; 7 | import ResearchActivities from "./ResearchActivities"; 8 | import ResearchReport from "./ResearchReport"; 9 | import ResearchTimer from "./ResearchTimer"; 10 | import CompletedQuestions from "./CompletedQuestions"; 11 | 12 | const QnA = () => { 13 | const { 14 | questions, 15 | isCompleted, 16 | topic, 17 | answers, 18 | setIsLoading, 19 | setActivities, 20 | setSources, 21 | setReport, 22 | } = useDeepResearchStore(); 23 | 24 | const { append, data, isLoading} = useChat({ 25 | api: "/api/deep-research", 26 | }); 27 | 28 | 29 | useEffect(() => { 30 | if (!data) return; 31 | 32 | // extract activities and sources 33 | const messages = data as unknown[]; 34 | const activities = messages 35 | .filter( 36 | (msg) => typeof msg === "object" && (msg as any).type === "activity" 37 | ) 38 | .map((msg) => (msg as any).content); 39 | 40 | setActivities(activities); 41 | 42 | const sources = activities 43 | .filter( 44 | (activity) => 45 | activity.type === "extract" && activity.status === "complete" 46 | ) 47 | .map((activity) => { 48 | const url = activity.message.split("from ")[1]; 49 | return { 50 | url, 51 | title: url?.split("/")[2] || url, 52 | }; 53 | }); 54 | setSources(sources); 55 | const reportData = messages.find( 56 | (msg) => typeof msg === "object" && (msg as any).type === "report" 57 | ); 58 | const report = 59 | typeof (reportData as any)?.content === "string" 60 | ? (reportData as any).content 61 | : ""; 62 | setReport(report); 63 | 64 | setIsLoading(isLoading); 65 | }, [data, setActivities, setSources, setReport, setIsLoading, isLoading]); 66 | 67 | useEffect(() => { 68 | if (isCompleted && questions.length > 0) { 69 | const clarifications = questions.map((question, index) => ({ 70 | question: question, 71 | answer: answers[index], 72 | })); 73 | 74 | append({ 75 | role: "user", 76 | content: JSON.stringify({ 77 | topic: topic, 78 | clarifications: clarifications, 79 | }), 80 | }); 81 | } 82 | }, [isCompleted, questions, answers, topic, append]); 83 | 84 | if (questions.length === 0) return null; 85 | 86 | return ( 87 |
88 | 89 | 90 | 91 | 92 | 93 |
94 | ); 95 | }; 96 | 97 | export default QnA; 98 | -------------------------------------------------------------------------------- /src/components/ui/deep-research/QuestionForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { z } from "zod" 3 | import { zodResolver } from "@hookform/resolvers/zod" 4 | import { useForm } from "react-hook-form" 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | Form, 8 | FormControl, 9 | FormField, 10 | FormItem, 11 | FormMessage, 12 | } from "@/components/ui/form" 13 | import { Textarea } from '../textarea' 14 | import { 15 | Card, 16 | CardContent, 17 | CardHeader, 18 | CardTitle, 19 | } from "@/components/ui/card" 20 | import { useDeepResearchStore } from '@/store/deepResearch' 21 | 22 | const formSchema = z.object({ 23 | answer: z.string().min(1, "Answer is required!") 24 | }) 25 | 26 | 27 | 28 | const QuestionForm = () => { 29 | 30 | const {questions, currentQuestion, answers, setCurrentQuestion, setAnswers, setIsCompleted, isLoading, isCompleted} = useDeepResearchStore() 31 | 32 | const form = useForm>({ 33 | resolver: zodResolver(formSchema), 34 | defaultValues: { 35 | answer: answers[currentQuestion] || "", 36 | }, 37 | }) 38 | 39 | // 2. Define a submit handler. 40 | function onSubmit(values: z.infer) { 41 | // Do something with the form values. 42 | // ✅ This will be type-safe and validated. 43 | const newAnswers = [...answers]; 44 | newAnswers[currentQuestion] = values.answer; 45 | setAnswers(newAnswers) 46 | 47 | if(currentQuestion < questions.length - 1){ 48 | setCurrentQuestion(currentQuestion + 1); 49 | form.reset() 50 | }else{ 51 | setIsCompleted(true) 52 | } 53 | } 54 | 55 | if(isCompleted) return; 56 | 57 | if (questions.length === 0) return; 58 | 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | Question {currentQuestion + 1} of {questions.length} 66 | 67 | 68 | 69 |

{questions[currentQuestion]}

70 |
71 | 72 | ( 76 | 77 | 78 |