From 489e6b556bfab6591d024537a21c6f67bef4e8ef Mon Sep 17 00:00:00 2001 From: zaidmukaddam Date: Sat, 23 Nov 2024 00:28:06 +0530 Subject: [PATCH] feat: multi search --- app/api/chat/route.ts | 134 +++++----- app/search/page.tsx | 195 ++------------- components/multi-search.tsx | 404 +++++++++++++++++++++++++++++++ components/ui/form-component.tsx | 3 +- package.json | 3 +- pnpm-lock.yaml | 44 ++-- tailwind.config.ts | 5 - 7 files changed, 504 insertions(+), 284 deletions(-) create mode 100644 components/multi-search.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index c42b2af..8780ac9 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -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); - const data = await tvly.search(query, { - topic: topic, - days: topic === "news" ? 7 : undefined, - maxResults: maxResults < 5 ? 5 : maxResults, - searchDepth: searchDepth, - includeAnswer: true, - includeImages: true, - includeImageDescriptions: includeImageDescriptions, - excludeDomains: exclude_domains, - }) + // Execute searches in parallel + const searchPromises = queries.map(async (query, index) => { + const data = await tvly.search(query, { + 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 { - url: obj.url, - title: obj.title, - content: obj.content, - raw_content: obj.raw_content, - published_date: obj.published_date, - }; - } - return { + return { + query, + results: data.results.map((obj: any) => ({ url: obj.url, title: obj.title, content: obj.content, raw_content: obj.raw_content, - }; - }, - ); + 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 } => + typeof image === 'object' && + image.description !== undefined && + image.description !== '' + ) + : data.images.map(({ url }: { url: string }) => sanitizeUrl(url)) + }; + }); - - const processedImages = 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 } => - 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, }; }, }), @@ -613,7 +587,7 @@ When asked a "What is" question, maintain the same format as the question and an console.log(`Photo fetch failed for "${place.name}":`, error); } - + // Get timezone for the location const tzResponse = await fetch( diff --git a/app/search/page.tsx b/app/search/page.tsx index cf4fa35..060dadf 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -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 ( - - - - - - {images.map((image, index) => ( - - {image.description} -

{image.description}

-
- ))} -
- - -
-
-
- ); -}; - - -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 ( -
- - - -
-
- -

Sources Found

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

{item.title}

-

{item.content}

-
-
- - {item.url} - -
- ))} -
- )} -
-
-
- {result && result.images && result.images.length > 0 && ( -
-
- -

Images

-
-
- {result.images.slice(0, 4).map((image: SearchImage, itemIndex: number) => ( -
handleImageClick(itemIndex)} - > - {image.description} - {itemIndex === 3 && result.images.length > 4 && ( -
- -
- )} -
- ))} -
-
- )} - {openDialog && result.images && ( - - )} -
- ); -}; - 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 ( - +

What'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 ( -
- {!result ? ( -
-
- - Running a search... -
-
- {[0, 1, 2].map((index) => ( - - ))} -
-
- ) : ( - - )} +
+
); } diff --git a/components/multi-search.tsx b/components/multi-search.tsx new file mode 100644 index 0000000..4afa57c --- /dev/null +++ b/components/multi-search.tsx @@ -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 }) => ( +
+ + {query} + + {count} + +
+); + +const ResultCard: React.FC = ({ result, index }) => { + return ( + +
+
+ +
+
+

+ {result.title} +

+ +
+
+ +
+

+ {result.content} +

+
+ + {result.published_date && ( +
+ +
+ )} +
+ ); +}; + +const ImageGrid: React.FC<{ images: SearchImage[]; onImageClick: (index: number) => void }> = ({ images, onImageClick }) => ( +
+ {images.slice(0, 4).map((image, index) => ( + onImageClick(index)} + whileHover={{ scale: 1.02 }} + > + {image.description} +
+

+ {image.description} +

+
+ {index === 3 && images.length > 4 && ( +
+ +{images.length - 4} +
+ )} +
+ ))} +
+); + +const SearchResults: React.FC = ({ searchData, topicType, onImageClick }) => ( +
+
+
+
+
+
+ +
+
+

Results for “{searchData.query}“

+
+ + {topicType} + + {searchData.results.length} results +
+
+
+
+
+ + +
+ {searchData.results.map((result, index) => ( + + ))} +
+
+
+ + + {searchData.images.length > 0 && ( +
+
+
+ +

Related Images

+
+
+ +
+ )} +
+); + +interface ContentDialogProps { + isOpen: boolean; + onClose: () => void; + result: SearchResult; +} + +const ContentDialog: React.FC = ({ isOpen, onClose, result }) => ( + + +
+
+
+ +
+ +
+ +
+
+

+ {result.content} +

+
+ {result.published_date && ( +
+ + +
+ )} +
+
+
+
+
+); + + +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 ( +
+
+
+ +
+
+ + Running searches... + + + Processing {args.queries.length} queries + +
+
+ + +
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+
+ ); + } + + return ( +
+ +
+
+ + {result.searches.map((search, index) => ( + + + + ))} + +
+
+ + + {result.searches.map((search, index) => ( + + { + setSelectedSearch(index); + setSelectedImage(imageIndex); + setGalleryOpen(true); + }} + /> + + ))} + +
+ + {galleryOpen && result.searches[selectedSearch].images && ( + + +
+
+ + {selectedImage + 1} / {result.searches[selectedSearch].images.length} + + +
+ +
+ {result.searches[selectedSearch].images[selectedImage].description} + {result.searches[selectedSearch].images[selectedImage].description && ( +
+

+ {result.searches[selectedSearch].images[selectedImage].description} +

+
+ )} +
+ +
+ +
+ +
+ +
+
+
+
+ )} +
+ ); +}; + +export default MultiSearch; \ No newline at end of file diff --git a/components/ui/form-component.tsx b/components/ui/form-component.tsx index b5e21c0..6c4f757 100644 --- a/components/ui/form-component.tsx +++ b/components/ui/form-component.tsx @@ -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 = ({ return (
0 || uploadQueue.length > 0 ? "bg-gray-100/70 dark:bg-neutral-800 p-1" : "bg-transparent" diff --git a/package.json b/package.json index a57f969..e5c11a8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16a327d..4da7c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/tailwind.config.ts b/tailwind.config.ts index d694d9f..bf8c865 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -88,11 +88,6 @@ const config = { }, }, plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], - safelist: [ - { - pattern: /katex-.*/, - }, - ], } satisfies Config export default config \ No newline at end of file