mplx+vercel+xai collab

This commit is contained in:
zaidmukaddam 2025-01-05 01:38:26 +05:30
parent 2c0017b66b
commit 4b59941791
17 changed files with 3882 additions and 5090 deletions

View File

@ -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

View File

@ -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:
<available_tools>
web_search, retrieve, get_weather_data, programming, text_translate, find_place, track_flight
</available_tools>
## 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 <library_name> in the code. This will help in running the code successfully. Always remember to install the libraries using !pip install <library_name> 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 <library_name>) 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=<no_of_seconds>)
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;

View File

@ -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<boolean> {
}
}
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:
<available_tools>
web_search, retrieve, get_weather_data, programming, text_translate, find_place, track_flight
</available_tools>
## 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 <library_name> in the code. This will help in running the code successfully. Always remember to install the libraries using !pip install <library_name> 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<string>();
const uniqueResults = searchResult.results.reduce<Array<typeof searchResult.results[0]>>((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<typeof product> =>
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);

View File

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

View File

@ -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<TrendingQuery[]> {
async function fetchGoogleTrends(countryCode: string = 'US'): Promise<TrendingQuery[]> {
const fetchTrends = async (geo: string): Promise<TrendingQuery[]> => {
try {
const response = await fetch(`https://trends.google.com/trends/trendingsearches/daily/rss?geo=${geo}`, {
@ -39,7 +40,7 @@ async function fetchGoogleTrends(): Promise<TrendingQuery[]> {
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<TrendingQuery[]> {
}
};
const trendsIN = await fetchTrends('IN');
const trendsUS = await fetchTrends('US');
const trends = await fetchTrends(countryCode);
return [...trendsIN, ...trendsUS];
return [ ...trends];
}
async function fetchRedditQuestions(): Promise<TrendingQuery[]> {
@ -95,11 +95,11 @@ async function fetchRedditQuestions(): Promise<TrendingQuery[]> {
}
}
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

View File

@ -1,5 +1,5 @@
import { redirect } from 'next/navigation'
export default async function NewPage() {
redirect('/search')
redirect('/')
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

236
components/movie-info.tsx Normal file
View File

@ -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 = () => (
<div className="flex flex-col max-h-[80vh] bg-white dark:bg-neutral-950">
<div className="relative w-full aspect-[16/9] sm:aspect-[21/9]">
{media.backdrop_path ? (
<Image
src={media.backdrop_path}
alt={media.title || media.name || ''}
fill
className="object-cover opacity-40 sm:opacity-60"
priority
unoptimized
/>
) : (
<div className="w-full h-full bg-neutral-200 dark:bg-neutral-800" />
)}
<div className="absolute inset-0 bg-gradient-to-t from-white via-white/90 to-white/70 dark:from-neutral-950 dark:via-neutral-950/90 dark:to-neutral-950/70" />
<div className="absolute bottom-0 left-0 right-0 p-3 sm:p-6">
<h2 className="text-xl sm:text-3xl font-bold text-black dark:text-white mb-1.5 sm:mb-2">
{media.title || media.name}
</h2>
<div className="flex flex-wrap items-center gap-3 text-black/90 dark:text-white/90">
<div className="flex items-center gap-2">
<Star className="w-4 h-4 text-yellow-400" />
<span>{media.vote_average.toFixed(1)}</span>
</div>
{(media.release_date || media.first_air_date) && (
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span>{formatDate(media.release_date || media.first_air_date || '')}</span>
</div>
)}
{(media.runtime || media.episode_run_time?.[0]) && (
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>{formatRuntime(media.runtime || media.episode_run_time?.[0] || 0)}</span>
</div>
)}
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto">
<div className="p-4 sm:p-6 space-y-6">
<div className="flex flex-wrap gap-2">
{media.genres.map(genre => (
<span
key={genre.id}
className="px-3 py-1 text-sm rounded-full bg-neutral-100 text-black dark:bg-neutral-900 dark:text-white"
>
{genre.name}
</span>
))}
</div>
<p className="text-black/80 dark:text-white/80 text-base sm:text-lg leading-relaxed">
{media.overview}
</p>
{media.credits?.cast && media.credits.cast.length > 0 && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-black/90 dark:text-white/90">Cast</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{media.credits.cast.slice(0, media.credits.cast.length).map(person => (
<div
key={person.id}
className="bg-neutral-100 dark:bg-neutral-900 rounded-lg p-2 space-y-2"
>
{person.profile_path ? (
<Image
src={person.profile_path}
alt={person.name}
width={185}
height={185}
className="w-full aspect-square rounded-lg object-cover"
/>
) : (
<div className="w-full aspect-square rounded-lg bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center">
<Users className="w-8 h-8 text-neutral-600 dark:text-neutral-400" />
</div>
)}
<div>
<p className="text-black dark:text-white font-medium truncate">{person.name}</p>
<p className="text-black/60 dark:text-white/60 text-sm truncate">{person.character}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
return (
<div className="my-4">
<motion.div
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="bg-neutral-100 dark:bg-neutral-900 rounded-xl overflow-hidden cursor-pointer"
onClick={() => setShowDetails(true)}
>
<div className="flex flex-col sm:flex-row gap-3 p-3 sm:p-4">
<div className="w-[120px] sm:w-40 mx-auto sm:mx-0 aspect-[2/3] relative rounded-lg overflow-hidden">
{media.poster_path ? (
<Image
src={media.poster_path}
alt={media.title || media.name || ''}
fill
className="object-cover"
/>
) : (
<div className="w-full h-full bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center">
{media.media_type === 'movie' ? (
<Film className="w-8 h-8 text-neutral-600 dark:text-neutral-400" />
) : (
<Tv className="w-8 h-8 text-neutral-600 dark:text-neutral-400" />
)}
</div>
)}
</div>
<div className="flex-1 min-w-0 space-y-2">
<div>
<h3 className="text-lg sm:text-xl font-bold text-black dark:text-white mb-1.5">
{media.title || media.name}
</h3>
<div className="flex flex-wrap items-center gap-2 text-sm text-black/80 dark:text-white/80">
<span className="capitalize">{media.media_type}</span>
<div className="flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-400" />
<span>{media.vote_average.toFixed(1)}</span>
</div>
</div>
</div>
<p className="text-sm sm:text-base text-black/70 dark:text-white/70 line-clamp-2 sm:line-clamp-3">
{media.overview}
</p>
{media.credits?.cast && (
<p className="text-xs sm:text-sm text-black/60 dark:text-white/60">
<span className="font-medium">Cast: </span>
{media.credits.cast.slice(0, 3).map(person => person.name).join(', ')}
</p>
)}
</div>
</div>
</motion.div>
{isMobile ? (
<Drawer open={showDetails} onOpenChange={setShowDetails}>
<DrawerContent className="h-[85vh] p-0 font-sans">
<DetailContent />
</DrawerContent>
</Drawer>
) : (
<Dialog open={showDetails} onOpenChange={setShowDetails}>
<DialogContent className="max-w-3xl p-0 overflow-hidden font-sans">
<DetailContent />
</DialogContent>
</Dialog>
)}
</div>
);
};
export default TMDBResult;

View File

@ -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 (
<motion.div
className="absolute w-full h-full cursor-grab"
style={{ x, y, rotateX, rotateY }}
drag
dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
dragElastic={0.4} // Reduced elasticity
whileTap={{ cursor: "grabbing" }}
onDragEnd={handleDragEnd}
>
{children}
</motion.div>
);
}
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 (
<div className="w-full h-full bg-white dark:bg-neutral-800 rounded-xl shadow-lg overflow-hidden">
<div className="relative h-[60%] bg-neutral-100 dark:bg-neutral-700 p-4">
<img
src={product.image}
alt={product.title}
className="w-full h-full object-contain mix-blend-multiply dark:mix-blend-normal"
/>
{discount && discount > 0 && (
<Badge
className="absolute top-4 right-4 bg-red-500 text-white"
variant="secondary"
>
{discount}% OFF
</Badge>
)}
</div>
<div className="p-4 h-[40%] flex flex-col justify-between">
<div>
<h3 className="font-medium text-lg line-clamp-2 mb-2 text-neutral-800 dark:text-neutral-200">
{product.title}
</h3>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-green-600 dark:text-green-400">
${formattedPrice}
</span>
{formattedOriginalPrice && (
<span className="text-sm line-through text-neutral-500">
${formattedOriginalPrice}
</span>
)}
</div>
</div>
<div className="flex items-center justify-between mt-4">
<div className="flex items-center gap-1">
{product.rating && (
<>
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<span className="text-sm text-neutral-600 dark:text-neutral-400">
{product.rating} {product.reviewCount && `(${product.reviewCount})`}
</span>
</>
)}
</div>
<span className="text-sm text-neutral-600 dark:text-neutral-400">
{product.source}
</span>
</div>
</div>
</div>
);
};
export const SwipeableProductStack = ({ products }: { products: ShoppingProduct[] }) => {
const [cards, setCards] = useState(products);
const [savedProducts, setSavedProducts] = useState<ShoppingProduct[]>([]);
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 (
<div className="flex flex-col items-center gap-6">
<div className="relative w-full max-w-md aspect-[3/4]" style={{ perspective: 1000 }}>
{cards.map((product, index) => (
<CardRotate
key={product.id}
onSendToBack={() => sendToBack(product.id)}
onSwipe={(direction) => sendToBack(product.id, direction)}
>
<motion.div
className="h-full w-full"
animate={{
rotateZ: (cards.length - index - 1) * 2, // Reduced rotation
scale: 1 - index * 0.03, // Reduced scale difference
y: index * 8, // Reduced vertical offset
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
<ProductCard product={product} />
</motion.div>
</CardRotate>
))}
</div>
<div className="flex items-center gap-4">
<Button
variant="outline"
size="lg"
className="rounded-full w-16 h-16 p-0" // Fixed size circular buttons
onClick={() => cards[cards.length - 1] && sendToBack(cards[cards.length - 1].id, 'left')}
>
<X className="h-8 w-8 text-red-500" />
</Button>
<Button
variant="outline"
size="lg"
className="rounded-full w-16 h-16 p-0" // Fixed size circular buttons
onClick={() => cards[cards.length - 1] && sendToBack(cards[cards.length - 1].id, 'right')}
>
<Heart className="h-8 w-8 text-green-500" />
</Button>
</div>
{savedProducts.length > 0 && (
<div className="w-full mt-8">
<h3 className="text-lg font-medium text-neutral-800 dark:text-neutral-200 mb-4">
Saved Products ({savedProducts.length})
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{savedProducts.map((product) => (
<Card key={product.id} className="overflow-hidden">
<CardContent className="p-3">
<img
src={product.image}
alt={product.title}
className="w-full aspect-square object-contain mb-2"
/>
<div className="text-sm font-medium line-clamp-1">{product.title}</div>
<div className="text-base font-bold text-green-600 dark:text-green-400">
${parseFloat(product.price).toFixed(2)}
</div>
<Button
variant="default"
size="sm"
className="w-full mt-2"
onClick={() => window.open(product.link, '_blank')}
>
View Details
</Button>
</CardContent>
</Card>
))}
</div>
</div>
)}
</div>
);
};

View File

@ -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<TrendingItem | null>(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<number, string> = {
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 = (
<div className="flex flex-col">
<div className="relative aspect-[16/9] sm:aspect-[21/9] w-full">
{selectedItem.backdrop_path ? (
<>
<img
src={selectedItem.backdrop_path}
alt={selectedItem.title || selectedItem.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent" />
</>
) : (
<div className="w-full h-full bg-gradient-to-br from-neutral-900 to-neutral-800" />
)}
<div className="absolute bottom-0 left-0 right-0 p-4 sm:p-6">
<h2 className="text-xl sm:text-3xl font-bold text-white line-clamp-2">
{selectedItem.title || selectedItem.name}
</h2>
<div className="flex items-center gap-3 mt-2">
<div className="flex items-center gap-1.5 text-yellow-400">
<Star className="w-4 h-4 fill-current" />
<span className="font-medium">{selectedItem.vote_average.toFixed(1)}</span>
</div>
{(selectedItem.release_date || selectedItem.first_air_date) && (
<div className="flex items-center gap-1.5 text-neutral-300">
<Calendar className="w-4 h-4" />
<span>{formatDate(selectedItem.release_date || selectedItem.first_air_date || '')}</span>
</div>
)}
</div>
</div>
</div>
<div className="p-4 sm:p-6 space-y-3 sm:space-y-4">
<div className="flex flex-wrap gap-2">
{selectedItem.genre_ids.map((genreId) => (
<span
key={genreId}
className="px-3 py-1 text-xs font-medium rounded-full bg-neutral-100 dark:bg-neutral-800 text-neutral-800 dark:text-neutral-200"
>
{genreMap[genreId]}
</span>
))}
</div>
<p className="text-neutral-700 dark:text-neutral-300 leading-relaxed">{selectedItem.overview}</p>
</div>
</div>
);
if (isMobile) {
return (
<Drawer open={!!selectedItem} onOpenChange={() => setSelectedItem(null)}>
<DrawerContent className="max-h-[85vh] overflow-y-auto">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<Dialog open={!!selectedItem} onOpenChange={() => setSelectedItem(null)}>
<DialogContent className="max-w-3xl p-0 overflow-hidden">{content}</DialogContent>
</Dialog>
);
};
return (
<div className="w-full my-4 sm:my-6">
<header className="flex items-center justify-between mb-4 sm:mb-6 px-4 sm:px-0">
<div className="flex items-center gap-2 sm:gap-3">
<div className="p-1.5 sm:p-2 bg-neutral-100 dark:bg-neutral-800 rounded-xl">
{type === 'movie' ? (
<Film className="w-4 h-4 sm:w-5 sm:h-5 text-neutral-900 dark:text-neutral-100" />
) : (
<Tv className="w-4 h-4 sm:w-5 sm:h-5 text-neutral-900 dark:text-neutral-100" />
)}
</div>
<div>
<h2 className="text-lg sm:text-xl font-semibold">
Trending {type === 'movie' ? 'Movies' : 'Shows'}
</h2>
<p className="text-xs sm:text-sm text-neutral-600 dark:text-neutral-400">Top picks for today</p>
</div>
</div>
<button
onClick={() => setShowAll(!showAll)}
className="flex items-center gap-1 text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100"
>
{showAll ? 'Show Less' : 'View All'}
<ChevronRight className="w-4 h-4" />
</button>
</header>
<div
className={`grid ${
isMobile
? 'grid-cols-2 gap-2'
: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4'
} px-4 sm:px-0`}
>
{displayedResults.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
className="group cursor-pointer"
onClick={() => setSelectedItem(item)}
>
<div className="relative aspect-[2/3] rounded-lg sm:rounded-xl overflow-hidden bg-neutral-100 dark:bg-neutral-800">
{item.poster_path ? (
<img
src={item.poster_path}
alt={item.title || item.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
{type === 'movie' ? (
<Film className="w-8 h-8 text-neutral-400" />
) : (
<Tv className="w-8 h-8 text-neutral-400" />
)}
</div>
)}
<div
className="absolute inset-0 bg-gradient-to-t
from-black/90 via-black/40 to-transparent
opacity-0 group-hover:opacity-100
transition-opacity duration-300
flex flex-col justify-end p-3 sm:p-4"
>
<div className="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
<div className="flex items-center gap-1.5 text-yellow-400 mb-1.5">
<Star className="w-3 h-3 sm:w-4 sm:h-4 fill-current" />
<span className="text-xs sm:text-sm font-medium text-white">
{item.vote_average.toFixed(1)}
</span>
</div>
<h3 className="text-white text-sm sm:text-base font-medium line-clamp-2 mb-1">
{item.title || item.name}
</h3>
<p className="text-neutral-300 text-xs sm:text-sm">
{formatDate(item.release_date || item.first_air_date || '')}
</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
{isMobile && showAll && (
<Drawer open={showAll} onOpenChange={() => setShowAll(false)}>
<DrawerContent className="bg-white dark:bg-neutral-900">
<div className="flex flex-col h-[90vh]">
<div className="flex items-center justify-between p-4 border-b border-neutral-200 dark:border-neutral-800">
<h3 className="text-lg font-semibold">
All Trending {type === 'movie' ? 'Movies' : 'Shows'}
</h3>
<button
onClick={() => setShowAll(false)}
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-full"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-2 gap-2 p-4">
{result.results.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
className="group cursor-pointer"
onClick={() => {
setSelectedItem(item);
setShowAll(false);
}}
>
<div className="relative aspect-[2/3] rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-800">
{item.poster_path ? (
<img
src={item.poster_path}
alt={item.title || item.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
{type === 'movie' ? (
<Film className="w-8 h-8 text-neutral-400" />
) : (
<Tv className="w-8 h-8 text-neutral-400" />
)}
</div>
)}
<div
className="absolute inset-0 bg-gradient-to-t
from-black/90 via-black/40 to-transparent
opacity-0 group-hover:opacity-100
transition-opacity duration-300
flex flex-col justify-end p-3"
>
<div className="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
<div className="flex items-center gap-1.5 text-yellow-400 mb-1.5">
<Star className="w-3 h-3 fill-current" />
<span className="text-xs font-medium text-white">
{item.vote_average.toFixed(1)}
</span>
</div>
<h3 className="text-white text-sm font-medium line-clamp-2 mb-1">
{item.title || item.name}
</h3>
<p className="text-neutral-300 text-xs">
{formatDate(item.release_date || item.first_air_date || '')}
</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</div>
</DrawerContent>
</Drawer>
)}
<DetailView />
</div>
);
};
export default TrendingResults;

View File

@ -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<SearchGroupId, {
description: '!text-neutral-600 dark:!text-neutral-500',
focus: 'focus:!ring-neutral-500 dark:focus:!ring-neutral-400'
},
shopping: {
bg: '!bg-white hover:!bg-green-50 dark:!bg-neutral-900/40 dark:hover:!bg-green-950/40',
bgHover: 'hover:!border-green-200 dark:hover:!border-green-500/30',
bgSelected: '!bg-green-50 dark:!bg-green-950/40 !border-green-500 dark:!border-green-400',
text: '!text-green-600 dark:!text-green-400',
description: '!text-neutral-600 dark:!text-neutral-500',
focus: 'focus:!ring-green-500 dark:focus:!ring-green-400'
},
writing: {
bg: '!bg-white hover:!bg-blue-50 dark:!bg-neutral-900/40 dark:hover:!bg-blue-950/40',
bgHover: 'hover:!border-blue-200 dark:hover:!border-blue-500/30',
bgSelected: '!bg-blue-50 dark:!bg-blue-950/40 !border-blue-500 dark:!border-blue-400',
text: '!text-blue-600 dark:!text-blue-400',
description: '!text-neutral-600 dark:!text-neutral-500',
focus: 'focus:!ring-blue-500 dark:focus:!ring-blue-400'
}
};
const DrawerSelectionContent = ({
@ -436,7 +402,7 @@ const DropdownSelectionContent = ({
selectedGroup: SearchGroupId,
onGroupSelect: (group: SearchGroup) => void
}) => (
<div className="grid grid-cols-2 lg:grid-cols-3 gap-1.5 p-0.5">
<div className="grid grid-cols-2 gap-1.5 p-0.5">
{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<FormComponentProps> = ({
return (
<div className={cn(
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300 z-[51]",
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300",
hasSubmitted ?? "z-[51]",
attachments.length > 0 || uploadQueue.length > 0
? "bg-gray-100/70 dark:bg-neutral-800 p-1"
: "bg-transparent"
@ -815,7 +782,7 @@ const FormComponent: React.FC<FormComponentProps> = ({
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",

View File

@ -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];

View File

@ -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: '/**'
},
]
},
};

View File

@ -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",

View File

@ -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:

View File

@ -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