Skip to content

Commit

Permalink
Added research agent and UI
Browse files Browse the repository at this point in the history
  • Loading branch information
mikepsinn committed Sep 4, 2024
1 parent 7a5e0bf commit 49fb655
Show file tree
Hide file tree
Showing 13 changed files with 511 additions and 164 deletions.
9 changes: 8 additions & 1 deletion app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type Message as AIMessage } from "ai"
import { prisma } from "@/lib/db"
import { getCurrentUser } from "@/lib/session"
import { type Chat } from "@/lib/types"

import {type ReportOutput, writeArticle, type ModelName} from "@/lib/agents/researcher/researcher"
type GetChatResult = Chat[] | null
type SetChatResults = Chat[]

Expand Down Expand Up @@ -166,3 +166,10 @@ export const clearAllChats = async (userId: string) => {
return revalidatePath(deletedChats.map((chat) => chat.path).join(", "))
}
}

export async function writeArticleAction(topic: string, modelName?: ModelName): Promise<ReportOutput> {
const article = await writeArticle(topic, { modelName: modelName })

revalidatePath('/')
return article
}
67 changes: 67 additions & 0 deletions app/researcher/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import { useState } from 'react'
import { writeArticleAction } from '@/app/actions'
import ArticleRenderer from '@/components/ArticleRenderer'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ReportOutput } from '@/lib/agents/researcher/researcher'
import GlobalBrainNetwork from "@/components/landingPage/global-brain-network"

export default function Home() {
const [article, setArticle] = useState<ReportOutput | null>(null)
const [error, setError] = useState('')
const [isGenerating, setIsGenerating] = useState(false)

async function handleSubmit(formData: FormData) {
const topic = formData.get('topic') as string
if (!topic) {
setError('Please enter a topic')
return
}

setIsGenerating(true)
setError('')

try {
const generatedArticle = await writeArticleAction(topic)
setArticle(generatedArticle)
} catch (err) {
setError('Failed to generate article. Please try again.')
} finally {
setIsGenerating(false)
}
}

return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Article Generator</h1>
<Card className="mb-8">
<CardHeader>
<CardTitle>Generate an Article</CardTitle>
<CardDescription>Enter a topic to generate an article</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={(e) => {
e.preventDefault()
handleSubmit(new FormData(e.currentTarget))
}} className="flex gap-4">
<Input type="text" name="topic" placeholder="Enter article topic" className="flex-grow" />
<Button type="submit" disabled={isGenerating}>
{isGenerating ? 'Generating...' : 'Generate Article'}
</Button>
</form>
</CardContent>
{error && (
<CardFooter>
<p className="text-red-500">{error}</p>
</CardFooter>
)}
</Card>

{isGenerating && <GlobalBrainNetwork />}
{!isGenerating && article && <ArticleRenderer {...article} />}
</main>
)
}
118 changes: 118 additions & 0 deletions components/ArticleRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {useState} from 'react'
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
import {Badge} from "@/components/ui/badge"
import {Separator} from "@/components/ui/separator"
import {Clock, Tag, Folder, Link2} from 'lucide-react'
import {ReportOutput} from '@/lib/agents/researcher/researcher'
import {CustomReactMarkdown} from "@/components/CustomReactMarkdown";

