feat: multi search

This commit is contained in:
zaidmukaddam 2024-11-23 00:28:06 +05:30
parent 1c09e86fb7
commit 489e6b556b
7 changed files with 504 additions and 284 deletions

View File

@ -32,16 +32,6 @@ function sanitizeUrl(url: string): string {
return url.replace(/\s+/g, '%20')
}
// Helper function to geocode an address
const geocodeAddress = async (address: string) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
const response = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(address)}.json?access_token=${mapboxToken}`
);
const data = await response.json();
return data.features[0];
};
export async function POST(req: Request) {
const { messages, model } = await req.json();
@ -144,105 +134,89 @@ When asked a "What is" question, maintain the same format as the question and an
- The response that include latex equations, use always follow the formats:
- Do not wrap any equation or formulas or any sort of math related block in round brackets() as it will crash the response.`,
tools: {
// Update the web_search tool parameters in app/api/chat/route.ts
web_search: tool({
description:
"Search the web for information with the given query, max results and search depth.",
description: "Search the web for information with multiple queries, max results and search depth.",
parameters: z.object({
query: z.string().describe("The search query to look up on the web."),
maxResults: z
queries: z.array(z.string().describe("Array of search queries to look up on the web.")),
maxResults: z.array(z
.number()
.describe(
"The maximum number of results to return. Default to be used is 10.",
),
topic: z
.describe("Array of maximum number of results to return per query. Default is 10.")),
topic: z.array(z
.enum(["general", "news"])
.describe("The topic type to search for. Default is general."),
searchDepth: z
.describe("Array of topic types to search for. Default is general.")),
searchDepth: z.array(z
.enum(["basic", "advanced"])
.describe(
"The search depth to use for the search. Default is basic.",
),
.describe("Array of search depths to use. Default is basic.")),
exclude_domains: z
.array(z.string())
.describe(
"A list of domains to specifically exclude from the search results. Default is None, which doesn't exclude any domains.",
),
.describe("A list of domains to exclude from all search results. Default is None."),
}),
execute: async ({
query,
queries,
maxResults,
topic,
searchDepth,
exclude_domains,
}: {
query: string;
maxResults: number;
topic: "general" | "news";
searchDepth: "basic" | "advanced";
queries: string[];
maxResults: number[];
topic: ("general" | "news")[];
searchDepth: ("basic" | "advanced")[];
exclude_domains?: string[];
}) => {
const apiKey = process.env.TAVILY_API_KEY;
const tvly = tavily({ apiKey });
const includeImageDescriptions = true
const includeImageDescriptions = true;
console.log("Query:", query);
console.log("Queries:", queries);
console.log("Max Results:", maxResults);
console.log("Topic:", topic);
console.log("Search Depth:", searchDepth);
console.log("Topics:", topic);
console.log("Search Depths:", searchDepth);
console.log("Exclude Domains:", exclude_domains);
// Execute searches in parallel
const searchPromises = queries.map(async (query, index) => {
const data = await tvly.search(query, {
topic: topic,
days: topic === "news" ? 7 : undefined,
maxResults: maxResults < 5 ? 5 : maxResults,
searchDepth: searchDepth,
topic: topic[index] || topic[0] || "general",
days: topic[index] === "news" ? 7 : undefined,
maxResults: maxResults[index] || maxResults[0] || 10,
searchDepth: searchDepth[index] || searchDepth[0] || "basic",
includeAnswer: true,
includeImages: true,
includeImageDescriptions: includeImageDescriptions,
excludeDomains: exclude_domains,
})
});
let context = data.results.map(
(obj: any, index: number) => {
if (topic === "news") {
return {
query,
results: data.results.map((obj: any) => ({
url: obj.url,
title: obj.title,
content: obj.content,
raw_content: obj.raw_content,
published_date: obj.published_date,
};
}
return {
url: obj.url,
title: obj.title,
content: obj.content,
raw_content: obj.raw_content,
};
},
);
const processedImages = includeImageDescriptions
published_date: topic[index] === "news" ? obj.published_date : undefined,
})),
images: includeImageDescriptions
? data.images
.map(({ url, description }: { url: string; description?: string }) => ({
url: sanitizeUrl(url),
description: description ?? ''
}))
.filter(
(
image: { url: string; description: string }
): image is { url: string; description: string } =>
(image: { url: string; description: string }): image is { url: string; description: string } =>
typeof image === 'object' &&
image.description !== undefined &&
image.description !== ''
)
: data.images.map(({ url }: { url: string }) => sanitizeUrl(url))
};
});
const searchResults = await Promise.all(searchPromises);
return {
results: context,
images: processedImages
searches: searchResults,
};
},
}),

