feat: improved UI
This commit is contained in:
parent
ed4d38eb2e
commit
d2a3efcae4
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
148
app/page.tsx
148
app/page.tsx
@ -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}`}>
|
||||
|
||||
57
components/ui/accordion.tsx
Normal file
57
components/ui/accordion.tsx
Normal 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 }
|
||||
@ -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",
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user