export default function ArticleRenderer(props: ReportOutput) {
const [expandedResult, setExpandedResult] = useState<string | null>(null)

const {
title,
description,
content,
sources,
tags,
category,
readingTime,
searchResults
} = props

return (
<div className="container mx-auto p-4 max-w-6xl">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="md:col-span-2">
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<Separator className="my-4 mx-auto w-[90%]" />
<CardContent>
<CustomReactMarkdown>
{content}
</CustomReactMarkdown>
</CardContent>
</Card>
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Article Info</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center space-x-2">
<Folder className="w-4 h-4"/>
<span>{category}</span>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4"/>
<span>{readingTime} min read</span>
</div>
<div className="flex flex-wrap gap-2">
<Tag className="w-4 h-4"/>
{tags?.map((tag, index) => (
<Badge key={index} variant="secondary">{tag}</Badge>
))}
</div>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>Sources</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{sources?.map((source, index) => (
<li key={index}>
<a href={source.url} target="_blank" rel="noopener noreferrer"
className="flex items-center space-x-2 text-blue-500 hover:underline">
<Link2 className="w-4 h-4"/>
<span>{source.title}</span>
</a>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</div>

<Card className="mt-8">
<CardHeader>
<CardTitle>Search Results</CardTitle>
</CardHeader>
<CardContent>
{searchResults?.map((result, index) => (
<div key={index} className="mb-4">
<h3 className="text-lg font-semibold">
<a href={result.url} target="_blank" rel="noopener noreferrer"
className="text-blue-500 hover:underline">
{result.title}
</a>
</h3>
{result.publishedDate && (
<p className="text-sm text-muted-foreground">
Published on: {new Date(result.publishedDate).toLocaleDateString()}
</p>
)}
<p className="mt-1">
{expandedResult === result.id ? result.text : `${result.text.slice(0, 150)}...`}
{result.text.length > 150 && (
<button
onClick={() => setExpandedResult(expandedResult === result.id ? null : result.id)}
className="ml-2 text-blue-500 hover:underline"
>
{expandedResult === result.id ? 'Show less' : 'Show more'}
</button>
)}
</p>
{index < (searchResults.length - 1) && <Separator className="my-4"/>}
</div>
))}
</CardContent>
</Card>
</div>
)
}
96 changes: 96 additions & 0 deletions components/CustomReactMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from "react"
import { ReactMarkdown } from "react-markdown/lib/react-markdown"
import rehypeRaw from "rehype-raw"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import { cn } from "@/lib/utils"
import { CodeBlock } from "./ui/code-block"

export const CustomReactMarkdown = React.memo(function CustomReactMarkdown({
children,
className,
...props
}: React.ComponentPropsWithoutRef<typeof ReactMarkdown>) {
return (
<ReactMarkdown
className={cn(
"prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words text-sm [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-6 [&_ol]:pl-6 [&>*]:mb-4 [&_li]:mb-2",
className
)}
rehypePlugins={[rehypeRaw as any, { allowDangerousHtml: true }]}
remarkPlugins={[remarkGfm, remarkMath]}
skipHtml={false}
components={{
p({ children }) {
return <p className="mb-0.5 last:mb-0">{children}</p>
},
br() {
return <></>
},
h1({ children }) {
return <h1 className="text-xl font-semibold">{children}</h1>
},
h2({ children }) {
return <h2 className="text-lg font-semibold">{children}</h2>
},
h3({ children }) {
return <h3 className="text-base font-semibold">{children}</h3>
},
a({ href, children, ...props }) {
let target = ""
if (href?.startsWith("http")) {
target = "_blank"
} else if (href?.startsWith("#")) {
target = "_self"
}
return (
<a
href={href}
target={target}
rel="noreferrer"
className="text-blue-600 hover:underline"
{...props}
>
{children}
</a>
)
},
code({ inline, className, children, ...props }) {
if (children && children.length) {
if (children[0] == "▍") {
return (
<span className="mt-1 animate-pulse cursor-default"></span>
)
}
children[0] = (children[0] as string).replace("`▍`", "▍")
}
const match = /language-(\w+)/.exec(className || "")
if (inline) {
return (
<code
className={cn(
"rounded-md bg-muted-foreground/30 px-1",
className
)}
{...props}
>
{children}
</code>
)
}
return (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ""}
value={String(children).replace(/\n$/, "")}
{...props}
/>
)
},
}}
{...props}
>
{children}
</ReactMarkdown>
)
})
Loading

0 comments on commit 49fb655

Please sign in to comment.