View File

@ -106,6 +106,7 @@ import WeatherChart from '@/components/weather-chart';
import InteractiveChart from '@/components/interactive-charts';
import NearbySearchMapView from '@/components/nearby-search-map-view';
import { MapComponent, MapContainer, MapSkeleton } from '@/components/map-components';
import MultiSearch from '@/components/multi-search';
export const maxDuration = 60;
@ -116,147 +117,6 @@ interface Attachment {
size: number;
}
interface SearchImage {
url: string;
description: string;
}
const ImageCarousel = ({ images, onClose }: { images: SearchImage[], onClose: () => void }) => {
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[90vw] max-h-[90vh] p-0 bg-white dark:bg-neutral-900">
<button
onClick={onClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground z-10"
>
<X className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
<span className="sr-only">Close</span>
</button>
<Carousel className="w-full h-full">
<CarouselContent>
{images.map((image, index) => (
<CarouselItem key={index} className="flex flex-col items-center justify-center p-4">
<img
src={image.url}
alt={image.description}
className="max-w-full max-h-[70vh] object-contain mb-4"
/>
<p className="text-center text-sm text-neutral-800 dark:text-neutral-200">{image.description}</p>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-4" />
<CarouselNext className="right-4" />
</Carousel>
</DialogContent>
</Dialog>
);
};
const WebSearchResults = ({ result, args }: { result: any, args: any }) => {
const [openDialog, setOpenDialog] = useState(false);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const handleImageClick = (index: number) => {
setSelectedImageIndex(index);
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
};
return (
<div>
<Accordion type="single" collapsible className="w-full mt-4">
<AccordionItem value="item-1" className='border-none'>
<AccordionTrigger className="hover:no-underline py-2">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<Newspaper className="h-5 w-5 text-primary" />
<h2 className='text-base font-semibold text-neutral-800 dark:text-neutral-200'>Sources Found</h2>
</div>
{result && (
<Badge variant="secondary" className='rounded-full bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200'>{result.results.length} results</Badge>
)}
</div>
</AccordionTrigger>
<AccordionContent>
{args?.query && (
<Badge variant="secondary" className="mb-4 text-xs sm:text-sm font-light rounded-full bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">
<SearchIcon className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
{args.query}
</Badge>
)}
{result && (
<div className="flex flex-row gap-4 overflow-x-auto pb-2">
{result.results.map((item: any, itemIndex: number) => (
<div key={itemIndex} className="flex flex-col w-[280px] flex-shrink-0 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg p-3">
<div className="flex items-start gap-3 mb-2">
<img
src={`https://www.google.com/s2/favicons?sz=128&domain=${new URL(item.url).hostname}`}
alt="Favicon"
className="w-8 h-8 sm:w-12 sm:h-12 flex-shrink-0 rounded-sm"
/>
<div className="flex-grow min-w-0">
<h3 className="text-sm font-semibold line-clamp-2 text-neutral-800 dark:text-neutral-200">{item.title}</h3>
<p className="text-xs text-neutral-600 dark:text-neutral-400 line-clamp-2 mt-1">{item.content}</p>
</div>
</div>
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary truncate hover:underline"
>
{item.url}
</a>
</div>
))}
</div>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
{result && result.images && result.images.length > 0 && (
<div className="mt-4">
<div className='flex items-center gap-2 cursor-pointer mb-2'>
<ImageIcon className="h-5 w-5 text-primary" />
<h3 className="text-base font-semibold text-neutral-800 dark:text-neutral-200">Images</h3>
</div>
<div className="grid grid-cols-4 gap-2">
{result.images.slice(0, 4).map((image: SearchImage, itemIndex: number) => (
<div
key={itemIndex}
className="relative aspect-square cursor-pointer overflow-hidden rounded-md"
onClick={() => handleImageClick(itemIndex)}
>
<img
src={image.url}
alt={image.description}
className="w-full h-full object-cover transition-transform duration-300 hover:scale-110"
/>
{itemIndex === 3 && result.images.length > 4 && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
<PlusCircledIcon className="w-8 h-8 text-white" />
</div>
)}
</div>
))}
</div>
</div>
)}
{openDialog && result.images && (
<ImageCarousel
images={result.images}
onClose={handleCloseDialog}
/>
)}
</div>
);
};
const HomeContent = () => {
const searchParams = useSearchParams();
const initialQuery = searchParams.get('query') || '';
@ -357,31 +217,33 @@ const HomeContent = () => {
const changelogs: Changelog[] = [
{
id: "1",
title: "Dark mode is here!",
title: "New Updates!",
images: [
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-dark-mode-promo.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-input-bar-promo.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-gpt-4o-back-Lwzx44RD4XofYLAmrEsLD3Fngnn33K.png"
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-maps-beta.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-multi-run.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-multi-results.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-claude.png"
],
content:
`## **Dark Mode**
`## **Nearby Map Search Beta**
The most requested feature is finally here! You can now toggle between light and dark mode. Default is set to your system preference.
The new Nearby Map Search tool is now available in beta! You can use it to find nearby places, restaurants, attractions, and more. Give it a try and let us know what you think!
## **New Input Bar Design**
## **Multi Search is here by default**
The input bar has been redesigned to make it more focused, user-friendly and accessible. The model selection dropdown has been moved to the bottom left corner inside the input bar.
The AI powered Multiple Query Search tool is now available by default. The LLM model will now automatically suggest multiple queries based on your input and run the searches in parallel.
## **GPT-4o is back!**
## **Claude 3.5 Sonnet(New) and 3.5 Haiku are here!**
GPT-4o has been re-enabled! You can use it by selecting the model from the dropdown.`,
The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now available on the platform.
`
}
];
const ChangeLogs: React.FC<{ open: boolean; setOpen: (open: boolean) => void }> = ({ open, setOpen }) => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="no-scrollbar max-h-[80vh] overflow-y-auto rounded-xl border-none p-0 gap-0 font-sans bg-white dark:bg-neutral-900">
<DialogContent className="no-scrollbar max-h-[80vh] overflow-y-auto rounded-xl border-none p-0 gap-0 font-sans bg-white dark:bg-neutral-900 z-[1000]">
<div className="w-full py-3 flex justify-center items-center border-b border-neutral-200 dark:border-neutral-700">
<h2 className="text-lg font-bold flex items-center gap-2 text-neutral-800 dark:text-neutral-100">
<Flame size={20} /> What&apos;s new
@ -958,33 +820,8 @@ GPT-4o has been re-enabled! You can use it by selecting the model from the dropd
if (toolInvocation.toolName === 'web_search') {
return (
<div>
{!result ? (
<div className="flex items-center justify-between w-full">
<div className='flex items-center gap-2'>
<Globe className="h-5 w-5 text-neutral-700 dark:text-neutral-300 animate-spin" />
<span className="text-neutral-700 dark:text-neutral-300 text-lg">Running a search...</span>
</div>
<div className="flex space-x-1">
{[0, 1, 2].map((index) => (
<motion.div
key={index}
className="w-2 h-2 bg-neutral-400 dark:bg-neutral-600 rounded-full"
initial={{ opacity: 0.3 }}
animate={{ opacity: 1 }}
transition={{
repeat: Infinity,
duration: 0.8,
delay: index * 0.2,
repeatType: "reverse",
}}
/>
))}
</div>
</div>
) : (
<WebSearchResults result={result} args={args} />
)}
<div className="mt-4">
<MultiSearch result={result} args={args} />
</div>
);
}

404
components/multi-search.tsx Normal file
View File

@ -0,0 +1,404 @@
/* eslint-disable @next/next/no-img-element */
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ArrowUpRight, Calendar, ChevronLeft, ChevronRight, Clock, Globe, ImageIcon, Newspaper, Search, X } from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
type SearchImage = {
url: string;
description: string;
};
type SearchResult = {
url: string;
title: string;
content: string;
raw_content: string;
published_date?: string;
};
type SearchQueryResult = {
query: string;
results: SearchResult[];
images: SearchImage[];
};
type MultiSearchResponse = {
searches: SearchQueryResult[];
};
type MultiSearchArgs = {
queries: string[];
maxResults: number[];
topic: ("general" | "news")[];
searchDepth: ("basic" | "advanced")[];
};
interface ResultCardProps {
result: SearchResult;
index: number;
}
interface GalleryProps {
images: SearchImage[];
onClose: () => void;
}
interface SearchResultsProps {
searchData: SearchQueryResult;
topicType: string;
onImageClick: (index: number) => void;
}
const SearchQueryTab: React.FC<{ query: string; count: number; isActive: boolean }> = ({ query, count, isActive }) => (
<div className="flex items-center gap-2">
<Search className="h-4 w-4" />
<span className="text-sm font-medium truncate max-w-[120px]">{query}</span>
<Badge variant="secondary" className={isActive ? 'dark:bg-white/20 dark:text-white bg-gray-200 text-gray-700' : 'dark:bg-neutral-800 bg-gray-100'}>
{count}
</Badge>
</div>
);
const ResultCard: React.FC<ResultCardProps> = ({ result, index }) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
className="relative flex flex-col w-[280px] dark:bg-neutral-900 bg-white rounded-xl dark:border-neutral-800 border-gray-200 border overflow-hidden flex-shrink-0"
>
<div className="flex items-start gap-3 p-3">
<div className="w-8 h-8 rounded-lg dark:bg-neutral-800 bg-gray-100 flex items-center justify-center flex-shrink-0">
<img
src={`https://www.google.com/s2/favicons?sz=128&domain=${new URL(result.url).hostname}`}
alt=""
className="w-5 h-5"
/>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium dark:text-neutral-100 text-gray-900 line-clamp-2 leading-tight">
{result.title}
</h3>
<div className="flex items-center justify-between mt-1">
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs dark:text-neutral-400 text-gray-500 hover:text-gray-700 dark:hover:text-neutral-300 flex items-center gap-1"
>
{new URL(result.url).hostname}
<ArrowUpRight className="h-3 w-3" />
</a>
</div>
</div>
</div>
<div className="px-3 pb-3">
<p className="text-sm dark:text-neutral-300 text-gray-600 line-clamp-3 leading-relaxed">
{result.content}
</p>
</div>
{result.published_date && (
<div className="px-3 py-2 mt-auto border-t dark:border-neutral-800 border-gray-200 dark:bg-neutral-900/50 bg-gray-50">
<time className="text-xs dark:text-neutral-500 text-gray-500 flex items-center gap-1.5">
<Calendar className="h-3 w-3" />
{new Date(result.published_date).toLocaleDateString()}
</time>
</div>
)}
</motion.div>
);
};
const ImageGrid: React.FC<{ images: SearchImage[]; onImageClick: (index: number) => void }> = ({ images, onImageClick }) => (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-4">
{images.slice(0, 4).map((image, index) => (
<motion.div
key={index}
className="relative aspect-square rounded-xl overflow-hidden cursor-pointer group"
onClick={() => onImageClick(index)}
whileHover={{ scale: 1.02 }}
>
<img
src={image.url}
alt={image.description}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<p className="absolute bottom-2 left-2 right-2 text-xs text-white line-clamp-2">
{image.description}
</p>
</div>
{index === 3 && images.length > 4 && (
<div className="absolute inset-0 flex items-center justify-center bg-black/60">
<span className="text-xl font-medium text-white">+{images.length - 4}</span>
</div>
)}
</motion.div>
))}
</div>
);
const SearchResults: React.FC<SearchResultsProps> = ({ searchData, topicType, onImageClick }) => (
<div className="space-y-6">
<div className="dark:bg-neutral-900 bg-white rounded-xl dark:border-neutral-800 border-gray-200 border">
<div className="p-3 border-b dark:border-neutral-800 border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg dark:bg-neutral-800 bg-gray-100">
<Newspaper className="h-4 w-4 dark:text-neutral-400 text-gray-500" />
</div>
<div>
<h2 className="dark:text-neutral-100 text-gray-900 font-medium">Results for &ldquo;{searchData.query}&ldquo;</h2>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary" className="dark:bg-neutral-800 bg-gray-100 dark:text-neutral-300 text-gray-600">
{topicType}
</Badge>
<span className="text-xs dark:text-neutral-500 text-gray-500">{searchData.results.length} results</span>
</div>
</div>
</div>
</div>
</div>
<ScrollArea>
<div className="flex gap-3 p-3 overflow-y-scroll">
{searchData.results.map((result, index) => (
<ResultCard key={index} result={result} index={index} />
))}
</div>
</ScrollArea>
</div>
{searchData.images.length > 0 && (
<div className="dark:bg-neutral-900 bg-white rounded-xl dark:border-neutral-800 border-gray-200 border">
<div className="p-4 border-b dark:border-neutral-800 border-gray-200">
<div className="flex items-center gap-3">
<ImageIcon className="h-5 w-5 dark:text-neutral-400 text-gray-500" />
<h3 className="dark:text-neutral-100 text-gray-900 font-medium">Related Images</h3>
</div>
</div>
<ImageGrid images={searchData.images} onImageClick={onImageClick} />
</div>
)}
</div>
);
interface ContentDialogProps {
isOpen: boolean;
onClose: () => void;
result: SearchResult;
}
const ContentDialog: React.FC<ContentDialogProps> = ({ isOpen, onClose, result }) => (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl h-fit p-0 dark:bg-neutral-900 bg-white dark:border-neutral-800 border-gray-200">
<div className="p-6 space-y-4">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg dark:bg-neutral-800 bg-gray-100 flex items-center justify-center flex-shrink-0">
<img
src={`https://www.google.com/s2/favicons?sz=128&domain=${new URL(result.url).hostname}`}
alt=""
className="w-6 h-6"
/>
</div>
<div className="flex-1 min-w-0">
<h2 className="text-xl font-semibold dark:text-neutral-100 text-gray-900">
{result.title}
</h2>
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="mt-2 text-sm dark:text-neutral-400 text-gray-500 hover:text-gray-700 dark:hover:text-neutral-300 flex items-center gap-1 w-fit"
>
{new URL(result.url).hostname}
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
</div>
<ScrollArea className="h-[60vh] w-full pr-4">
<div className="space-y-4">
<div className="prose prose-invert max-w-none">
<p className="dark:text-neutral-200 text-gray-700 leading-relaxed whitespace-pre-wrap">
{result.content}
</p>
</div>
{result.published_date && (
<div className="flex items-center gap-2 text-sm dark:text-neutral-500 text-gray-500 pt-4 border-t dark:border-neutral-800 border-gray-200">
<Calendar className="h-4 w-4" />
<time>
Published on {new Date(result.published_date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</div>
)}
</div>
</ScrollArea>
</div>
</DialogContent>
</Dialog>
);
const MultiSearch: React.FC<{ result: MultiSearchResponse | null; args: MultiSearchArgs }> = ({ result, args }) => {
const [activeTab, setActiveTab] = useState("0");
const [galleryOpen, setGalleryOpen] = useState(false);
const [selectedSearch, setSelectedSearch] = useState(0);
const [selectedImage, setSelectedImage] = useState(0);
// Replace the current loading state in MultiSearch component with this:
if (!result) {
return (
<div className="flex items-center justify-between w-full dark:bg-neutral-900 bg-white rounded-xl dark:border-neutral-800 border-gray-200 border p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg dark:bg-neutral-800 bg-gray-100">
<Globe className="h-5 w-5 dark:text-neutral-400 text-gray-500 animate-spin" />
</div>
<div className="flex flex-col">
<span className="dark:text-neutral-100 text-gray-900 font-medium">
Running searches...
</span>
<span className="text-sm dark:text-neutral-500 text-gray-500">
Processing {args.queries.length} queries
</span>
</div>
</div>
<motion.div className="flex items-center gap-2">
<Clock className="h-4 w-4 dark:text-neutral-400 text-gray-500" />
<div className="flex gap-1">
{[0, 1, 2].map((index) => (
<motion.div
key={index}
className="w-2 h-2 rounded-full dark:bg-neutral-700 bg-gray-300"
initial={{ opacity: 0.3 }}
animate={{ opacity: 1 }}
transition={{
repeat: Infinity,
duration: 0.8,
delay: index * 0.2,
repeatType: "reverse",
}}
/>
))}
</div>
</motion.div>
</div>
);
}
return (
<div className="w-full max-w-2xl">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="relative w-full">
<div className="w-full overflow-y-scroll">
<TabsList className="inline-flex h-auto gap-2 dark:bg-neutral-900 bg-white p-2 rounded-xl dark:border-neutral-800 border-gray-200 border">
{result.searches.map((search, index) => (
<TabsTrigger
key={index}
value={index.toString()}
className="flex-shrink-0 px-3 py-2 rounded-lg transition-all data-[state=active]:dark:bg-neutral-800 data-[state=active]:bg-gray-200 data-[state=active]:text-gray-900 data-[state=active]:dark:text-white text-gray-400 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200"
>
<SearchQueryTab
query={search.query}
count={search.results.length}
isActive={activeTab === index.toString()}
/>
</TabsTrigger>
))}
</TabsList>
</div>
</div>
<AnimatePresence mode="wait">
{result.searches.map((search, index) => (
<TabsContent key={index} value={index.toString()}>
<SearchResults
searchData={search}
topicType={args.topic[index] || args.topic[0]}
onImageClick={(imageIndex) => {
setSelectedSearch(index);
setSelectedImage(imageIndex);
setGalleryOpen(true);
}}
/>
</TabsContent>
))}
</AnimatePresence>
</Tabs>
{galleryOpen && result.searches[selectedSearch].images && (
<Dialog open={galleryOpen} onOpenChange={setGalleryOpen}>
<DialogContent className="max-w-3xl max-h-[85vh] p-0 dark:bg-neutral-900 bg-white dark:border-neutral-800 border-gray-200">
<div className="relative w-full h-full">
<div className="absolute right-3 top-3 z-50 flex items-center gap-2">
<span className="px-2 py-1 rounded-md dark:bg-neutral-800 bg-gray-100 text-xs dark:text-neutral-300 text-gray-600">
{selectedImage + 1} / {result.searches[selectedSearch].images.length}
</span>
<button
onClick={() => setGalleryOpen(false)}
className="p-1.5 rounded-md dark:bg-neutral-800 bg-gray-100 dark:text-neutral-400 text-gray-500 hover:text-gray-700 dark:hover:text-neutral-200"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
<div className="relative w-full h-full">
<img
src={result.searches[selectedSearch].images[selectedImage].url}
alt={result.searches[selectedSearch].images[selectedImage].description}
className="max-h-[70vh] object-contain rounded-md mx-auto p-4"
/>
{result.searches[selectedSearch].images[selectedImage].description && (
<div className="absolute inset-x-0 bottom-4 mx-4 p-2 bg-gradient-to-t from-black/80 to-transparent rounded-b-md">
<p className="text-xs text-white/90 text-center">
{result.searches[selectedSearch].images[selectedImage].description}
</p>
</div>
)}
</div>
<div className="absolute inset-y-0 left-0 flex items-center">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 ml-2 dark:bg-neutral-800 bg-gray-100 dark:hover:bg-neutral-700 hover:bg-gray-200"
onClick={() => setSelectedImage(prev =>
prev === 0 ? result.searches[selectedSearch].images.length - 1 : prev - 1
)}
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 mr-2 dark:bg-neutral-800 bg-gray-100 dark:hover:bg-neutral-700 hover:bg-gray-200"
onClick={() => setSelectedImage(prev =>
prev === result.searches[selectedSearch].images.length - 1 ? 0 : prev + 1
)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
};
export default MultiSearch;

View File

@ -1,4 +1,5 @@
/* eslint-disable @next/next/no-img-element */
// /components/ui/form-component.tsx
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import { ChatRequestOptions, CreateMessage, Message } from 'ai';
@ -431,7 +432,7 @@ const FormComponent: React.FC<FormComponentProps> = ({
return (
<div className={cn(
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300 z-[9999]",
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300 z-[99]",
attachments.length > 0 || uploadQueue.length > 0
? "bg-gray-100/70 dark:bg-neutral-800 p-1"
: "bg-transparent"

View File

@ -11,11 +11,12 @@
"dependencies": {
"@ai-sdk/anthropic": "^0.0.55",
"@ai-sdk/azure": "^0.0.51",
"@ai-sdk/cohere": "latest",
"@ai-sdk/cohere": "^1.0.3",
"@ai-sdk/google": "^0.0.55",
"@ai-sdk/groq": "^0.0.1",
"@ai-sdk/mistral": "^0.0.41",
"@ai-sdk/openai": "^0.0.58",
"@ai-sdk/xai": "^1.0.3",
"@e2b/code-interpreter": "^1.0.3",
"@foobar404/wave": "^2.0.5",
"@mendable/firecrawl-js": "^1.4.3",

View File

@ -12,8 +12,8 @@ dependencies:
specifier: ^0.0.51
version: 0.0.51(zod@3.23.8)
'@ai-sdk/cohere':
specifier: latest
version: 1.0.0(zod@3.23.8)
specifier: ^1.0.3
version: 1.0.3(zod@3.23.8)
'@ai-sdk/google':
specifier: ^0.0.55
version: 0.0.55(zod@3.23.8)
@ -26,6 +26,9 @@ dependencies:
'@ai-sdk/openai':
specifier: ^0.0.58
version: 0.0.58(zod@3.23.8)
'@ai-sdk/xai':
specifier: ^1.0.3
version: 1.0.3(zod@3.23.8)
'@e2b/code-interpreter':
specifier: ^1.0.3
version: 1.0.3
@ -279,14 +282,14 @@ packages:
zod: 3.23.8
dev: false
/@ai-sdk/cohere@1.0.0(zod@3.23.8):
resolution: {integrity: sha512-iN2Ww2VeRnprQBJ7dCp65DtdzCY/53+CA3UmM7Rhn8IZCTqRHkVoZebzv3ZOTb9pikO4CUWqV9yh1oUhQDgyow==}
/@ai-sdk/cohere@1.0.3(zod@3.23.8):
resolution: {integrity: sha512-SDjPinUcGzTNiSMN+9zs1fuAcP8rU1/+CmDWAGu7eMhwVGDurgiOqscC0Oqs/aLsodLt/sFeOvyqj86DAknpbg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
dependencies:
'@ai-sdk/provider': 1.0.0
'@ai-sdk/provider-utils': 2.0.0(zod@3.23.8)
'@ai-sdk/provider': 1.0.1
'@ai-sdk/provider-utils': 2.0.2(zod@3.23.8)
zod: 3.23.8
dev: false
@ -425,8 +428,8 @@ packages:
zod: 3.23.8
dev: false
/@ai-sdk/provider-utils@2.0.0(zod@3.23.8):
resolution: {integrity: sha512-uITgVJByhtzuQU2ZW+2CidWRmQqTUTp6KADevy+4aRnmILZxY2LCt+UZ/ZtjJqq0MffwkuQPPY21ExmFAQ6kKA==}
/@ai-sdk/provider-utils@2.0.2(zod@3.23.8):
resolution: {integrity: sha512-IAvhKhdlXqiSmvx/D4uNlFYCl8dWT+M9K+IuEcSgnE2Aj27GWu8sDIpAf4r4Voc+wOUkOECVKQhFo8g9pozdjA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -434,9 +437,9 @@ packages:
zod:
optional: true
dependencies:
'@ai-sdk/provider': 1.0.0
'@ai-sdk/provider': 1.0.1
eventsource-parser: 3.0.0
nanoid: 5.0.8
nanoid: 3.3.7
secure-json-parse: 2.7.0
zod: 3.23.8
dev: false
@ -469,8 +472,8 @@ packages:
json-schema: 0.4.0
dev: false
/@ai-sdk/provider@1.0.0:
resolution: {integrity: sha512-Sj29AzooJ7SYvhPd+AAWt/E7j63E9+AzRnoMHUaJPRYzOd/WDrVNxxv85prF9gDcQ7XPVlSk9j6oAZV9/DXYpA==}
/@ai-sdk/provider@1.0.1:
resolution: {integrity: sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==}
engines: {node: '>=18'}
dependencies:
json-schema: 0.4.0
@ -562,6 +565,17 @@ packages:
- zod
dev: false
/@ai-sdk/xai@1.0.3(zod@3.23.8):
resolution: {integrity: sha512-Z3ovBU21Wp87EPwkLoP0K4SNkyIzwQk+YAFuBPnRLCSVtBESeMarcI5zDVvBJ0lmQalRX1ZBAs8U1FvQ4T9mqw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
dependencies:
'@ai-sdk/provider': 1.0.1
'@ai-sdk/provider-utils': 2.0.2(zod@3.23.8)
zod: 3.23.8
dev: false
/@alloc/quick-lru@5.2.0:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@ -5251,12 +5265,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/nanoid@5.0.8:
resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true

View File

@ -88,11 +88,6 @@ const config = {
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
safelist: [
{
pattern: /katex-.*/,
},
],
} satisfies Config
export default config