├── .env.example ├── .gitignore ├── README.md ├── ai ├── agents │ └── deep-research.ts └── tools.ts ├── app ├── api │ └── chat │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── deep-research.tsx ├── markdown.tsx ├── message.tsx ├── steps.tsx ├── ui │ ├── alert.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── input.tsx │ ├── label.tsx │ ├── scroll-area.tsx │ ├── textarea.tsx │ └── tooltip.tsx └── use-scroll-to-bottom.tsx ├── eslint.config.mjs ├── lib └── utils.ts ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="" 2 | EXA_API_KEY="" -------------------------------------------------------------------------------- /.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 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /ai/agents/deep-research.ts: -------------------------------------------------------------------------------- 1 | // deepResearch(researchPrompt, depth (d), breadth (b)) 2 | // --> generate b search queries 3 | // ----> for each search query, search web, and then pass results to LLM to generate a list of (3) learnings, and follow up questions (b - based on overall research prompt) 4 | // ----> recursively call deepResearch(followUpQuestion, depth-1, Math.ceil(breadth/2)) until d = 0. 5 | 6 | import Exa from "exa-js"; 7 | import "dotenv/config"; 8 | import { DataStreamWriter, generateObject, generateText, tool } from "ai"; 9 | import { openai } from "@ai-sdk/openai"; 10 | import { z } from "zod"; 11 | 12 | export const SYSTEM_PROMPT = `You are an expert researcher. Today is ${new Date().toISOString()}. Follow these instructions when responding: 13 | - You may be asked to research subjects that is after your knowledge cutoff, assume the user is right when presented with news. 14 | - The user is a highly experienced analyst, no need to simplify it, be as detailed as possible and make sure your response is correct. 15 | - Be highly organized. 16 | - Suggest solutions that I didn't think about. 17 | - Be proactive and anticipate my needs. 18 | - Treat me as an expert in all subject matter. 19 | - Mistakes erode my trust, so be accurate and thorough. 20 | - Provide detailed explanations, I'm comfortable with lots of detail. 21 | - Value good arguments over authorities, the source is irrelevant. 22 | - Consider new technologies and contrarian ideas, not just the conventional wisdom. 23 | - You may use high levels of speculation or prediction, just flag it for me. 24 | - You must provide links to sources used. Ideally these are inline e.g. [this documentation](https://documentation.com/this) 25 | `; 26 | 27 | export const exa = new Exa(process.env.EXA_API_KEY); 28 | 29 | type SearchResult = { 30 | title: string; 31 | url: string; 32 | content: string; 33 | publishedDate: string; 34 | favicon: string; 35 | }; 36 | 37 | export type Research = { 38 | learnings: string[]; 39 | sources: SearchResult[]; 40 | questionsExplored: string[]; 41 | searchQueries: string[]; 42 | }; 43 | 44 | const searchWeb = async (query: string) => { 45 | const { results } = await exa.searchAndContents(query, { 46 | livecrawl: "always", 47 | numResults: 3, 48 | type: "keyword", 49 | }); 50 | return results.map((r) => ({ 51 | title: r.title, 52 | url: r.url, 53 | content: r.text, 54 | publishedDate: r.publishedDate, 55 | favicon: r.favicon, 56 | })) as SearchResult[]; 57 | }; 58 | 59 | const generateSearchQueries = async ( 60 | query: string, 61 | breadth: number, 62 | learnings?: string[], 63 | ) => { 64 | const { 65 | object: { queries }, 66 | } = await generateObject({ 67 | system: SYSTEM_PROMPT, 68 | model: openai("o3-mini"), 69 | prompt: `Given the following prompt from the user, generate a list of SERP queries to research the topic. Ensure at least one is almost identical to the initial prompt. Return a maximum of ${breadth} queries, but feel free to return less if the original prompt is clear. Make sure each query is unique and not similar to each other: ${query}\n\n${ 70 | learnings 71 | ? `Here are some learnings from previous research, use them to generate more specific queries: ${learnings.join( 72 | "\n", 73 | )}` 74 | : "" 75 | }`, 76 | schema: z.object({ 77 | queries: z 78 | .array( 79 | z.object({ 80 | query: z.string().describe("The SERP query"), 81 | researchGoal: z 82 | .string() 83 | .describe( 84 | "First talk about the goal of the research that this query is meant to accomplish, then go deeper into how to advance the research once the results are found, mention additional research directions. Be as specific as possible, especially for additional research directions.", 85 | ), 86 | }), 87 | ) 88 | .describe(`List of SERP queries, max of ${breadth}`), 89 | }), 90 | }); 91 | return queries; 92 | }; 93 | 94 | const generateLearnings = async ( 95 | query: string, 96 | results: SearchResult[], 97 | numberOfLearnings: number, 98 | numberOfFollowUpQuestions: number, 99 | ) => { 100 | const { 101 | object: { followUpQuestions, learnings }, 102 | } = await generateObject({ 103 | model: openai("o3-mini"), 104 | system: SYSTEM_PROMPT, 105 | prompt: `Given the following contents from a SERP search for the query ${query}, generate a list of learnings from the contents. Return a maximum of ${numberOfLearnings} learnings, but feel free to return less if the contents are clear. Make sure each learning is unique and not similar to each other. The learnings should be concise and to the point, as detailed and information dense as possible. Make sure to include any entities like people, places, companies, products, things, etc in the learnings, as well as any exact metrics, numbers, or dates. The learnings will be used to research the topic further.\n\n${results 106 | .map((content) => `\n${content.content}\n`) 107 | .join("\n")}`, 108 | schema: z.object({ 109 | learnings: z 110 | .array(z.string()) 111 | .describe(`List of learnings, max of ${numberOfLearnings}`), 112 | followUpQuestions: z 113 | .array(z.string()) 114 | .describe( 115 | `List of follow-up questions to research the topic further, max of ${numberOfFollowUpQuestions}`, 116 | ), 117 | }), 118 | }); 119 | return { 120 | learnings, 121 | followUpQuestions, 122 | }; 123 | }; 124 | 125 | const deepResearch = async ( 126 | prompt: string, 127 | depth: number = 1, 128 | breadth: number = 3, 129 | accumulatedResearch: Research = { 130 | learnings: [], 131 | sources: [], 132 | questionsExplored: [], 133 | searchQueries: [], 134 | }, 135 | dataStream: DataStreamWriter, 136 | ): Promise => { 137 | // Base case: regardless whether accumulatedResearch is present or empty, if depth is 0 we stop. 138 | if (depth === 0) { 139 | return accumulatedResearch; 140 | } 141 | 142 | dataStream.writeMessageAnnotation({ 143 | status: { title: `Generating search queries for "${prompt}"` }, 144 | }); 145 | const searchQueries = await generateSearchQueries( 146 | prompt, 147 | breadth, 148 | accumulatedResearch.learnings, 149 | ); 150 | dataStream.writeMessageAnnotation({ 151 | status: { title: `Generated search queries for "${prompt}"` }, 152 | }); 153 | 154 | // Process each query and merge the results rather than overwrite 155 | const subResults = await Promise.all( 156 | searchQueries.map(async ({ query, researchGoal }) => { 157 | dataStream.writeMessageAnnotation({ 158 | status: { title: `Searching the web for "${query}"` }, 159 | }); 160 | const results = await searchWeb(query); 161 | results.forEach(async (source) => { 162 | dataStream.writeMessageAnnotation({ 163 | source: { title: source.title, url: source.url }, 164 | }); 165 | await new Promise((resolve) => setTimeout(resolve, 1000)); 166 | }); 167 | 168 | dataStream.writeMessageAnnotation({ 169 | status: { title: `Analyzing search results for "${query}"` }, 170 | }); 171 | 172 | const { learnings, followUpQuestions } = await generateLearnings( 173 | query, 174 | results, 175 | 3, 176 | breadth, 177 | ); 178 | const nextQuery = 179 | `Previous research goal: ${researchGoal}` + 180 | ` Follow-up directions: ${followUpQuestions.map((q) => `\n${q}`).join("")}`.trim(); 181 | 182 | // Make the recursive call 183 | dataStream.writeMessageAnnotation({ 184 | status: { 185 | title: `Diving deeper to understand "${followUpQuestions.slice(0, 3).join(", ")}"`, 186 | }, 187 | }); 188 | const subResearch = await deepResearch( 189 | nextQuery, 190 | depth - 1, 191 | Math.ceil(breadth / 2), 192 | undefined, 193 | dataStream, 194 | ); 195 | 196 | subResearch.sources.forEach((source) => { 197 | dataStream.writeMessageAnnotation({ 198 | source: { title: source.title, url: source.url }, 199 | }); 200 | }); 201 | 202 | // Merge the research found at this level with the research in the child call. 203 | return { 204 | learnings, 205 | sources: results, 206 | questionsExplored: followUpQuestions, 207 | searchQueries: [query, ...subResearch.searchQueries], 208 | // Also merge in subResearch learnings, sources, and questions. 209 | subLearnings: subResearch.learnings, 210 | subSources: subResearch.sources, 211 | subQuestionsExplored: subResearch.questionsExplored, 212 | }; 213 | }), 214 | ); 215 | for (const res of subResults) { 216 | accumulatedResearch.learnings.push(...res.learnings, ...res.subLearnings); 217 | accumulatedResearch.sources.push(...res.sources, ...res.subSources); 218 | accumulatedResearch.questionsExplored.push( 219 | ...res.questionsExplored, 220 | ...res.subQuestionsExplored, 221 | ); 222 | accumulatedResearch.searchQueries.push(...res.searchQueries); 223 | } 224 | 225 | return accumulatedResearch; 226 | }; 227 | 228 | const generateReport = async (prompt: string, research: Research) => { 229 | const { learnings, sources, questionsExplored, searchQueries } = research; 230 | const { text: report } = await generateText({ 231 | model: openai("o3-mini"), 232 | system: SYSTEM_PROMPT + "\n- Write in markdown sytax.", 233 | prompt: `Generate a comprehensive report focused on "${prompt}". The main research findings should be drawn from the learnings below, with the search queries and related questions explored serving as supplementary context. Focus on synthesizing the key insights into a coherent narrative around the main topic. 234 | 235 | 236 | ${learnings.map((l) => `\n${l}`).join("")} 237 | 238 | 239 | 240 | ${searchQueries.map((q) => `\n${q}`).join("")} 241 | 242 | 243 | 244 | ${questionsExplored.map((q) => `\n${q}`).join("")} 245 | 246 | 247 | 248 | ${sources.map((s) => `\n${JSON.stringify({ ...s, content: s.content.slice(0, 350) })}`).join("")} 249 | 250 | `, 251 | }); 252 | const { object } = await generateObject({ 253 | model: openai("gpt-4o-mini"), 254 | prompt: 255 | "Generate a punchy title (5 words) for the following report:\n\n" + 256 | report, 257 | schema: z.object({ 258 | title: z.string(), 259 | }), 260 | }); 261 | 262 | return { report, title: object.title }; 263 | }; 264 | 265 | export const deepResearchTool = (dataStream: DataStreamWriter) => 266 | tool({ 267 | description: "Use this tool to conduct a deep research on a given topic.", 268 | parameters: z.object({ 269 | prompt: z 270 | .string() 271 | .min(1) 272 | .max(1000) 273 | .describe( 274 | "This should take the user's exact prompt. Extract from the context but do not infer or change in any way.", 275 | ), 276 | depth: z.number().min(1).max(3).default(1).describe("Default to 1 unless the user specifically references otherwise"), 277 | breadth: z.number().min(1).max(5).default(3).describe("Default to 3 unless the user specifically references otherwise"), 278 | }), 279 | execute: async ({ prompt, depth, breadth }) => { 280 | console.log({ prompt, depth, breadth }); 281 | dataStream.writeMessageAnnotation({ 282 | status: { title: "Beginning deep research" }, 283 | }); 284 | const research = await deepResearch( 285 | prompt, 286 | depth, 287 | breadth, 288 | undefined, 289 | dataStream, 290 | ); 291 | dataStream.writeMessageAnnotation({ 292 | status: { title: "Generating report" }, 293 | }); 294 | const report = await generateReport(prompt, research); 295 | dataStream.writeMessageAnnotation({ 296 | status: { title: "Successfully generated report" }, 297 | }); 298 | 299 | return { 300 | report, 301 | research: { 302 | ...research, 303 | sources: Array.from( 304 | new Map( 305 | research.sources.map((s) => [ 306 | s.url, 307 | { ...s, content: s.content.slice(0, 50) + "..." }, 308 | ]), 309 | ).values(), 310 | ), 311 | }, 312 | }; 313 | }, 314 | }); -------------------------------------------------------------------------------- /ai/tools.ts: -------------------------------------------------------------------------------- 1 | import Exa from "exa-js"; 2 | import "dotenv/config"; 3 | import { tool as createTool } from "ai"; 4 | import { z } from "zod"; 5 | 6 | export const exa = new Exa(process.env.EXA_API_KEY); 7 | 8 | const webSearch = createTool({ 9 | description: "Use this tool to search the web for information.", 10 | parameters: z.object({ 11 | query: z 12 | .string() 13 | .min(1) 14 | .max(200) 15 | .describe( 16 | "The search query - be specific and include terms like 'vs', 'features', 'comparison' for better results", 17 | ), 18 | limit: z 19 | .number() 20 | .min(1) 21 | .max(10) 22 | .default(5) 23 | .describe("The number of results to return"), 24 | }), 25 | execute: async ({ query, limit }) => { 26 | const { results } = await exa.searchAndContents(query, { 27 | numResults: limit, 28 | startPublishedDate: new Date("2025-01-01").toISOString(), 29 | }); 30 | // Process and clean the results 31 | return results.map((result) => ({ 32 | title: result.title, 33 | url: result.url, 34 | snippet: result.text, // Limit snippet length 35 | domain: new URL(result.url).hostname, // Extract domain for source context 36 | date: result.publishedDate || "Date not available", // Include publish date when available 37 | })); 38 | }, 39 | }); 40 | 41 | export const tools = { 42 | webSearch, 43 | }; 44 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { deepResearchTool } from "@/ai/agents/deep-research"; 2 | import { tools } from "@/ai/tools"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { createDataStreamResponse, streamText } from "ai"; 5 | 6 | // Allow streaming responses up to 120 seconds 7 | export const maxDuration = 120; 8 | 9 | export async function POST(req: Request) { 10 | const { messages } = await req.json(); 11 | 12 | return createDataStreamResponse({ 13 | execute: async (dataStream) => { 14 | const result = streamText({ 15 | model: openai("gpt-4o"), 16 | messages, 17 | system: 18 | "You are a helpful assistant. Do not repeat the results of deepResearch tool calls. You can report (max 2 sentences) that the tool has been used successfully. Do not call multiple tools at once.", 19 | tools: { 20 | ...tools, 21 | deepResearch: deepResearchTool(dataStream), 22 | }, 23 | }); 24 | result.mergeIntoDataStream(dataStream); 25 | }, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoalbanese/aie-deepresearch/6bc7623b8bf9ab39de0661ede9cc237a57915b98/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 240 10% 3.9%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --muted: 240 4.8% 95.9%; 22 | --muted-foreground: 240 3.8% 46.1%; 23 | --accent: 240 4.8% 95.9%; 24 | --accent-foreground: 240 5.9% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 5.9% 90%; 28 | --input: 240 5.9% 90%; 29 | --ring: 240 10% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 240 10% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 240 10% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 240 10% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 240 5.9% 10%; 46 | --secondary: 240 3.7% 15.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 240 3.7% 15.9%; 49 | --muted-foreground: 240 5% 64.9%; 50 | --accent: 240 3.7% 15.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 240 3.7% 15.9%; 55 | --input: 240 3.7% 15.9%; 56 | --ring: 240 4.9% 83.9%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | 74 | ::-webkit-scrollbar { 75 | width: 0; 76 | background: transparent; 77 | display: none; 78 | } 79 | 80 | * { 81 | scrollbar-width: none; 82 | -ms-overflow-style: none; 83 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 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 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { PreviewMessage } from "@/components/message"; 4 | import { Input } from "@/components/ui/input"; 5 | import { useScrollToBottom } from "@/components/use-scroll-to-bottom"; 6 | import { useChat } from "@ai-sdk/react"; 7 | import { ArrowRight } from "lucide-react"; 8 | 9 | export default function Chat() { 10 | const { messages, input, handleInputChange, handleSubmit, error, isLoading } = 11 | useChat({ 12 | maxSteps: 10, 13 | }); 14 | const [containerRef, endRef] = useScrollToBottom(); 15 | 16 | if (error) return
{error.message}
; 17 | 18 | return ( 19 |
20 |
25 | {messages.map((message) => ( 26 | 31 | ))} 32 | {/* @ts-expect-error ref */} 33 |
34 |
35 | 36 |
37 |
38 | 44 | 50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /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": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 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 | } -------------------------------------------------------------------------------- /components/deep-research.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogHeader, 6 | DialogTitle, 7 | DialogTrigger, 8 | } from "@/components/ui/dialog"; 9 | import { ScrollArea } from "@/components/ui/scroll-area"; 10 | import type { Research } from "@/ai/agents/deep-research"; 11 | import type { JSONValue, ToolInvocation } from "ai"; 12 | import { useEffect, useState } from "react"; 13 | import { motion, AnimatePresence } from "framer-motion"; 14 | import { FileText, LinkIcon } from "lucide-react"; 15 | import { DeepResearchStatus, StatusUpdate } from "./steps"; 16 | import { Markdown } from "./markdown"; 17 | 18 | function formatElapsedTime(ms: number) { 19 | const seconds = Math.floor(ms / 1000); 20 | const minutes = Math.floor(seconds / 60); 21 | return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`; 22 | } 23 | 24 | export const DeepResearch = ({ 25 | toolInvocation, 26 | annotations, 27 | }: { 28 | toolInvocation: ToolInvocation; 29 | annotations?: JSONValue[]; 30 | }) => { 31 | const { state } = toolInvocation; 32 | const [startTime] = useState(Date.now()); 33 | const [elapsedTime, setElapsedTime] = useState(0); 34 | 35 | useEffect(() => { 36 | if (state !== "result") { 37 | const interval = setInterval(() => { 38 | setElapsedTime(Date.now() - startTime); 39 | }, 1000); 40 | 41 | return () => clearInterval(interval); 42 | } 43 | }, [startTime, state]); 44 | 45 | const statusUpdates = annotations 46 | ?.filter( 47 | ( 48 | annotation, 49 | ): annotation is { status: { title: string; description?: string } } => 50 | annotation !== null && 51 | typeof annotation === "object" && 52 | "status" in annotation, 53 | ) 54 | .map((update) => update.status); 55 | 56 | const steps: StatusUpdate[] = (statusUpdates || []).map((status) => ({ 57 | title: status.title, 58 | description: status.description || "", 59 | })); 60 | 61 | const sourceUpdates = Array.from( 62 | new Set( 63 | annotations?.filter( 64 | ( 65 | annotation, 66 | ): annotation is { source: { title: string; url: string } } => 67 | annotation !== null && 68 | typeof annotation === "object" && 69 | "source" in annotation, 70 | ), 71 | ), 72 | ) as { 73 | source: { title: string; url: string }; 74 | }[]; 75 | 76 | if (state === "result") { 77 | const { result } = toolInvocation; 78 | const { 79 | report, 80 | research, 81 | }: { report: { report: string; title: string }; research: Research } = 82 | result; 83 | 84 | return ( 85 | 91 |
92 |
93 |
94 | 95 |

Deep Research

96 |
97 |
98 | Completed in {formatElapsedTime(elapsedTime)} 99 |
100 |
101 | 102 | 103 | 104 | 105 |

{report.title}

106 |
107 | {report.report.slice(0, 250)} 108 |
109 |
110 |
111 | 112 | 113 | 114 | 115 | {report.title} 116 | 117 | 118 | 119 |
120 | {report.report} 121 |
122 |
123 |
124 |
125 | 126 | {research.sources && research.sources.length > 0 && ( 127 | 128 | 129 | Deep Research used{" "} 130 | {Array.from(new Set(research.sources)).length} sources. 131 | 132 | 133 | 134 | 135 | 136 | Sources Used 137 | 138 | 139 | 140 |

141 | The following sources were used in the research: 142 |

143 |
144 | 145 | {Array.from(new Set(research.sources)).map( 146 | (source, index) => ( 147 | 154 | 160 |
161 | {/* eslint-disable-next-line @next/next/no-img-element */} 162 | Favicon 167 | {source.title} 168 |
169 |

170 | {source.content} 171 |

172 |
173 |
174 | ), 175 | )} 176 |
177 |
178 |
179 |
180 |
181 | )} 182 |
183 |
184 | ); 185 | } 186 | 187 | return ( 188 | 194 |
195 |
196 |
197 | 198 |

Deep Research

199 |
200 |
201 | Time elapsed: {formatElapsedTime(elapsedTime)} 202 |
203 |
204 | 205 |
206 | 207 | 208 | {sourceUpdates && sourceUpdates.length > 0 && ( 209 | 215 |
216 | Sources found so far:{" "} 217 | { 218 | Array.from(new Set(sourceUpdates.map((s) => s.source.url))) 219 | .length 220 | } 221 |
222 |
223 | )} 224 |
225 |
226 |
227 | ); 228 | }; 229 | -------------------------------------------------------------------------------- /components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React, { memo } from "react"; 3 | import ReactMarkdown, { type Components } from "react-markdown"; 4 | import remarkGfm from "remark-gfm"; 5 | 6 | const components: Partial = { 7 | pre: ({ children }) => <>{children}, 8 | ol: ({ node, children, ...props }) => { 9 | return ( 10 |
    11 | {children} 12 |
13 | ); 14 | }, 15 | li: ({ node, children, ...props }) => { 16 | return ( 17 |
  • 18 | {children} 19 |
  • 20 | ); 21 | }, 22 | ul: ({ node, children, ...props }) => { 23 | return ( 24 |
      25 | {children} 26 |
    27 | ); 28 | }, 29 | strong: ({ node, children, ...props }) => { 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }, 36 | a: ({ node, children, ...props }) => { 37 | return ( 38 | // @ts-expect-error 39 | 45 | {children} 46 | 47 | ); 48 | }, 49 | h1: ({ node, children, ...props }) => { 50 | return ( 51 |

    52 | {children} 53 |

    54 | ); 55 | }, 56 | h2: ({ node, children, ...props }) => { 57 | return ( 58 |

    59 | {children} 60 |

    61 | ); 62 | }, 63 | h3: ({ node, children, ...props }) => { 64 | return ( 65 |

    66 | {children} 67 |

    68 | ); 69 | }, 70 | h4: ({ node, children, ...props }) => { 71 | return ( 72 |

    73 | {children} 74 |

    75 | ); 76 | }, 77 | h5: ({ node, children, ...props }) => { 78 | return ( 79 |
    80 | {children} 81 |
    82 | ); 83 | }, 84 | h6: ({ node, children, ...props }) => { 85 | return ( 86 |
    87 | {children} 88 |
    89 | ); 90 | }, 91 | }; 92 | 93 | const remarkPlugins = [remarkGfm]; 94 | 95 | const NonMemoizedMarkdown = ({ children }: { children: string }) => { 96 | return ( 97 | 98 | {children} 99 | 100 | ); 101 | }; 102 | 103 | export const Markdown = memo( 104 | NonMemoizedMarkdown, 105 | (prevProps, nextProps) => prevProps.children === nextProps.children, 106 | ); 107 | -------------------------------------------------------------------------------- /components/message.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { Message } from "ai"; 4 | import cx from "classnames"; 5 | import { AnimatePresence, motion } from "framer-motion"; 6 | import { memo } from "react"; 7 | import equal from "fast-deep-equal"; 8 | 9 | import { Markdown } from "./markdown"; 10 | import { cn } from "@/lib/utils"; 11 | import { SparklesIcon } from "lucide-react"; 12 | import { DeepResearch } from "./deep-research"; 13 | 14 | const PurePreviewMessage = ({ 15 | message, 16 | }: { 17 | message: Message; 18 | isLoading: boolean; 19 | }) => { 20 | return ( 21 | 22 | 28 |
    34 | {message.role === "assistant" && ( 35 |
    36 |
    37 | 38 |
    39 |
    40 | )} 41 | 42 |
    43 | {message.parts?.map((part) => { 44 | switch (part.type) { 45 | case "text": 46 | return ( 47 |
    51 |
    57 | {part.text as string} 58 |
    59 |
    60 | ); 61 | case "tool-invocation": 62 | const { toolName, toolCallId, state } = part.toolInvocation; 63 | return ( 64 |
    70 | {toolName === "deepResearch" ? ( 71 | 75 | ) : toolName === "webSearch" ? ( 76 | state === "result" ? null : ( 77 | 83 | Searching the web... 84 | 85 | ) 86 | ) : null} 87 |
    88 | ); 89 | 90 | default: 91 | return null; 92 | } 93 | })} 94 |
    95 |
    96 |
    97 |
    98 | ); 99 | }; 100 | 101 | export const PreviewMessage = memo( 102 | PurePreviewMessage, 103 | (prevProps, nextProps) => { 104 | if (prevProps.isLoading !== nextProps.isLoading) return false; 105 | if (prevProps.message.reasoning !== nextProps.message.reasoning) 106 | return false; 107 | if (prevProps.message.annotations !== nextProps.message.annotations) 108 | return false; 109 | if (prevProps.message.content !== nextProps.message.content) return false; 110 | if ( 111 | !equal( 112 | prevProps.message.toolInvocations, 113 | nextProps.message.toolInvocations, 114 | ) 115 | ) 116 | return false; 117 | 118 | return true; 119 | }, 120 | ); 121 | -------------------------------------------------------------------------------- /components/steps.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { motion, AnimatePresence } from "framer-motion"; 3 | 4 | export interface StatusUpdate { 5 | title: string; 6 | description: string; 7 | } 8 | 9 | interface StatusProps { 10 | updates: StatusUpdate[]; 11 | className?: string; 12 | } 13 | 14 | export function DeepResearchStatus({ updates, className }: StatusProps) { 15 | if (updates.length === 0) return null; 16 | const currentUpdate = updates[updates.length - 1]; 17 | 18 | return ( 19 |
    20 | 21 | 32 |

    33 | {currentUpdate.title} 34 |

    35 | {/* {currentUpdate.description && ( 36 |

    37 | {currentUpdate.description} 38 |

    39 | )} */} 40 |
    41 |
    42 |
    43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
    32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
    44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
    56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
    33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm 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", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
    41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
    53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
    61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
    73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
    67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
    81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |