feat: improved UI

This commit is contained in:
zaidmukaddam 2024-08-09 11:18:35 +05:30
parent ed4d38eb2e
commit d2a3efcae4
5 changed files with 236 additions and 66 deletions

View File

@ -87,6 +87,7 @@ export async function POST(req: Request) {
},
onFinish: async (event) => {
console.log(event.text);
console.log("Called " + event.toolCalls?.map((toolCall) => toolCall.toolName));
}
});

View File

@ -11,20 +11,24 @@ import { motion, AnimatePresence } from 'framer-motion';
import {
SearchIcon,
LinkIcon,
Check,
Loader2,
ChevronDown,
ChevronUp,
FastForward,
Sparkles,
ArrowRight,
BookCheck
Globe
} from 'lucide-react';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -63,54 +67,66 @@ 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 (
<Card key={index} className="mb-4 border border-muted">
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2 flex-wrap">
<div className='flex items-center gap-2'>
{result ? <Check className="h-5 w-5 text-green-500" /> : <Loader2 className="h-5 w-5 text-primary animate-spin" />}
<span className="text-sm sm:text-base">{result ? 'Used' : 'Using'} {toolInvocation.toolName === 'web_search' ? 'Web Search' : toolInvocation.toolName}</span>
</div>
<Button
onClick={() => setShowToolResults(prev => ({ ...prev, [index]: !prev[index] }))}
className='ml-2 text-xs sm:text-sm'
variant="secondary"
>
{showToolResults[index] ? 'Hide Results' : 'Show Results'}
{showToolResults[index] ? <ChevronUp className="ml-2 h-4 w-4" /> : <ChevronDown className="ml-2 h-4 w-4" />}
</Button>
</CardTitle>
</CardHeader>
<CardContent>
{args?.query && (
<Badge variant="secondary" className="mb-2 text-xs sm:text-sm">
<SearchIcon className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
{args.query}
</Badge>
)}
{showToolResults[index] && result && (
<ScrollArea className="h-[200px] sm:h-[300px] w-full rounded-md border border-muted p-2 sm:p-4 mt-2">
{result.results.map((item: any, itemIndex: number) => (
<div key={itemIndex} className="mb-4 pb-4 border-b last:border-b-0">
<h3 className="text-sm sm:text-lg font-semibold mb-1 text-secondary-foreground">{item.title}</h3>
<p className="text-xs sm:text-sm text-muted-foreground mb-1">{item.content}</p>
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-500 hover:underline flex items-center"
>
<LinkIcon className="h-3 w-3 mr-1" />
<span className="truncate">{item.url}</span>
</a>
<Accordion type="single" collapsible className="w-full mt-4">
<AccordionItem value={`item-${index}`}>
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2 text-sm sm:text-base">
<Globe className="h-5 w-5 text-primary" />
<span>Web Search</span>
</div>
{!result && (
<div className="flex items-center gap-2">
<Loader2 className="h-5 w-5 text-primary animate-spin" />
<span className="text-sm text-muted-foreground">Searching the web...</span>
</div>
))}
</ScrollArea>
)}
</CardContent>
</Card>
)}
{result && (
<Badge variant="secondary" className='mr-1'>{result.results.length} results</Badge>
)}
</div>
</AccordionTrigger>
<AccordionContent>
{args?.query && (
<Badge variant="secondary" className="mb-2 text-xs sm:text-sm">
<SearchIcon className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
{args.query}
</Badge>
)}
{result && (
<ScrollArea className="h-[300px] w-full rounded-md">
<div className="grid grid-cols-2 gap-4">
{result.results.map((item: any, itemIndex: number) => (
<Card key={itemIndex} className="flex flex-col h-full shadow-none">
<CardHeader className="pb-2">
{/* favicon here */}
<img src={`https://www.google.com/s2/favicons?domain=${new URL(item.url).hostname}`} alt="Favicon" className="w-5 h-5 flex-shrink-0 rounded-full" />
<CardTitle className="text-sm font-semibold line-clamp-2">{item.title}</CardTitle>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-xs text-muted-foreground line-clamp-3">{item.content}</p>
</CardContent>
<div className="px-6 py-2 bg-muted rounded-b-xl">
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline flex items-center"
>
<span className="truncate">{item.url}</span>
</a>
</div>
</Card>
))}
</div>
</ScrollArea>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
@ -153,7 +169,7 @@ export default function Home() {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className="prose text-sm sm:text-base"
className="prose text-sm sm:text-base text-pretty text-left"
components={{
a: ({ href, children }) => {
const index = citationLinks.findIndex((link: { link: string | undefined; }) => link.link === href);
@ -214,7 +230,7 @@ export default function Home() {
return (
<div className="flex flex-col font-sans items-center min-h-screen p-2 sm:p-4 bg-background text-foreground transition-all duration-500">
<div className={`w-full max-w-xl sm:max-w-2xl space-y-4 sm:space-y-6 ${hasSubmitted ? 'mt-16 sm:mt-20' : 'mt-[15vh] sm:mt-[20vh]'}`}>
<div className={`w-full max-w-xl sm:max-w-2xl space-y-4 sm:space-y-6 p-1 ${hasSubmitted ? 'mt-16 sm:mt-20' : 'mt-[15vh] sm:mt-[20vh]'}`}>
<motion.div
initial={false}
animate={hasSubmitted ? { scale: 1.2 } : { scale: 1 }}
@ -236,7 +252,7 @@ export default function Home() {
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<div className="relative mb-4">
<div className="relative px-2 mb-4">
<button
onClick={() => setIsModelSelectorOpen(!isModelSelectorOpen)}
className={`flex items-center font-semibold ${models.find((model) => model.name === selectedModel)?.name.includes('Quality') ? 'text-purple-500' : 'text-green-500'} focus:outline-none focus:ring-0 `}
@ -278,7 +294,7 @@ export default function Home() {
)}
</div>
<form onSubmit={handleFormSubmit} className="flex items-center space-x-2 mb-4 sm:mb-6">
<form onSubmit={handleFormSubmit} className="flex items-center space-x-2 px-2 mb-4 sm:mb-6">
<div className="relative flex-1">
<Input
ref={inputRef}
@ -368,19 +384,19 @@ export default function Home() {
{messages.map((message, index) => (
<div key={index}>
{message.role === 'assistant' && message.content && (
<Card className="bg-card text-card-foreground border border-muted !mb-20 sm:!mb-16">
<CardContent className="p-3 sm:p-4">
<div
className='flex items-center gap-2 mb-2'
>
<BookCheck className="size-4 sm:size-5 text-primary" />
<h2 className="text-lg sm:text-xl font-semibold">Answer</h2>
</div>
<div className="text-sm sm:text-base">
<MarkdownRenderer content={message.content} />
</div>
</CardContent>
</Card>
<div
className='!mb-20 sm:!mb-18'
>
<div
className='flex items-center gap-2 mb-2'
>
<Sparkles className="size-4 sm:size-5 text-primary" />
<h2 className="text-lg font-semibold">Answer</h2>
</div>
<div className="text-sm">
<MarkdownRenderer content={message.content} />
</div>
</div>
)}
{message.toolInvocations?.map((toolInvocation: ToolInvocation, toolIndex: number) => (
<div key={`tool-${toolIndex}`}>

View File

@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -11,6 +11,7 @@
"dependencies": {
"@ai-sdk/anthropic": "^0.0.37",
"@ai-sdk/openai": "^0.0.40",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-scroll-area": "^1.1.0",

View File

@ -11,6 +11,9 @@ dependencies:
'@ai-sdk/openai':
specifier: ^0.0.40
version: 0.0.40(zod@3.23.8)
'@radix-ui/react-accordion':
specifier: ^1.2.0
version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-hover-card':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
@ -567,6 +570,34 @@ packages:
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
dev: false
/@radix-ui/react-accordion@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-collapsible': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==}
peerDependencies:
@ -587,6 +618,56 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collapsible@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@ -686,6 +767,20 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==}
peerDependencies: