diff --git a/app/actions.ts b/app/actions.ts new file mode 100644 index 0000000..24b9486 --- /dev/null +++ b/app/actions.ts @@ -0,0 +1,35 @@ +'use server'; + +import { generateObject } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { z } from 'zod'; + +export interface Message { + role: 'user' | 'assistant'; + content: string; +} + +export async function suggestQuestions(history: Message[]) { + 'use server'; + + const { object } = await generateObject({ + model: openai('gpt-4o-mini'), + temperature: 0, + system: +`You are a search engine query generator. You 'have' to create 3 questions for the search engine based on the message history which has been provided to you. +The questions should be open-ended and should encourage further discussion while maintaining the whole context. Limit it to 5-10 words per question. +Always put the user input's context is some way so that the next search knows what to search for exactly. +Never use pronouns in the questions as they blur the context.`, + messages: history, + schema: z.object({ + questions: z.array( + z.string() + ) + .describe('The generated questions based on the message history.') + }), + }); + + return { + questions: object.questions + }; +} \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico index 250f01d..8494e10 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/page.tsx b/app/page.tsx index 6fab615..41d3ad5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,22 +1,32 @@ /* eslint-disable @next/next/no-img-element */ "use client"; -import React, { useRef, useCallback, useState, useEffect, ReactNode, useMemo } from 'react'; +import +React, +{ + useRef, + useCallback, + useState, + useEffect, + ReactNode, + useMemo +} from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useChat } from 'ai/react'; import { ToolInvocation } from 'ai'; import { toast } from 'sonner'; import { motion, AnimatePresence } from 'framer-motion'; +import Image from 'next/image'; +import { suggestQuestions, Message } from './actions'; import { SearchIcon, - LinkIcon, - Loader2, ChevronDown, FastForward, Sparkles, ArrowRight, - Globe + Globe, + AlignLeft } from 'lucide-react'; import { HoverCard, @@ -33,7 +43,6 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { ScrollArea } from '@/components/ui/scroll-area'; export default function Home() { const inputRef = useRef(null); @@ -41,7 +50,7 @@ export default function Home() { const [hasSubmitted, setHasSubmitted] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const bottomRef = useRef(null); - const [showToolResults, setShowToolResults] = useState<{ [key: number]: boolean }>({}); + const [suggestedQuestions, setSuggestedQuestions] = useState([]); const [isModelSelectorOpen, setIsModelSelectorOpen] = useState(false); const [selectedModel, setSelectedModel] = useState('Speed'); const [showExamples, setShowExamples] = useState(false) @@ -52,6 +61,14 @@ export default function Home() { model: selectedModel === 'Speed' ? 'gpt-4o-mini' : selectedModel === 'Quality (GPT)' ? 'gpt-4o' : 'claude-3-5-sonnet-20240620', }, maxToolRoundtrips: 1, + onFinish: async (message, { finishReason }) => { + if (finishReason === 'stop') { + const newHistory: Message[] = [{ role: "user", content: lastSubmittedQuery, }, { role: "assistant", content: message.content }]; + const { questions } = await suggestQuestions(newHistory); + setSuggestedQuestions(questions); + } + setIsAnimating(false); + }, onError: (error) => { console.error("Chat error:", error); toast.error("An error occurred. Please try again."); @@ -67,66 +84,93 @@ export default function Home() { const renderToolInvocation = (toolInvocation: ToolInvocation, index: number) => { const args = JSON.parse(JSON.stringify(toolInvocation.args)); const result = 'result' in toolInvocation ? JSON.parse(JSON.stringify(toolInvocation.result)) : null; - + return ( - - - -
-
- - Web Search -
- {!result && ( -
- - Searching the web... -
- )} - {result && ( - {result.results.length} results - )} +
+ {!result ? ( +
+
+ + Searching the web...
- - - {args?.query && ( - - - {args.query} - - )} - {result && ( - -
- {result.results.map((item: any, itemIndex: number) => ( - - - {/* favicon here */} - Favicon - {item.title} - - -

{item.content}

-
- -
- ))} +
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+ ) : + + + +
+
+ +

Web Search Results Found

+
+ {result && ( + {result.results.length} results + )}
-
- )} -
- - + + + {args?.query && ( + + + {args.query} + + )} + {result && ( +
+ {result.results.map((item: any, itemIndex: number) => ( + + + Favicon + {item.title} + + +

{item.content}

+
+ +
+ ))} +
+ )} +
+ + } +
); }; @@ -136,12 +180,12 @@ export default function Home() { return ( - + {index + 1} - Favicon + Favicon {citationLink} @@ -164,7 +208,7 @@ export default function Home() { text, link, })); - }, [content]); // Recompute only if content changes + }, [content]); return ( { setLastSubmittedQuery(query.trim()); setHasSubmitted(true); + setSuggestedQuestions([]); + setIsAnimating(true); await append({ content: query.trim(), role: 'user' @@ -212,19 +258,31 @@ export default function Home() { if (input.trim()) { setMessages([]); setLastSubmittedQuery(input.trim()); - handleSubmit(e); setHasSubmitted(true); setIsAnimating(true); - setShowToolResults({}); + setSuggestedQuestions([]); + handleSubmit(e); } else { toast.error("Please enter a search query."); } }, [input, setMessages, handleSubmit]); + const handleSuggestedQuestionClick = useCallback(async (question: string) => { + setMessages([]); + setLastSubmittedQuery(question.trim()); + setHasSubmitted(true); + setSuggestedQuestions([]); + setIsAnimating(true); + await append({ + content: question.trim(), + role: 'user' + }); + }, [append, setMessages]); + const exampleQueries = [ - "Best programming languages in 2024", - "How to build a responsive website", - "Latest trends in AI technology", + "Meta Llama 3.1 405B", + "Latest on Paris Olympics", + "What is Github Models?", "OpenAI GPT-4o mini" ]; @@ -271,7 +329,7 @@ export default function Home() { setSelectedModel(model.name); setIsModelSelectorOpen(false); }} - className="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center" + className={`w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center ${models.indexOf(model) === 0 ? 'rounded-t-md' : models.indexOf(model) === models.length - 1 ? 'rounded-b-md' : ''}`} >
@@ -303,7 +361,7 @@ export default function Home() { value={input} onChange={(e) => setInput(e.target.value)} disabled={isLoading} - className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 text-sm sm:text-base" + className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" onFocus={() => setShowExamples(true)} onBlur={() => setShowExamples(false)} /> @@ -385,15 +443,15 @@ export default function Home() {
{message.role === 'assistant' && message.content && (
- -

Answer

+ +

Answer

-
+
@@ -405,8 +463,34 @@ export default function Home() { ))}
))} -
+ {suggestedQuestions.length > 0 && ( + +
+ +

Suggested questions

+
+
+ {suggestedQuestions.map((question, index) => ( + + ))} +
+
+ )}
+
@@ -416,7 +500,7 @@ export default function Home() { animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 50 }} transition={{ duration: 0.5 }} - className="fixed bottom-4 transform -translate-x-1/2 w-full max-w-[90%] sm:max-w-md md:max-w-2xl mt-3" + className="fixed bottom-4 transform -translate-x-1/2 w-full max-w-xl md:max-w-2xl mt-3" >
@@ -427,7 +511,7 @@ export default function Home() { value={input} onChange={(e) => setInput(e.target.value)} disabled={isLoading} - className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 text-sm sm:text-base" + className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" />