diff --git a/app/actions.ts b/app/actions.ts index df5f11d..f603edd 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,44 +1,43 @@ -'use server'; +"use server"; -import { generateObject } from 'ai'; -import { createOpenAI as createGroq } from '@ai-sdk/openai'; -import { z } from 'zod'; +import { env } from "@/env.mjs"; +import { createOpenAI as createGroq } from "@ai-sdk/openai"; +import { generateObject } from "ai"; +import { z } from "zod"; export interface Message { - role: 'user' | 'assistant'; - content: string; + role: "user" | "assistant"; + content: string; } const groq = createGroq({ - baseURL: 'https://api.groq.com/openai/v1', - apiKey: process.env.GROQ_API_KEY, + baseURL: "https://api.groq.com/openai/v1", + apiKey: env.GROQ_API_KEY, }); export async function suggestQuestions(history: Message[]) { - 'use server'; + "use server"; - const { object } = await generateObject({ - model: groq('llama-3.1-70b-versatile'), - temperature: 0, - system: - `You are a search engine query generator. You 'have' to create 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. + const { object } = await generateObject({ + model: groq("llama-3.1-70b-versatile"), + temperature: 0, + system: `You are a search engine query generator. You 'have' to create 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. For weather based converations sent to you, always generate questions that are about news, sports, or other topics that are not related to the weather. For programming based conversations, always generate questions that are about the algorithms, data structures, or other topics that are related to it or an improvement of the question. For location based conversations, always generate questions that are about the culture, history, or other topics that are related to the location. Never use pronouns in the questions as they blur the context.`, - messages: history, - schema: z.object({ - questions: z.array( - z.string() - ) - .describe('The generated questions based on the message history.') - }), - }); + messages: history, + schema: z.object({ + questions: z + .array(z.string()) + .describe("The generated questions based on the message history."), + }), + }); - return { - questions: object.questions - }; -} \ No newline at end of file + return { + questions: object.questions, + }; +} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 78b34da..74716a2 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,28 +1,29 @@ -import { openai } from '@ai-sdk/openai' -import { convertToCoreMessages, streamText, tool } from "ai"; +import { env } from "@/env.mjs"; +import { openai } from "@ai-sdk/openai"; import { CodeInterpreter } from "@e2b/code-interpreter"; -import { z } from "zod"; import { geolocation } from "@vercel/functions"; +import { convertToCoreMessages, streamText, tool } from "ai"; +import { z } from "zod"; // 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: openai("gpt-4o-mini"), - messages: convertToCoreMessages(messages), - temperature: 0, - maxTokens: 800, - system: ` + const { messages } = await req.json(); + const { latitude, longitude, city } = geolocation(req); + + const result = await streamText({ + model: openai("gpt-4o-mini"), + messages: convertToCoreMessages(messages), + temperature: 0, + maxTokens: 800, + system: ` You are an AI web search engine that helps users find information on the internet. 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!!!!! -The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. +The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}. The user is located in ${city}(${latitude}, ${longitude}). Here are the tools available to you: @@ -42,7 +43,7 @@ Here is the general guideline per tool to follow when responding to user queries - 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 location search tools return images in the response, please do not include them in the response at all costs. -- Never write a base64 image in the response at all costs. +- Never write a base64 image in the response at all costs. - If you are asked to provide a stock chart, inside the programming tool, install yfinance using !pip install 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. - Never run web_search tool for stock chart queries at all costs. @@ -58,309 +59,368 @@ DO NOT write any kind of html sort of tags(<>) or lists in the response at AL Format your response in paragraphs(min 4) with 3-6 sentences each, keeping it brief but 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. -Never respond to user before running any tool like -- saying 'Certainly! Let me blah blah blah' -- or 'To provide you with the best answer, I will blah blah blah' +Never respond to user before running any tool like +- saying 'Certainly! Let me blah blah blah' +- or 'To provide you with the best answer, I will blah blah blah' - or that 'Based on search results, I think blah blah blah' at ALL COSTS!! Just run the tool and provide the answer.`, - 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 - .enum(["general", "news"]) - .describe("The topic type to search for. Default is general."), - searchDepth: z - .enum(["basic", "advanced"]) - .describe( - "The search depth to use for the search. Default is basic.", - ), - exclude_domains: z - .array(z.string()) - .optional() - .describe( - "A list of domains to specifically exclude from the search results. Default is None, which doesn't exclude any domains.", - ), - }), - execute: async ({ - query, - maxResults, - topic, - searchDepth, - exclude_domains, - }: { - query: string; - maxResults: number; - topic: "general" | "news"; - searchDepth: "basic" | "advanced"; - exclude_domains?: 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, - exclude_domains: exclude_domains, - }); - - 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, - exclude_domains: exclude_domains, - }); - } - - 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.notebook.execCell(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`; - } - } - - sandbox.close(); - 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.enum(["textquery", "phonenumber"]).describe("The type of input (textquery or phonenumber)."), - }), - execute: async ({ input, inputtype }: { input: string; inputtype: "textquery" | "phonenumber" }) => { - 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; - }, - }), - }, - toolChoice: "auto", - }); - - return result.toAIStreamResponse(); + 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 + .enum(["general", "news"]) + .describe("The topic type to search for. Default is general."), + searchDepth: z + .enum(["basic", "advanced"]) + .describe( + "The search depth to use for the search. Default is basic.", + ), + exclude_domains: z + .array(z.string()) + .optional() + .describe( + "A list of domains to specifically exclude from the search results. Default is None, which doesn't exclude any domains.", + ), + }), + execute: async ({ + query, + maxResults, + topic, + searchDepth, + exclude_domains, + }: { + query: string; + maxResults: number; + topic: "general" | "news"; + searchDepth: "basic" | "advanced"; + exclude_domains?: string[]; + }) => { + const apiKey = 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, + exclude_domains: exclude_domains, + }); + + 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, + exclude_domains: exclude_domains, + }); + } + + const response = await fetch("https://api.tavily.com/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + const data = await response.json(); + + const 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 = 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.notebook.execCell(code); + let message = ""; + const 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 (const 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`; + } + } + + sandbox.close(); + 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 = 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 + .enum(["textquery", "phonenumber"]) + .describe("The type of input (textquery or phonenumber)."), + }), + execute: async ({ + input, + inputtype, + }: { input: string; inputtype: "textquery" | "phonenumber" }) => { + const apiKey = 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 = 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; + }, + }), + }, + toolChoice: "auto", + }); + + return result.toAIStreamResponse(); } diff --git a/app/page.tsx b/app/page.tsx index 45e5bb7..86b6f63 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,1372 +1,1558 @@ /* eslint-disable @next/next/no-img-element */ "use client"; -import -React, -{ - useRef, - useCallback, - useState, - useEffect, - useMemo, - memo -} from 'react'; -import ReactMarkdown, { Components } from 'react-markdown'; -import { useRouter } from 'next/navigation'; -import remarkGfm from 'remark-gfm'; -import { useChat } from 'ai/react'; -import { ToolInvocation } from 'ai'; -import { toast } from 'sonner'; -import { motion, AnimatePresence } from 'framer-motion'; -import Image from 'next/image'; -import { suggestQuestions, Message } from './actions'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { - SearchIcon, - Sparkles, - ArrowRight, - Globe, - AlignLeft, - Newspaper, - Copy, - Cloud, - Code, - Check, - Loader2, - User2, - Edit2, - Heart, - X, - MapPin, - Star, - Plus, - Terminal, - ImageIcon, - Download, -} from 'lucide-react'; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, + HoverCard, + HoverCardContent, + HoverCardTrigger, } from "@/components/ui/hover-card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { ToolInvocation } from "ai"; +import { useChat } from "ai/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { + AlignLeft, + ArrowRight, + Check, + Cloud, + Code, + Copy, + Download, + Edit2, + Globe, + Heart, + Loader2, + MapPin, + Newspaper, + Plus, + SearchIcon, + Sparkles, + Star, + User2, + X, +} from "lucide-react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import React, { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism"; +import remarkGfm from "remark-gfm"; +import { toast } from "sonner"; +import { type Message, suggestQuestions } from "./actions"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Line, LineChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, } from "@/components/ui/chart"; -import { GitHubLogoIcon } from '@radix-ui/react-icons'; -import { Skeleton } from '@/components/ui/skeleton'; -import Link from 'next/link'; -import { cn } from '@/lib/utils'; +import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { env } from "@/env.mjs"; +import { GitHubLogoIcon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + XAxis, + YAxis, +} from "recharts"; export const maxDuration = 60; declare global { - interface Window { - google: any; - initMap: () => void; - } + interface Window { + google: any; + initMap: () => void; + } } export default function Home() { - const router = useRouter(); - const inputRef = useRef(null); - const [lastSubmittedQuery, setLastSubmittedQuery] = useState(""); - const [hasSubmitted, setHasSubmitted] = useState(false); - const [isAnimating, setIsAnimating] = useState(false); - const bottomRef = useRef(null); - const [suggestedQuestions, setSuggestedQuestions] = useState([]); - const [showExamples, setShowExamples] = useState(false) - const [isEditingQuery, setIsEditingQuery] = useState(false); - - const { isLoading, input, messages, setInput, append, handleSubmit, setMessages } = useChat({ - api: '/api/chat', - maxToolRoundtrips: 1, - onFinish: async (message, { finishReason }) => { - if (finishReason === 'stop') { - const newHistory: Message[] = [{ role: "user", content: lastSubmittedQuery, }, { role: "assistant", content: message.content }]; - const { questions } = await suggestQuestions(newHistory); - setSuggestedQuestions(questions); - } - setIsAnimating(false); - }, - onError: (error) => { - console.error("Chat error:", error); - toast.error("An error occurred.", { - description: "We must have ran out of credits. Sponsor us on GitHub to keep this service running.", - action: { - label: "Sponsor", - onClick: () => window.open("https://git.new/mplx", "_blank"), - }, - }); - }, - }); - - const CopyButton = ({ text }: { text: string }) => { - const [isCopied, setIsCopied] = useState(false); - - return ( - - ); - }; - - // Weather chart components - - interface WeatherDataPoint { - date: string; - minTemp: number; - maxTemp: number; - } - - const WeatherChart: React.FC<{ result: any }> = React.memo(({ result }) => { - const { chartData, minTemp, maxTemp } = useMemo(() => { - const weatherData: WeatherDataPoint[] = result.list.map((item: any) => ({ - date: new Date(item.dt * 1000).toLocaleDateString(), - minTemp: Number((item.main.temp_min - 273.15).toFixed(1)), - maxTemp: Number((item.main.temp_max - 273.15).toFixed(1)), - })); - - // Group data by date and calculate min and max temperatures - const groupedData: { [key: string]: WeatherDataPoint } = weatherData.reduce((acc, curr) => { - if (!acc[curr.date]) { - acc[curr.date] = { ...curr }; - } else { - acc[curr.date].minTemp = Math.min(acc[curr.date].minTemp, curr.minTemp); - acc[curr.date].maxTemp = Math.max(acc[curr.date].maxTemp, curr.maxTemp); - } - return acc; - }, {} as { [key: string]: WeatherDataPoint }); - - const chartData = Object.values(groupedData); - - // Calculate overall min and max temperatures - const minTemp = Math.min(...chartData.map(d => d.minTemp)); - const maxTemp = Math.max(...chartData.map(d => d.maxTemp)); - - return { chartData, minTemp, maxTemp }; - }, [result]); - - const chartConfig: ChartConfig = useMemo(() => ({ - minTemp: { - label: "Min Temp.", - color: "hsl(var(--chart-1))", - }, - maxTemp: { - label: "Max Temp.", - color: "hsl(var(--chart-2))", - }, - }), []); - - return ( - - - Weather Forecast for {result.city.name} - - Showing min and max temperatures for the next 5 days - - - - - - - - new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} - /> - `${value}°C`} - /> - } /> - - - - - - - -
-
-
- {result.city.name}, {result.city.country} -
-
- Next 5 days forecast -
-
-
-
-
- ); - }); - - WeatherChart.displayName = 'WeatherChart'; - - - // Google Maps components - - const isValidCoordinate = (coord: number) => { - return typeof coord === 'number' && !isNaN(coord) && isFinite(coord); - }; - - const loadGoogleMapsScript = (callback: () => void) => { - if (window.google && window.google.maps) { - callback(); - return; - } - - const existingScript = document.getElementById('googleMapsScript'); - if (existingScript) { - existingScript.remove(); - } - - window.initMap = callback; - const script = document.createElement('script'); - script.id = 'googleMapsScript'; - script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places,marker&callback=initMap`; - script.async = true; - script.defer = true; - document.head.appendChild(script); - }; - - const MapComponent = React.memo(({ center, places }: { center: { lat: number; lng: number }, places: any[] }) => { - const mapRef = useRef(null); - const [mapError, setMapError] = useState(null); - const googleMapRef = useRef(null); - const markersRef = useRef([]); - - const memoizedCenter = useMemo(() => center, [center]); - const memoizedPlaces = useMemo(() => places, [places]); - - const initializeMap = useCallback(async () => { - if (mapRef.current && isValidCoordinate(memoizedCenter.lat) && isValidCoordinate(memoizedCenter.lng)) { - const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary; - const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; - - if (!googleMapRef.current) { - googleMapRef.current = new Map(mapRef.current, { - center: memoizedCenter, - zoom: 14, - mapId: "347ff92e0c7225cf", - }); - } else { - googleMapRef.current.setCenter(memoizedCenter); - } - - // Clear existing markers - markersRef.current.forEach(marker => marker.map = null); - markersRef.current = []; - - memoizedPlaces.forEach((place) => { - if (isValidCoordinate(place.location.lat) && isValidCoordinate(place.location.lng)) { - const marker = new AdvancedMarkerElement({ - map: googleMapRef.current, - position: place.location, - title: place.name, - }); - markersRef.current.push(marker); - } - }); - } else { - setMapError('Invalid coordinates provided'); - } - }, [memoizedCenter, memoizedPlaces]); - - useEffect(() => { - loadGoogleMapsScript(() => { - try { - initializeMap(); - } catch (error) { - console.error('Error initializing map:', error); - setMapError('Failed to initialize Google Maps'); - } - }); - - return () => { - // Clean up markers when component unmounts - markersRef.current.forEach(marker => marker.map = null); - }; - }, [initializeMap]); - - if (mapError) { - return
{mapError}
; - } - - return
; - }); - - MapComponent.displayName = 'MapComponent'; - - const MapSkeleton = () => ( - - ); - - const PlaceDetails = ({ place }: { place: any }) => ( -
-
-

{place.name}

-

- {place.vicinity} -

-
- {place.rating && ( - - - {place.rating} ({place.user_ratings_total}) - - )} -
- ); - - const MapEmbed = memo(({ location, zoom = 15 }: { location: string, zoom?: number }) => { - const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; - const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(location)}&zoom=${zoom}`; - - return ( -
- -
- ); - }); - - MapEmbed.displayName = 'MapEmbed'; - - const FindPlaceResult = memo(({ result }: { result: any }) => { - const place = result.candidates[0]; - const location = `${place.geometry.location.lat},${place.geometry.location.lng}`; - - return ( - - - - - {place.name} - - - - -
-

Address: {place.formatted_address}

- {place.rating && ( -
- Rating: - - - {place.rating} - -
- )} - {place.opening_hours && ( -

Open now: {place.opening_hours.open_now ? 'Yes' : 'No'}

- )} -
-
-
- ); - }); - - FindPlaceResult.displayName = 'FindPlaceResult'; - - const TextSearchResult = memo(({ result }: { result: any }) => { - const centerLocation = result.results[0]?.geometry?.location; - const mapLocation = centerLocation ? `${centerLocation.lat},${centerLocation.lng}` : ''; - - return ( - - - - - Text Search Results - - - - {mapLocation && } - - - Place Details - -
- {result.results.map((place: any, index: number) => ( -
-
-

{place.name}

-

- {place.formatted_address} -

-
- {place.rating && ( - - - {place.rating} ({place.user_ratings_total}) - - )} -
- ))} -
-
-
-
-
-
- ); - }); - - TextSearchResult.displayName = 'TextSearchResult'; - - - const renderToolInvocation = (toolInvocation: ToolInvocation, index: number) => { - const args = JSON.parse(JSON.stringify(toolInvocation.args)); - const result = 'result' in toolInvocation ? JSON.parse(JSON.stringify(toolInvocation.result)) : null; - - if (toolInvocation.toolName === 'nearby_search') { - if (!result) { - return ( -
-
- - Searching nearby places... -
-
- {[0, 1, 2].map((index) => ( - - ))} -
-
- ); - } - - if (isLoading) { - return ( - - - - - - - - - ); - } - - return ( - - - - - Nearby {args.type ? args.type.charAt(0).toUpperCase() + args.type.slice(1) + 's' : 'Places'} - {args.keyword && {args.keyword}} - - - - - - - Place Details - -
- {result.results.map((place: any, placeIndex: number) => ( - - ))} -
-
-
-
-
-
- ); - } - - if (toolInvocation.toolName === 'find_place') { - if (!result) { - return ( -
-
- - Finding place... -
- - {[0, 1, 2].map((index) => ( - - ))} - -
- ); - } - - return ; - } - - if (toolInvocation.toolName === 'text_search') { - if (!result) { - return ( -
-
- - Searching places... -
- - {[0, 1, 2].map((index) => ( - - ))} - -
- ); - } - - return ; - } - - if (toolInvocation.toolName === 'get_weather_data') { - if (!result) { - return ( -
-
- - Fetching weather data... -
-
- {[0, 1, 2].map((index) => ( - - ))} -
-
- ); - } - - if (isLoading) { - return ( - - - - - -
- - - ); - } - - return ; - } - - if (toolInvocation.toolName === 'programming') { - return ( -
-
- - Programming -
- - - - Code - - - Output - - {result?.images && result.images.length > 0 && ( - - Images - - )} - - -
- - {args.code} - -
- -
-
-
- -
- {result ? ( - <> -
-                      {result.message}
-                    
-
- -
- - ) : ( -
-
- - Executing code... -
-
- )} -
-
- {result?.images && result.images.length > 0 && ( - -
- {result.images.map((img: { format: 'png' | 'jpeg' | 'svg', data: string }, imgIndex: number) => ( -
-
-

Image {imgIndex + 1}

- -
-
- {`Generated -
-
- ))} -
-
- )} -
-
- ); - } - - if (toolInvocation.toolName === 'nearby_search') { - if (!result) { - return ( -
-
- - Searching nearby places... -
-
- {[0, 1, 2].map((index) => ( - - ))} -
-
- ); - } - - const mapUrl = `https://www.google.com/maps/embed/v1/search?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&q=${encodeURIComponent(args.type)}¢er=${result.results[0].geometry.location.lat},${result.results[0].geometry.location.lng}&zoom=14`; - - return ( - - - - - Nearby {args.type.charAt(0).toUpperCase() + args.type.slice(1)}s - - - -
- -
-
- {result.results.map((place: any, placeIndex: number) => ( -
-
-

{place.name}

-

{place.vicinity}

-
- - {place.rating} ★ ({place.user_ratings_total}) - -
- ))} -
-
-
- ); - } - - return ( -
- {!result ? ( -
-
- - Running a search... -
-
- {[0, 1, 2].map((index) => ( - - ))} -
-
- ) : - - - -
-
- -

Sources Found

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

{item.content}

-
- -
- ))} -
- )} -
-
-
} -
- ); - }; - - interface CitationComponentProps { - href: string; - children: React.ReactNode; - index: number; - } - - const CitationComponent: React.FC = React.memo(({ href, index }) => { - const faviconUrl = `https://www.google.com/s2/favicons?sz=128&domain=${new URL(href).hostname}`; - - return ( - - - - {index + 1} - - - - Favicon - - {href} - - - - ); - }); - - CitationComponent.displayName = "CitationComponent"; - - interface MarkdownRendererProps { - content: string; - } - - const MarkdownRenderer: React.FC = React.memo(({ content }) => { - const citationLinks = useMemo(() => { - return [...content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(([_, text, link]) => ({ - text, - link, - })); - }, [content]); - - const components: Partial = useMemo(() => ({ - a: ({ href, children }) => { - if (!href) return null; - const index = citationLinks.findIndex((link) => link.link === href); - return index !== -1 ? ( - - {children} - - ) : ( - - {children} - - ); - }, - }), [citationLinks]); - - return ( - - {content} - - ); - }); - - MarkdownRenderer.displayName = "MarkdownRenderer"; - - - useEffect(() => { - if (bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [messages, suggestedQuestions]); - - const handleExampleClick = useCallback(async (query: string) => { - setLastSubmittedQuery(query.trim()); - setHasSubmitted(true); - setSuggestedQuestions([]); - setIsAnimating(true); - await append({ - content: query.trim(), - role: 'user' - }); - }, [append]); - - const handleFormSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (input.trim()) { - setMessages([]); - setLastSubmittedQuery(input.trim()); - setHasSubmitted(true); - setIsAnimating(true); - setSuggestedQuestions([]); - handleSubmit(e); - } else { - toast.error("Please enter a search query."); - } - }, [input, setMessages, handleSubmit]); - - const handleSuggestedQuestionClick = useCallback(async (question: string) => { - setMessages([]); - setLastSubmittedQuery(question.trim()); - setHasSubmitted(true); - setSuggestedQuestions([]); - setIsAnimating(true); - await append({ - content: question.trim(), - role: 'user' - }); - }, [append, setMessages]); - - const handleQueryEdit = useCallback(() => { - setIsAnimating(true) - setIsEditingQuery(true); - setInput(lastSubmittedQuery); - }, [lastSubmittedQuery, setInput]); - - const handleQuerySubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (input.trim()) { - setLastSubmittedQuery(input.trim()); - setIsEditingQuery(false); - setMessages([]); - setHasSubmitted(true); - setIsAnimating(true); - setSuggestedQuestions([]); - handleSubmit(e); - } else { - toast.error("Please enter a search query."); - } - }, [input, setMessages, handleSubmit]); - - const exampleQueries = [ - "Weather in Doha", - "What is new with Grok 2.0?", - "Count the number of r's in strawberry", - "Explain Claude 3.5 Sonnet" - ]; - - const Navbar = () => ( -
- - - -
- - - - - - - -

Sponsor this project on GitHub

-
-
-
-
-
- ); - - return ( -
- - -
- {!hasSubmitted && -
-

MiniPerplx

-

- In search for minimalism and simplicity -

-
- } - - {!hasSubmitted && ( - -
-
- setInput(e.target.value)} - disabled={isLoading} - className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" - onFocus={() => setShowExamples(true)} - onBlur={() => setShowExamples(false)} - /> - -
-
- -
-
-
- {exampleQueries.map((message, index) => ( - - ))} -
-
-
-
- )} -
- - - - {hasSubmitted && ( - setIsAnimating(false)} - > -
- - - - - {isEditingQuery ? ( -
- setInput(e.target.value)} - className="flex-grow" - /> - - -
- ) : ( - - - -

- {lastSubmittedQuery} -

-
- -

{lastSubmittedQuery}

-
-
-
- )} -
- {!isEditingQuery && ( - - - )} -
-
- )} -
- -
- {messages.map((message, index) => ( -
- {message.role === 'assistant' && message.content && ( -
-
-
- -

Answer

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

Suggested questions

-
-
- {suggestedQuestions.map((question, index) => ( - - ))} -
-
- )} -
-
-
- - - {hasSubmitted && !isAnimating && ( - -
-
- setInput(e.target.value)} - disabled={isLoading} - className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" - /> - -
-
-
- )} -
-
- ); -} \ No newline at end of file + const router = useRouter(); + const inputRef = useRef(null); + const [lastSubmittedQuery, setLastSubmittedQuery] = useState(""); + const [hasSubmitted, setHasSubmitted] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + const bottomRef = useRef(null); + const [suggestedQuestions, setSuggestedQuestions] = useState([]); + const [showExamples, setShowExamples] = useState(false); + const [isEditingQuery, setIsEditingQuery] = useState(false); + + const { + isLoading, + input, + messages, + setInput, + append, + handleSubmit, + setMessages, + } = useChat({ + api: "/api/chat", + maxToolRoundtrips: 1, + onFinish: async (message, { finishReason }) => { + if (finishReason === "stop") { + const newHistory: Message[] = [ + { role: "user", content: lastSubmittedQuery }, + { role: "assistant", content: message.content }, + ]; + const { questions } = await suggestQuestions(newHistory); + setSuggestedQuestions(questions); + } + setIsAnimating(false); + }, + onError: (error) => { + console.error("Chat error:", error); + toast.error("An error occurred.", { + description: + "We must have ran out of credits. Sponsor us on GitHub to keep this service running.", + action: { + label: "Sponsor", + onClick: () => window.open("https://git.new/mplx", "_blank"), + }, + }); + }, + }); + + const CopyButton = ({ text }: { text: string }) => { + const [isCopied, setIsCopied] = useState(false); + + return ( + + ); + }; + + // Weather chart components + + interface WeatherDataPoint { + date: string; + minTemp: number; + maxTemp: number; + } + + const WeatherChart: React.FC<{ result: any }> = React.memo(({ result }) => { + const { chartData, minTemp, maxTemp } = useMemo(() => { + const weatherData: WeatherDataPoint[] = result.list.map((item: any) => ({ + date: new Date(item.dt * 1000).toLocaleDateString(), + minTemp: Number((item.main.temp_min - 273.15).toFixed(1)), + maxTemp: Number((item.main.temp_max - 273.15).toFixed(1)), + })); + + // Group data by date and calculate min and max temperatures + const groupedData: { [key: string]: WeatherDataPoint } = + weatherData.reduce( + (acc, curr) => { + if (!acc[curr.date]) { + acc[curr.date] = { ...curr }; + } else { + acc[curr.date].minTemp = Math.min( + acc[curr.date].minTemp, + curr.minTemp, + ); + acc[curr.date].maxTemp = Math.max( + acc[curr.date].maxTemp, + curr.maxTemp, + ); + } + return acc; + }, + {} as { [key: string]: WeatherDataPoint }, + ); + + const chartData = Object.values(groupedData); + + // Calculate overall min and max temperatures + const minTemp = Math.min(...chartData.map((d) => d.minTemp)); + const maxTemp = Math.max(...chartData.map((d) => d.maxTemp)); + + return { chartData, minTemp, maxTemp }; + }, [result]); + + const chartConfig: ChartConfig = useMemo( + () => ({ + minTemp: { + label: "Min Temp.", + color: "hsl(var(--chart-1))", + }, + maxTemp: { + label: "Max Temp.", + color: "hsl(var(--chart-2))", + }, + }), + [], + ); + + return ( + + + Weather Forecast for {result.city.name} + + Showing min and max temperatures for the next 5 days + + + + + + + + + new Date(value).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }) + } + /> + `${value}°C`} + /> + } /> + + + + + + + +
+
+
+ {result.city.name}, {result.city.country} +
+
+ Next 5 days forecast +
+
+
+
+
+ ); + }); + + WeatherChart.displayName = "WeatherChart"; + + // Google Maps components + + const isValidCoordinate = (coord: number) => { + return typeof coord === "number" && !isNaN(coord) && isFinite(coord); + }; + + const loadGoogleMapsScript = (callback: () => void) => { + if (window.google && window.google.maps) { + callback(); + return; + } + + const existingScript = document.getElementById("googleMapsScript"); + if (existingScript) { + existingScript.remove(); + } + + window.initMap = callback; + const script = document.createElement("script"); + script.id = "googleMapsScript"; + script.src = `https://maps.googleapis.com/maps/api/js?key=${env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places,marker&callback=initMap`; + script.async = true; + script.defer = true; + document.head.appendChild(script); + }; + + const MapComponent = React.memo( + ({ + center, + places, + }: { center: { lat: number; lng: number }; places: any[] }) => { + const mapRef = useRef(null); + const [mapError, setMapError] = useState(null); + const googleMapRef = useRef(null); + const markersRef = useRef([]); + + const memoizedCenter = useMemo(() => center, [center]); + const memoizedPlaces = useMemo(() => places, [places]); + + const initializeMap = useCallback(async () => { + if ( + mapRef.current && + isValidCoordinate(memoizedCenter.lat) && + isValidCoordinate(memoizedCenter.lng) + ) { + const { Map } = (await google.maps.importLibrary( + "maps", + )) as google.maps.MapsLibrary; + const { AdvancedMarkerElement } = (await google.maps.importLibrary( + "marker", + )) as google.maps.MarkerLibrary; + + if (!googleMapRef.current) { + googleMapRef.current = new Map(mapRef.current, { + center: memoizedCenter, + zoom: 14, + mapId: "347ff92e0c7225cf", + }); + } else { + googleMapRef.current.setCenter(memoizedCenter); + } + + // Clear existing markers + markersRef.current.forEach((marker) => (marker.map = null)); + markersRef.current = []; + + memoizedPlaces.forEach((place) => { + if ( + isValidCoordinate(place.location.lat) && + isValidCoordinate(place.location.lng) + ) { + const marker = new AdvancedMarkerElement({ + map: googleMapRef.current, + position: place.location, + title: place.name, + }); + markersRef.current.push(marker); + } + }); + } else { + setMapError("Invalid coordinates provided"); + } + }, [memoizedCenter, memoizedPlaces]); + + useEffect(() => { + loadGoogleMapsScript(() => { + try { + initializeMap(); + } catch (error) { + console.error("Error initializing map:", error); + setMapError("Failed to initialize Google Maps"); + } + }); + + return () => { + // Clean up markers when component unmounts + markersRef.current.forEach((marker) => (marker.map = null)); + }; + }, [initializeMap]); + + if (mapError) { + return ( +
+ {mapError} +
+ ); + } + + return
; + }, + ); + + MapComponent.displayName = "MapComponent"; + + const MapSkeleton = () => ; + + const PlaceDetails = ({ place }: { place: any }) => ( +
+
+

{place.name}

+

+ {place.vicinity} +

+
+ {place.rating && ( + + + {place.rating} ({place.user_ratings_total}) + + )} +
+ ); + + const MapEmbed = memo( + ({ location, zoom = 15 }: { location: string; zoom?: number }) => { + const apiKey = env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; + const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(location)}&zoom=${zoom}`; + + return ( +
+ +
+ ); + }, + ); + + MapEmbed.displayName = "MapEmbed"; + + const FindPlaceResult = memo(({ result }: { result: any }) => { + const place = result.candidates[0]; + const location = `${place.geometry.location.lat},${place.geometry.location.lng}`; + + return ( + + + + + {place.name} + + + + +
+

+ Address: {place.formatted_address} +

+ {place.rating && ( +
+ Rating: + + + {place.rating} + +
+ )} + {place.opening_hours && ( +

+ Open now:{" "} + {place.opening_hours.open_now ? "Yes" : "No"} +

+ )} +
+
+
+ ); + }); + + FindPlaceResult.displayName = "FindPlaceResult"; + + const TextSearchResult = memo(({ result }: { result: any }) => { + const centerLocation = result.results[0]?.geometry?.location; + const mapLocation = centerLocation + ? `${centerLocation.lat},${centerLocation.lng}` + : ""; + + return ( + + + + + Text Search Results + + + + {mapLocation && } + + + Place Details + +
+ {result.results.map((place: any, index: number) => ( +
+
+

{place.name}

+

+ {place.formatted_address} +

+
+ {place.rating && ( + + + {place.rating} ({place.user_ratings_total}) + + )} +
+ ))} +
+
+
+
+
+
+ ); + }); + + TextSearchResult.displayName = "TextSearchResult"; + + const renderToolInvocation = ( + toolInvocation: ToolInvocation, + index: number, + ) => { + const args = JSON.parse(JSON.stringify(toolInvocation.args)); + const result = + "result" in toolInvocation + ? JSON.parse(JSON.stringify(toolInvocation.result)) + : null; + + if (toolInvocation.toolName === "nearby_search") { + if (!result) { + return ( +
+
+ + + Searching nearby places... + +
+
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+ ); + } + + if (isLoading) { + return ( + + + + + + + + + ); + } + + return ( + + + + + + Nearby{" "} + {args.type + ? args.type.charAt(0).toUpperCase() + args.type.slice(1) + "s" + : "Places"} + + {args.keyword && ( + {args.keyword} + )} + + + + + + + + Place Details + + +
+ {result.results.map((place: any, placeIndex: number) => ( + + ))} +
+
+
+
+
+
+ ); + } + + if (toolInvocation.toolName === "find_place") { + if (!result) { + return ( +
+
+ + Finding place... +
+ + {[0, 1, 2].map((index) => ( + + ))} + +
+ ); + } + + return ; + } + + if (toolInvocation.toolName === "text_search") { + if (!result) { + return ( +
+
+ + + Searching places... + +
+ + {[0, 1, 2].map((index) => ( + + ))} + +
+ ); + } + + return ; + } + + if (toolInvocation.toolName === "get_weather_data") { + if (!result) { + return ( +
+
+ + + Fetching weather data... + +
+
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+ ); + } + + if (isLoading) { + return ( + + + + + +
+ + + ); + } + + return ; + } + + if (toolInvocation.toolName === "programming") { + return ( +
+
+ + Programming +
+ + + + Code + + + Output + + {result?.images && result.images.length > 0 && ( + + Images + + )} + + +
+ + {args.code} + +
+ +
+
+
+ +
+ {result ? ( + <> +
+											{result.message}
+										
+
+ +
+ + ) : ( +
+
+ + + Executing code... + +
+
+ )} +
+
+ {result?.images && result.images.length > 0 && ( + +
+ {result.images.map( + ( + img: { format: "png" | "jpeg" | "svg"; data: string }, + imgIndex: number, + ) => ( +
+
+

+ Image {imgIndex + 1} +

+ +
+
+ {`Generated +
+
+ ), + )} +
+
+ )} +
+
+ ); + } + + if (toolInvocation.toolName === "nearby_search") { + if (!result) { + return ( +
+
+ + + Searching nearby places... + +
+
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+ ); + } + + const mapUrl = `https://www.google.com/maps/embed/v1/search?key=${env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&q=${encodeURIComponent(args.type)}¢er=${result.results[0].geometry.location.lat},${result.results[0].geometry.location.lng}&zoom=14`; + + return ( + + + + + + Nearby {args.type.charAt(0).toUpperCase() + args.type.slice(1)}s + + + + +
+ +
+
+ {result.results.map((place: any, placeIndex: number) => ( +
+
+

{place.name}

+

+ {place.vicinity} +

+
+ + {place.rating} ★ ({place.user_ratings_total}) + +
+ ))} +
+
+
+ ); + } + + return ( +
+ {!result ? ( +
+
+ + + Running a search... + +
+
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+ ) : ( + + + +
+
+ +

Sources Found

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

+ {item.content} +

+
+ +
+ ))} +
+ )} +
+
+
+ )} +
+ ); + }; + + interface CitationComponentProps { + href: string; + children: React.ReactNode; + index: number; + } + + const CitationComponent: React.FC = React.memo( + ({ href, index }) => { + const faviconUrl = `https://www.google.com/s2/favicons?sz=128&domain=${new URL(href).hostname}`; + + return ( + + + + {index + 1} + + + + Favicon + + {href} + + + + ); + }, + ); + + CitationComponent.displayName = "CitationComponent"; + + interface MarkdownRendererProps { + content: string; + } + + const MarkdownRenderer: React.FC = React.memo( + ({ content }) => { + const citationLinks = useMemo(() => { + return [...content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map( + ([_, text, link]) => ({ + text, + link, + }), + ); + }, [content]); + + const components: Partial = useMemo( + () => ({ + a: ({ href, children }) => { + if (!href) return null; + const index = citationLinks.findIndex((link) => link.link === href); + return index !== -1 ? ( + + {children} + + ) : ( + + {children} + + ); + }, + }), + [citationLinks], + ); + + return ( + + {content} + + ); + }, + ); + + MarkdownRenderer.displayName = "MarkdownRenderer"; + + useEffect(() => { + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages, suggestedQuestions]); + + const handleExampleClick = useCallback( + async (query: string) => { + setLastSubmittedQuery(query.trim()); + setHasSubmitted(true); + setSuggestedQuestions([]); + setIsAnimating(true); + await append({ + content: query.trim(), + role: "user", + }); + }, + [append], + ); + + const handleFormSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (input.trim()) { + setMessages([]); + setLastSubmittedQuery(input.trim()); + setHasSubmitted(true); + setIsAnimating(true); + setSuggestedQuestions([]); + handleSubmit(e); + } else { + toast.error("Please enter a search query."); + } + }, + [input, setMessages, handleSubmit], + ); + + const handleSuggestedQuestionClick = useCallback( + async (question: string) => { + setMessages([]); + setLastSubmittedQuery(question.trim()); + setHasSubmitted(true); + setSuggestedQuestions([]); + setIsAnimating(true); + await append({ + content: question.trim(), + role: "user", + }); + }, + [append, setMessages], + ); + + const handleQueryEdit = useCallback(() => { + setIsAnimating(true); + setIsEditingQuery(true); + setInput(lastSubmittedQuery); + }, [lastSubmittedQuery, setInput]); + + const handleQuerySubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (input.trim()) { + setLastSubmittedQuery(input.trim()); + setIsEditingQuery(false); + setMessages([]); + setHasSubmitted(true); + setIsAnimating(true); + setSuggestedQuestions([]); + handleSubmit(e); + } else { + toast.error("Please enter a search query."); + } + }, + [input, setMessages, handleSubmit], + ); + + const exampleQueries = [ + "Weather in Doha", + "What is new with Grok 2.0?", + "Count the number of r's in strawberry", + "Explain Claude 3.5 Sonnet", + ]; + + const Navbar = () => ( +
+ + + +
+ + + + + + + +

Sponsor this project on GitHub

+
+
+
+
+
+ ); + + return ( +
+ + +
+ {!hasSubmitted && ( +
+

+ MiniPerplx +

+

+ In search for minimalism and simplicity +

+
+ )} + + {!hasSubmitted && ( + +
+
+ setInput(e.target.value)} + disabled={isLoading} + className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" + onFocus={() => setShowExamples(true)} + onBlur={() => setShowExamples(false)} + /> + +
+
+ +
+
+
+ {exampleQueries.map((message, index) => ( + + ))} +
+
+
+
+ )} +
+ + + {hasSubmitted && ( + setIsAnimating(false)} + > +
+ + + + + {isEditingQuery ? ( +
+ setInput(e.target.value)} + className="flex-grow" + /> + + +
+ ) : ( + + + +

+ {lastSubmittedQuery} +

+
+ +

{lastSubmittedQuery}

+
+
+
+ )} +
+ {!isEditingQuery && ( + + + + )} +
+
+ )} +
+ +
+ {messages.map((message, index) => ( +
+ {message.role === "assistant" && message.content && ( +
+
+
+ +

Answer

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

Suggested questions

+
+
+ {suggestedQuestions.map((question, index) => ( + + ))} +
+
+ )} +
+
+
+ + + {hasSubmitted && !isAnimating && ( + +
+
+ setInput(e.target.value)} + disabled={isLoading} + className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" + /> + +
+
+
+ )} +
+
+ ); +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..0501c6c Binary files /dev/null and b/bun.lockb differ diff --git a/env.mjs b/env.mjs new file mode 100644 index 0000000..4ca19ad --- /dev/null +++ b/env.mjs @@ -0,0 +1,37 @@ +// src/env.mjs +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + /* + * Serverside Environment variables, not available on the client. + * Will throw if you access these variables on the client. + */ + server: { + GROQ_API_KEY: z.string().min(1), + TAVILY_API_KEY: z.string().min(1), + OPENWEATHER_API_KEY: z.string().min(1), + GOOGLE_MAPS_API_KEY: z.string().min(1), + }, + /* + * Environment variables available on the client (and server). + * + * 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_. + */ + client: { + NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: z.string().min(1), + }, + /* + * Due to how Next.js bundles environment variables on Edge and Client, + * we need to manually destructure them to make sure all are included in bundle. + * + * 💡 You'll get type errors if not all variables from `server` & `client` are included here. + */ + runtimeEnv: { + GROQ_API_KEY: process.env.GROQ_API_KEY, + TAVILY_API_KEY: process.env.TAVILY_API_KEY, + OPENWEATHER_API_KEY: process.env.OPENWEATHER_API_KEY, + GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY, + NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, + }, +}); diff --git a/package.json b/package.json index e0aef42..6da2a14 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@t3-oss/env-nextjs": "^0.11.0", "@tailwindcss/typography": "^0.5.13", "@types/googlemaps": "^3.43.3", "@vercel/analytics": "^1.3.1",