From 4b59941791135938f678627afad650c9fb5e6746 Mon Sep 17 00:00:00 2001 From: zaidmukaddam Date: Sun, 5 Jan 2025 01:38:26 +0530 Subject: [PATCH] mplx+vercel+xai collab --- README.md | 70 +- app/actions.ts | 218 +- app/api/chat/route.ts | 414 +-- app/api/cohere/route.ts | 421 --- app/api/trending/route.ts | 21 +- app/new/page.tsx | 2 +- app/page.tsx | 3812 ++++++++++++++++----- app/search/page.tsx | 2853 +-------------- components/movie-info.tsx | 236 ++ components/shopping-cards.tsx | 223 -- components/trending-tv-movies-results.tsx | 309 ++ components/ui/form-component.tsx | 63 +- lib/utils.ts | 58 +- next.config.mjs | 14 + package.json | 13 +- pnpm-lock.yaml | 243 +- tailwind.config.ts | 2 +- 17 files changed, 3882 insertions(+), 5090 deletions(-) delete mode 100644 app/api/cohere/route.ts create mode 100644 components/movie-info.tsx delete mode 100644 components/shopping-cards.tsx create mode 100644 components/trending-tv-movies-results.tsx diff --git a/README.md b/README.md index fc08b0e..636e6c2 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,6 @@ A minimalistic AI-powered search engine that helps you find information on the internet. -## Set MiniPerplx as your default search engine - -1. **Open the Chrome browser settings**: - - Click on the three vertical dots in the upper right corner of the browser. - - Select "Settings" from the dropdown menu. - -2. **Go to the search engine settings**: - - In the left sidebar, click on "Search engine." - - Then select "Manage search engines and site search." - -3. **Add a new search engine**: - - Click on "Add" next to "Site search." - -4. **Set the search engine name**: - - Enter `MiniPerplx` in the "Search engine" field. - -5. **Set the search engine URL**: - - Enter `https://mplx.run/search?query=%s&model=azure:gpt4o-mini` in the "URL with %s in place of query" field. - -6. **Set the search engine shortcut**: - - Enter `mp` in the "Shortcut" field. - -7. **Set Default**: - - Click on the three dots next to the search engine you just added. - - Select "Make default" from the dropdown menu. - -After completing these steps, you should be able to use MiniPerplx as your default search engine in Chrome. - ## ProductHunt Launch Upvote MiniPerplx on ProductHunt to show your support! @@ -52,6 +24,11 @@ Upvote MiniPerplx on ProductHunt to show your support! - **Product Search**: Search for products on Amazon. - **X Posts Search**: Search for posts on X.com. - **Flight Tracker**: Track flights using AviationStack's API. +- **Trending Movies and TV Shows**: Get information about trending movies and TV shows. +- **Movie or TV Show Search**: Get information about any movie or TV show. + +## LLM used +- [xAI's Grok](https://x.ai/grok) ## Built with - [Next.js](https://nextjs.org/) @@ -68,16 +45,37 @@ Upvote MiniPerplx on ProductHunt to show your support! - [Exa.AI](https://exa.ai/) - [AviationStack](https://aviationstack.com/) -## LLM used -- [OpenAI's GPT 4o mini](https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/) -- [OpenAI's GPT 4o](https://openai.com/index/hello-gpt-4o/) -- [Anthropic's Claude 3.5 Sonnet](https://www.anthropic.com/news/claude-3-5-sonnet/) -- [Anthropic's Claude 3.5 Haiku](https://www.anthropic.com/claude/haiku) -- [xAI's Grok](https://x.ai/grok) - ### Deploy your own -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzaidmukaddam%2Fminiperplx&env=OPENAI_API_KEY,ANTHROPIC_API_KEY,GROQ_API_KEY,TAVILY_API_KEY,OPENWEATHER_API_KEY,E2B_API_KEY&envDescription=API%20keys%20needed%20for%20application) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzaidmukaddam%2Fminiperplx&env=XAI_API_KEY,GROQ_API_KEY,TAVILY_API_KEY,OPENWEATHER_API_KEY,E2B_API_KEY&envDescription=API%20keys%20needed%20for%20application) + +## Set MiniPerplx as your default search engine + +1. **Open the Chrome browser settings**: + - Click on the three vertical dots in the upper right corner of the browser. + - Select "Settings" from the dropdown menu. + +2. **Go to the search engine settings**: + - In the left sidebar, click on "Search engine." + - Then select "Manage search engines and site search." + +3. **Add a new search engine**: + - Click on "Add" next to "Site search." + +4. **Set the search engine name**: + - Enter `MiniPerplx` in the "Search engine" field. + +5. **Set the search engine URL**: + - Enter `https://mplx.run?q=%s` in the "URL with %s in place of query" field. + +6. **Set the search engine shortcut**: + - Enter `mp` in the "Shortcut" field. + +7. **Set Default**: + - Click on the three dots next to the search engine you just added. + - Select "Make default" from the dropdown menu. + +After completing these steps, you should be able to use MiniPerplx as your default search engine in Chrome. ### Local development diff --git a/app/actions.ts b/app/actions.ts index 4b01217..12e222d 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,9 +1,9 @@ // app/actions.ts 'use server'; -import { generateObject, CoreMessage } from 'ai'; -import { google } from '@ai-sdk/google' +import { generateObject } from 'ai'; import { z } from 'zod'; +import { xai } from '@ai-sdk/xai'; export async function suggestQuestions(history: any[]) { 'use server'; @@ -11,15 +11,13 @@ export async function suggestQuestions(history: any[]) { console.log(history); const { object } = await generateObject({ - model: google('gemini-1.5-flash-8b', { - structuredOutputs: true, - }), - temperature: 1, + model: xai("grok-2-1212"), + temperature: 0, maxTokens: 300, - topP: 0.95, - topK: 40, + topP: 0.3, + topK: 7, system: - `You are a search engine query generator. You 'have' to create only '3' questions for the search engine based on the message history which has been provided to you. + `You are a search engine query/questions generator. You 'have' to create only '3' questions for the search engine based on the message history which has been provided to you. The questions should be open-ended and should encourage further discussion while maintaining the whole context. Limit it to 5-10 words per question. Always put the user input's context is some way so that the next search knows what to search for exactly. Try to stick to the context of the conversation and avoid asking questions that are too general or too specific. @@ -106,134 +104,122 @@ export async function fetchMetadata(url: string) { } -type SearchGroupId = 'web' | 'academic' | 'shopping' | 'youtube' | 'x' | 'writing'; +type SearchGroupId = 'web' | 'academic' | 'youtube' | 'x' ; const groupTools = { web: [ - 'get_weather_data', 'find_place', 'programming', - 'web_search', 'text_translate', 'nearby_search', - 'x_search', 'youtube_search', 'shopping_search', - 'academic_search', 'track_flight' + 'thinking_canvas', + 'web_search', 'get_weather_data', 'programming', + 'retrieve', 'text_translate', + 'nearby_search', 'track_flight', + 'tmdb_search', 'trending_movies', 'trending_tv', ] as const, academic: ['academic_search', 'programming'] as const, - shopping: ['shopping_search', 'programming'] as const, youtube: ['youtube_search'] as const, x: ['x_search'] as const, - writing: [] as const, } as const; const groupPrompts = { web: ` -You are an expert AI web search engine called MiniPerplx, that helps users find information on the internet with no bullshit talks. -Always start with running the tool(s) and then and then only write your response AT ALL COSTS!! -Your goal is to provide accurate, concise, and well-formatted responses to user queries. -Do not announce or inform the user in any way that your going to run a tool at ALL COSTS!! Just 'run' it and then write your response AT ALL COSTS!!!!! + You are an expert AI web search engine called MiniPerplx, designed to help users find information on the internet with no unnecessary chatter. + Always **run the tool first exactly once** before composing your response. **This is non-negotiable.** + + Your goals: + - Stay concious and aware of the guidelines. + - Provide accurate, concise, and well-formatted responses. + - Avoid hallucinations or fabrications. Stick to verified facts and provide proper citations. + - Follow formatting guidelines strictly. + + **Today's Date:** ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })} + Comply with user requests to the best of your abilities using the appropriate tools. Maintain composure and follow the guidelines. + + + ### Response Guidelines: + 1. **Tools First:** + Plan the tools to run inside the 'thinking_canvas' tool. + Always run the appropriate tool before composing your response. + Do not run the same tool twice with identical parameters as it leads to redundancy and wasted resources. **This is non-negotiable.** + Once you get the content or results from the tools, start writing your response immediately. -Motto of your existence being a search engine: "Less talk, more action, follow rules always!", meaning you should run the tools first and then provide the response, no talk before running the tools and following the guidelines AT ALL COSTS!! + 2. **Content Rules:** + - Responses must be informative, long and detailed, yet clear and concise like a textbook. + - Use structured answers with headings (no H1). + - Prefer bullet points over plain paragraphs but points can be long. + - Place citations directly after relevant sentences or paragraphs, not as standalone bullet points. + - Do not truncate sentences inside citations. Always finish the sentence before placing the citation. + + 3. **Latex and Currency Formatting:** + - Use '$' for inline equations and '$$' for block equations. + - Avoid using '$' for currency. Use "USD" instead. + -The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. -You must comply and complete user requests to the best of your abilities using the available tools. Remember to use the appropriate tool for each task. No need to panic, just follow the guidelines and you'll do great! -Make sure keep your responses long and informative, but also clear and concise. Avoid unnecessary information and stick to the point. -Always put citations at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided. + ### Tool-Specific Guidelines: + #### Thinking Canvas: + - Use this tool to plan your responses before running other tools. + - Do not write in markdown format inside the 'thinking_canvas' tool. + - The content should be in plain text like inside a todo list. + - Mention the tools you plan to run and the order of execution. + - Mention the number of times you plan to run each tool is 1 at most so you don't hallucinate. + - Don't include the tool parameters in the 'thinking_canvas' tool except the queries of the tools. -Here are the tools available to you: - -web_search, retrieve, get_weather_data, programming, text_translate, find_place, track_flight - - -## Basic Guidelines: -Always remember to run the appropriate tool first, then compose your response based on the information gathered. -Run tools step by step and not combined in a single response at all costs!! -Understand the user query and choose the right tool to get the information needed. Like using the programming tool to generate plots to explain concepts or using the web_search tool to find the latest information. -All tool should be called only once per response. All tool call parameters are mandatory always! -Format your response: give a structured answer with headings for each section no h1 tho. try to use bullet points instead of just a plain paragraph. put citation after each bullet point instead of at the end of the whole answer. Answers should be very informative and detailed. No short answers at all costs!! -Do not ever complete the sentence inside the citation at all costs!! Always complete the sentence and then put the citation at the end after the last word of the sentence not as the last word of the sentence. -Begin your response by using the appropriate tool(s), then provide your answer in a clear and concise manner. -Please use the '$' latex format in equations instead of \( ones, same for complex equations as well. - -## Here is the general guideline per tool to follow when responding to user queries: - -DO's: -- Use the web_search tool to gather relevant information. The query should only be the word that need's context for search. Then write the response based on the information gathered. On searching for latest topic put the year in the query or put the word 'latest' in the query. -- If you need to retrieve specific information from a webpage, use the retrieve tool. Analyze the user's query to set the topic type either normal or news. Then, compose your response based on the retrieved information. -- If you are given a url to retrieve information from, always use the retrieve tool to get the information from the URL. This will help in getting the accurate information from the URL. -- For weather-related queries, use the get_weather_data tool. The weather results are 5 days weather forecast data with 3-hour step. Then, provide the weather information in your response. -- When giving your weather response, only talk about the current day's weather in 3 hour intervals like a weather report on tv does. Do not provide the weather for the next 5 days. -- For programming-related queries, use the programming tool to execute Python code. Code can be multilined. Then, compose your response based on the output of the code execution. -- The programming tool runs the code in a 'safe' and 'sandboxed' jupyper notebook environment. Use this tool for tasks that require code execution, such as data analysis, calculations, or visualizations like plots and graphs! Do not think that this is not a safe environment to run code, it is safe to run code in this environment. -- The programming tool can be used to install libraries using !pip install in the code. This will help in running the code successfully. Always remember to install the libraries using !pip install in the code at all costs!! -- For queries about finding a specific place, use the find_place tool. Provide the information about the location and then compose your response based on the information gathered. -- For queries about nearby places, use the nearby_search tool. Provide the location and radius in the parameters, then compose your response based on the information gathered. -- Adding Country name in the location search will help in getting the accurate results. Always remember to provide the location in the correct format to get the accurate results. -- For text translation queries, use the text_translate tool. Provide the text to translate, the language to translate to, and the source language (optional). Then, compose your response based on the translated text. -- For stock chart and details queries, use the programming tool with yfinance package along with the rest of the code, which will have plot code of stock chart and code to print the variables storing the stock data. Then, compose your response based on the output of the code execution. -- Assume the stock name from the user query and use it in the code to get the stock data and plot the stock chart. This will help in getting the stock chart for the user query. ALWAYS REMEMBER TO INSTALL YFINANCE USING !pip install yfinance AT ALL COSTS!! - -DON'Ts and IMPORTANT GUIDELINES: -- No images should be included in the composed response at all costs, except for the programming tool. -- DO NOT TALK BEFORE RUNNING THE TOOL AT ALL COSTS!! JUST RUN THE TOOL AND THEN WRITE YOUR RESPONSE AT ALL COSTS!!!!! -- Do not call the same tool twice in a single response at all costs!! -- Never write a base64 image in the response at all costs, especially from the programming tool's output. -- Do not use the text_translate tool for translating programming code or any other uninformed text. Only run the tool for translating on user's request. -- Do not use the retrieve tool for general web searches. It is only for retrieving specific information from a URL. -- Show plots from the programming tool using plt.show() function. The tool will automatically capture the plot and display it in the response. -- If asked for multiple plots, make it happen in one run of the tool. The tool will automatically capture the plots and display them in the response. -- the web search may return an incorrect latex format, please correct it before using it in the response. Check the Latex in Markdown rules for more information. -- The location search tools return images in the response, please DO NOT include them in the response at all costs!!!!!!!! This is extremely important to follow!! -- Do not use the $ symbol in the stock chart queries at all costs. Use the word USD instead of the $ symbol in the stock chart queries. -- Never run web_search tool for stock chart queries at all costs. - -# Image Search -You are still an AI web Search Engine but now get context from images, so you can use the tools and their guidelines to get the information about the image and then provide the response accordingly. -Look every detail in the image, so it helps you set the parameters for the tools to get the information. -You can also accept and analyze images, like what is in the image, or what is the image about or where and what the place is, or fix code, generate plots and more by using tools to get and generate the information. -Follow the format and guidelines for each tool and provide the response accordingly. Remember to use the appropriate tool for each task. No need to panic, just follow the guidelines and you'll do great! - -## Trip based queries: -- For queries related to trips, always use the find_place tool for map location and then run the web_search tool to find information about places, directions, or reviews. -- Calling web and find place tools in the same response is allowed, but do not call the same tool in a response at all costs!! -- For nearby search queries, use the nearby_search tool to find places around a location. Provide the location and radius in the parameters, then compose your response based on the information gathered. -- Never call find_place tool before or after the nearby_search tool in the same response at all costs!! THIS IS NOT ALLOWED AT ALL COSTS!!! - -## Programming Tool Guidelines: -The programming tool is actually a Python-Only Code interpreter, so you can run any Python code in it. -- This tool should not be called more than once in a response. -- The only python libraries that are pre-installed are matplotlib, aiohttp (v3.9.3), beautifulsoup4 (v4.12.3), bokeh (v3.3.4), gensim (v4.3.2), imageio (v2.34.0), joblib (v1.3.2), librosa (v0.10.1), matplotlib (v3.8.3), nltk (v3.8.1), numpy (v1.26.4), opencv-python (v4.9.0.80), openpyxl (v3.1.2), pandas (v1.5.3), plotly (v5.19.0), pytest (v8.1.0), python-docx (v1.1.0), pytz (v2024.1), requests (v2.26.0), scikit-image (v0.22.0), scikit-learn (v1.4.1.post1), scipy (v1.12.0), seaborn (v0.13.2), soundfile (v0.12.1), spacy (v3.7.4), textblob (v0.18.0), tornado (v6.4), urllib3 (v1.26.7), xarray (v2024.2.0), xlrd (v2.0.1), sympy (v1.12) and yfinance. -- Always mention the generated urls in the response after running the code! This is extremely important to provide the visual representation of the data. -- Never run GUI based code in the programming tool at all costs. This is not allowed at all costs!! -- No other libraries can be installed in the programming tool at all costs. The libraries that are pre-installed are the only ones that can be used in the programming tool. -- Do not use any other language other than Python in the programming tool at all costs. This is not allowed at all costs!! - -## Citations Format: -Citations should always be placed at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided. -When citing sources(citations), use the following styling only: Claude 3.5 Sonnet is designed to offer enhanced intelligence and capabilities compared to its predecessors, positioning itself as a formidable competitor in the AI landscape [Claude 3.5 Sonnet raises the..](https://www.anthropic.com/news/claude-3-5-sonnet). -ALWAYS REMEMBER TO USE THE CITATIONS FORMAT CORRECTLY AT ALL COSTS!! ANY SINGLE ITCH IN THE FORMAT WILL CRASH THE RESPONSE!! -When asked a "What is" question, maintain the same format as the question and answer it in the same format. - -## Latex in Respone rules: -Latex should be wrapped with $ symbol for inline and $$ for block equations as they are supported in the response.`, + #### Multi Query Web Search: + - Use this tool for multiple queries in one call. + - Specify the year or "latest" in queries to fetch recent information. + + #### Retrieve Tool: + - Use this for extracting information from specific URLs, categorized as "normal" or "news." + - Do not use this tool for general web searches. + + #### Weather Data: + - Provide only the current day's weather in 3-hour intervals. Avoid forecasts for subsequent days. + + #### Programming Tool: + - Use this Python-only sandbox for calculations, data analysis, or visualizations. + - Include library installations (!pip install ) in the code where required. + - Use 'plt.show()' for plots, and mention generated URLs for outputs. + + #### Nearby Search: + - Use location and radius parameters. Adding the country name improves accuracy. + + #### Translation: + - Only use the text_translate tool for user-requested translations. + + #### Stock Charts: + - Assume stock names from user queries. Use 'yfinance' and include installation commands. + + #### Image Search: + - Analyze image details to determine tool parameters. + + #### Movie/TV Show Queries: + - Use relevant tools for trending or specific movie/TV show information. Do not include images in responses. + - For this tool make the exception of just listing the top 5 movies or TV shows in your written response. + + ### Prohibited Actions: + - Never write your thoughts or preamble before running a tool. + - Avoid running the same tool twice with identical parameters. + - Do not include images in responses unless explicitly allowed (e.g., plots from the programming tool). + - Avoid GUI-based Python code. + - Do not run 'web_search' for stock queries. + + ### Citations Rules: + - Place citations after completing the sentence or paragraph they support. + - Format: [Source Title](URL). + - Ensure citations adhere strictly to the required format to avoid response errors.`, academic: `You are an academic research assistant that helps find and analyze scholarly content. The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. Focus on peer-reviewed papers, citations, and academic sources. - Do not talk in bullet points or lists at all costs as it unpresentable. + Do not talk in bullet points or lists at all costs as it is unpresentable. Provide summaries, key points, and references. Latex should be wrapped with $ symbol for inline and $$ for block equations as they are supported in the response. No matter what happens, always provide the citations at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided. Citation format: [Author et al. (Year) Title](URL) - Always run the tools first and then write the response. - `, - shopping: `You are a shopping assistant that helps users find and compare products. - The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. - Focus on providing accurate pricing, product details, and merchant information. - Do not show the images of the products at all costs. - Talk about the product details and pricing only. - Do not talk in bullet points or lists at all costs. - Compare options and highlight key features and best values.`, + Always run the tools first and then write the response.`, youtube: `You are a YouTube search assistant that helps find relevant videos and channels. + Just call the tool and run the search and then talk in long details in 2-6 paragraphs. The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. - Provide video titles, channel names, view counts, and publish dates. + Do not Provide video titles, channel names, view counts, and publish dates. Do not talk in bullet points or lists at all costs. - Provide important details and summaries of the videos in paragraphs. + Provide complete explainations of the videos in paragraphs. Give citations with timestamps and video links to insightful content. Don't just put timestamp at 0:00. Citation format: [Title](URL ending with parameter t=) Do not provide the video thumbnail in the response at all costs.`, @@ -243,11 +229,7 @@ Latex should be wrapped with $ symbol for inline and $$ for block equations as t No need to say that you are calling the tool, just call the tools first and run the search; then talk in long details in 2-6 paragraphs. Always provide the citations at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided. - Citation format: [Post Title](URL) - `, - writing: `You are a writing assistant that helps users with writing, conversation, coding, poems, haikus, long essays or intellectual topics. - Latex should be wrapped with $ symbol for inline and $$ for block equations as they are supported in the response. - Do not use the \( and \) for inline equations, use the $ symbol instead at all costs!!`, + Citation format: [Post Title](URL)`, } as const; diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 6118f3b..fd33aee 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,15 +1,11 @@ // /app/api/chat/route.ts import { z } from "zod"; -import { createAzure } from '@ai-sdk/azure'; -import { anthropic } from '@ai-sdk/anthropic' import { xai } from '@ai-sdk/xai' -import { google } from '@ai-sdk/google' import Exa from 'exa-js' import { convertToCoreMessages, streamText, tool, - experimental_createProviderRegistry, smoothStream } from "ai"; import { BlobRequestAbortedError, put } from '@vercel/blob'; @@ -17,6 +13,9 @@ import CodeInterpreter from "@e2b/code-interpreter"; import FirecrawlApp from '@mendable/firecrawl-js'; import { tavily } from '@tavily/core' import { getGroupConfig } from "@/app/actions"; +import { geolocation, ipAddress } from '@vercel/functions' +import { Ratelimit } from "@upstash/ratelimit"; // for deno: see above +import { Redis } from "@upstash/redis"; // see below for cloudflare and fastly adapters // Allow streaming responses up to 60 seconds export const maxDuration = 120; @@ -96,20 +95,6 @@ interface VideoResult { summary?: string; } -// Azure setup -const azure = createAzure({ - resourceName: process.env.AZURE_RESOURCE_NAME, - apiKey: process.env.AZURE_API_KEY, -}); - -// Provider registry -const registry = experimental_createProviderRegistry({ - anthropic, - azure, - google, - xai, -}); - function sanitizeUrl(url: string): string { return url.replace(/\s+/g, '%20') } @@ -132,116 +117,41 @@ async function isValidImageUrl(url: string): Promise { } } -const defaultsystemPrompt = ` -You are an expert AI web search engine called MiniPerplx, that helps users find information on the internet with no bullshit talks. -Always start with running the tool(s) and then and then only write your response AT ALL COSTS!! -Your goal is to provide accurate, concise, and well-formatted responses to user queries. -Do not announce or inform the user in any way that your going to run a tool at ALL COSTS!! Just 'run' it and then write your response AT ALL COSTS!!!!! - -Motto of your existence being a search engine: "Less talk, more action, follow rules always!", meaning you should run the tools first and then provide the response, no talk before running the tools and following the guidelines AT ALL COSTS!! - -The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. -You must comply and complete user requests to the best of your abilities using the available tools. Remember to use the appropriate tool for each task. No need to panic, just follow the guidelines and you'll do great! -Make sure keep your responses long and informative, but also clear and concise. Avoid unnecessary information and stick to the point. -Always put citations at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided. - -Here are the tools available to you: - -web_search, retrieve, get_weather_data, programming, text_translate, find_place, track_flight - - -## Basic Guidelines: -Always remember to run the appropriate tool first, then compose your response based on the information gathered. -Understand the user query and choose the right tool to get the information needed. Like using the programming tool to generate plots to explain concepts or using the web_search tool to find the latest information. -All tool should be called only once per response. All tool call parameters are mandatory always! -Format your response in paragraphs(min 6) with 3-8 sentences each, keeping it informative. DO NOT use pointers or make lists of any kind at ALL! -Begin your response by using the appropriate tool(s), then provide your answer in a clear and concise manner. -Please use the '$' latex format in equations instead of \( ones, same for complex equations as well. - -## Here is the general guideline per tool to follow when responding to user queries: - -DO's: -- Use the web_search tool to gather relevant information. The query should only be the word that need's context for search. Then write the response based on the information gathered. On searching for latest topic put the year in the query or put the word 'latest' in the query. -- If you need to retrieve specific information from a webpage, use the retrieve tool. Analyze the user's query to set the topic type either normal or news. Then, compose your response based on the retrieved information. -- If you are given a url to retrieve information from, always use the retrieve tool to get the information from the URL. This will help in getting the accurate information from the URL. -- For weather-related queries, use the get_weather_data tool. The weather results are 5 days weather forecast data with 3-hour step. Then, provide the weather information in your response. -- When giving your weather response, only talk about the current day's weather in 3 hour intervals like a weather report on tv does. Do not provide the weather for the next 5 days. -- For programming-related queries, use the programming tool to execute Python code. Code can be multilined. Then, compose your response based on the output of the code execution. -- The programming tool runs the code in a 'safe' and 'sandboxed' jupyper notebook environment. Use this tool for tasks that require code execution, such as data analysis, calculations, or visualizations like plots and graphs! Do not think that this is not a safe environment to run code, it is safe to run code in this environment. -- The programming tool can be used to install libraries using !pip install in the code. This will help in running the code successfully. Always remember to install the libraries using !pip install in the code at all costs!! -- For queries about finding a specific place, use the find_place tool. Provide the information about the location and then compose your response based on the information gathered. -- For queries about nearby places, use the nearby_search tool. Provide the location and radius in the parameters, then compose your response based on the information gathered. -- Adding Country name in the location search will help in getting the accurate results. Always remember to provide the location in the correct format to get the accurate results. -- For text translation queries, use the text_translate tool. Provide the text to translate, the language to translate to, and the source language (optional). Then, compose your response based on the translated text. -- For stock chart and details queries, use the programming tool with yfinance package along with the rest of the code, which will have plot code of stock chart and code to print the variables storing the stock data. Then, compose your response based on the output of the code execution. -- Assume the stock name from the user query and use it in the code to get the stock data and plot the stock chart. This will help in getting the stock chart for the user query. ALWAYS REMEMBER TO INSTALL YFINANCE USING !pip install yfinance AT ALL COSTS!! - -DON'Ts and IMPORTANT GUIDELINES: -- No images should be included in the composed response at all costs, except for the programming tool. -- DO NOT TALK BEFORE RUNNING THE TOOL AT ALL COSTS!! JUST RUN THE TOOL AND THEN WRITE YOUR RESPONSE AT ALL COSTS!!!!! -- Do not call the same tool twice in a single response at all costs!! -- Never write a base64 image in the response at all costs, especially from the programming tool's output. -- Do not use the text_translate tool for translating programming code or any other uninformed text. Only run the tool for translating on user's request. -- Do not use the retrieve tool for general web searches. It is only for retrieving specific information from a URL. -- Show plots from the programming tool using plt.show() function. The tool will automatically capture the plot and display it in the response. -- If asked for multiple plots, make it happen in one run of the tool. The tool will automatically capture the plots and display them in the response. -- the web search may return an incorrect latex format, please correct it before using it in the response. Check the Latex in Markdown rules for more information. -- The location search tools return images in the response, please DO NOT include them in the response at all costs!!!!!!!! This is extremely important to follow!! -- Do not use the $ symbol in the stock chart queries at all costs. Use the word USD instead of the $ symbol in the stock chart queries. -- Never run web_search tool for stock chart queries at all costs. - -# Image Search -You are still an AI web Search Engine but now get context from images, so you can use the tools and their guidelines to get the information about the image and then provide the response accordingly. -Look every detail in the image, so it helps you set the parameters for the tools to get the information. -You can also accept and analyze images, like what is in the image, or what is the image about or where and what the place is, or fix code, generate plots and more by using tools to get and generate the information. -Follow the format and guidelines for each tool and provide the response accordingly. Remember to use the appropriate tool for each task. No need to panic, just follow the guidelines and you'll do great! - -## Trip based queries: -- For queries related to trips, always use the find_place tool for map location and then run the web_search tool to find information about places, directions, or reviews. -- Calling web and find place tools in the same response is allowed, but do not call the same tool in a response at all costs!! -- For nearby search queries, use the nearby_search tool to find places around a location. Provide the location and radius in the parameters, then compose your response based on the information gathered. -- Never call find_place tool before or after the nearby_search tool in the same response at all costs!! THIS IS NOT ALLOWED AT ALL COSTS!!! - -## Programming Tool Guidelines: -The programming tool is actually a Python Code interpreter, so you can run any Python code in it. -- This tool should not be called more than once in a response. -- The only python libraries that are pre-installed are matplotlib, aiohttp (v3.9.3), beautifulsoup4 (v4.12.3), bokeh (v3.3.4), gensim (v4.3.2), imageio (v2.34.0), joblib (v1.3.2), librosa (v0.10.1), matplotlib (v3.8.3), nltk (v3.8.1), numpy (v1.26.4), opencv-python (v4.9.0.80), openpyxl (v3.1.2), pandas (v1.5.3), plotly (v5.19.0), pytest (v8.1.0), python-docx (v1.1.0), pytz (v2024.1), requests (v2.26.0), scikit-image (v0.22.0), scikit-learn (v1.4.1.post1), scipy (v1.12.0), seaborn (v0.13.2), soundfile (v0.12.1), spacy (v3.7.4), textblob (v0.18.0), tornado (v6.4), urllib3 (v1.26.7), xarray (v2024.2.0), xlrd (v2.0.1), sympy (v1.12) and yfinance. -- Always mention the generated urls in the response after running the code! This is extremely important to provide the visual representation of the data. - -## Citations Format: -You will get more than 10 results from the web_search tool, so you can use minimum 8 citations in the response. -Citations should always be placed at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided. -When citing sources(citations), use the following styling only: Claude 3.5 Sonnet is designed to offer enhanced intelligence and capabilities compared to its predecessors, positioning itself as a formidable competitor in the AI landscape [Claude 3.5 Sonnet raises the..](https://www.anthropic.com/news/claude-3-5-sonnet). -ALWAYS REMEMBER TO USE THE CITATIONS FORMAT CORRECTLY AT ALL COSTS!! ANY SINGLE ITCH IN THE FORMAT WILL CRASH THE RESPONSE!! -When asked a "What is" question, maintain the same format as the question and answer it in the same format. - -## Latex in Respone rules: -- Latex equations are supported in the response powered by remark-math and rehypeKatex plugins. - - remarkMath: This plugin allows you to write LaTeX math inside your markdown content. It recognizes math enclosed in dollar signs ($ ... $ for inline and $$ ... $$ for block). - - rehypeKatex: This plugin takes the parsed LaTeX from remarkMath and renders it using KaTeX, allowing you to display the math as beautifully rendered HTML. - -- 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.`; +const ratelimit = new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(100, "1 d"), + analytics: true, + prefix: "mplx", +}); export async function POST(req: Request) { const { messages, model, group } = await req.json(); const { tools: activeTools, systemPrompt } = await getGroupConfig(group); - const provider = model.split(":")[0]; + const identifier = ipAddress(req) || "api"; + const { success } = await ratelimit.limit(identifier); + + if (!success) { + return new Response("Rate limit exceeded for 100 searches a day.", { status: 429 }); + } const result = streamText({ - model: registry.languageModel(model), + model: xai(model), messages: convertToCoreMessages(messages), - temperature: provider === "azure" ? 0.72 : 0.2, - topP: 0.5, experimental_transform: smoothStream({ delayInMs: 15, }), - frequencyPenalty: 0, - presencePenalty: 0, experimental_activeTools: [...activeTools], - system: systemPrompt || defaultsystemPrompt, + system: systemPrompt, tools: { + thinking_canvas: tool({ + description: "Write your plan of action in a canvas based on the user's input.", + parameters: z.object({ + title: z.string().describe("The title of the canvas."), + content: z.array(z.string()).describe("The content of the canvas."), + }), + execute: async ({ title, content }: { title: string, content: string[] }) => { return { title, content }; }, + }), web_search: tool({ description: "Search the web for information with multiple queries, max results and search depth.", parameters: z.object({ @@ -388,6 +298,161 @@ export async function POST(req: Request) { } }, }), + tmdb_search: tool({ + description: "Search for a movie or TV show using TMDB API", + parameters: z.object({ + query: z.string().describe("The search query for movies/TV shows"), + }), + execute: async ({ query }: { query: string }) => { + const TMDB_API_KEY = process.env.TMDB_API_KEY; + const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; + + try { + // First do a multi-search to get the top result + const searchResponse = await fetch( + `${TMDB_BASE_URL}/search/multi?query=${encodeURIComponent(query)}&include_adult=true&language=en-US&page=1`, + { + headers: { + 'Authorization': `Bearer ${TMDB_API_KEY}`, + 'accept': 'application/json' + } + } + ); + + const searchResults = await searchResponse.json(); + + // Get the first movie or TV show result + const firstResult = searchResults.results.find( + (result: any) => result.media_type === 'movie' || result.media_type === 'tv' + ); + + if (!firstResult) { + return { result: null }; + } + + // Get detailed information for the media + const detailsResponse = await fetch( + `${TMDB_BASE_URL}/${firstResult.media_type}/${firstResult.id}?language=en-US`, + { + headers: { + 'Authorization': `Bearer ${TMDB_API_KEY}`, + 'accept': 'application/json' + } + } + ); + + const details = await detailsResponse.json(); + + // Get additional credits information + const creditsResponse = await fetch( + `${TMDB_BASE_URL}/${firstResult.media_type}/${firstResult.id}/credits?language=en-US`, + { + headers: { + 'Authorization': `Bearer ${TMDB_API_KEY}`, + 'accept': 'application/json' + } + } + ); + + const credits = await creditsResponse.json(); + + // Format the result + const result = { + ...details, + media_type: firstResult.media_type, + credits: { + cast: credits.cast?.slice(0, 5).map((person: any) => ({ + ...person, + profile_path: person.profile_path ? + `https://image.tmdb.org/t/p/original${person.profile_path}` : null + })) || [], + director: credits.crew?.find((person: any) => person.job === 'Director')?.name, + writer: credits.crew?.find((person: any) => + person.job === 'Screenplay' || person.job === 'Writer' + )?.name, + }, + poster_path: details.poster_path ? + `https://image.tmdb.org/t/p/original${details.poster_path}` : null, + backdrop_path: details.backdrop_path ? + `https://image.tmdb.org/t/p/original${details.backdrop_path}` : null, + }; + + return { result }; + + } catch (error) { + console.error("TMDB search error:", error); + throw error; + } + }, + }), + trending_movies: tool({ + description: "Get trending movies from TMDB", + parameters: z.object({}), + execute: async () => { + const TMDB_API_KEY = process.env.TMDB_API_KEY; + const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; + + try { + const response = await fetch( + `${TMDB_BASE_URL}/trending/movie/day?language=en-US`, + { + headers: { + 'Authorization': `Bearer ${TMDB_API_KEY}`, + 'accept': 'application/json' + } + } + ); + + const data = await response.json(); + const results = data.results.map((movie: any) => ({ + ...movie, + poster_path: movie.poster_path ? + `https://image.tmdb.org/t/p/original${movie.poster_path}` : null, + backdrop_path: movie.backdrop_path ? + `https://image.tmdb.org/t/p/original${movie.backdrop_path}` : null, + })); + + return { results }; + } catch (error) { + console.error("Trending movies error:", error); + throw error; + } + }, + }), + trending_tv: tool({ + description: "Get trending TV shows from TMDB", + parameters: z.object({}), + execute: async () => { + const TMDB_API_KEY = process.env.TMDB_API_KEY; + const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; + + try { + const response = await fetch( + `${TMDB_BASE_URL}/trending/tv/day?language=en-US`, + { + headers: { + 'Authorization': `Bearer ${TMDB_API_KEY}`, + 'accept': 'application/json' + } + } + ); + + const data = await response.json(); + const results = data.results.map((show: any) => ({ + ...show, + poster_path: show.poster_path ? + `https://image.tmdb.org/t/p/original${show.poster_path}` : null, + backdrop_path: show.backdrop_path ? + `https://image.tmdb.org/t/p/original${show.backdrop_path}` : null, + })); + + return { results }; + } catch (error) { + console.error("Trending TV shows error:", error); + throw error; + } + }, + }), academic_search: tool({ description: "Search academic papers and research.", parameters: z.object({ @@ -524,124 +589,6 @@ export async function POST(req: Request) { } }, }), - shopping_search: tool({ - description: "Search for products using Exa and Canopy API.", - parameters: z.object({ - query: z.string().describe("The search query for products"), - // keyword: z.string().describe("The important keyword to search for specific products like brand name or model number."), - }), - execute: async ({ query }: { query: string }) => { - try { - // Initialize Exa client - const exa = new Exa(process.env.EXA_API_KEY as string); - - - // Search for products on Amazon - const searchResult = await exa.search( - query, - { - type: "auto", - numResults: 20, - includeDomains: ["amazon.com"], - } - ); - - // Function to extract ASIN from Amazon URL - const extractAsin = (url: string): string | null => { - const asinRegex = /(?:dp|gp\/product)\/([A-Z0-9]{10})/; - const match = url.match(asinRegex); - return match ? match[1] : null; - }; - - // Remove duplicates by ASIN - const seenAsins = new Set(); - const uniqueResults = searchResult.results.reduce>((acc, result) => { - const asin = extractAsin(result.url); - if (asin && !seenAsins.has(asin)) { - seenAsins.add(asin); - acc.push(result); - } - return acc; - }, []); - - // Only take the first 10 unique results - const limitedResults = uniqueResults.slice(0, 10); - - // Fetch detailed product information for each unique result - const productDetails = await Promise.all( - limitedResults.map(async (result) => { - const asin = extractAsin(result.url); - if (!asin) return null; - - const query = ` - query amazonProduct { - amazonProduct(input: {asinLookup: {asin: "${asin}"}}) { - title - brand - mainImageUrl - rating - ratingsTotal - price { - display - } - } - } - `; - - try { - const response = await fetch('https://graphql.canopyapi.co/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'API-KEY': process.env.CANOPY_API_KEY as string, - }, - body: JSON.stringify({ query }), - next: { revalidate: 3600 } // Cache for 1 hour - }); - - if (!response.ok) { - console.error(`Failed to fetch details for ASIN ${asin}:`, await response.text()); - return null; - } - - const canopyData = await response.json(); - const amazonProduct = canopyData.data?.amazonProduct; - - if (!amazonProduct) return null; - - return { - title: amazonProduct.title, - url: result.url, - image: amazonProduct.mainImageUrl, - price: amazonProduct.price.display, - rating: amazonProduct.rating, - reviewCount: amazonProduct.ratingsTotal, - }; - } catch (error) { - console.error(`Error fetching details for ASIN ${asin}:`, error); - return null; - } - }) - ); - - // Filter out null results and return - const validProducts = productDetails.filter((product): product is NonNullable => - product !== null - ); - - // Log results for debugging - console.log(`Found ${searchResult.results.length} total results`); - console.log(`Filtered to ${uniqueResults.length} unique ASINs`); - console.log(`Returning ${validProducts.length} valid products`); - - return validProducts; - - } catch (error) { - console.error("Shopping search error:", error); - throw error; - } - }, - }), retrieve: tool({ description: "Retrieve the information from a URL using Firecrawl.", parameters: z.object({ @@ -1194,7 +1141,6 @@ export async function POST(req: Request) { }, }), }, - toolChoice: "auto", onChunk(event) { if (event.chunk.type === "tool-call") { console.log("Called Tool: ", event.chunk.toolName); diff --git a/app/api/cohere/route.ts b/app/api/cohere/route.ts deleted file mode 100644 index f2e93cf..0000000 --- a/app/api/cohere/route.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { cohere } from '@ai-sdk/cohere' -import { convertToCoreMessages, streamText, tool } from "ai"; -import CodeInterpreter from "@e2b/code-interpreter"; -import { z } from "zod"; -import { geolocation } from "@vercel/functions"; - -// Allow streaming responses up to 30 seconds -export const maxDuration = 60; - -export async function POST(req: Request) { - const { messages } = await req.json(); - const { latitude, longitude, city } = geolocation(req) - - const result = await streamText({ - model: cohere("command-r-plus"), - messages: convertToCoreMessages(messages), - system: `## Task & Context - -You are an AI-powered web search engine designed to help users find information on the internet. Your primary goal is to provide accurate, concise, and well-formatted responses to user queries. You have access to various tools for gathering information, including web search, webpage retrieval, weather data, programming execution, and location-based searches. - -The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })} and the user's location (city, latitude, longitude) is provided. You must always use the appropriate tool(s) before composing your response, and you should not announce or inform the user that you're using a tool. - -Available tools, their instructions, and parameters: - -1. web_search: - Instructions: Use this tool to gather relevant information. The query should only be the word that needs context for search. On searching for the latest topic, put the year in the query or put the word 'latest' in the query. - Parameters: - - query: The search query to look up on the web. - - maxResults: The maximum number of results to return (default: 10). - - topic: The topic type to search for ("general" or "news", default: "general"). - - searchDepth: The search depth to use ("basic" or "advanced", default: "basic"). - - exclude_domains: Optional list of domains to exclude from the search results. - -2. retrieve: - Instructions: Use this tool to retrieve specific information from a webpage. Analyze the user's query to set the topic type to either normal or news. - Parameters: - - url: The URL to retrieve information from. - -3. get_weather_data: - Instructions: Use this tool for weather-related queries. The weather results are 5 days weather forecast data with 3-hour steps. - Parameters: - - lat: The latitude of the location. - - lon: The longitude of the location. - -4. programming: - Instructions: Use this tool for programming-related queries to execute Python code. The print() function doesn't work with this tool, so just put variable names at the end separated with commas to print them. Use plt.show() to display plots. - Parameters: - - code: The Python code to execute. - -5. nearby_search: - Instructions: Use this tool for queries about nearby places or businesses. - Parameters: - - location: The location to search near (e.g., "New York City"). - - type: The type of place to search for (e.g., restaurant, cafe, park). - - keyword: Optional keyword to refine the search. - - radius: The radius of the search area in meters (max 50000, default: 1500). - -6. find_place: - Instructions: Use this tool for queries about finding a specific place. - Parameters: - - input: The place to search for (e.g., "Museum of Contemporary Art Australia"). - - inputtype: The type of input ("textquery" or "phonenumber"). - -7. text_search: - Instructions: Use this tool for text-based searches of places. - Parameters: - - query: The search query (e.g., "123 main street"). - - location: Optional location to center the search (e.g., "42.3675294,-71.186966"). - - radius: Optional radius of the search area in meters (max 50000). - -## Style Guide - -1. Response Structure: - - Format your response in 4-6 paragraphs, with 3-6 sentences each. - - Keep responses brief but informative. - - Do not use pointers or make lists of any kind. - - Begin your response by using the appropriate tool(s), then provide your answer clearly and concisely. - - Never include base64 images in the response or any kind of image URLs AT ALL COSTS!!! - -2. Tool Usage: - - Always run the appropriate tool first, then compose your response based on the gathered information. - - Use each tool only once per response. - - Do not announce or mention tool usage in your response. - -3. Citations: - - Place citations at the end of each paragraph and at the end of sentences where the information is used. - - Use the following citation format: [Title..](URL). - - Always use the citation format correctly. - -4. Specific Query Handling: - - For "What is" questions, maintain the same format as the question and answer accordingly. - - For stock chart queries, use the programming tool to install yfinance and create the chart. - -5. Formatting Restrictions: - - Do not use any HTML-like tags or create lists in the response. - - Do not include enclosing tags for the response. - - Never write base64 images in the response. - -6. Response Initiation: - - Do not begin responses with phrases like "Certainly!", "To provide you with the best answer...", or "Based on search results...". - - Directly provide the answer after running the necessary tool(s). - -7. Language: - - Respond in the language used or requested by the user. - -8. Additional Notes: - - Show plots from the programming tool using plt.show() function. The tool will automatically capture the plot and display it in the response. - - If asked for multiple plots, make it happen in one run of the tool. - - The location search tools return images in the response; please do not include them in the response. - - Never run the web_search tool for stock chart queries. - -Remember to always run the appropriate tool(s) first and compose your response based on the gathered information, adhering to these style guidelines.`, - temperature: 0, - maxTokens: 800, - tools: { - web_search: tool({ - description: - "Search the web for information with the given query, max results and search depth.", - parameters: z.object({ - query: z.string().describe("The search query to look up on the web."), - maxResults: z - .number() - .describe( - "The maximum number of results to return. Default to be used is 10.", - ), - topic: z - .string() - .describe("The topic type to search for. Only 'general' and 'news' are allowed. Default is 'general'."), - searchDepth: z - .string() - .describe( - "The search depth to use for the search. Only 'basic' and 'advanced' are allowed. Default is 'basic'." - ), - }), - execute: async ({ - query, - maxResults, - topic, - searchDepth, - }: { - query: string; - maxResults: number; - topic: string; - searchDepth: string; - }) => { - const apiKey = process.env.TAVILY_API_KEY; - - let body = JSON.stringify({ - api_key: apiKey, - query, - topic: topic, - max_results: maxResults < 5 ? 5 : maxResults, - search_depth: searchDepth, - include_answers: true, - }); - - if (topic === "news") { - body = JSON.stringify({ - api_key: apiKey, - query, - topic: topic, - days: 7, - max_results: maxResults < 5 ? 5 : maxResults, - search_depth: searchDepth, - include_answers: true, - }); - } - - const response = await fetch("https://api.tavily.com/search", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body, - }); - - const data = await response.json(); - - let context = data.results.map( - (obj: { url: any; content: any; title: any; raw_content: any, published_date: any }) => { - if (topic === "news") { - return { - 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, - }; - }, - ); - - return { - results: context, - }; - }, - }), - retrieve: tool({ - description: "Retrieve the information from a URL.", - parameters: z.object({ - url: z.string().describe("The URL to retrieve the information from."), - }), - execute: async ({ url }: { url: string }) => { - let hasError = false; - - let results; - try { - const response = await fetch(`https://r.jina.ai/${url}`, { - method: "GET", - headers: { - Accept: "application/json", - "X-With-Generated-Alt": "true", - }, - }); - const json = await response.json(); - if (!json.data || json.data.length === 0) { - hasError = true; - } else { - // Limit the content to 5000 characters - if (json.data.content.length > 5000) { - json.data.content = json.data.content.slice(0, 5000); - } - results = { - results: [ - { - title: json.data.title, - content: json.data.content, - url: json.data.url, - }, - ], - query: "", - images: [], - }; - } - } catch (error) { - hasError = true; - console.error("Retrieve API error:", error); - } - - if (hasError || !results) { - return results; - } - - return results; - }, - }), - get_weather_data: tool({ - description: "Get the weather data for the given coordinates.", - parameters: z.object({ - lat: z.number().describe("The latitude of the location."), - lon: z.number().describe("The longitude of the location."), - }), - execute: async ({ lat, lon }: { lat: number; lon: number }) => { - const apiKey = process.env.OPENWEATHER_API_KEY; - const response = await fetch( - `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}`, - ); - const data = await response.json(); - return data; - }, - }), - programming: tool({ - description: "Write and execute Python code.", - parameters: z.object({ - code: z.string().describe("The Python code to execute."), - }), - execute: async ({ code }: { code: string }) => { - const sandbox = await CodeInterpreter.create(); - const execution = await sandbox.runCode(code); - let message = ""; - let images = []; - - if (execution.results.length > 0) { - for (const result of execution.results) { - if (result.isMainResult) { - message += `${result.text}\n`; - } else { - message += `${result.text}\n`; - } - if (result.formats().length > 0) { - const formats = result.formats(); - for (let format of formats) { - if (format === "png") { - images.push({ format: "png", data: result.png }); - } else if (format === "jpeg") { - images.push({ format: "jpeg", data: result.jpeg }); - } else if (format === "svg") { - images.push({ format: "svg", data: result.svg }); - } - } - } - } - } - - if (execution.logs.stdout.length > 0 || execution.logs.stderr.length > 0) { - if (execution.logs.stdout.length > 0) { - message += `${execution.logs.stdout.join("\n")}\n`; - } - if (execution.logs.stderr.length > 0) { - message += `${execution.logs.stderr.join("\n")}\n`; - } - } - - return { message: message.trim(), images }; - }, - }), - nearby_search: tool({ - description: "Search for nearby places using Google Maps API.", - parameters: z.object({ - location: z.string().describe("The location to search near (e.g., 'New York City' or '1600 Amphitheatre Parkway, Mountain View, CA')."), - type: z.string().describe("The type of place to search for (e.g., restaurant, cafe, park)."), - keyword: z.string().optional().describe("An optional keyword to refine the search."), - radius: z.number().default(3000).describe("The radius of the search area in meters (max 50000, default 3000)."), - }), - execute: async ({ location, type, keyword, radius }: { location: string; type: string; keyword?: string; radius: number }) => { - const apiKey = process.env.GOOGLE_MAPS_API_KEY; - - // First, use the Geocoding API to get the coordinates - const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${apiKey}`; - const geocodeResponse = await fetch(geocodeUrl); - const geocodeData = await geocodeResponse.json(); - - if (geocodeData.status !== "OK" || !geocodeData.results[0]) { - throw new Error("Failed to geocode the location"); - } - - const { lat, lng } = geocodeData.results[0].geometry.location; - - // perform the nearby search - let searchUrl = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${lng}&radius=${radius}&type=${type}&key=${apiKey}`; - - if (keyword) { - searchUrl += `&keyword=${encodeURIComponent(keyword)}`; - } - - const searchResponse = await fetch(searchUrl); - const searchData = await searchResponse.json(); - - return { - results: searchData.results.slice(0, 5).map((place: any) => ({ - name: place.name, - vicinity: place.vicinity, - rating: place.rating, - user_ratings_total: place.user_ratings_total, - place_id: place.place_id, - location: place.geometry.location, - })), - center: { lat, lng }, - formatted_address: geocodeData.results[0].formatted_address, - }; - }, - }), - find_place: tool({ - description: "Find a specific place using Google Maps API.", - parameters: z.object({ - input: z.string().describe("The place to search for (e.g., 'Museum of Contemporary Art Australia')."), - inputtype: z.string().describe("The type of input (textquery or phonenumber)."), - }), - execute: async ({ input, inputtype }: { input: string; inputtype: string }) => { - const apiKey = process.env.GOOGLE_MAPS_API_KEY; - const url = `https://maps.googleapis.com/maps/api/place/findplacefromtext/json?fields=formatted_address,name,rating,opening_hours,geometry&input=${encodeURIComponent(input)}&inputtype=${inputtype}&key=${apiKey}`; - - const response = await fetch(url); - const data = await response.json(); - - return data; - }, - }), - text_search: tool({ - description: "Perform a text-based search for places using Google Maps API.", - parameters: z.object({ - query: z.string().describe("The search query (e.g., '123 main street')."), - location: z.string().optional().describe("The location to center the search (e.g., '42.3675294,-71.186966')."), - radius: z.number().optional().describe("The radius of the search area in meters (max 50000)."), - }), - execute: async ({ query, location, radius }: { query: string; location?: string; radius?: number }) => { - const apiKey = process.env.GOOGLE_MAPS_API_KEY; - let url = `https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(query)}&key=${apiKey}`; - - if (location) { - url += `&location=${encodeURIComponent(location)}`; - } - if (radius) { - url += `&radius=${radius}`; - } - - const response = await fetch(url); - const data = await response.json(); - - return data; - }, - }), - track_flight: tool({ - description: "Track flight information and status", - parameters: z.object({ - flight_number: z.string().describe("The flight number to track"), - }), - execute: async ({ flight_number }: { flight_number: string }) => { - try { - const response = await fetch( - `https://api.aviationstack.com/v1/flights?access_key=${process.env.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}` - ); - return await response.json(); - } catch (error) { - console.error('Flight tracking error:', error); - throw error; - } - }, - }), - }, - toolChoice: "auto", - }); - - return result.toDataStreamResponse(); -} diff --git a/app/api/trending/route.ts b/app/api/trending/route.ts index adebe59..b6b0547 100644 --- a/app/api/trending/route.ts +++ b/app/api/trending/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server'; import { generateObject } from 'ai'; -import { groq } from '@ai-sdk/groq' import { z } from 'zod'; +import { geolocation } from '@vercel/functions'; +import { xai } from '@ai-sdk/xai'; export interface TrendingQuery { icon: string; @@ -15,7 +16,7 @@ interface RedditPost { }; } -async function fetchGoogleTrends(): Promise { +async function fetchGoogleTrends(countryCode: string = 'US'): Promise { const fetchTrends = async (geo: string): Promise => { try { const response = await fetch(`https://trends.google.com/trends/trendingsearches/daily/rss?geo=${geo}`, { @@ -39,7 +40,7 @@ async function fetchGoogleTrends(): Promise { const itemsWithCategoryAndIcon = await Promise.all(items.map(async item => { const { object } = await generateObject({ - model: groq("llama-3.2-3b-preview"), + model: xai("grok-beta"), prompt: `Give the category for the topic from the existing values only in lowercase only: ${item.replace(/<\/?title>/g, '')} - if the topic category isn't present in the list, please select 'trending' only!`, @@ -61,10 +62,9 @@ async function fetchGoogleTrends(): Promise { } }; - const trendsIN = await fetchTrends('IN'); - const trendsUS = await fetchTrends('US'); + const trends = await fetchTrends(countryCode); - return [...trendsIN, ...trendsUS]; + return [ ...trends]; } async function fetchRedditQuestions(): Promise { @@ -95,11 +95,11 @@ async function fetchRedditQuestions(): Promise { } } -async function fetchFromMultipleSources() { +async function fetchFromMultipleSources(countryCode: string) { const [googleTrends, // redditQuestions ] = await Promise.all([ - fetchGoogleTrends(), + fetchGoogleTrends(countryCode), // fetchRedditQuestions(), ]); @@ -110,9 +110,10 @@ async function fetchFromMultipleSources() { .sort(() => Math.random() - 0.5); } -export async function GET() { +export async function GET(req: Request) { try { - const trends = await fetchFromMultipleSources(); + const countryCode = geolocation(req).countryRegion ?? 'US'; + const trends = await fetchFromMultipleSources(countryCode); if (trends.length === 0) { // Fallback queries if both sources fail diff --git a/app/new/page.tsx b/app/new/page.tsx index ac8602b..5852151 100644 --- a/app/new/page.tsx +++ b/app/new/page.tsx @@ -1,5 +1,5 @@ import { redirect } from 'next/navigation' export default async function NewPage() { - redirect('/search') + redirect('/') } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index efd2fd6..b3bdcbb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,925 +1,2909 @@ +/* eslint-disable @next/next/no-img-element */ "use client"; +import 'katex/dist/katex.min.css'; -import React, { useState, useEffect, useRef } from 'react' -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { - NavigationMenu, - NavigationMenuContent, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, - navigationMenuTriggerStyle, -} from "@/components/ui/navigation-menu" -import MovingGradient from "@/components/animata/background/moving-gradient"; -import { - Search, - Zap, - Code, - Cloud, - Link, - MapPin, - Globe, - Mic, - ArrowRight, - Github, - LucideIcon, - Server, - Palette, - Cpu, - Menu, - X, - BarChart, - CircleDot, - ShoppingBasket -} from "lucide-react" -import NextLink from "next/link" -import { - motion, - useScroll, - useTransform, - useSpring, - useInView, - AnimatePresence, - useAnimation -} from "framer-motion" -import { cn } from '@/lib/utils'; -import { Tweet } from 'react-tweet' +import +React, +{ + useRef, + useCallback, + useState, + useEffect, + useMemo, + Suspense +} from 'react'; +import ReactMarkdown from 'react-markdown'; +import { useTheme } from 'next-themes'; +import Marked, { ReactRenderer } from 'marked-react'; +import Latex from 'react-latex-next'; +import { useSearchParams } from 'next/navigation'; +import { useChat } from 'ai/react'; +import { ToolInvocation } from 'ai'; +import { toast } from 'sonner'; +import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; -import { TweetGrid } from '@/components/ui/tweet-grid'; -import { Newspaper, XLogo, YoutubeLogo } from '@phosphor-icons/react'; +import { + fetchMetadata, + generateSpeech, + suggestQuestions +} from './actions'; +import { Wave } from "@foobar404/wave"; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneLight, oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { + Sparkles, + ArrowRight, + Globe, + AlignLeft, + Copy, + Cloud, + Code, + Check, + Loader2, + User2, + Heart, + X, + MapPin, + Plus, + Download, + Flame, + Sun, + Pause, + Play, + TrendingUpIcon, + Calendar, + Calculator, + ChevronDown, + Edit2, + ChevronUp, + Moon, + Star, + YoutubeIcon, + LucideIcon, + FileText, + Book, + ExternalLink, + Building, + Users, + Brain, + TrendingUp, + Plane, + Film, + Tv, + ListTodo +} from 'lucide-react'; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; +import { GitHubLogoIcon, TextIcon } from '@radix-ui/react-icons'; +import Link from 'next/link'; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel"; +import { cn, SearchGroupId } from '@/lib/utils'; +import { + Table, + TableBody, + TableCell, + TableRow, +} from "@/components/ui/table"; +import Autoplay from 'embla-carousel-autoplay'; +import FormComponent from '@/components/ui/form-component'; +import WeatherChart from '@/components/weather-chart'; +import InteractiveChart from '@/components/interactive-charts'; +import { MapComponent, MapContainer } from '@/components/map-components'; +import MultiSearch from '@/components/multi-search'; +import { CurrencyDollar, Flag, RoadHorizon, SoccerBall, TennisBall, XLogo } from '@phosphor-icons/react'; +import { BorderTrail } from '@/components/core/border-trail'; +import { TextShimmer } from '@/components/core/text-shimmer'; +import { Tweet } from 'react-tweet'; +import NearbySearchMapView from '@/components/nearby-search-map-view'; +import { Separator } from '@/components/ui/separator'; +import { TrendingQuery } from './api/trending/route'; +import { FlightTracker } from '@/components/flight-tracker'; +import { InstallPrompt } from '@/components/InstallPrompt'; +import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import { vs } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import TMDBResult from '@/components/movie-info'; +import TrendingResults from '@/components/trending-tv-movies-results'; -function BentoCard({ - title, - icon: Icon, - description, - children, - gradient, - className, + +export const maxDuration = 60; + +interface Attachment { + name: string; + contentType: string; + url: string; + size: number; +} + +interface XResult { + id: string; + url: string; + title: string; + author?: string; + publishedDate?: string; + text: string; + highlights?: string[]; + tweetId: string; +} + +interface AcademicResult { + title: string; + url: string; + author?: string | null; + publishedDate?: string; + summary: string; +} + +// Updated SearchLoadingState with new colors and states +const SearchLoadingState = ({ + icon: Icon, + text, + color }: { - title: string; - icon: React.ElementType; - description: string; - children?: React.ReactNode; - gradient?: string; - className?: string; -}) { - return ( - -
-
-
- -

