feat: Enabled Follow up conversations

fix: bottom input bar disappearance issue
This commit is contained in:
zaidmukaddam 2024-08-20 23:58:31 +05:30
parent d20357fa6a
commit 365ee9d580
2 changed files with 96 additions and 137 deletions

View File

@ -4,17 +4,12 @@ import { generateObject } from 'ai';
import { createOpenAI as createGroq } from '@ai-sdk/openai'; import { createOpenAI as createGroq } from '@ai-sdk/openai';
import { z } from 'zod'; import { z } from 'zod';
export interface Message {
role: 'user' | 'assistant';
content: string;
}
const groq = createGroq({ const groq = createGroq({
baseURL: 'https://api.groq.com/openai/v1', baseURL: 'https://api.groq.com/openai/v1',
apiKey: process.env.GROQ_API_KEY, apiKey: process.env.GROQ_API_KEY,
}); });
export async function suggestQuestions(history: Message[]) { export async function suggestQuestions(history: any[]) {
'use server'; 'use server';
const { object } = await generateObject({ const { object } = await generateObject({
@ -31,10 +26,7 @@ For location based conversations, always generate questions that are about the c
Never use pronouns in the questions as they blur the context.`, Never use pronouns in the questions as they blur the context.`,
messages: history, messages: history,
schema: z.object({ schema: z.object({
questions: z.array( questions: z.array(z.string()).describe('The generated questions based on the message history.')
z.string()
)
.describe('The generated questions based on the message history.')
}), }),
}); });

View File

@ -12,14 +12,13 @@ React,
memo memo
} from 'react'; } from 'react';
import ReactMarkdown, { Components } from 'react-markdown'; import ReactMarkdown, { Components } from 'react-markdown';
import { useRouter } from 'next/navigation';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { useChat } from 'ai/react'; import { useChat } from 'ai/react';
import { ToolInvocation } from 'ai'; import { ToolInvocation } from 'ai';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image'; import Image from 'next/image';
import { suggestQuestions, Message } from './actions'; import { suggestQuestions } from './actions';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { import {
@ -41,8 +40,6 @@ import {
MapPin, MapPin,
Star, Star,
Plus, Plus,
Terminal,
ImageIcon,
Download, Download,
} from 'lucide-react'; } from 'lucide-react';
import { import {
@ -51,7 +48,6 @@ import {
HoverCardTrigger, HoverCardTrigger,
} from "@/components/ui/hover-card"; } from "@/components/ui/hover-card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -85,7 +81,6 @@ import {
import { GitHubLogoIcon } from '@radix-ui/react-icons'; import { GitHubLogoIcon } from '@radix-ui/react-icons';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import Link from 'next/link'; import Link from 'next/link';
import { cn } from '@/lib/utils';
export const maxDuration = 60; export const maxDuration = 60;
@ -97,26 +92,24 @@ declare global {
} }
export default function Home() { export default function Home() {
const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [lastSubmittedQuery, setLastSubmittedQuery] = useState(""); const [lastSubmittedQuery, setLastSubmittedQuery] = useState("");
const [hasSubmitted, setHasSubmitted] = useState(false); const [hasSubmitted, setHasSubmitted] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null); const bottomRef = useRef<HTMLDivElement>(null);
const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([]); const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([]);
const [showExamples, setShowExamples] = useState(false) const [showExamples, setShowExamples] = useState(false)
const [isEditingQuery, setIsEditingQuery] = useState(false); const [isEditingMessage, setIsEditingMessage] = useState(false);
const [editingMessageIndex, setEditingMessageIndex] = useState(-1);
const { isLoading, input, messages, setInput, append, handleSubmit, setMessages } = useChat({ const { isLoading, input, messages, setInput, append, handleSubmit, setMessages } = useChat({
api: '/api/chat', api: '/api/chat',
maxToolRoundtrips: 1, maxToolRoundtrips: 1,
onFinish: async (message, { finishReason }) => { onFinish: async (message, { finishReason }) => {
if (finishReason === 'stop') { if (finishReason === 'stop') {
const newHistory: Message[] = [{ role: "user", content: lastSubmittedQuery, }, { role: "assistant", content: message.content }]; const newHistory = [...messages, { role: "user", content: lastSubmittedQuery }, { role: "assistant", content: message.content }];
const { questions } = await suggestQuestions(newHistory); const { questions } = await suggestQuestions(newHistory);
setSuggestedQuestions(questions); setSuggestedQuestions(questions);
} }
setIsAnimating(false);
}, },
onError: (error) => { onError: (error) => {
console.error("Chat error:", error); console.error("Chat error:", error);
@ -1007,6 +1000,14 @@ export default function Home() {
MarkdownRenderer.displayName = "MarkdownRenderer"; MarkdownRenderer.displayName = "MarkdownRenderer";
const lastUserMessageIndex = useMemo(() => {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === 'user') {
return i;
}
}
return -1;
}, [messages]);
useEffect(() => { useEffect(() => {
if (bottomRef.current) { if (bottomRef.current) {
@ -1018,59 +1019,57 @@ export default function Home() {
setLastSubmittedQuery(query.trim()); setLastSubmittedQuery(query.trim());
setHasSubmitted(true); setHasSubmitted(true);
setSuggestedQuestions([]); setSuggestedQuestions([]);
setIsAnimating(true);
await append({ await append({
content: query.trim(), content: query.trim(),
role: 'user' role: 'user'
}); });
}, [append]); }, [append]);
const handleSuggestedQuestionClick = useCallback(async (question: string) => {
setLastSubmittedQuery(question.trim());
setHasSubmitted(true);
setSuggestedQuestions([]);
await append({
content: question.trim(),
role: 'user'
});
}, [append]);
const handleFormSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => { const handleFormSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (input.trim()) { if (input.trim()) {
setMessages([]);
setLastSubmittedQuery(input.trim()); setLastSubmittedQuery(input.trim());
setHasSubmitted(true); setHasSubmitted(true);
setIsAnimating(true);
setSuggestedQuestions([]); setSuggestedQuestions([]);
handleSubmit(e); handleSubmit(e);
} else { } else {
toast.error("Please enter a search query."); toast.error("Please enter a search query.");
} }
}, [input, setMessages, handleSubmit]); }, [input, handleSubmit]);
const handleSuggestedQuestionClick = useCallback(async (question: string) => { const handleMessageEdit = useCallback((index: number) => {
setMessages([]); setIsEditingMessage(true);
setLastSubmittedQuery(question.trim()); setEditingMessageIndex(index);
setHasSubmitted(true); setInput(messages[index].content);
setSuggestedQuestions([]); }, [messages, setInput]);
setIsAnimating(true);
await append({
content: question.trim(),
role: 'user'
});
}, [append, setMessages]);
const handleQueryEdit = useCallback(() => { const handleMessageUpdate = useCallback((e: React.FormEvent<HTMLFormElement>) => {
setIsAnimating(true)
setIsEditingQuery(true);
setInput(lastSubmittedQuery);
}, [lastSubmittedQuery, setInput]);
const handleQuerySubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (input.trim()) { if (input.trim()) {
setLastSubmittedQuery(input.trim()); const updatedMessages = [...messages];
setIsEditingQuery(false); updatedMessages[editingMessageIndex] = { ...updatedMessages[editingMessageIndex], content: input.trim() };
setMessages([]); setMessages(updatedMessages);
setHasSubmitted(true); setIsEditingMessage(false);
setIsAnimating(true); setEditingMessageIndex(-1);
setSuggestedQuestions([]); setInput('');
handleSubmit(e); append({
content: input.trim(),
role: 'user'
});
} else { } else {
toast.error("Please enter a search query."); toast.error("Please enter a valid message.");
} }
}, [input, setMessages, handleSubmit]); }, [input, messages, editingMessageIndex, setMessages, setInput, append]);
const exampleQueries = [ const exampleQueries = [
"Weather in Doha", "Weather in Doha",
@ -1214,94 +1213,63 @@ export default function Home() {
</AnimatePresence> </AnimatePresence>
<AnimatePresence> <div className="space-y-4 sm:space-y-6 mb-24">
{hasSubmitted && ( {messages.map((message, index) => (
<motion.div <div key={index}>
initial={{ opacity: 0, y: 50 }} {message.role === 'user' && (
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
onAnimationComplete={() => setIsAnimating(false)}
>
<div className="flex items-center space-x-2 mb-4">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<User2 className="size-5 sm:size-6 text-primary flex-shrink-0" />
</motion.div>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }} transition={{ duration: 0.5 }}
className="flex-grow min-w-0" className="flex items-center space-x-2 mb-4"
> >
{isEditingQuery ? ( <User2 className="size-5 sm:size-6 text-primary flex-shrink-0" />
<form onSubmit={handleQuerySubmit} className="flex items-center space-x-2"> <div className="flex-grow min-w-0">
<Input {isEditingMessage && editingMessageIndex === index ? (
value={input} <form onSubmit={handleMessageUpdate} className="flex items-center space-x-2">
onChange={(e) => setInput(e.target.value)} <Input
className="flex-grow" value={input}
/> onChange={(e) => setInput(e.target.value)}
<Button className="flex-grow"
variant="secondary" />
size="sm" <Button
type="button" variant="secondary"
onClick={() => { size="sm"
setIsEditingQuery(false) type="button"
setInput('') onClick={() => {
}} setIsEditingMessage(false)
disabled={isLoading} setEditingMessageIndex(-1)
> setInput('')
<X size={16} /> }}
</Button> disabled={isLoading}
<Button type="submit" size="sm"> >
<ArrowRight size={16} /> <X size={16} />
</Button> </Button>
</form> <Button type="submit" size="sm">
) : ( <ArrowRight size={16} />
<TooltipProvider> </Button>
<Tooltip> </form>
<TooltipTrigger asChild> ) : (
<p className="text-xl sm:text-2xl font-medium font-serif truncate"> <p className="text-xl sm:text-2xl font-medium font-serif truncate">
{lastSubmittedQuery} {message.content}
</p> </p>
</TooltipTrigger> )}
<TooltipContent> </div>
<p>{lastSubmittedQuery}</p> {!isEditingMessage && index === lastUserMessageIndex && (
</TooltipContent> <Button
</Tooltip> variant="ghost"
</TooltipProvider> size="sm"
onClick={() => handleMessageEdit(index)}
className="ml-2"
disabled={isLoading}
>
<Edit2 size={16} />
</Button>
)} )}
</motion.div> </motion.div>
{!isEditingQuery && (<motion.div )}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex-shrink-0 flex flex-row items-center gap-2"
>
<Button
variant="ghost"
size="sm"
onClick={handleQueryEdit}
className="ml-2"
disabled={isLoading}
>
<Edit2 size={16} />
</Button>
</motion.div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="space-y-4 sm:space-y-6">
{messages.map((message, index) => (
<div key={index}>
{message.role === 'assistant' && message.content && ( {message.role === 'assistant' && message.content && (
<div className={`${suggestedQuestions.length === 0 ? '!mb-20 sm:!mb-18' : ''}`}> <div>
<div className='flex items-center justify-between mb-2'> <div className='flex items-center justify-between mb-2'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Sparkles className="size-5 text-primary" /> <Sparkles className="size-5 text-primary" />
@ -1327,7 +1295,7 @@ export default function Home() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }} exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="w-full max-w-xl sm:max-w-2xl !mb-20 !sm:mb-18" className="w-full max-w-xl sm:max-w-2xl"
> >
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<AlignLeft className="w-5 h-5 text-primary" /> <AlignLeft className="w-5 h-5 text-primary" />
@ -1352,7 +1320,7 @@ export default function Home() {
</div> </div>
<AnimatePresence> <AnimatePresence>
{hasSubmitted && !isAnimating && ( {hasSubmitted && (
<motion.div <motion.div
initial={{ opacity: 0, y: 50 }} initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -1363,7 +1331,6 @@ export default function Home() {
<form onSubmit={handleFormSubmit} className="flex items-center space-x-2"> <form onSubmit={handleFormSubmit} className="flex items-center space-x-2">
<div className="relative flex-1"> <div className="relative flex-1">
<Input <Input
ref={inputRef}
name="search" name="search"
placeholder="Ask a new question..." placeholder="Ask a new question..."
value={input} value={input}
@ -1376,7 +1343,7 @@ export default function Home() {
size={'icon'} size={'icon'}
variant={'ghost'} variant={'ghost'}
className="absolute right-2 top-1/2 transform -translate-y-1/2" className="absolute right-2 top-1/2 transform -translate-y-1/2"
disabled={input.length === 0} disabled={input.length === 0 || isLoading}
> >
<ArrowRight size={20} /> <ArrowRight size={20} />
</Button> </Button>