{title}

-
-
-
{description}
- {children} -
-
- ); -} - -const TestimonialSection: React.FC = () => { - const tweetIds = [ - "1825543755748782500", - "1825876424755941787", - "1827580223606669661", - "1825574082345136506", - "1825973306924872143", - "1825821083817103852" - ]; - - return ( -
-
-

- What People Are Saying -

-
- -
- -
-
- ); -}; - -function GetStarted() { - return ( - -
-
Try
-
MiniPerplx
- -
- -
-
-
-
- - ); -} - -function MinimalisticSearch() { - return ( - -
-
- - Clean interface -
-
- - Focused results -
-
- - Distraction-free -
-
-
- ); -} - -function AIPowered() { - return ( - -
-
- Natural Language -
-
-
-
-
- Context Understanding -
-
-
-
-
- Adaptive Learning -
-
-
-
-
-
- ); -} - -function LightningFast() { - return ( - - ); -} - -const AboutUsSection: React.FC = () => { - return ( -
-
-

- About MiniPerplx -

-
- - - - -
-
-
- ); -}; - - -const MarqueeTestimonials: React.FC = () => { - const testimonials = [ - "Absolutely love MiniPerplx! 🚀", - "Game-changer for my workflow. 💼", - "Simplicity at its finest. ✨", - "Can't imagine working without it now. 🙌", - "MiniPerplx is a must-have tool! 🛠️", - ]; - - return ( -
- - {testimonials.concat(testimonials).map((text, index) => ( - - {text} - - ))} - -
- ) -} - -interface FeatureCardProps { - icon: LucideIcon; - title: string; - description: string; -} - -const FeatureCard: React.FC = ({ icon: Icon, title, description }) => ( - - - - - - {title} - - -

{description}

-
-
-) - -interface Star { - x: number; - y: number; - size: number; - name: string; - category: string; -} - -const TechConstellation: React.FC = () => { - const [stars, setStars] = useState([]) - const [hoveredCategory, setHoveredCategory] = useState(null) - const constellationRef = useRef(null) - - const techStack = [ - { - category: "Core Technologies", - icon: Server, - items: ["Next.js", "React", "TypeScript", "Vercel AI SDK", "Tailwind CSS"] - }, - { - category: "UI & Styling", - icon: Palette, - items: ["shadcn/ui", "Framer Motion", "Lucide Icons"] - }, - { - category: "AI Services & APIs", - icon: Cpu, - items: ["Azure OpenAI", "Tavily AI", "e2b.dev", "OpenWeatherMap", "Google Maps API", "Firecrawl"] - } - ]; - - useEffect(() => { - if (constellationRef.current) { - const { width, height } = constellationRef.current.getBoundingClientRect() - const newStars: Star[] = [] - const centerX = width / 2 - const centerY = height / 2 - const maxRadius = Math.min(width, height) * 0.4 // 40% of the smaller dimension - - techStack.forEach((category, categoryIndex) => { - const categoryAngle = (categoryIndex / techStack.length) * Math.PI * 2 - const categoryRadius = maxRadius * 0.8 // 80% of maxRadius for category centers - - const categoryCenterX = centerX + Math.cos(categoryAngle) * categoryRadius - const categoryCenterY = centerY + Math.sin(categoryAngle) * categoryRadius - - category.items.forEach((item, index) => { - const itemAngle = categoryAngle + (index / category.items.length - 0.5) * Math.PI * 0.5 - const itemRadius = Math.random() * maxRadius * 0.3 + maxRadius * 0.1 // Between 10% and 40% of maxRadius - - const x = categoryCenterX + Math.cos(itemAngle) * itemRadius - const y = categoryCenterY + Math.sin(itemAngle) * itemRadius - - newStars.push({ - x, - y, - size: Math.random() * 2 + 2, - name: item, - category: category.category - }) - }) - }) - - setStars(newStars) - } - }, []) - - const getStarColor = (category: string) => { - switch (category) { - case "Core Technologies": - return "#FFD700" - case "UI & Styling": - return "#00CED1" - case "AI Services & APIs": - return "#FF69B4" - default: - return "#FFFFFF" - } - } - - return ( - -
- - {stars.map((star, index) => ( - - - - - -
{star.name}
-
{star.category}
-
-
- ))} -
- {hoveredCategory && ( - - {stars - .filter((star) => star.category === hoveredCategory) - .map((star, index, filteredStars) => { - const nextStar = filteredStars[(index + 1) % filteredStars.length] - return ( - - ) - })} - - )} -
- {techStack.map((category, index) => ( - setHoveredCategory(category.category)} - onMouseLeave={() => setHoveredCategory(null)} - > -
- {category.category} - - ))} -
-
- - ) -} - -interface AnimatedSectionProps { - children: React.ReactNode; - className?: string; - delay?: number; -} - -const AnimatedSection: React.FC = ({ children, className, delay = 0 }) => { - const ref = useRef(null) - const isInView = useInView(ref, { once: true, margin: "-100px" }) - - return ( - - {children} - - ) -} - -const TryButton: React.FC = () => { - return ( - - Try MiniPerplx - - - ) -} - -const ScrollProgress: React.FC = () => { - const { scrollYProgress } = useScroll() - const scaleX = useSpring(scrollYProgress, { - stiffness: 100, - damping: 30, - restDelta: 0.001 - }) - return ( - - ) -} - -const FloatingIcon: React.FC<{ Icon: LucideIcon }> = ({ Icon }) => ( - - - -) - -const FloatingIcons: React.FC = () => { - const icons = [Search, Zap, Code, Cloud, Link, MapPin, Globe, Mic, Github, XLogo, Newspaper, YoutubeLogo] - - return ( -
-
- {icons.map((Icon, index) => ( - - ))} -
-
- {icons.slice(0, 4).map((Icon, index) => ( - - ))} -
-
- ) -} - - -const NavItem: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => { - return ( -
  • - - - {children} - - -
  • - ) -} - - -const MobileNavItem: React.FC<{ href: string; children: React.ReactNode; onClick: () => void }> = ({ href, children, onClick }) => { - return ( -
  • - - {children} - -
  • - ) -} - -const LandingPage: React.FC = () => { - const { scrollYProgress } = useScroll() - const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]) - const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.95]) - const y = useTransform(scrollYProgress, [0, 0.2], [0, -50]) - - const [isMenuOpen, setIsMenuOpen] = useState(false) - - const toggleMenu = () => setIsMenuOpen(!isMenuOpen) - - - const [mounted, setMounted] = useState(false) - useEffect(() => setMounted(true), []) - React.useEffect(() => { - if (isMenuOpen) { - document.body.style.overflow = 'hidden' - } else { - document.body.style.overflow = 'unset' - } - - return () => { - document.body.style.overflow = 'unset' - } - }, [isMenuOpen]) - - useEffect(() => { - document.documentElement.style.scrollBehavior = 'smooth'; - - return () => { - document.documentElement.style.scrollBehavior = ''; + icon: LucideIcon, + text: string, + color: "red" | "green" | "orange" | "violet" | "gray" | "blue" +}) => { + // Map of color variants + const colorVariants = { + red: { + background: "bg-red-50 dark:bg-red-950", + border: "from-red-200 via-red-500 to-red-200 dark:from-red-400 dark:via-red-500 dark:to-red-700", + text: "text-red-500", + icon: "text-red-500" + }, + green: { + background: "bg-green-50 dark:bg-green-950", + border: "from-green-200 via-green-500 to-green-200 dark:from-green-400 dark:via-green-500 dark:to-green-700", + text: "text-green-500", + icon: "text-green-500" + }, + orange: { + background: "bg-orange-50 dark:bg-orange-950", + border: "from-orange-200 via-orange-500 to-orange-200 dark:from-orange-400 dark:via-orange-500 dark:to-orange-700", + text: "text-orange-500", + icon: "text-orange-500" + }, + violet: { + background: "bg-violet-50 dark:bg-violet-950", + border: "from-violet-200 via-violet-500 to-violet-200 dark:from-violet-400 dark:via-violet-500 dark:to-violet-700", + text: "text-violet-500", + icon: "text-violet-500" + }, + gray: { + background: "bg-neutral-50 dark:bg-neutral-950", + border: "from-neutral-200 via-neutral-500 to-neutral-200 dark:from-neutral-400 dark:via-neutral-500 dark:to-neutral-700", + text: "text-neutral-500", + icon: "text-neutral-500" + }, + blue: { + background: "bg-blue-50 dark:bg-blue-950", + border: "from-blue-200 via-blue-500 to-blue-200 dark:from-blue-400 dark:via-blue-500 dark:to-blue-700", + text: "text-blue-500", + icon: "text-blue-500" + } }; - }, []); - if (!mounted) return null + const variant = colorVariants[color]; - const features = [ - { icon: Globe, title: "Web Search", description: "Powered by Tavily AI for comprehensive web results." }, - { icon: Code, title: "Code Interpreter", description: "Utilize e2b.dev for advanced code interpretation and execution." }, - { icon: Cloud, title: "Weather Forecast", description: "Get accurate weather information via OpenWeatherMap." }, - { icon: YoutubeLogo, title: "Youtube Search", description: "Summarize web content quickly with FireCrawl's Scrape API." }, - { icon: XLogo, title: "Search X Posts", description: "Search for posts on X.com" }, - { icon: Newspaper, title: "Research Paper Search", description: "Search for research papers on arXiv and more" }, - { icon: MapPin, title: "Location Search", description: "Find places and nearby locations using Google Maps API, Mapbox and TripAdvisior API." }, - { icon: Mic, title: "Translation & TTS", description: "Translate text and convert to speech with Elevenlabs TTS and Microsoft's Translation API." }, - { icon: ShoppingBasket, title: "Product Search", description: "Search for products on Amazon." }, - ] + return ( + + + +
    +
    +
    + + +
    +
    + + {text} + +
    + {[...Array(3)].map((_, i) => ( +
    + ))} +
    +
    +
    +
    + + + ); +}; - const containerVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.5, - when: "beforeChildren", - staggerChildren: 0.1 - } - } - }; - - const itemVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { duration: 0.5 } - } - }; - - return ( -
    - -
    - - MiniPerplx - - - - - Explore - -
      - -
      About Us
      -

      Learn more about MiniPerplx and our mission.

      -
      - -
      Features
      -

      Discover the powerful capabilities of MiniPerplx.

      -
      - -
      Tech Stack
      -

      Explore the technologies powering MiniPerplx.

      -
      - -
      Testimonials
      -

      See what others are saying about MiniPerplx.

      -
      -
    -
    -
    - - - - Try It - - - -
    -
    - -
    - - - {isMenuOpen && ( - - - - )} - - -
    -
    - -
    -
    - - Introducing MiniPerplx - - - A minimalistic AI search engine designed to deliver answers in the simplest and most elegant way possible.✨ - - - - - - - - MiniPerplx - A minimalistic AI-powered search engine. | Product Hunt - - - Peerlist - Peerlist - - {/* */} - - There's an AI for that - - - Uneed Embed Badge - - - Uneed Embed Badge - - - -
    -
    -
    - -
    -
    -

    - Powerful Features -

    -
    - {features.map((feature, index) => ( - - ))} -
    -
    -
    - -
    -
    -

    - Our Tech Constellation -

    -

    - Explore the universe of technologies powering MiniPerplx. Hover over the stars to discover the constellations of our tech stack. -

    - - - -
    -
    - - - - -
    -
    -
    -
    - -

    - Ready to Experience MiniPerplx? -

    -
    - -

    - Discover the power of minimalistic AI search. -

    -
    - - - - -
    -
    - -
    -
    -
    - -
    - -

    - MiniPerplx -

    -
    - -

    © {new Date().getFullYear()} MiniPerplx. All rights reserved.

    -
    -
    -
    - {[...Array(20)].map((_, i) => ( - - ))} -
    -
    -
    -
    - ) +// Base YouTube Types +interface VideoDetails { + title?: string; + author_name?: string; + author_url?: string; + thumbnail_url?: string; + type?: string; + provider_name?: string; + provider_url?: string; + height?: number; + width?: number; } -export default LandingPage \ No newline at end of file +interface VideoResult { + videoId: string; + url: string; + details?: VideoDetails; + captions?: string; + timestamps?: string[]; + views?: string; + likes?: string; + summary?: string; +} + +interface YouTubeSearchResponse { + results: VideoResult[]; +} + +// UI Component Types +interface YouTubeCardProps { + video: VideoResult; + index: number; +} + +const VercelIcon = ({ size = 16 }: { size: number }) => { + return ( + + + + ); +}; + + +const XAIIcon = ({ size = 16 }: { size: number }) => { + return ( + + + + ); +} + +const YouTubeCard: React.FC = ({ video, index }) => { + const [timestampsExpanded, setTimestampsExpanded] = useState(false); + const [transcriptExpanded, setTranscriptExpanded] = useState(false); + + if (!video) return null; + + return ( + + {/* Thumbnail */} + + {video.details?.thumbnail_url ? ( + {video.details?.title + ) : ( +
    + +
    + )} +
    + +
    + + +
    + {/* Title and Channel */} +
    + + {video.details?.title || 'YouTube Video'} + + + {video.details?.author_name && ( + +
    + +
    + + {video.details.author_name} + + + )} +
    + + {/* Interactive Sections */} + {(video.timestamps && video.timestamps?.length > 0 || video.captions) && ( +
    + + + {/* Timestamps */} + {video.timestamps && video.timestamps.length > 0 && ( +
    +
    +

    Key Moments

    + +
    +
    + {video.timestamps + .slice(0, timestampsExpanded ? undefined : 3) + .map((timestamp, i) => ( +
    + {timestamp} +
    + ))} +
    +
    + )} + + {/* Transcript */} + {video.captions && ( + <> + {video.timestamps && video.timestamps!.length > 0 && } +
    +
    +

    Transcript

    + +
    + {transcriptExpanded && ( +
    +

    + {video.captions} +

    +
    + )} +
    + + )} +
    + )} +
    +
    + ); +}; + +const SponsorDialog = ({ + open, + onClose +}: { + open: boolean; + onClose: () => void; +}) => { + const isMobile = useMediaQuery("(max-width: 768px)"); + + const handleDismiss = () => { + localStorage.setItem('dismissedSponsor', 'true'); + onClose(); + }; + + const SponsorContent = () => ( +
    +
    +
    + +
    +
    +

    + Support MiniPerplx +

    +

    + Help keep MiniPerplx running, bring in the best LLMs and be ad-free. Your support enables continuous improvements and new features. +

    +
    +
    + +
    + + +
    + + +
    +
    + +
    + Your support means the world to us! ❤️ +
    +
    + ); + + if (isMobile) { + return ( + + + + Support the Project + + + + + ); + } + + return ( + + + + + + ); +}; + +const HomeContent = () => { + const searchParams = useSearchParams(); + + // Memoize initial values to prevent re-calculation + const initialState = useMemo(() => ({ + query: searchParams.get('query') || searchParams.get('q') || '', + model: searchParams.get('model') || 'grok-2-1212' + }), []); // Empty dependency array as we only want this on mount + + const lastSubmittedQueryRef = useRef(initialState.query); + const [hasSubmitted, setHasSubmitted] = useState(() => !!initialState.query); + const [selectedModel, setSelectedModel] = useState(initialState.model); + const bottomRef = useRef(null); + const [suggestedQuestions, setSuggestedQuestions] = useState([]); + const [isEditingMessage, setIsEditingMessage] = useState(false); + const [editingMessageIndex, setEditingMessageIndex] = useState(-1); + const [attachments, setAttachments] = useState([]); + const fileInputRef = useRef(null); + const inputRef = useRef(null); + const initializedRef = useRef(false); + const [selectedGroup, setSelectedGroup] = useState('web'); + + // At the top with other state declarations + const [showSponsorDialog, setShowSponsorDialog] = useState(false); + const [hasDismissedSponsor, setHasDismissedSponsor] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('dismissedSponsor') === 'true'; + } + return false; + }); + + const CACHE_KEY = 'trendingQueriesCache'; + const CACHE_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds + + // Add this type definition + interface TrendingQueriesCache { + data: TrendingQuery[]; + timestamp: number; + } + + const getTrendingQueriesFromCache = (): TrendingQueriesCache | null => { + if (typeof window === 'undefined') return null; + + const cached = localStorage.getItem(CACHE_KEY); + if (!cached) return null; + + const parsedCache = JSON.parse(cached) as TrendingQueriesCache; + const now = Date.now(); + + if (now - parsedCache.timestamp > CACHE_DURATION) { + localStorage.removeItem(CACHE_KEY); + return null; + } + + return parsedCache; + }; + + const { theme } = useTheme(); + + const [openChangelog, setOpenChangelog] = useState(false); + + const [trendingQueries, setTrendingQueries] = useState([]); + + const { isLoading, input, messages, setInput, append, handleSubmit, setMessages, reload, stop } = useChat({ + maxSteps: 8, + body: { + model: selectedModel, + group: selectedGroup, + }, + onFinish: async (message, { finishReason }) => { + console.log("[finish reason]:", finishReason); + if (message.content && finishReason === 'stop' || finishReason === 'length') { + const newHistory = [...messages, { role: "user", content: lastSubmittedQueryRef.current }, { role: "assistant", content: message.content }]; + const { questions } = await suggestQuestions(newHistory); + setSuggestedQuestions(questions); + } + }, + onError: (error) => { + console.error("Chat error:", error.cause, error.message); + toast.error("An error occurred.", { + description: `Oops! An error occurred while processing your request. ${error.message}`, + }); + }, + }); + + // Add this useEffect in the HomeContent component + useEffect(() => { + if (!hasDismissedSponsor) { + const timer = setTimeout(() => { + setShowSponsorDialog(true); + }, 30000); // Show after 30 seconds + + return () => clearTimeout(timer); + } + }, [hasDismissedSponsor]); + + useEffect(() => { + if (!initializedRef.current && initialState.query && !messages.length) { + initializedRef.current = true; + setHasSubmitted(true); + console.log("[initial query]:", initialState.query); + append({ + content: initialState.query, + role: 'user' + }); + } + }, [initialState.query, append, setInput, messages.length]); + + useEffect(() => { + const fetchTrending = async () => { + // Check cache first + const cached = getTrendingQueriesFromCache(); + if (cached) { + setTrendingQueries(cached.data); + return; + } + + try { + const res = await fetch('/api/trending'); + if (!res.ok) throw new Error('Failed to fetch trending queries'); + const data = await res.json(); + + // Store in cache + const cacheData: TrendingQueriesCache = { + data, + timestamp: Date.now() + }; + localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); + + setTrendingQueries(data); + } catch (error) { + console.error('Error fetching trending queries:', error); + setTrendingQueries([]); + } + }; + + fetchTrending(); + }, []); + + const ThemeToggle: React.FC = () => { + const { theme, setTheme } = useTheme(); + + return ( + + ); + }; + + + const CopyButton = ({ text }: { text: string }) => { + const [isCopied, setIsCopied] = useState(false); + + return ( + + ); + }; + + type Changelog = { + id: string; + images: string[]; + content: string; + title: string; + }; + + const changelogs: Changelog[] = [ + { + id: "1", + title: "The Unexpected Collab", + images: [ + "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-collab.jpeg", + ], + content: ` +## **MiniPerplx x Vercel x xAI Collab** + +Excited to annouce that MiniPerplx has partnered with Vercel and xAI to bring you the best of AI search experience. +Grok 2 models are now available for you to try out. +` + } + ]; + + + const ChangeLogs = ({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) => { + const isMobile = useMediaQuery("(max-width: 768px)"); + + const ChangelogContent = () => ( + <> + {/* Fixed Header */} +
    +

    + What's new +

    +
    + +
    + {changelogs.map((changelog) => ( +
    + {/* Carousel */} + + + {changelog.images.map((image, index) => ( + + {changelog.title} + + ))} + + + + {/* Content Section */} +
    +

    + {changelog.title} +

    +
    + ( +

    + ), + p: ({ node, className, ...props }) => ( +

    + ), + a: ({ node, className, ...props }) => ( + + ), + }} + className="text-sm text-left pr-2" + > + {changelog.content} + +

    +
    +
    + ))} +
    + + ); + + if (isMobile) { + return ( + + + + + + + + ); + } + + return ( + + + + + + ); + }; + + const TranslationTool: React.FC<{ toolInvocation: ToolInvocation; result: any }> = ({ toolInvocation, result }) => { + const [isPlaying, setIsPlaying] = useState(false); + const [audioUrl, setAudioUrl] = useState(null); + const [isGeneratingAudio, setIsGeneratingAudio] = useState(false); + const audioRef = useRef(null); + const canvasRef = useRef(null); + const waveRef = useRef(null); + + useEffect(() => { + return () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + } + }; + }, []); + + useEffect(() => { + if (audioUrl && audioRef.current && canvasRef.current) { + waveRef.current = new Wave(audioRef.current, canvasRef.current); + waveRef.current.addAnimation(new waveRef.current.animations.Lines({ + lineColor: "rgb(203, 113, 93)", + lineWidth: 2, + mirroredY: true, + count: 100, + })); + } + }, [audioUrl]); + + const handlePlayPause = async () => { + if (!audioUrl && !isGeneratingAudio) { + setIsGeneratingAudio(true); + try { + const { audio } = await generateSpeech(result.translatedText, 'alloy'); + setAudioUrl(audio); + setIsGeneratingAudio(false); + } catch (error) { + console.error("Error generating speech:", error); + setIsGeneratingAudio(false); + } + } else if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + const handleReset = () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + setIsPlaying(false); + } + }; + + if (!result) { + return ( + + +
    +
    +
    +
    +
    +
    + ); + } + + return ( + + +
    +
    + +
    +
    +
    + +
    +
    + The phrase {toolInvocation.args.text} translates from {result.detectedLanguage} to {toolInvocation.args.to} as {result.translatedText} in {toolInvocation.args.to}. +
    +
    +
    +
    + {audioUrl && ( +
    + ); + }; + + + + interface TableData { + title: string; + content: string; + } + + interface ResultsOverviewProps { + result: { + image: string; + title: string; + description: string; + table_data: TableData[]; + }; + } + + const ResultsOverview: React.FC = React.memo(({ result }) => { + const [showAll, setShowAll] = useState(false); + + const visibleData = useMemo(() => { + return showAll ? result.table_data : result.table_data.slice(0, 3); + }, [showAll, result.table_data]); + + return ( + + +
    + {result.image && ( +
    + {result.title} +
    + )} +
    + {result.title} +

    {result.description}

    +
    +
    +
    + + + + {visibleData.map((item, index) => ( + + {item.title} + {item.content} + + ))} + +
    + {result.table_data.length > 3 && ( + + )} +
    +
    + ); + }); + + ResultsOverview.displayName = 'ResultsOverview'; + + const renderToolInvocation = useCallback( + (toolInvocation: ToolInvocation, index: number) => { + const args = JSON.parse(JSON.stringify(toolInvocation.args)); + const result = 'result' in toolInvocation ? JSON.parse(JSON.stringify(toolInvocation.result)) : null; + + if (toolInvocation.toolName === 'thinking_canvas') { + return ( + +
    + + +
    +
    + +
    +
    + {args.title} +

    {args.content.length} steps

    +
    +
    + +
    +
    +
    + {args.content.map((thought: string, i: number) => ( + + + {i + 1} + +

    + {thought} +

    +
    + ))} +
    +
    +
    + ); + } + + // Find place results + if (toolInvocation.toolName === 'find_place') { + if (!result) { + return ; + } + + const { features } = result; + if (!features || features.length === 0) return null; + + return ( + + {/* Map Container */} +
    +
    + + {features.length} Locations Found + +
    + + ({ + name: feature.name, + location: { + lat: feature.geometry.coordinates[1], + lng: feature.geometry.coordinates[0], + }, + vicinity: feature.formatted_address, + }))} + zoom={features.length > 1 ? 12 : 15} + /> +
    + + {/* Place Details Footer */} +
    + {features.map((place: any, index: any) => { + const isGoogleResult = place.source === 'google'; + + return ( +
    +
    +
    + {place.feature_type === 'street_address' || place.feature_type === 'street' ? ( + + ) : place.feature_type === 'locality' ? ( + + ) : ( + + )} +
    + +
    +

    + {place.name} +

    + {place.formatted_address && ( +

    + {place.formatted_address} +

    + )} + + {place.feature_type.replace(/_/g, ' ')} + +
    + +
    + + + + + + Copy Coordinates + + + + + + + + + View in Maps + + +
    +
    +
    + ); + })} +
    +
    + ); + } + + if (toolInvocation.toolName === 'tmdb_search') { + if (!result) { + return ; + } + + return ; + } + + if (toolInvocation.toolName === 'trending_movies') { + if (!result) { + return ; + } + return ; + } + + if (toolInvocation.toolName === 'trending_tv') { + if (!result) { + return ; + } + return ; + } + + + if (toolInvocation.toolName === 'x_search') { + if (!result) { + return ; + } + + const PREVIEW_COUNT = 3; + + // Shared content component + const FullTweetList = () => ( +
    + {result.map((post: XResult, index: number) => ( + + + + ))} +
    + ); + + return ( + + +
    +
    + +
    +
    + Latest from X +

    + {result.length} tweets found +

    +
    +
    +
    +
    +
    +
    + {result.slice(0, PREVIEW_COUNT).map((post: XResult, index: number) => ( + + + + ))} +
    +
    + + {/* Gradient overlay */} +
    + + {/* Show More Buttons - Desktop Sheet */} +
    + {/* Desktop Sheet */} +
    + + + + + + + All Tweets + + + + +
    + + {/* Mobile Drawer */} +
    + + + + + + + All Tweets + +
    + +
    +
    +
    +
    +
    +
    + + ); + } + + if (toolInvocation.toolName === 'youtube_search') { + if (!result) { + return ; + } + + const youtubeResult = result as YouTubeSearchResponse; + + return ( + + + +
    +
    + +
    +
    +

    + YouTube Results +

    +
    + + {youtubeResult.results.length} videos + +
    +
    +
    +
    + + +
    + {youtubeResult.results.map((video, index) => ( + + ))} +
    +
    +
    +
    + ); + } + + // Academic search results continued... + if (toolInvocation.toolName === 'academic_search') { + if (!result) { + return ; + } + + return ( + + +
    +
    + +
    +
    + Academic Papers +

    Found {result.results.length} papers

    +
    +
    +
    +
    +
    + {result.results.map((paper: AcademicResult, index: number) => ( + +
    + {/* Background with gradient border */} +
    + + {/* Main content container */} +
    + {/* Title */} +

    + {paper.title} +

    + + {/* Authors with better overflow handling */} + {paper.author && ( +
    +
    + + + {paper.author.split(';') + .slice(0, 2) // Take first two authors + .join(', ') + + (paper.author.split(';').length > 2 ? ' et al.' : '') + } + +
    +
    + )} + + {/* Date if available */} + {paper.publishedDate && ( +
    +
    + + {new Date(paper.publishedDate).toLocaleDateString()} +
    +
    + )} + + {/* Summary with gradient border */} +
    +
    +

    + {paper.summary} +

    +
    + + {/* Actions */} +
    + + + {paper.url.includes('arxiv.org') && ( + + )} +
    +
    +
    + + ))} +
    +
    + + ); + } + + if (toolInvocation.toolName === 'nearby_search') { + if (!result) { + return ( +
    +
    + + + Finding nearby {args.type}... + +
    + + {[0, 1, 2].map((index) => ( + + ))} + +
    + ); + } + + console.log(result); + + return ( +
    + +
    + ); + } + + if (toolInvocation.toolName === 'text_search') { + if (!result) { + return ( +
    +
    + + Searching places... +
    + + {[0, 1, 2].map((index) => ( + + ))} + +
    + ); + } + + const centerLocation = result.results[0]?.geometry?.location; + return ( + ({ + name: place.name, + location: place.geometry.location, + vicinity: place.formatted_address + }))} + /> + ); + } + + if (toolInvocation.toolName === 'get_weather_data') { + if (!result) { + return ( +
    +
    + + Fetching weather data... +
    +
    + {[0, 1, 2].map((index) => ( + + ))} +
    +
    + ); + } + return ; + } + + if (toolInvocation.toolName === 'programming') { + return ( + + + +
    +
    + +

    Programming

    +
    + {!result ? ( + + + Executing + + ) : ( + + + Executed + + )} +
    +
    + +
    +
    + {args.icon === 'stock' && } + {args.icon === 'default' && } + {args.icon === 'date' && } + {args.icon === 'calculation' && } + {args.title} +
    + + + + Code + + + Output + + {result?.images && result.images.length > 0 && ( + + Images + + )} + {result?.chart && ( + + Visualization + + )} + + +
    + + {args.code} + + +
    + +
    +
    +
    + +
    + {result ? ( + <> +
    +                                                            {result.message}
    +                                                        
    +
    + +
    + + ) : ( +
    +
    + + Executing code... +
    +
    + )} +
    +
    + {result?.images && result.images.length > 0 && ( + +
    + {result.images.map((img: { format: string, url: string }, imgIndex: number) => ( +
    +
    +

    Image {imgIndex + 1}

    + {img.url && img.url.trim() !== '' && ( + + )} +
    +
    + {img.url && img.url.trim() !== '' ? ( + {`Generated + ) : ( +
    + Image upload failed or URL is empty +
    + )} +
    +
    + ))} +
    +
    + )} + {result?.chart && ( + + + + )} +
    +
    +
    +
    +
    + ); + } + + if (toolInvocation.toolName === 'web_search') { + return ( +
    + +
    + ); + } + + if (toolInvocation.toolName === 'retrieve') { + if (!result) { + return ( +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); + } + + return ( +
    +
    +
    +
    +
    + +
    +
    +

    + {result.results[0].title} +

    +

    + {result.results[0].description} +

    +
    + + {result.results[0].language || 'Unknown'} + + + + View source + +
    +
    +
    +
    + +
    +
    + +
    + + View content +
    + +
    +
    +
    + {result.results[0].content} +
    +
    +
    +
    +
    + ); + } + if (toolInvocation.toolName === 'text_translate') { + return ; + } + + if (toolInvocation.toolName === 'results_overview') { + if (!result) { + return ( +
    +
    + + Generating overview... +
    +
    + ); + } + + return ; + } + + if (toolInvocation.toolName === 'track_flight') { + if (!result) { + return ( +
    +
    + + Tracking flight... +
    +
    + {[0, 1, 2].map((index) => ( + + ))} +
    +
    + ); + } + + if (result.error) { + return ( +
    + Error tracking flight: {result.error} +
    + ); + } + + return ( +
    + +
    + ); + } + + return null; + }, + [ResultsOverview, theme] + ); + + interface MarkdownRendererProps { + content: string; + } + + interface CitationLink { + text: string; + link: string; + } + + interface LinkMetadata { + title: string; + description: string; + } + + const isValidUrl = (str: string) => { + try { + new URL(str); + return true; + } catch { + return false; + } + }; + + const MarkdownRenderer: React.FC = ({ content }) => { + const [metadataCache, setMetadataCache] = useState>({}); + + const citationLinks = useMemo(() => { + return Array.from(content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)).map(([_, text, link]) => ({ + text, + link, + })); + }, [content]); + + const inlineMathRegex = /\$([^\$]+)\$/g; + const blockMathRegex = /\$\$([^\$]+)\$\$/g; + + const isValidLatex = (text: string): boolean => { + // Basic validation - checks for balanced delimiters + return !(text.includes('\\') && !text.match(/\\[a-zA-Z{}\[\]]+/)); + } + + const renderLatexString = (text: string) => { + let parts = []; + let lastIndex = 0; + let match; + + // Try to match inline math first ($...$) + while ((match = /\$([^\$]+)\$/g.exec(text.slice(lastIndex))) !== null) { + const mathText = match[1]; + const fullMatch = match[0]; + const matchIndex = lastIndex + match.index; + + // Add text before math + if (matchIndex > lastIndex) { + parts.push(text.slice(lastIndex, matchIndex)); + } + + // Only render as LaTeX if valid + if (isValidLatex(mathText)) { + parts.push({fullMatch}); + } else { + parts.push(fullMatch); + } + + lastIndex = matchIndex + fullMatch.length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + + return parts.length > 0 ? parts : text; + }; + + const fetchMetadataWithCache = useCallback(async (url: string) => { + if (metadataCache[url]) { + return metadataCache[url]; + } + + const metadata = await fetchMetadata(url); + if (metadata) { + setMetadataCache(prev => ({ ...prev, [url]: metadata })); + } + return metadata; + }, [metadataCache]); + + interface CodeBlockProps { + language: string | undefined; + children: string; + } + + const CodeBlock = React.memo(({ language, children }: CodeBlockProps) => { + const [isCopied, setIsCopied] = useState(false); + const { theme } = useTheme(); + + const handleCopy = useCallback(async () => { + await navigator.clipboard.writeText(children); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }, [children]); + + return ( +
    +
    +
    +
    + {language || 'text'} +
    + +
    + +
    + + {children} + +
    +
    +
    + ); + }, (prevProps, nextProps) => + prevProps.children === nextProps.children && + prevProps.language === nextProps.language + ); + + CodeBlock.displayName = 'CodeBlock'; + + const LinkPreview = ({ href }: { href: string }) => { + const [metadata, setMetadata] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + React.useEffect(() => { + setIsLoading(true); + fetchMetadataWithCache(href).then((data) => { + setMetadata(data); + setIsLoading(false); + }); + }, [href]); + + if (isLoading) { + return ( +
    + +
    + ); + } + + const domain = new URL(href).hostname; + + return ( +
    +
    + Favicon + {domain} +
    +
    +

    + {metadata?.title || "Untitled"} +

    + {metadata?.description && ( +

    + {metadata.description} +

    + )} +
    +
    + ); + }; + + const renderHoverCard = (href: string, text: React.ReactNode, isCitation: boolean = false) => { + return ( + + + + {text} + + + + + + + ); + }; + + const renderer: Partial = { + text(text: string) { + if (!text.includes('$')) return text; + + return ( + + {text} + + ); + }, + paragraph(children) { + if (typeof children === 'string' && children.includes('$')) { + return ( +

    + + {children} + +

    + ); + } + return

    {children}

    ; + }, + code(children, language) { + return {String(children)}; + }, + link(href, text) { + const citationIndex = citationLinks.findIndex(link => link.link === href); + if (citationIndex !== -1) { + return ( + + {renderHoverCard(href, citationIndex + 1, true)} + + ); + } + return isValidUrl(href) ? renderHoverCard(href, text) :
    {text}; + }, + heading(children, level) { + const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; + const className = `text-${4 - level}xl font-bold my-4 text-neutral-800 dark:text-neutral-100`; + return {children}; + }, + list(children, ordered) { + const ListTag = ordered ? 'ol' : 'ul'; + return {children}; + }, + listItem(children) { + return
  • {children}
  • ; + }, + blockquote(children) { + return
    {children}
    ; + }, + }; + + return ( +
    + {content} +
    + ); + }; + + + + const lastUserMessageIndex = useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === 'user') { + return i; + } + } + return -1; + }, [messages]); + + useEffect(() => { + const handleScroll = () => { + const userScrolled = window.innerHeight + window.scrollY < document.body.offsetHeight; + if (!userScrolled && bottomRef.current && (messages.length > 0 || suggestedQuestions.length > 0)) { + bottomRef.current.scrollIntoView({ behavior: "smooth" }); + } + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [messages, suggestedQuestions]); + + const handleExampleClick = async (card: TrendingQuery) => { + const exampleText = card.text; + // track("search example", { query: exampleText }); + lastSubmittedQueryRef.current = exampleText; + setHasSubmitted(true); + setSuggestedQuestions([]); + await append({ + content: exampleText.trim(), + role: 'user', + }); + }; + + const handleSuggestedQuestionClick = useCallback(async (question: string) => { + setHasSubmitted(true); + setSuggestedQuestions([]); + + await append({ + content: question.trim(), + role: 'user' + }); + }, [append]); + + const handleMessageEdit = useCallback((index: number) => { + setIsEditingMessage(true); + setEditingMessageIndex(index); + setInput(messages[index].content); + }, [messages, setInput]); + + const handleMessageUpdate = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (input.trim()) { + const updatedMessages = [...messages]; + updatedMessages[editingMessageIndex] = { ...updatedMessages[editingMessageIndex], content: input.trim() }; + setMessages(updatedMessages); + setIsEditingMessage(false); + setEditingMessageIndex(-1); + handleSubmit(e); + } else { + toast.error("Please enter a valid message."); + } + }, [input, messages, editingMessageIndex, setMessages, handleSubmit]); + + interface NavbarProps { } + + const Navbar: React.FC = () => { + return ( +
    + + + +
    + + + Deploy with Vercel + Deploy + + + + + + + +

    Sponsor this project on GitHub

    +
    +
    +
    + +
    +
    + ); + }; + + const SuggestionCards: React.FC<{ + trendingQueries: TrendingQuery[]; + }> = ({ trendingQueries }) => { + const [isLoading, setIsLoading] = useState(true); + const scrollRef = useRef(null); + const [isPaused, setIsPaused] = useState(false); + const scrollIntervalRef = useRef(); + const [isTouchDevice, setIsTouchDevice] = useState(false); + + useEffect(() => { + setIsLoading(false); + setIsTouchDevice('ontouchstart' in window); + }, [trendingQueries]); + + useEffect(() => { + if (isTouchDevice) return; // Disable auto-scroll on touch devices + + const startScrolling = () => { + if (!scrollRef.current || isPaused) return; + scrollRef.current.scrollLeft += 1; // Reduced speed + + // Reset scroll when reaching end + if (scrollRef.current.scrollLeft >= + (scrollRef.current.scrollWidth - scrollRef.current.clientWidth)) { + scrollRef.current.scrollLeft = 0; + } + }; + + scrollIntervalRef.current = setInterval(startScrolling, 30); + + return () => { + if (scrollIntervalRef.current) { + clearInterval(scrollIntervalRef.current); + } + }; + }, [isPaused, isTouchDevice]); + + if (isLoading || trendingQueries.length === 0) { + return ( +
    + {/* Overlay with Loading Text */} +
    +
    +
    + + Loading trending queries +
    +
    +
    + + {/* Background Cards */} +
    + {[1, 2, 3].map((_, index) => ( +
    +
    +
    +
    +
    +
    + ))} +
    +
    + ); + } + + const getIconForCategory = (category: string) => { + const iconMap = { + trending: , + community: , + science: , + tech: , + travel: , + politics: , + health: , + sports: , + finance: , + football: , + }; + return iconMap[category as keyof typeof iconMap] || ; + }; + + return ( +
    + {/* Gradient Fades */} +
    +
    + +
    !isTouchDevice && setIsPaused(true)} + onMouseLeave={() => !isTouchDevice && setIsPaused(false)} + onTouchStart={() => setIsPaused(true)} + onTouchEnd={() => setIsPaused(false)} + > + {Array(20).fill(trendingQueries).flat().map((query, index) => ( + + ))} +
    +
    + ); + }; + + const handleModelChange = useCallback((newModel: string) => { + setSelectedModel(newModel); + setSuggestedQuestions([]); + reload({ body: { model: newModel } }); + }, [reload]); + + const resetSuggestedQuestions = useCallback(() => { + setSuggestedQuestions([]); + }, []); + + + const memoizedMessages = useMemo(() => messages, [messages]); + + const memoizedSuggestionCards = useMemo(() => ( + + ), [trendingQueries]); + + return ( +
    + + +
    + {!hasSubmitted && ( +
    + setOpenChangelog(true)} + className="cursor-pointer gap-1 mb-2 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200" + variant="secondary" + > + What's new + +

    MiniPerplx

    +

    + In search for minimalism and simplicity +

    +
    + + Powered by + +
    + + + + Vercel + + + {/* span with a + */} + + + + + + xAI Grok + + +
    +
    +
    + )} + + + {!hasSubmitted && ( + + + {memoizedSuggestionCards} + + )} + + +
    + {memoizedMessages.map((message, index) => ( +
    + {message.role === 'user' && ( + + +
    + {isEditingMessage && editingMessageIndex === index ? ( +
    + setInput(e.target.value)} + className="flex-grow bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100" + /> + + +
    + ) : ( +
    +

    + {message.content} +

    +
    + {message.experimental_attachments?.map((attachment, attachmentIndex) => ( +
    + {attachment.contentType!.startsWith('image/') && ( + {attachment.name + )} +
    + ))} +
    +
    + )} +
    + + {!isEditingMessage && index === lastUserMessageIndex && ( +
    + +
    + )} +
    + )} + {message.role === 'assistant' && message.content !== null && !message.toolInvocations && ( +
    +
    +
    + +

    Answer

    +
    +
    + +
    +
    +
    + +
    +
    + )} + {message.toolInvocations?.map((toolInvocation: ToolInvocation, toolIndex: number) => ( +
    + {renderToolInvocation(toolInvocation, toolIndex)} +
    + ))} +
    + ))} + + {suggestedQuestions.length > 0 && ( + +
    + +

    Suggested questions

    +
    +
    + {suggestedQuestions.map((question, index) => ( + + ))} +
    +
    + )} +
    +
    +
    + + + {hasSubmitted && ( + + + + )} + + {!hasSubmitted && ( + + +
    + © {new Date().getFullYear()} MiniPerplx +
    + + + + @zaidmukaddam + + + +

    + Follow me on +

    +
    +
    +
    +
    + )} + +
    + ); +} + +const LoadingFallback = () => ( +
    +
    +

    + MiniPerplx +

    +

    + Loading your minimalist AI experience... +

    + +
    +
    +); + +const Home = () => { + return ( + }> + + + + ); +}; + +export default Home; \ No newline at end of file diff --git a/app/search/page.tsx b/app/search/page.tsx index c4bc88b..5852151 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -1,2850 +1,5 @@ -/* eslint-disable @next/next/no-img-element */ -"use client"; -import 'katex/dist/katex.min.css'; +import { redirect } from 'next/navigation' -import -React, -{ - useRef, - useCallback, - useState, - useEffect, - useMemo, - Suspense -} from 'react'; -import ReactMarkdown from 'react-markdown'; -import { useTheme } from 'next-themes'; -import Marked, { ReactRenderer } from 'marked-react'; -import Latex from 'react-latex-next'; -import { track } from '@vercel/analytics'; -import { useSearchParams } from 'next/navigation'; -import { useChat } from 'ai/react'; -import { ToolInvocation } from 'ai'; -import { toast } from 'sonner'; -import { motion, AnimatePresence } from 'framer-motion'; -import Image from 'next/image'; -import { - fetchMetadata, - generateSpeech, - suggestQuestions -} from '../actions'; -import { Wave } from "@foobar404/wave"; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneLight, oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { - Sparkles, - ArrowRight, - Globe, - AlignLeft, - Copy, - Cloud, - Code, - Check, - Loader2, - User2, - Heart, - X, - MapPin, - Plus, - Download, - Flame, - Sun, - Pause, - Play, - TrendingUpIcon, - Calendar, - Calculator, - ChevronDown, - Edit2, - ChevronUp, - Moon, - ShoppingBasket, - Star, - YoutubeIcon, - LucideIcon, - FileText, - Book, - ExternalLink, - Building, - Users, - Brain, - TrendingUp, - Plane -} from 'lucide-react'; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; -import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; -import { GitHubLogoIcon, TextIcon } from '@radix-ui/react-icons'; -import Link from 'next/link'; -import { Dialog, DialogContent } from "@/components/ui/dialog"; -import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel"; -import { cn, SearchGroupId } from '@/lib/utils'; -import { - Table, - TableBody, - TableCell, - TableRow, -} from "@/components/ui/table"; -import Autoplay from 'embla-carousel-autoplay'; -import FormComponent from '@/components/ui/form-component'; -import WeatherChart from '@/components/weather-chart'; -import InteractiveChart from '@/components/interactive-charts'; -import { MapComponent, MapContainer } from '@/components/map-components'; -import MultiSearch from '@/components/multi-search'; -import { CurrencyDollar, Flag, RoadHorizon, SoccerBall, TennisBall, XLogo } from '@phosphor-icons/react'; -import { BorderTrail } from '@/components/core/border-trail'; -import { TextShimmer } from '@/components/core/text-shimmer'; -import { Tweet } from 'react-tweet'; -import NearbySearchMapView from '@/components/nearby-search-map-view'; -import { Separator } from '@/components/ui/separator'; -import { TrendingQuery } from '../api/trending/route'; -import { FlightTracker } from '@/components/flight-tracker'; -import { InstallPrompt } from '@/components/InstallPrompt'; -import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { vs } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { useMediaQuery } from '@/hooks/use-media-query'; - - -export const maxDuration = 60; - -interface Attachment { - name: string; - contentType: string; - url: string; - size: number; -} - -interface ShoppingProduct { - title: string; - url: string; - image: string; - price: string; - rating?: number; - reviewCount?: number; -} - -interface RedditResult { - title: string; - url: string; - subreddit?: string; - score?: number; -} - -interface XResult { - id: string; - url: string; - title: string; - author?: string; - publishedDate?: string; - text: string; - highlights?: string[]; - tweetId: string; -} - -interface YouTubeVideo { - title: string; - link: string; - snippet?: string; - imageUrl?: string; - duration?: string; - source?: string; - channel?: string; - date?: string; -} - -interface AcademicResult { - title: string; - url: string; - author?: string | null; - publishedDate?: string; - summary: string; -} - -interface YouTubeVideo { - videoId: string; - url: string; - title: string; - description?: string; - author?: string; - publishedDate?: string; - views?: string; - likes?: string; - subscribers?: string; - summary?: string; - thumbnail?: string; -} - - -/* -Mapbox API interfaces -*/ - -interface MapboxCoordinates { - longitude: number; - latitude: number; -} - -interface MapboxContext { - street?: { - mapbox_id: string; - name: string; - }; - postcode?: { - mapbox_id: string; - name: string; - }; - locality?: { - mapbox_id: string; - name: string; - wikidata_id?: string; - }; - place?: { - mapbox_id: string; - name: string; - wikidata_id?: string; - }; - district?: { - mapbox_id: string; - name: string; - wikidata_id?: string; - }; - region?: { - mapbox_id: string; - name: string; - wikidata_id?: string; - region_code?: string; - region_code_full?: string; - }; - country?: { - mapbox_id: string; - name: string; - wikidata_id?: string; - country_code: string; - country_code_alpha_3: string; - }; -} - -interface MapboxFeature { - id: string; - type: string; - geometry: { - type: string; - coordinates: [number, number]; // [longitude, latitude] - }; - properties: { - mapbox_id: string; - feature_type: 'street' | 'locality' | 'address' | string; - name: string; - name_preferred?: string; - full_address?: string; - coordinates: MapboxCoordinates; - place_formatted?: string; - bbox?: [number, number, number, number]; - context?: MapboxContext; - }; -} - -// Simplified feature interface for the UI -interface SimplifiedFeature { - id: string; - name: string; - formatted_address?: string; - geometry: { - type: string; - coordinates: [number, number]; - }; - context?: MapboxContext; - place_formatted?: string; - feature_type: string; - coordinates: MapboxCoordinates; - bbox?: [number, number, number, number]; -} - -/* -Mapbox API interfaces end -*/ - -// Updated SearchLoadingState with new colors and states -const SearchLoadingState = ({ - icon: Icon, - text, - color -}: { - icon: LucideIcon, - text: string, - color: "red" | "green" | "orange" | "violet" | "gray" | "blue" -}) => { - // Map of color variants - const colorVariants = { - red: { - background: "bg-red-50 dark:bg-red-950", - border: "from-red-200 via-red-500 to-red-200 dark:from-red-400 dark:via-red-500 dark:to-red-700", - text: "text-red-500", - icon: "text-red-500" - }, - green: { - background: "bg-green-50 dark:bg-green-950", - border: "from-green-200 via-green-500 to-green-200 dark:from-green-400 dark:via-green-500 dark:to-green-700", - text: "text-green-500", - icon: "text-green-500" - }, - orange: { - background: "bg-orange-50 dark:bg-orange-950", - border: "from-orange-200 via-orange-500 to-orange-200 dark:from-orange-400 dark:via-orange-500 dark:to-orange-700", - text: "text-orange-500", - icon: "text-orange-500" - }, - violet: { - background: "bg-violet-50 dark:bg-violet-950", - border: "from-violet-200 via-violet-500 to-violet-200 dark:from-violet-400 dark:via-violet-500 dark:to-violet-700", - text: "text-violet-500", - icon: "text-violet-500" - }, - gray: { - background: "bg-neutral-50 dark:bg-neutral-950", - border: "from-neutral-200 via-neutral-500 to-neutral-200 dark:from-neutral-400 dark:via-neutral-500 dark:to-neutral-700", - text: "text-neutral-500", - icon: "text-neutral-500" - }, - blue: { - background: "bg-blue-50 dark:bg-blue-950", - border: "from-blue-200 via-blue-500 to-blue-200 dark:from-blue-400 dark:via-blue-500 dark:to-blue-700", - text: "text-blue-500", - icon: "text-blue-500" - } - }; - - const variant = colorVariants[color]; - - return ( - - - -
    -
    -
    - - -
    -
    - - {text} - -
    - {[...Array(3)].map((_, i) => ( -
    - ))} -
    -
    -
    -
    - - - ); -}; - -// Base YouTube Types -interface VideoDetails { - title?: string; - author_name?: string; - author_url?: string; - thumbnail_url?: string; - type?: string; - provider_name?: string; - provider_url?: string; - height?: number; - width?: number; -} - -interface VideoResult { - videoId: string; - url: string; - details?: VideoDetails; - captions?: string; - timestamps?: string[]; - views?: string; - likes?: string; - summary?: string; -} - -interface YouTubeSearchResponse { - results: VideoResult[]; -} - -// UI Component Types -interface YouTubeCardProps { - video: VideoResult; - index: number; -} - - -const YouTubeCard: React.FC = ({ video, index }) => { - const [timestampsExpanded, setTimestampsExpanded] = useState(false); - const [transcriptExpanded, setTranscriptExpanded] = useState(false); - - if (!video) return null; - - return ( - - {/* Thumbnail */} - - {video.details?.thumbnail_url ? ( - {video.details?.title - ) : ( -
    - -
    - )} -
    - -
    - - -
    - {/* Title and Channel */} -
    - - {video.details?.title || 'YouTube Video'} - - - {video.details?.author_name && ( - -
    - -
    - - {video.details.author_name} - - - )} -
    - - {/* Interactive Sections */} - {(video.timestamps && video.timestamps?.length > 0 || video.captions) && ( -
    - - - {/* Timestamps */} - {video.timestamps && video.timestamps.length > 0 && ( -
    -
    -

    Key Moments

    - -
    -
    - {video.timestamps - .slice(0, timestampsExpanded ? undefined : 3) - .map((timestamp, i) => ( -
    - {timestamp} -
    - ))} -
    -
    - )} - - {/* Transcript */} - {video.captions && ( - <> - {video.timestamps && video.timestamps!.length > 0 && } -
    -
    -

    Transcript

    - -
    - {transcriptExpanded && ( -
    -

    - {video.captions} -

    -
    - )} -
    - - )} -
    - )} -
    -
    - ); -}; - -const HomeContent = () => { - const searchParams = useSearchParams(); - - // Memoize initial values to prevent re-calculation - const initialState = useMemo(() => ({ - query: searchParams.get('query') || '', - model: searchParams.get('model') || 'azure:gpt4o-mini' - }), []); // Empty dependency array as we only want this on mount - - const lastSubmittedQueryRef = useRef(initialState.query); - const [hasSubmitted, setHasSubmitted] = useState(() => !!initialState.query); - const [selectedModel, setSelectedModel] = useState(initialState.model); - const bottomRef = useRef(null); - const [suggestedQuestions, setSuggestedQuestions] = useState([]); - const [isEditingMessage, setIsEditingMessage] = useState(false); - const [editingMessageIndex, setEditingMessageIndex] = useState(-1); - const [attachments, setAttachments] = useState([]); - const fileInputRef = useRef(null); - const inputRef = useRef(null); - const initializedRef = useRef(false); - const [selectedGroup, setSelectedGroup] = useState('web'); - - const CACHE_KEY = 'trendingQueriesCache'; - const CACHE_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds - - // Add this type definition - interface TrendingQueriesCache { - data: TrendingQuery[]; - timestamp: number; - } - - const getTrendingQueriesFromCache = (): TrendingQueriesCache | null => { - if (typeof window === 'undefined') return null; - - const cached = localStorage.getItem(CACHE_KEY); - if (!cached) return null; - - const parsedCache = JSON.parse(cached) as TrendingQueriesCache; - const now = Date.now(); - - if (now - parsedCache.timestamp > CACHE_DURATION) { - localStorage.removeItem(CACHE_KEY); - return null; - } - - return parsedCache; - }; - - const { theme } = useTheme(); - - const [openChangelog, setOpenChangelog] = useState(false); - - const [trendingQueries, setTrendingQueries] = useState([]); - - const { isLoading, input, messages, setInput, append, handleSubmit, setMessages, reload, stop } = useChat({ - maxSteps: 10, - body: { - model: selectedModel, - group: selectedGroup, - }, - onFinish: async (message, { finishReason }) => { - console.log("[finish reason]:", finishReason); - if (message.content && finishReason === 'stop' || finishReason === 'length') { - const newHistory = [...messages, { role: "user", content: lastSubmittedQueryRef.current }, { role: "assistant", content: message.content }]; - const { questions } = await suggestQuestions(newHistory); - setSuggestedQuestions(questions); - } - }, - onError: (error) => { - console.error("Chat error:", error.cause, error.message); - toast.error("An error occurred.", { - description: "We must have ran out of credits. Sponsor us on GitHub to keep this service running.", - action: { - label: "Sponsor", - onClick: () => window.open("https://git.new/mplx", "_blank"), - }, - }); - }, - }); - - useEffect(() => { - if (!initializedRef.current && initialState.query && !messages.length) { - initializedRef.current = true; - setHasSubmitted(true); - console.log("[initial query]:", initialState.query); - append({ - content: initialState.query, - role: 'user' - }); - } - }, [initialState.query, append, setInput, messages.length]); - - useEffect(() => { - const fetchTrending = async () => { - // Check cache first - const cached = getTrendingQueriesFromCache(); - if (cached) { - setTrendingQueries(cached.data); - return; - } - - try { - const res = await fetch('/api/trending'); - if (!res.ok) throw new Error('Failed to fetch trending queries'); - const data = await res.json(); - - // Store in cache - const cacheData: TrendingQueriesCache = { - data, - timestamp: Date.now() - }; - localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); - - setTrendingQueries(data); - } catch (error) { - console.error('Error fetching trending queries:', error); - setTrendingQueries([]); - } - }; - - fetchTrending(); - }, []); - - const ThemeToggle: React.FC = () => { - const { theme, setTheme } = useTheme(); - - return ( - - ); - }; - - - const CopyButton = ({ text }: { text: string }) => { - const [isCopied, setIsCopied] = useState(false); - - return ( - - ); - }; - - type Changelog = { - id: string; - images: string[]; - content: string; - title: string; - }; - - const changelogs: Changelog[] = [ - { - id: "1", - title: "December Updates", - images: [ - "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-search-groups.png", - "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-grok2.png", - "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-track-flights.png", - "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-reterive-back.png", - ], - content:` -## **Search Groups** - -You can now switch between different search groups like Default, Youtube, X.com, Academic, and more to get results from different sources. - -## **Added Grok 2.0 Vision model** - -xAI's Grok 2.0 Vision model is now available on the platform. - -## **Default Search Engine** - -You can now set MiniPerplx as the default search engine for the platform. Follow the instructions in the [Readme](https://github.com/zaidmukaddam/miniperplx/blob/main/README.md) to set it as default. - -## **Track Flights** - -You can now track flights in real-time with the new Flight Tracker tool. - -## **PWA support** - -The platform now supports Progressive Web App(PWA) features. You can now install the platform as an app on your device. - -## **Reterive tool is back!** - -You can now use the Retrieve tool to get information from the a specific URL. - -## **Trending Queries** - -You can now view the trending queries on the platform. -` - } - ]; - - - const ChangeLogs = ({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) => { - const isMobile = useMediaQuery("(max-width: 768px)"); - - const ChangelogContent = () => ( - <> - {/* Fixed Header */} -
    -

    - What's new -

    -
    - -
    - {changelogs.map((changelog) => ( -
    - {/* Carousel */} - - - {changelog.images.map((image, index) => ( - - {changelog.title} - - ))} - - - - {/* Content Section */} -
    -

    - {changelog.title} -

    -
    - ( -

    - ), - p: ({ node, className, ...props }) => ( -

    - ), - a: ({ node, className, ...props }) => ( - - ), - }} - className="text-sm text-left pr-2" - > - {changelog.content} - -

    -
    -
    - ))} -
    - - ); - - if (isMobile) { - return ( - - - - - - - - ); - } - - return ( - - - - - - ); - }; - - - - const TranslationTool: React.FC<{ toolInvocation: ToolInvocation; result: any }> = ({ toolInvocation, result }) => { - const [isPlaying, setIsPlaying] = useState(false); - const [audioUrl, setAudioUrl] = useState(null); - const [isGeneratingAudio, setIsGeneratingAudio] = useState(false); - const audioRef = useRef(null); - const canvasRef = useRef(null); - const waveRef = useRef(null); - - useEffect(() => { - return () => { - if (audioRef.current) { - audioRef.current.pause(); - audioRef.current.src = ''; - } - }; - }, []); - - useEffect(() => { - if (audioUrl && audioRef.current && canvasRef.current) { - waveRef.current = new Wave(audioRef.current, canvasRef.current); - waveRef.current.addAnimation(new waveRef.current.animations.Lines({ - lineColor: "rgb(203, 113, 93)", - lineWidth: 2, - mirroredY: true, - count: 100, - })); - } - }, [audioUrl]); - - const handlePlayPause = async () => { - if (!audioUrl && !isGeneratingAudio) { - setIsGeneratingAudio(true); - try { - const { audio } = await generateSpeech(result.translatedText, 'alloy'); - setAudioUrl(audio); - setIsGeneratingAudio(false); - } catch (error) { - console.error("Error generating speech:", error); - setIsGeneratingAudio(false); - } - } else if (audioRef.current) { - if (isPlaying) { - audioRef.current.pause(); - } else { - audioRef.current.play(); - } - setIsPlaying(!isPlaying); - } - }; - - const handleReset = () => { - if (audioRef.current) { - audioRef.current.pause(); - audioRef.current.currentTime = 0; - setIsPlaying(false); - } - }; - - if (!result) { - return ( - - -
    -
    -
    -
    -
    -
    - ); - } - - return ( - - -
    -
    - -
    -
    -
    - -
    -
    - The phrase {toolInvocation.args.text} translates from {result.detectedLanguage} to {toolInvocation.args.to} as {result.translatedText} in {toolInvocation.args.to}. -
    -
    -
    -
    - {audioUrl && ( -
    - ); - }; - - - - interface TableData { - title: string; - content: string; - } - - interface ResultsOverviewProps { - result: { - image: string; - title: string; - description: string; - table_data: TableData[]; - }; - } - - const ResultsOverview: React.FC = React.memo(({ result }) => { - const [showAll, setShowAll] = useState(false); - - const visibleData = useMemo(() => { - return showAll ? result.table_data : result.table_data.slice(0, 3); - }, [showAll, result.table_data]); - - return ( - - -
    - {result.image && ( -
    - {result.title} -
    - )} -
    - {result.title} -

    {result.description}

    -
    -
    -
    - - - - {visibleData.map((item, index) => ( - - {item.title} - {item.content} - - ))} - -
    - {result.table_data.length > 3 && ( - - )} -
    -
    - ); - }); - - ResultsOverview.displayName = 'ResultsOverview'; - - const renderToolInvocation = useCallback( - (toolInvocation: ToolInvocation, index: number) => { - const args = JSON.parse(JSON.stringify(toolInvocation.args)); - const result = 'result' in toolInvocation ? JSON.parse(JSON.stringify(toolInvocation.result)) : null; - - // Find place results - if (toolInvocation.toolName === 'find_place') { - if (!result) { - return ; - } - - const { features } = result; - if (!features || features.length === 0) return null; - - return ( - - {/* Map Container */} -
    -
    - - {features.length} Locations Found - -
    - - ({ - name: feature.name, - location: { - lat: feature.geometry.coordinates[1], - lng: feature.geometry.coordinates[0], - }, - vicinity: feature.formatted_address, - }))} - zoom={features.length > 1 ? 12 : 15} - /> -
    - - {/* Place Details Footer */} -
    - {features.map((place: any, index: any) => { - const isGoogleResult = place.source === 'google'; - - return ( -
    -
    -
    - {place.feature_type === 'street_address' || place.feature_type === 'street' ? ( - - ) : place.feature_type === 'locality' ? ( - - ) : ( - - )} -
    - -
    -

    - {place.name} -

    - {place.formatted_address && ( -

    - {place.formatted_address} -

    - )} - - {place.feature_type.replace(/_/g, ' ')} - -
    - -
    - - - - - - Copy Coordinates - - - - - - - - - View in Maps - - -
    -
    -
    - ); - })} -
    -
    - ); - } - - // Shopping search results - if (toolInvocation.toolName === 'shopping_search') { - if (!result) { - return ; - } - - return ( - - -
    -
    - -
    -
    - Shopping Results -

    Scroll to see more products

    -
    -
    -
    - -
    - {result.map((product: ShoppingProduct) => ( - - -
    - {product.title} - {product.rating && ( -
    - - {product.rating} -
    - )} -
    - - - {product.title} - -

    - {product.price} -

    - -
    -
    -
    - ))} -
    -
    -
    - ); - } - - if (toolInvocation.toolName === 'x_search') { - if (!result) { - return ; - } - - const PREVIEW_COUNT = 3; - - // Shared content component - const FullTweetList = () => ( -
    - {result.map((post: XResult, index: number) => ( - - - - ))} -
    - ); - - return ( - - -
    -
    - -
    -
    - Latest from X -

    - {result.length} tweets found -

    -
    -
    -
    -
    -
    -
    - {result.slice(0, PREVIEW_COUNT).map((post: XResult, index: number) => ( - - - - ))} -
    -
    - - {/* Gradient overlay */} -
    - - {/* Show More Buttons - Desktop Sheet */} -
    - {/* Desktop Sheet */} -
    - - - - - - - All Tweets - - - - -
    - - {/* Mobile Drawer */} -
    - - - - - - - All Tweets - -
    - -
    -
    -
    -
    -
    -
    - - ); - } - - if (toolInvocation.toolName === 'youtube_search') { - if (!result) { - return ; - } - - const youtubeResult = result as YouTubeSearchResponse; - - return ( - - - -
    -
    - -
    -
    -

    - YouTube Results -

    -
    - - {youtubeResult.results.length} videos - -
    -
    -
    -
    - - -
    - {youtubeResult.results.map((video, index) => ( - - ))} -
    -
    -
    -
    - ); - } - - // Academic search results continued... - if (toolInvocation.toolName === 'academic_search') { - if (!result) { - return ; - } - - return ( - - -
    -
    - -
    -
    - Academic Papers -

    Found {result.results.length} papers

    -
    -
    -
    -
    -
    - {result.results.map((paper: AcademicResult, index: number) => ( - -
    - {/* Background with gradient border */} -
    - - {/* Main content container */} -
    - {/* Title */} -

    - {paper.title} -

    - - {/* Authors with better overflow handling */} - {paper.author && ( -
    -
    - - - {paper.author.split(';') - .slice(0, 2) // Take first two authors - .join(', ') + - (paper.author.split(';').length > 2 ? ' et al.' : '') - } - -
    -
    - )} - - {/* Date if available */} - {paper.publishedDate && ( -
    -
    - - {new Date(paper.publishedDate).toLocaleDateString()} -
    -
    - )} - - {/* Summary with gradient border */} -
    -
    -

    - {paper.summary} -

    -
    - - {/* Actions */} -
    - - - {paper.url.includes('arxiv.org') && ( - - )} -
    -
    -
    - - ))} -
    -
    - - ); - } - - if (toolInvocation.toolName === 'nearby_search') { - if (!result) { - return ( -
    -
    - - - Finding nearby {args.type}... - -
    - - {[0, 1, 2].map((index) => ( - - ))} - -
    - ); - } - - console.log(result); - - return ( -
    - -
    - ); - } - - if (toolInvocation.toolName === 'text_search') { - if (!result) { - return ( -
    -
    - - Searching places... -
    - - {[0, 1, 2].map((index) => ( - - ))} - -
    - ); - } - - const centerLocation = result.results[0]?.geometry?.location; - return ( - ({ - name: place.name, - location: place.geometry.location, - vicinity: place.formatted_address - }))} - /> - ); - } - - if (toolInvocation.toolName === 'get_weather_data') { - if (!result) { - return ( -
    -
    - - Fetching weather data... -
    -
    - {[0, 1, 2].map((index) => ( - - ))} -
    -
    - ); - } - return ; - } - - if (toolInvocation.toolName === 'programming') { - return ( - - - -
    -
    - -

    Programming

    -
    - {!result ? ( - - - Executing - - ) : ( - - - Executed - - )} -
    -
    - -
    -
    - {args.icon === 'stock' && } - {args.icon === 'default' && } - {args.icon === 'date' && } - {args.icon === 'calculation' && } - {args.title} -
    - - - - Code - - - Output - - {result?.images && result.images.length > 0 && ( - - Images - - )} - {result?.chart && ( - - Visualization - - )} - - -
    - - {args.code} - - -
    - -
    -
    -
    - -
    - {result ? ( - <> -
    -                                                            {result.message}
    -                                                        
    -
    - -
    - - ) : ( -
    -
    - - Executing code... -
    -
    - )} -
    -
    - {result?.images && result.images.length > 0 && ( - -
    - {result.images.map((img: { format: string, url: string }, imgIndex: number) => ( -
    -
    -

    Image {imgIndex + 1}

    - {img.url && img.url.trim() !== '' && ( - - )} -
    -
    - {img.url && img.url.trim() !== '' ? ( - {`Generated - ) : ( -
    - Image upload failed or URL is empty -
    - )} -
    -
    - ))} -
    -
    - )} - {result?.chart && ( - - - - )} -
    -
    -
    -
    -
    - ); - } - - if (toolInvocation.toolName === 'web_search') { - return ( -
    - -
    - ); - } - - if (toolInvocation.toolName === 'retrieve') { - if (!result) { - return ( -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - ); - } - - return ( -
    -
    -
    -
    -
    - -
    -
    -

    - {result.results[0].title} -

    -

    - {result.results[0].description} -

    -
    - - {result.results[0].language || 'Unknown'} - - - - View source - -
    -
    -
    -
    - -
    -
    - -
    - - View content -
    - -
    -
    -
    - {result.results[0].content} -
    -
    -
    -
    -
    - ); - } - if (toolInvocation.toolName === 'text_translate') { - return ; - } - - if (toolInvocation.toolName === 'results_overview') { - if (!result) { - return ( -
    -
    - - Generating overview... -
    -
    - ); - } - - return ; - } - - if (toolInvocation.toolName === 'track_flight') { - if (!result) { - return ( -
    -
    - - Tracking flight... -
    -
    - {[0, 1, 2].map((index) => ( - - ))} -
    -
    - ); - } - - if (result.error) { - return ( -
    - Error tracking flight: {result.error} -
    - ); - } - - return ( -
    - -
    - ); - } - - return null; - }, - [ResultsOverview, theme] - ); - - interface MarkdownRendererProps { - content: string; - } - - interface CitationLink { - text: string; - link: string; - } - - interface LinkMetadata { - title: string; - description: string; - } - - const isValidUrl = (str: string) => { - try { - new URL(str); - return true; - } catch { - return false; - } - }; - - const MarkdownRenderer: React.FC = ({ content }) => { - const [metadataCache, setMetadataCache] = useState>({}); - - const citationLinks = useMemo(() => { - return Array.from(content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)).map(([_, text, link]) => ({ - text, - link, - })); - }, [content]); - - const inlineMathRegex = /\$([^\$]+)\$/g; - const blockMathRegex = /\$\$([^\$]+)\$\$/g; - - const isValidLatex = (text: string): boolean => { - // Basic validation - checks for balanced delimiters - return !(text.includes('\\') && !text.match(/\\[a-zA-Z{}\[\]]+/)); - } - - const renderLatexString = (text: string) => { - let parts = []; - let lastIndex = 0; - let match; - - // Try to match inline math first ($...$) - while ((match = /\$([^\$]+)\$/g.exec(text.slice(lastIndex))) !== null) { - const mathText = match[1]; - const fullMatch = match[0]; - const matchIndex = lastIndex + match.index; - - // Add text before math - if (matchIndex > lastIndex) { - parts.push(text.slice(lastIndex, matchIndex)); - } - - // Only render as LaTeX if valid - if (isValidLatex(mathText)) { - parts.push({fullMatch}); - } else { - parts.push(fullMatch); - } - - lastIndex = matchIndex + fullMatch.length; - } - - // Add remaining text - if (lastIndex < text.length) { - parts.push(text.slice(lastIndex)); - } - - return parts.length > 0 ? parts : text; - }; - - const fetchMetadataWithCache = useCallback(async (url: string) => { - if (metadataCache[url]) { - return metadataCache[url]; - } - - const metadata = await fetchMetadata(url); - if (metadata) { - setMetadataCache(prev => ({ ...prev, [url]: metadata })); - } - return metadata; - }, [metadataCache]); - - interface CodeBlockProps { - language: string | undefined; - children: string; - } - - const CodeBlock = React.memo(({ language, children }: CodeBlockProps) => { - const [isCopied, setIsCopied] = useState(false); - const { theme } = useTheme(); - - const handleCopy = useCallback(async () => { - await navigator.clipboard.writeText(children); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - }, [children]); - - return ( -
    -
    -
    -
    - {language || 'text'} -
    - -
    - -
    - - {children} - -
    -
    -
    - ); - }, (prevProps, nextProps) => - prevProps.children === nextProps.children && - prevProps.language === nextProps.language - ); - - CodeBlock.displayName = 'CodeBlock'; - - const LinkPreview = ({ href }: { href: string }) => { - const [metadata, setMetadata] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - React.useEffect(() => { - setIsLoading(true); - fetchMetadataWithCache(href).then((data) => { - setMetadata(data); - setIsLoading(false); - }); - }, [href]); - - if (isLoading) { - return ( -
    - -
    - ); - } - - const domain = new URL(href).hostname; - - return ( -
    -
    - Favicon - {domain} -
    -
    -

    - {metadata?.title || "Untitled"} -

    - {metadata?.description && ( -

    - {metadata.description} -

    - )} -
    -
    - ); - }; - - const renderHoverCard = (href: string, text: React.ReactNode, isCitation: boolean = false) => { - return ( - - - - {text} - - - - - - - ); - }; - - const renderer: Partial = { - text(text: string) { - if (!text.includes('$')) return text; - - return ( - - {text} - - ); - }, - paragraph(children) { - if (typeof children === 'string' && children.includes('$')) { - return ( -

    - - {children} - -

    - ); - } - return

    {children}

    ; - }, - code(children, language) { - return {String(children)}; - }, - link(href, text) { - const citationIndex = citationLinks.findIndex(link => link.link === href); - if (citationIndex !== -1) { - return ( - - {renderHoverCard(href, citationIndex + 1, true)} - - ); - } - return isValidUrl(href) ? renderHoverCard(href, text) :
    {text}; - }, - heading(children, level) { - const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; - const className = `text-${4 - level}xl font-bold my-4 text-neutral-800 dark:text-neutral-100`; - return {children}; - }, - list(children, ordered) { - const ListTag = ordered ? 'ol' : 'ul'; - return {children}; - }, - listItem(children) { - return
  • {children}
  • ; - }, - blockquote(children) { - return
    {children}
    ; - }, - }; - - return ( -
    - {content} -
    - ); - }; - - - - const lastUserMessageIndex = useMemo(() => { - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === 'user') { - return i; - } - } - return -1; - }, [messages]); - - useEffect(() => { - const handleScroll = () => { - const userScrolled = window.innerHeight + window.scrollY < document.body.offsetHeight; - if (!userScrolled && bottomRef.current && (messages.length > 0 || suggestedQuestions.length > 0)) { - bottomRef.current.scrollIntoView({ behavior: "smooth" }); - } - }; - - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, [messages, suggestedQuestions]); - - const handleExampleClick = async (card: TrendingQuery) => { - const exampleText = card.text; - track("search example", { query: exampleText }); - lastSubmittedQueryRef.current = exampleText; - setHasSubmitted(true); - setSuggestedQuestions([]); - await append({ - content: exampleText.trim(), - role: 'user', - }); - }; - - const handleSuggestedQuestionClick = useCallback(async (question: string) => { - setHasSubmitted(true); - setSuggestedQuestions([]); - - await append({ - content: question.trim(), - role: 'user' - }); - }, [append]); - - const handleMessageEdit = useCallback((index: number) => { - setIsEditingMessage(true); - setEditingMessageIndex(index); - setInput(messages[index].content); - }, [messages, setInput]); - - const handleMessageUpdate = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (input.trim()) { - const updatedMessages = [...messages]; - updatedMessages[editingMessageIndex] = { ...updatedMessages[editingMessageIndex], content: input.trim() }; - setMessages(updatedMessages); - setIsEditingMessage(false); - setEditingMessageIndex(-1); - handleSubmit(e); - } else { - toast.error("Please enter a valid message."); - } - }, [input, messages, editingMessageIndex, setMessages, handleSubmit]); - - interface NavbarProps { } - - const Navbar: React.FC = () => { - return ( -
    - - - -
    - - - - - - - -

    Sponsor this project on GitHub

    -
    -
    -
    - -
    -
    - ); - }; - - const SuggestionCards: React.FC<{ - selectedModel: string; - trendingQueries: TrendingQuery[]; - }> = ({ selectedModel, trendingQueries }) => { - const [isLoading, setIsLoading] = useState(true); - const scrollRef = useRef(null); - const [isPaused, setIsPaused] = useState(false); - const scrollIntervalRef = useRef(); - const [isTouchDevice, setIsTouchDevice] = useState(false); - - useEffect(() => { - setIsLoading(false); - setIsTouchDevice('ontouchstart' in window); - }, [trendingQueries]); - - useEffect(() => { - if (isTouchDevice) return; // Disable auto-scroll on touch devices - - const startScrolling = () => { - if (!scrollRef.current || isPaused) return; - scrollRef.current.scrollLeft += 1; // Reduced speed - - // Reset scroll when reaching end - if (scrollRef.current.scrollLeft >= - (scrollRef.current.scrollWidth - scrollRef.current.clientWidth)) { - scrollRef.current.scrollLeft = 0; - } - }; - - scrollIntervalRef.current = setInterval(startScrolling, 30); - - return () => { - if (scrollIntervalRef.current) { - clearInterval(scrollIntervalRef.current); - } - }; - }, [isPaused, isTouchDevice]); - - if (isLoading || trendingQueries.length === 0) { - return ( -
    - {/* Overlay with Loading Text */} -
    -
    -
    - - Loading trending queries -
    -
    -
    - - {/* Background Cards */} -
    - {[1, 2, 3].map((_, index) => ( -
    -
    -
    -
    -
    -
    - ))} -
    -
    - ); - } - - const getIconForCategory = (category: string) => { - const iconMap = { - trending: , - community: , - science: , - tech: , - travel: , - politics: , - health: , - sports: , - finance: , - football: , - }; - return iconMap[category as keyof typeof iconMap] || ; - }; - - return ( -
    - {/* Gradient Fades */} -
    -
    - -
    !isTouchDevice && setIsPaused(true)} - onMouseLeave={() => !isTouchDevice && setIsPaused(false)} - onTouchStart={() => setIsPaused(true)} - onTouchEnd={() => setIsPaused(false)} - > - {Array(20).fill(trendingQueries).flat().map((query, index) => ( - - ))} -
    -
    - ); - }; - - const handleModelChange = useCallback((newModel: string) => { - setSelectedModel(newModel); - setSuggestedQuestions([]); - reload({ body: { model: newModel } }); - }, [reload]); - - const resetSuggestedQuestions = useCallback(() => { - setSuggestedQuestions([]); - }, []); - - - const memoizedMessages = useMemo(() => messages, [messages]); - - const memoizedSuggestionCards = useMemo(() => ( - - ), [selectedModel, trendingQueries]); - - return ( -
    - - -
    - {!hasSubmitted && ( -
    - setOpenChangelog(true)} - className="cursor-pointer gap-1 mb-2 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200" - variant="secondary" - > - What's new - -

    MiniPerplx

    -

    - In search for minimalism and simplicity -

    -
    - )} - - {!hasSubmitted && ( - - - {memoizedSuggestionCards} - - )} - - - -
    - {memoizedMessages.map((message, index) => ( -
    - {message.role === 'user' && ( - - -
    - {isEditingMessage && editingMessageIndex === index ? ( -
    - setInput(e.target.value)} - className="flex-grow bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100" - /> - - -
    - ) : ( -
    -

    - {message.content} -

    -
    - {message.experimental_attachments?.map((attachment, attachmentIndex) => ( -
    - {attachment.contentType!.startsWith('image/') && ( - {attachment.name - )} -
    - ))} -
    -
    - )} -
    - - {!isEditingMessage && index === lastUserMessageIndex && ( -
    - -
    - )} -
    - )} - {message.role === 'assistant' && message.content !== null && !message.toolInvocations && ( -
    -
    -
    - -

    Answer

    -
    -
    - -
    -
    -
    - -
    -
    - )} - {message.toolInvocations?.map((toolInvocation: ToolInvocation, toolIndex: number) => ( -
    - {renderToolInvocation(toolInvocation, toolIndex)} -
    - ))} -
    - ))} - - {suggestedQuestions.length > 0 && ( - -
    - -

    Suggested questions

    -
    -
    - {suggestedQuestions.map((question, index) => ( - - ))} -
    -
    - )} -
    -
    -
    - - - {hasSubmitted && ( - - - - )} - - -
    - ); -} - -const LoadingFallback = () => ( -
    -
    -

    - MiniPerplx -

    -

    - Loading your minimalist AI experience... -

    - -
    -
    -); - -const Home = () => { - return ( - }> - - - - ); -}; - -export default Home; \ No newline at end of file +export default async function NewPage() { + redirect('/') +} \ No newline at end of file diff --git a/components/movie-info.tsx b/components/movie-info.tsx new file mode 100644 index 0000000..ec2f3f4 --- /dev/null +++ b/components/movie-info.tsx @@ -0,0 +1,236 @@ +/* eslint-disable @next/next/no-img-element */ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Film, Tv, Star, Calendar, Clock, Users } from 'lucide-react'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Drawer, DrawerContent } from "@/components/ui/drawer"; +import Image from 'next/image'; + +interface MediaDetails { + id: number; + media_type: 'movie' | 'tv'; + title?: string; + name?: string; + overview: string; + poster_path: string | null; + backdrop_path: string | null; + vote_average: number; + vote_count: number; + release_date?: string; + first_air_date?: string; + runtime?: number; + episode_run_time?: number[]; + genres: Array<{ id: number; name: string }>; + credits: { + cast: Array<{ + id: number; + name: string; + character: string; + profile_path: string | null; + }>; + }; + origin_country?: string[]; + original_language: string; + production_companies?: Array<{ + id: number; + name: string; + logo_path: string | null; + }>; +} + +interface TMDBResultProps { + result: { + result: MediaDetails | null; + }; +} + +const TMDBResult = ({ result }: TMDBResultProps) => { + const [showDetails, setShowDetails] = useState(false); + const isMobile = useMediaQuery("(max-width: 768px)"); + + if (!result.result) return null; + const media = result.result; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + }; + + const formatRuntime = (minutes: number) => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${hours}h ${mins}m`; + }; + + const DetailContent = () => ( +
    +
    + {media.backdrop_path ? ( + {media.title + ) : ( +
    + )} +
    +
    +

    + {media.title || media.name} +

    +
    +
    + + {media.vote_average.toFixed(1)} +
    + {(media.release_date || media.first_air_date) && ( +
    + + {formatDate(media.release_date || media.first_air_date || '')} +
    + )} + {(media.runtime || media.episode_run_time?.[0]) && ( +
    + + {formatRuntime(media.runtime || media.episode_run_time?.[0] || 0)} +
    + )} +
    +
    +
    + +
    +
    +
    + {media.genres.map(genre => ( + + {genre.name} + + ))} +
    + +

    + {media.overview} +

    + + {media.credits?.cast && media.credits.cast.length > 0 && ( +
    +

    Cast

    +
    + {media.credits.cast.slice(0, media.credits.cast.length).map(person => ( +
    + {person.profile_path ? ( + {person.name} + ) : ( +
    + +
    + )} +
    +

    {person.name}

    +

    {person.character}

    +
    +
    + ))} +
    +
    + )} +
    +
    +
    + ); + + return ( +
    + setShowDetails(true)} + > +
    +
    + {media.poster_path ? ( + {media.title + ) : ( +
    + {media.media_type === 'movie' ? ( + + ) : ( + + )} +
    + )} +
    + +
    +
    +

    + {media.title || media.name} +

    +
    + {media.media_type} +
    + + {media.vote_average.toFixed(1)} +
    +
    +
    + +

    + {media.overview} +

    + + {media.credits?.cast && ( +

    + Cast: + {media.credits.cast.slice(0, 3).map(person => person.name).join(', ')} +

    + )} +
    +
    +
    + + {isMobile ? ( + + + + + + ) : ( + + + + + + )} +
    + ); +}; + +export default TMDBResult; \ No newline at end of file diff --git a/components/shopping-cards.tsx b/components/shopping-cards.tsx deleted file mode 100644 index 9d01ab2..0000000 --- a/components/shopping-cards.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"use client"; -/* eslint-disable @next/next/no-img-element */ -import { motion, PanInfo, useMotionValue, useTransform } from "framer-motion"; -import { Badge } from "./ui/badge"; -import { Heart, Star, X } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; -import { Button } from "./ui/button"; -import { Card, CardContent } from "./ui/card"; - - -interface ShoppingProduct { - id: string | number; - title: string; - price: string; - originalPrice?: string; - currency: string; - image: string; - link: string; - source: string; - rating?: string | null; - reviewCount?: string | null; - delivery: string; -} - -interface CardRotateProps { - children: React.ReactNode; - onSendToBack: () => void; - onSwipe?: (direction: 'left' | 'right') => void; -} - -function CardRotate({ children, onSendToBack, onSwipe }: CardRotateProps) { - const x = useMotionValue(0); - const y = useMotionValue(0); - // Reduced rotation values for more subtle effect - const rotateX = useTransform(y, [-100, 100], [15, -15]); - const rotateY = useTransform(x, [-100, 100], [-15, 15]); - - function handleDragEnd(_: any, info: PanInfo) { - const threshold = 100; - if (Math.abs(info.offset.x) > threshold) { - onSendToBack(); - if (onSwipe) { - onSwipe(info.offset.x > 0 ? 'right' : 'left'); - } - } else { - x.set(0); - y.set(0); - } - } - - return ( - - {children} - - ); -} - -const ProductCard = ({ product }: { product: ShoppingProduct }) => { - const formattedPrice = parseFloat(product.price).toFixed(2); - const formattedOriginalPrice = product.originalPrice ? parseFloat(product.originalPrice).toFixed(2) : null; - const discount = formattedOriginalPrice ? - Math.round(((parseFloat(formattedOriginalPrice) - parseFloat(formattedPrice)) / parseFloat(formattedOriginalPrice)) * 100) : null; - - return ( -
    -
    - {product.title} - {discount && discount > 0 && ( - - {discount}% OFF - - )} -
    -
    -
    -

    - {product.title} -

    -
    - - ${formattedPrice} - - {formattedOriginalPrice && ( - - ${formattedOriginalPrice} - - )} -
    -
    -
    -
    - {product.rating && ( - <> - - - {product.rating} {product.reviewCount && `(${product.reviewCount})`} - - - )} -
    - - {product.source} - -
    -
    -
    - ); -}; - -export const SwipeableProductStack = ({ products }: { products: ShoppingProduct[] }) => { - const [cards, setCards] = useState(products); - const [savedProducts, setSavedProducts] = useState([]); - - const sendToBack = (id: string | number, direction?: 'left' | 'right') => { - setCards((prev) => { - const newCards = [...prev]; - const index = newCards.findIndex((card) => card.id === id); - const [card] = newCards.splice(index, 1); - - if (direction === 'right') { - setSavedProducts(prev => [...prev, card]); - toast.success('Product saved!'); - } - - newCards.unshift(card); - return newCards; - }); - }; - - return ( -
    -
    - {cards.map((product, index) => ( - sendToBack(product.id)} - onSwipe={(direction) => sendToBack(product.id, direction)} - > - - - - - ))} -
    - -
    - - -
    - - {savedProducts.length > 0 && ( -
    -

    - Saved Products ({savedProducts.length}) -

    -
    - {savedProducts.map((product) => ( - - - {product.title} -
    {product.title}
    -
    - ${parseFloat(product.price).toFixed(2)} -
    - -
    -
    - ))} -
    -
    - )} -
    - ); -}; \ No newline at end of file diff --git a/components/trending-tv-movies-results.tsx b/components/trending-tv-movies-results.tsx new file mode 100644 index 0000000..52fee07 --- /dev/null +++ b/components/trending-tv-movies-results.tsx @@ -0,0 +1,309 @@ +/* eslint-disable @next/next/no-img-element */ +import React, { useMemo, useState } from 'react'; +import { motion } from 'framer-motion'; +import { Film, Tv, Star, Calendar, ChevronRight, X } from 'lucide-react'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Drawer, DrawerContent } from '@/components/ui/drawer'; + +interface TrendingItem { + id: number; + title?: string; + name?: string; + overview: string; + poster_path: string | null; + backdrop_path: string | null; + vote_average: number; + release_date?: string; + first_air_date?: string; + genre_ids: number[]; + popularity: number; +} + +interface TrendingResultsProps { + result: { + results: TrendingItem[]; + }; + type: 'movie' | 'tv'; +} + +const TrendingResults = ({ result, type }: TrendingResultsProps) => { + const [selectedItem, setSelectedItem] = useState(null); + const [showAll, setShowAll] = useState(false); + const isMobile = useMediaQuery('(max-width: 768px)'); + + const displayedResults = useMemo(() => { + return showAll ? result.results : result.results.slice(0, isMobile ? 4 : 10); + }, [result.results, showAll, isMobile]); + + const genreMap: Record = { + 28: 'Action', + 12: 'Adventure', + 16: 'Animation', + 35: 'Comedy', + 80: 'Crime', + 99: 'Documentary', + 18: 'Drama', + 10751: 'Family', + 14: 'Fantasy', + 36: 'History', + 27: 'Horror', + 10402: 'Music', + 9648: 'Mystery', + 10749: 'Romance', + 878: 'Sci-Fi', + 53: 'Thriller', + 10752: 'War', + 37: 'Western', + 10759: 'Action & Adventure', + 10765: 'Sci-Fi & Fantasy', + 10768: 'War & Politics', + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + }); + }; + + const DetailView = () => { + if (!selectedItem) return null; + + const content = ( +
    +
    + {selectedItem.backdrop_path ? ( + <> + {selectedItem.title +
    + + ) : ( +
    + )} +
    +

    + {selectedItem.title || selectedItem.name} +

    +
    +
    + + {selectedItem.vote_average.toFixed(1)} +
    + {(selectedItem.release_date || selectedItem.first_air_date) && ( +
    + + {formatDate(selectedItem.release_date || selectedItem.first_air_date || '')} +
    + )} +
    +
    +
    + +
    +
    + {selectedItem.genre_ids.map((genreId) => ( + + {genreMap[genreId]} + + ))} +
    + +

    {selectedItem.overview}

    +
    +
    + ); + + if (isMobile) { + return ( + setSelectedItem(null)}> + + {content} + + + ); + } + + return ( + setSelectedItem(null)}> + {content} + + ); + }; + + return ( +
    +
    +
    +
    + {type === 'movie' ? ( + + ) : ( + + )} +
    +
    +

    + Trending {type === 'movie' ? 'Movies' : 'Shows'} +

    +

    Top picks for today

    +
    +
    + +
    + +
    + {displayedResults.map((item, index) => ( + setSelectedItem(item)} + > +
    + {item.poster_path ? ( + {item.title + ) : ( +
    + {type === 'movie' ? ( + + ) : ( + + )} +
    + )} +
    +
    +
    + + + {item.vote_average.toFixed(1)} + +
    +

    + {item.title || item.name} +

    +

    + {formatDate(item.release_date || item.first_air_date || '')} +

    +
    +
    +
    +
    + ))} +
    + + {isMobile && showAll && ( + setShowAll(false)}> + +
    +
    +

    + All Trending {type === 'movie' ? 'Movies' : 'Shows'} +

    + +
    +
    +
    + {result.results.map((item, index) => ( + { + setSelectedItem(item); + setShowAll(false); + }} + > +
    + {item.poster_path ? ( + {item.title + ) : ( +
    + {type === 'movie' ? ( + + ) : ( + + )} +
    + )} +
    +
    +
    + + + {item.vote_average.toFixed(1)} + +
    +

    + {item.title || item.name} +

    +

    + {formatDate(item.release_date || item.first_air_date || '')} +

    +
    +
    +
    +
    + ))} +
    +
    +
    +
    +
    + )} + + +
    + ); +}; + +export default TrendingResults; \ No newline at end of file diff --git a/components/ui/form-component.tsx b/components/ui/form-component.tsx index 2e09057..2c1ef6c 100644 --- a/components/ui/form-component.tsx +++ b/components/ui/form-component.tsx @@ -1,15 +1,14 @@ /* eslint-disable @next/next/no-img-element */ // /components/ui/form-component.tsx -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useState, useRef, useCallback } from 'react'; import { motion } from 'framer-motion'; import { ChatRequestOptions, CreateMessage, Message } from 'ai'; -import { track } from '@vercel/analytics'; import { toast } from 'sonner'; import { Button } from '../ui/button'; import { Textarea } from '../ui/textarea'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'; import useWindowSize from '@/hooks/use-window-size'; -import { Sparkles, X, Zap, Cpu, Search, ChevronDown, Check, Atom } from 'lucide-react'; +import { X, Zap, ChevronDown, ScanEye } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -18,7 +17,6 @@ import { } from "@/components/ui/dropdown-menu" import { cn, SearchGroup, SearchGroupId, searchGroups } from '@/lib/utils'; import { useMediaQuery } from '@/hooks/use-media-query'; -import { XLogo } from '@phosphor-icons/react'; interface ModelSwitcherProps { selectedModel: string; @@ -27,39 +25,23 @@ interface ModelSwitcherProps { } const models = [ - { value: "azure:gpt4o-mini", label: "GPT-4o Mini", icon: Zap, description: "God speed, good quality", color: "emerald", vision: true }, - { value: "anthropic:claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku", icon: Sparkles, description: "Good quality, high speed", color: "orange", vision: false }, - { value: "xai:grok-2-vision-1212", label: "Grok 2.0 Vision", icon: XLogo, description: "Good quality, normal speed", color: "glossyblack", vision: true }, - { value: "anthropic:claude-3-5-sonnet-latest", label: "Claude 3.5 Sonnet (New)", icon: Sparkles, description: "High quality, good speed", color: "indigo", vision: true }, - { value: "azure:gpt-4o", label: "GPT-4o", icon: Cpu, description: "Higher quality, normal speed", color: "blue", vision: true }, + { value: "grok-2-1212", label: "Grok 2.0", icon: Zap, description: "Most intelligent text model", color: "glossyblack", vision: false }, + { value: "grok-2-vision-1212", icon: ScanEye, label: "Grok 2.0 Vision", description: "Most intelligent vision model", color: "offgray", vision: true }, ]; - const getColorClasses = (color: string, isSelected: boolean = false) => { const baseClasses = "transition-colors duration-200"; const selectedClasses = isSelected ? "!bg-opacity-90 dark:!bg-opacity-90" : ""; switch (color) { - case 'emerald': - return isSelected - ? `${baseClasses} ${selectedClasses} !bg-emerald-500 dark:!bg-emerald-600 !text-white hover:!bg-emerald-600 dark:hover:!bg-emerald-700` - : `${baseClasses} !text-emerald-700 dark:!text-emerald-300 hover:!bg-emerald-200 dark:hover:!bg-emerald-800/70`; - case 'indigo': - return isSelected - ? `${baseClasses} ${selectedClasses} !bg-indigo-500 dark:!bg-indigo-600 !text-white hover:!bg-indigo-600 dark:hover:!bg-indigo-700` - : `${baseClasses} !text-indigo-700 dark:!text-indigo-300 hover:!bg-indigo-200 dark:hover:!bg-indigo-800/70`; - case 'blue': - return isSelected - ? `${baseClasses} ${selectedClasses} !bg-blue-500 dark:!bg-blue-600 !text-white hover:!bg-blue-600 dark:hover:!bg-blue-700` - : `${baseClasses} !text-blue-700 dark:!text-blue-300 hover:!bg-blue-200 dark:hover:!bg-blue-800/70`; - case 'orange': - return isSelected - ? `${baseClasses} ${selectedClasses} !bg-orange-500 dark:!bg-orange-600 !text-white hover:!bg-orange-600 dark:hover:!bg-orange-700` - : `${baseClasses} !text-orange-700 dark:!text-orange-300 hover:!bg-orange-200 dark:hover:!bg-orange-800/70`; case 'glossyblack': return isSelected - ? `${baseClasses} ${selectedClasses} bg-gradient-to-br from-black to-neutral-800 !text-white shadow-inner` - : `${baseClasses} !text-black dark:!text-white hover:!bg-black/10 dark:hover:!bg-black/40`; + ? `${baseClasses} ${selectedClasses} !bg-[#2D2D2D] dark:!bg-[#333333] !text-white hover:!text-white hover:!bg-[#1a1a1a] dark:hover:!bg-[#444444]` + : `${baseClasses} !text-[#4A4A4A] dark:!text-[#F0F0F0] hover:!text-white hover:!bg-[#1a1a1a] dark:hover:!bg-[#333333]`; + case 'offgray': + return isSelected + ? `${baseClasses} ${selectedClasses} !bg-[#4B5457] dark:!bg-[#707677] !text-white hover:!text-white hover:!bg-[#707677] dark:hover:!bg-[#4B5457]` + : `${baseClasses} !text-[#5C6366] dark:!text-[#D1D5D6] hover:!text-white hover:!bg-[#707677] dark:hover:!bg-[#4B5457]`; default: return isSelected ? `${baseClasses} ${selectedClasses} !bg-neutral-500 dark:!bg-neutral-600 !text-white hover:!bg-neutral-600 dark:hover:!bg-neutral-700` @@ -351,22 +333,6 @@ const themeColors: Record void }) => ( -
    +
    {searchGroups.map((group) => { const Icon = group.icon; const isSelected = selectedGroup === group.id; @@ -593,7 +559,7 @@ const GroupSelector = ({ selectedGroup, onGroupSelect }: GroupSelectorProps) => align="start" sideOffset={8} className={cn( - "w-[600px] font-sans z-[60] -ml-2 mt-1", + "w-[400px] font-sans z-[60] -ml-2 mt-1", "border border-neutral-200 dark:border-neutral-800", "bg-white dark:bg-neutral-900", "shadow-lg rounded-lg" @@ -770,7 +736,8 @@ const FormComponent: React.FC = ({ return (
    0 || uploadQueue.length > 0 ? "bg-gray-100/70 dark:bg-neutral-800 p-1" : "bg-transparent" @@ -815,7 +782,7 @@ const FormComponent: React.FC = ({ onFocus={handleFocus} onBlur={handleBlur} className={cn( - "min-h-[40px] max-h-[300px] w-full resize-none rounded-lg", + "min-h-[56px] max-h-[400px] w-full resize-none rounded-lg", "overflow-x-hidden", "text-base leading-relaxed", "bg-neutral-100 dark:bg-neutral-900", diff --git a/lib/utils.ts b/lib/utils.ts index a1136c7..e7bfa21 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,14 +1,14 @@ // /lib/utils.ts import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" -import { Globe, Book, ShoppingBasket, YoutubeIcon, Pen } from 'lucide-react' -import { RedditLogo, XLogo } from '@phosphor-icons/react' +import { Globe, Book, YoutubeIcon, Pen } from 'lucide-react' +import { Brain, XLogo } from '@phosphor-icons/react' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -export type SearchGroupId = 'web' | 'academic' | 'shopping' | 'youtube' | 'x' | 'writing'; +export type SearchGroupId = 'web' | 'academic' | 'youtube' | 'x'; export const searchGroups = [ { @@ -17,64 +17,24 @@ export const searchGroups = [ description: 'Search across the entire internet', icon: Globe, }, + { + id: 'x' as const, + name: 'X', + description: 'Search X(Twitter) posts and content', + icon: XLogo, + }, { id: 'academic' as const, name: 'Academic', description: 'Search academic papers and research', icon: Book, }, - { - id: 'shopping' as const, - name: 'Shopping', - description: 'Find products and compare prices', - icon: ShoppingBasket, - }, { id: 'youtube' as const, name: 'YouTube', description: 'Search YouTube videos in real-time', icon: YoutubeIcon, }, - { - id: 'x' as const, - name: 'X', - description: 'Search X(Twitter) posts and content', - icon: XLogo, - }, - { - id: 'writing' as const, - name: 'Writing', - description: 'Chat or Talk without web search.', - icon: Pen, - } ] as const; -export const groupTools = { - web: ['web_search', 'retrieve', 'programming'] as const, - academic: ['academic_search', 'retrieve', 'programming'] as const, - shopping: ['shopping_search', 'programming'] as const, - youtube: ['youtube_search'] as const, - x: ['x_search'] as const, - writing: [] as const, -} as const; - -export const groupPrompts = { - web: `You are an expert AI web search engine, that helps users find information on the internet. - Always start with running the search tool and then provide accurate, concise responses. - Format your response in clear paragraphs with citations.`, - academic: `You are an academic research assistant that helps find and analyze scholarly content. - Focus on peer-reviewed papers, citations, and academic sources. - Always include proper citations and summarize key findings.`, - shopping: `You are a shopping assistant that helps users find and compare products. - Focus on providing accurate pricing, product details, and merchant information. - Compare options and highlight key features and best values.`, - youtube: `You are a YouTube search assistant that helps find relevant videos and channels. - Provide video titles, channel names, view counts, and publish dates. - Summarize video content and highlight key moments.`, - reddit: `You are a Reddit content curator that helps find relevant posts and discussions. - Search across subreddits and provide post titles, vote counts, and comment highlights. - Summarize key discussions and community consensus.`, - writing: `You are a writing assistant that helps users with writing, conversation, or intellectual topics.`, -} as const; - export type SearchGroup = typeof searchGroups[number]; diff --git a/next.config.mjs b/next.config.mjs index 6cbb0ca..96b0566 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -63,6 +63,20 @@ const nextConfig = { port: '', pathname: '**' }, + // image.tmdb.org + { + protocol: 'https', + hostname: 'image.tmdb.org', + port: '', + pathname: '/t/p/original/**' + }, + // image.tmdb.org + { + protocol: 'https', + hostname: 'image.tmdb.org', + port: '', + pathname: '/**' + }, ] }, }; diff --git a/package.json b/package.json index 022ce58..3979ca9 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,11 @@ "lint": "next lint" }, "dependencies": { - "@ai-sdk/anthropic": "^1.0.5", - "@ai-sdk/azure": "^1.0.10", - "@ai-sdk/cohere": "^1.0.3", - "@ai-sdk/google": "^1.0.11", - "@ai-sdk/groq": "^0.0.1", - "@ai-sdk/mistral": "^0.0.41", - "@ai-sdk/openai": "^0.0.58", - "@ai-sdk/xai": "^1.0.6", + "@ai-sdk/xai": "^1.0.14", "@e2b/code-interpreter": "^1.0.3", "@foobar404/wave": "^2.0.5", "@mendable/firecrawl-js": "^1.9.7", + "@openrouter/ai-sdk-provider": "^0.0.6", "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-collapsible": "^1.1.1", @@ -41,7 +35,7 @@ "@types/mapbox-gl": "^3.4.0", "@types/unist": "^3.0.3", "@upstash/ratelimit": "^2.0.3", - "@upstash/redis": "^1.34.0", + "@upstash/redis": "^1.34.2", "@vercel/analytics": "^1.3.1", "@vercel/blob": "^0.23.4", "@vercel/functions": "^1.4.0", @@ -81,6 +75,7 @@ "remark-math": "^6.0.0", "sonner": "^1.5.0", "tailwind-merge": "^2.4.0", + "tailwind-scrollbar": "4.0.0-beta.0", "tailwindcss-animate": "^1.0.7", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c5336b..b78700c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,30 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: - '@ai-sdk/anthropic': - specifier: ^1.0.5 - version: 1.0.5(zod@3.24.1) - '@ai-sdk/azure': - specifier: ^1.0.10 - version: 1.0.10(zod@3.24.1) - '@ai-sdk/cohere': - specifier: ^1.0.3 - version: 1.0.3(zod@3.24.1) - '@ai-sdk/google': - specifier: ^1.0.11 - version: 1.0.11(zod@3.24.1) - '@ai-sdk/groq': - specifier: ^0.0.1 - version: 0.0.1(zod@3.24.1) - '@ai-sdk/mistral': - specifier: ^0.0.41 - version: 0.0.41(zod@3.24.1) - '@ai-sdk/openai': - specifier: ^0.0.58 - version: 0.0.58(zod@3.24.1) '@ai-sdk/xai': - specifier: ^1.0.6 - version: 1.0.6(zod@3.24.1) + specifier: ^1.0.14 + version: 1.0.14(zod@3.24.1) '@e2b/code-interpreter': specifier: ^1.0.3 version: 1.0.3 @@ -38,6 +17,9 @@ dependencies: '@mendable/firecrawl-js': specifier: ^1.9.7 version: 1.9.7(ws@8.18.0) + '@openrouter/ai-sdk-provider': + specifier: ^0.0.6 + version: 0.0.6(zod@3.24.1) '@phosphor-icons/react': specifier: ^2.1.7 version: 2.1.7(react-dom@18.3.1)(react@18.3.1) @@ -102,7 +84,7 @@ dependencies: specifier: ^2.0.3 version: 2.0.3 '@upstash/redis': - specifier: ^1.34.0 + specifier: ^1.34.2 version: 1.34.2 '@vercel/analytics': specifier: ^1.3.1 @@ -221,6 +203,9 @@ dependencies: tailwind-merge: specifier: ^2.4.0 version: 2.5.3 + tailwind-scrollbar: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0(tailwindcss@3.4.13) tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.13) @@ -277,92 +262,14 @@ devDependencies: packages: - /@ai-sdk/anthropic@1.0.5(zod@3.24.1): - resolution: {integrity: sha512-qNEB7AYz6W0HTHbhJk/brhGZtjivcRdberD1fn3aCdvzlQ321q1EOTc2k7TvfE+PmNCZbp/uutBbWPGHHODKpw==} + /@ai-sdk/openai-compatible@0.0.13(zod@3.24.1): + resolution: {integrity: sha512-fuauXYKac6PBuf8m52tWcWQW0UCScEkwTaOyr00TcPeK3dd8nPP+ZJzSYE5QhFg7rwi9EH3ahIFqSX1biXhdkQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 dependencies: - '@ai-sdk/provider': 1.0.2 - '@ai-sdk/provider-utils': 2.0.4(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/azure@1.0.10(zod@3.24.1): - resolution: {integrity: sha512-drbmzYS0iPRU/I3xnzphxNsYvSMjYhoq8gK34zShjeJGpPbewZPsXbPrncX6gdrkD4JR021yCahPc9E6RpXr5Q==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/openai': 1.0.8(zod@3.24.1) - '@ai-sdk/provider': 1.0.2 - '@ai-sdk/provider-utils': 2.0.4(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/cohere@1.0.3(zod@3.24.1): - resolution: {integrity: sha512-SDjPinUcGzTNiSMN+9zs1fuAcP8rU1/+CmDWAGu7eMhwVGDurgiOqscC0Oqs/aLsodLt/sFeOvyqj86DAknpbg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/provider': 1.0.1 - '@ai-sdk/provider-utils': 2.0.2(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/google@1.0.11(zod@3.24.1): - resolution: {integrity: sha512-snp66p4BurhOmy2QUTlkZR8nFizx+F60t9v/2ld/fhxTK4G+QMHBUZpBujkW1gQEfE13fEOd43wCE1SQgP46Tw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/provider': 1.0.2 - '@ai-sdk/provider-utils': 2.0.4(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/groq@0.0.1(zod@3.24.1): - resolution: {integrity: sha512-M8XHUovs2UqOx6xlhABXXCGlzbgeErSyIwvH1LQeDl3Z2CbSSgvttc0k6irm4J7ViuULE5XcIDQurXijIePWqQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/provider': 0.0.24 - '@ai-sdk/provider-utils': 1.0.20(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/mistral@0.0.41(zod@3.24.1): - resolution: {integrity: sha512-UTVtdC61AF4KQWnM3VAoo6/gi7G1frL3qVlKyW5toiRAUjCdeqLJUF2ho2iO8yqf+qIT6j57jWT3o6pqREy3Wg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/provider': 0.0.23 - '@ai-sdk/provider-utils': 1.0.19(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/openai@0.0.58(zod@3.24.1): - resolution: {integrity: sha512-Eao1L0vzfXdymgvc5FDHwV2g2A7BCWml1cShNA+wliY1RL7NNREGcuQvBDNoggB9PM24fawzZyk0ZJ5jlo9Q0w==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/provider': 0.0.23 - '@ai-sdk/provider-utils': 1.0.18(zod@3.24.1) - zod: 3.24.1 - dev: false - - /@ai-sdk/openai@1.0.8(zod@3.24.1): - resolution: {integrity: sha512-wcTHM9qgRWGYVO3WxPSTN/RwnZ9R5/17xyo61iUCCSCZaAuJyh6fKddO0/oamwDp3BG7g+4wbfAyuTo32H+fHw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - dependencies: - '@ai-sdk/provider': 1.0.2 - '@ai-sdk/provider-utils': 2.0.4(zod@3.24.1) + '@ai-sdk/provider': 1.0.3 + '@ai-sdk/provider-utils': 2.0.5(zod@3.24.1) zod: 3.24.1 dev: false @@ -382,8 +289,8 @@ packages: zod: 3.24.1 dev: false - /@ai-sdk/provider-utils@1.0.18(zod@3.24.1): - resolution: {integrity: sha512-9u/XE/dB1gsIGcxiC5JfGOLzUz+EKRXt66T8KYWwDg4x8d02P+fI/EPOgkf+T4oLBrcQgvs4GPXPKoXGPJxBbg==} + /@ai-sdk/provider-utils@1.0.22(zod@3.24.1): + resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -391,56 +298,8 @@ packages: zod: optional: true dependencies: - '@ai-sdk/provider': 0.0.23 + '@ai-sdk/provider': 0.0.26 eventsource-parser: 1.1.2 - nanoid: 3.3.6 - secure-json-parse: 2.7.0 - zod: 3.24.1 - dev: false - - /@ai-sdk/provider-utils@1.0.19(zod@3.24.1): - resolution: {integrity: sha512-p02Fq5Mnc8T6nwRBN1Iaou8YXvN1sDS6hbmJaD5UaRbXjizbh+8rpFS/o7jqAHTwf3uHCDitP3pnODyHdc/CDQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - dependencies: - '@ai-sdk/provider': 0.0.23 - eventsource-parser: 1.1.2 - nanoid: 3.3.6 - secure-json-parse: 2.7.0 - zod: 3.24.1 - dev: false - - /@ai-sdk/provider-utils@1.0.20(zod@3.24.1): - resolution: {integrity: sha512-ngg/RGpnA00eNOWEtXHenpX1MsM2QshQh4QJFjUfwcqHpM5kTfG7je7Rc3HcEDP+OkRVv2GF+X4fC1Vfcnl8Ow==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - dependencies: - '@ai-sdk/provider': 0.0.24 - eventsource-parser: 1.1.2 - nanoid: 3.3.6 - secure-json-parse: 2.7.0 - zod: 3.24.1 - dev: false - - /@ai-sdk/provider-utils@2.0.2(zod@3.24.1): - resolution: {integrity: sha512-IAvhKhdlXqiSmvx/D4uNlFYCl8dWT+M9K+IuEcSgnE2Aj27GWu8sDIpAf4r4Voc+wOUkOECVKQhFo8g9pozdjA==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - dependencies: - '@ai-sdk/provider': 1.0.1 - eventsource-parser: 3.0.0 nanoid: 3.3.8 secure-json-parse: 2.7.0 zod: 3.24.1 @@ -462,6 +321,22 @@ packages: zod: 3.24.1 dev: false + /@ai-sdk/provider-utils@2.0.5(zod@3.24.1): + resolution: {integrity: sha512-2M7vLhYN0ThGjNlzow7oO/lsL+DyMxvGMIYmVQvEYaCWhDzxH5dOp78VNjJIVwHzVLMbBDigX3rJuzAs853idw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + dependencies: + '@ai-sdk/provider': 1.0.3 + eventsource-parser: 3.0.0 + nanoid: 3.3.8 + secure-json-parse: 2.7.0 + zod: 3.24.1 + dev: false + /@ai-sdk/provider@0.0.22: resolution: {integrity: sha512-smZ1/2jL/JSKnbhC6ama/PxI2D/psj+YAe0c0qpd5ComQCNFltg72VFf0rpUSFMmFuj1pCCNoBOCrvyl8HTZHQ==} engines: {node: '>=18'} @@ -469,22 +344,8 @@ packages: json-schema: 0.4.0 dev: false - /@ai-sdk/provider@0.0.23: - resolution: {integrity: sha512-oAc49O5+xypVrKM7EUU5P/Y4DUL4JZUWVxhejoAVOTOl3WZUEWsMbP3QZR+TrimQIsS0WR/n9UuF6U0jPdp0tQ==} - engines: {node: '>=18'} - dependencies: - json-schema: 0.4.0 - dev: false - - /@ai-sdk/provider@0.0.24: - resolution: {integrity: sha512-XMsNGJdGO+L0cxhhegtqZ8+T6nn4EoShS819OvCgI2kLbYTIvk0GWFGD0AXJmxkxs3DrpsJxKAFukFR7bvTkgQ==} - engines: {node: '>=18'} - dependencies: - json-schema: 0.4.0 - dev: false - - /@ai-sdk/provider@1.0.1: - resolution: {integrity: sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==} + /@ai-sdk/provider@0.0.26: + resolution: {integrity: sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==} engines: {node: '>=18'} dependencies: json-schema: 0.4.0 @@ -497,6 +358,13 @@ packages: json-schema: 0.4.0 dev: false + /@ai-sdk/provider@1.0.3: + resolution: {integrity: sha512-WiuJEpHTrltOIzv3x2wx4gwksAHW0h6nK3SoDzjqCOJLu/2OJ1yASESTIX+f07ChFykHElVoP80Ol/fe9dw6tQ==} + engines: {node: '>=18'} + dependencies: + json-schema: 0.4.0 + dev: false + /@ai-sdk/react@1.0.6(react@18.3.1)(zod@3.24.1): resolution: {integrity: sha512-8Hkserq0Ge6AEi7N4hlv2FkfglAGbkoAXEZ8YSp255c3PbnZz6+/5fppw+aROmZMOfNwallSRuy1i/iPa2rBpQ==} engines: {node: '>=18'} @@ -532,14 +400,15 @@ packages: zod-to-json-schema: 3.24.1(zod@3.24.1) dev: false - /@ai-sdk/xai@1.0.6(zod@3.24.1): - resolution: {integrity: sha512-bNEAJMSyjMNJIUx9bEr0o3PvP+s4tTU+GzuzG9OVhNc8Zx28kHGXogl4SjDkOgCKHOLixE9RIKuVojZFOACdww==} + /@ai-sdk/xai@1.0.14(zod@3.24.1): + resolution: {integrity: sha512-mZnbiDZjNNT2kzgeZDv7s36ZfqFbRLjZhX9nG4ky4btEsCW4KHauz4hR+V2MCOXEosfrwzXMLzjlIPQY2WnCIw==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 dependencies: - '@ai-sdk/provider': 1.0.2 - '@ai-sdk/provider-utils': 2.0.4(zod@3.24.1) + '@ai-sdk/openai-compatible': 0.0.13(zod@3.24.1) + '@ai-sdk/provider': 1.0.3 + '@ai-sdk/provider-utils': 2.0.5(zod@3.24.1) zod: 3.24.1 dev: false @@ -874,6 +743,17 @@ packages: engines: {node: '>=12.4.0'} dev: true + /@openrouter/ai-sdk-provider@0.0.6(zod@3.24.1): + resolution: {integrity: sha512-gQY8xIAjL+KnralHetMhNRcSf0Xx2gRSKUQNadXSXQhcrSnjT53qJtYELLSR1elkOCiDkggV4ce7ROqDYOaJ+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + dependencies: + '@ai-sdk/provider': 0.0.26 + '@ai-sdk/provider-utils': 1.0.22(zod@3.24.1) + zod: 3.24.1 + dev: false + /@opentelemetry/api@1.9.0: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -6358,6 +6238,15 @@ packages: resolution: {integrity: sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==} dev: false + /tailwind-scrollbar@4.0.0-beta.0(tailwindcss@3.4.13): + resolution: {integrity: sha512-d6qwt3rYDgsKNaQGLW0P6N1TN/87xYZDjH6/PimtFvij2NgC5i3M6mEuVKR4Ixb2u3SvMBT95t7+xzJGJRzXtA==} + engines: {node: '>=12.13.0'} + peerDependencies: + tailwindcss: ^4.0.0-beta.8 + dependencies: + tailwindcss: 3.4.13 + dev: false + /tailwindcss-animate@1.0.7(tailwindcss@3.4.13): resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: diff --git a/tailwind.config.ts b/tailwind.config.ts index bf8c865..b14317c 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -87,7 +87,7 @@ const config = { }, }, }, - plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], + plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography"),require("tailwind-scrollbar")], } satisfies Config export default config \ No newline at end of file