Refactor environment variable handling and improve API key management

- Introduced `client.ts` and `server.ts` for structured environment variable management using `@t3-oss/env-nextjs`.
- Updated references to environment variables in various files to use the new `clientEnv` and `serverEnv` objects.
- Enhanced the `next.config.mjs` file to validate environment variables during build.
- Added new dependencies: `@t3-oss/env-nextjs` and `jiti` for improved environment handling.
- Cleaned up imports and ensured consistent usage of environment variables across the application.
This commit is contained in:
simplr-sh 2025-01-06 10:28:54 +05:30
parent adf96e516f
commit 186bd3c2cc
11 changed files with 5017 additions and 3943 deletions

View File

@ -1,9 +1,10 @@
// app/actions.ts // app/actions.ts
'use server'; 'use server';
import { serverEnv } from '@/env/server';
import { xai } from '@ai-sdk/xai';
import { generateObject } from 'ai'; import { generateObject } from 'ai';
import { z } from 'zod'; import { z } from 'zod';
import { xai } from '@ai-sdk/xai';
export async function suggestQuestions(history: any[]) { export async function suggestQuestions(history: any[]) {
'use server'; 'use server';
@ -18,7 +19,7 @@ export async function suggestQuestions(history: any[]) {
topK: 7, topK: 7,
system: system:
`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. `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. 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. 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. 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 weather based converations sent to you, always generate questions that are about news, sports, or other topics that are not related to the weather.
@ -37,7 +38,7 @@ Do not use pronouns like he, she, him, his, her, etc. in the questions as they b
}; };
} }
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY; const ELEVENLABS_API_KEY = serverEnv.ELEVENLABS_API_KEY;
export async function generateSpeech(text: string, voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = "alloy") { export async function generateSpeech(text: string, voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = "alloy") {
@ -121,37 +122,37 @@ const groupTools = {
const groupPrompts = { const groupPrompts = {
web: ` web: `
You are an expert AI web search engine called MiniPerplx, designed to help users find information on the internet with no unnecessary chatter. 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.** Always **run the tool first exactly once** before composing your response. **This is non-negotiable.**
Your goals: Your goals:
- Stay concious and aware of the guidelines. - Stay concious and aware of the guidelines.
- Provide accurate, concise, and well-formatted responses. - Provide accurate, concise, and well-formatted responses.
- Avoid hallucinations or fabrications. Stick to verified facts and provide proper citations. - Avoid hallucinations or fabrications. Stick to verified facts and provide proper citations.
- Follow formatting guidelines strictly. - Follow formatting guidelines strictly.
**Today's Date:** ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })} **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. Comply with user requests to the best of your abilities using the appropriate tools. Maintain composure and follow the guidelines.
### Response Guidelines: ### Response Guidelines:
1. **Tools First:** 1. **Tools First:**
Plan the tools to run inside the 'thinking_canvas' tool. Plan the tools to run inside the 'thinking_canvas' tool.
Always run the appropriate tool before composing your response. 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.** 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. Once you get the content or results from the tools, start writing your response immediately.
2. **Content Rules:** 2. **Content Rules:**
- Responses must be informative, long and detailed, yet clear and concise like a textbook. - Responses must be informative, long and detailed, yet clear and concise like a textbook.
- Use structured answers with headings (no H1). - Use structured answers with headings (no H1).
- Prefer bullet points over plain paragraphs but points can be long. - Prefer bullet points over plain paragraphs but points can be long.
- Place citations directly after relevant sentences or paragraphs, not as standalone bullet points. - 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. - Do not truncate sentences inside citations. Always finish the sentence before placing the citation.
3. **Latex and Currency Formatting:** 3. **Latex and Currency Formatting:**
- Use '$' for inline equations and '$$' for block equations. - Use '$' for inline equations and '$$' for block equations.
- Avoid using '$' for currency. Use "USD" instead. - Avoid using '$' for currency. Use "USD" instead.
### Tool-Specific Guidelines: ### Tool-Specific Guidelines:
#### Thinking Canvas: #### Thinking Canvas:
@ -163,63 +164,63 @@ const groupPrompts = {
- Don't include the tool parameters in the 'thinking_canvas' tool except the queries of the tools. - Don't include the tool parameters in the 'thinking_canvas' tool except the queries of the tools.
#### Multi Query Web Search: #### Multi Query Web Search:
- Use this tool for multiple queries in one call. - Use this tool for multiple queries in one call.
- Specify the year or "latest" in queries to fetch recent information. - Specify the year or "latest" in queries to fetch recent information.
#### Retrieve Tool: #### Retrieve Tool:
- Use this for extracting information from specific URLs provided. - Use this for extracting information from specific URLs provided.
- Do not use this tool for general web searches. - Do not use this tool for general web searches.
#### Weather Data: #### Weather Data:
- Run the tool with the location and date parameters directly no need to plan in the thinking canvas. - Run the tool with the location and date parameters directly no need to plan in the thinking canvas.
- When you get the weather data, talk about the weather conditions and what to wear or do in that weather. - When you get the weather data, talk about the weather conditions and what to wear or do in that weather.
- Answer in paragraphs and no need of citations for this tool. - Answer in paragraphs and no need of citations for this tool.
#### Programming Tool: #### Programming Tool:
- Use this Python-only sandbox for calculations, data analysis, or visualizations. - Use this Python-only sandbox for calculations, data analysis, or visualizations.
- Include library installations (!pip install <library_name>) in the code where required. - Include library installations (!pip install <library_name>) in the code where required.
- Use 'plt.show()' for plots, and mention generated URLs for outputs. - Use 'plt.show()' for plots, and mention generated URLs for outputs.
#### Nearby Search: #### Nearby Search:
- Use location and radius parameters. Adding the country name improves accuracy. - Use location and radius parameters. Adding the country name improves accuracy.
#### Translation: #### Translation:
- Only use the text_translate tool for user-requested translations. - Only use the text_translate tool for user-requested translations.
#### Stock Charts: #### Stock Charts:
- Assume stock names from user queries. Use the programming tool with Python code including 'yfinance'. - Assume stock names from user queries. Use the programming tool with Python code including 'yfinance'.
- Once the response is ready, talk about the stock's performance and trends, and then finish with the stock chart like this ![Stock Chart](URL). - Once the response is ready, talk about the stock's performance and trends, and then finish with the stock chart like this ![Stock Chart](URL).
#### Image Search: #### Image Search:
- Analyze image details to determine tool parameters. - Analyze image details to determine tool parameters.
#### Movie/TV Show Queries: #### Movie/TV Show Queries:
- Use relevant tools for trending or specific movie/TV show information. Do not include images in responses. - 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. - For this tool make the exception of just listing the top 5 movies or TV shows in your written response.
### Prohibited Actions: ### Prohibited Actions:
- Never write your thoughts or preamble before running a tool. - Never write your thoughts or preamble before running a tool.
- Avoid running the same tool twice with same parameters. - Avoid running the same tool twice with same parameters.
- Do not include images in responses unless explicitly allowed (e.g., plots from the programming tool). - Do not include images in responses unless explicitly allowed (e.g., plots from the programming tool).
- Avoid running GUI-based Python code in the programming tool. - Avoid running GUI-based Python code in the programming tool.
- Do not run 'web_search' for stock queries. - Do not run 'web_search' for stock queries.
### Citations Rules: ### Citations Rules:
- Place citations after completing the sentence or paragraph they support. - Place citations after completing the sentence or paragraph they support.
- Format: [Source Title](URL). - Format: [Source Title](URL).
- Ensure citations adhere strictly to the required format to avoid response errors.`, - 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. 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" })}. 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. Focus on peer-reviewed papers, citations, and academic sources.
Do not talk in bullet points or lists at all costs as it is unpresentable. Do not talk in bullet points or lists at all costs as it is unpresentable.
Provide summaries, key points, and references. 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. 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. 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) Citation format: [Author et al. (Year) Title](URL)
Always run the tools first and then write the response.`, Always run the tools first and then write the response.`,
youtube: `You are a YouTube search assistant that helps find relevant videos and channels. 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. 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" })}. The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}.
Do not 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. Do not talk in bullet points or lists at all costs.
Provide complete explainations of the videos in paragraphs. Provide complete explainations of the videos in paragraphs.
@ -227,7 +228,7 @@ const groupPrompts = {
Citation format: [Title](URL ending with parameter t=<no_of_seconds>) Citation format: [Title](URL ending with parameter t=<no_of_seconds>)
Do not provide the video thumbnail in the response at all costs.`, Do not provide the video thumbnail in the response at all costs.`,
x: `You are a X/Twitter content curator that helps find relevant posts. x: `You are a X/Twitter content curator that helps find relevant posts.
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" })}.
Once you get the content from the tools only write in paragraphs. Once you get the content from the tools only write in paragraphs.
No need to say that you are calling the tool, just call the tools first and run the search; 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. then talk in long details in 2-6 paragraphs.

View File

@ -1,21 +1,21 @@
// /app/api/chat/route.ts // /app/api/chat/route.ts
import { z } from "zod"; import { getGroupConfig } from "@/app/actions";
import { xai } from '@ai-sdk/xai' import { serverEnv } from "@/env/server";
import Exa from 'exa-js' import { xai } from '@ai-sdk/xai';
import {
convertToCoreMessages,
streamText,
tool,
smoothStream
} from "ai";
import { BlobRequestAbortedError, put } from '@vercel/blob';
import CodeInterpreter from "@e2b/code-interpreter"; import CodeInterpreter from "@e2b/code-interpreter";
import FirecrawlApp from '@mendable/firecrawl-js'; import FirecrawlApp from '@mendable/firecrawl-js';
import { tavily } from '@tavily/core' 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 { Ratelimit } from "@upstash/ratelimit"; // for deno: see above
import { Redis } from "@upstash/redis"; // see below for cloudflare and fastly adapters import { Redis } from "@upstash/redis"; // see below for cloudflare and fastly adapters
import { BlobRequestAbortedError, put } from '@vercel/blob';
import {
convertToCoreMessages,
smoothStream,
streamText,
tool
} from "ai";
import Exa from 'exa-js';
import { z } from "zod";
// Allow streaming responses up to 60 seconds // Allow streaming responses up to 60 seconds
export const maxDuration = 120; export const maxDuration = 120;
@ -182,7 +182,7 @@ export async function POST(req: Request) {
searchDepth: ("basic" | "advanced")[]; searchDepth: ("basic" | "advanced")[];
exclude_domains?: string[]; exclude_domains?: string[];
}) => { }) => {
const apiKey = process.env.TAVILY_API_KEY; const apiKey = serverEnv.TAVILY_API_KEY;
const tvly = tavily({ apiKey }); const tvly = tavily({ apiKey });
const includeImageDescriptions = true; const includeImageDescriptions = true;
@ -258,7 +258,7 @@ export async function POST(req: Request) {
}), }),
execute: async ({ query }: { query: string }) => { execute: async ({ query }: { query: string }) => {
try { try {
const exa = new Exa(process.env.EXA_API_KEY as string); const exa = new Exa(serverEnv.EXA_API_KEY as string);
const result = await exa.searchAndContents( const result = await exa.searchAndContents(
query, query,
@ -304,7 +304,7 @@ export async function POST(req: Request) {
query: z.string().describe("The search query for movies/TV shows"), query: z.string().describe("The search query for movies/TV shows"),
}), }),
execute: async ({ query }: { query: string }) => { execute: async ({ query }: { query: string }) => {
const TMDB_API_KEY = process.env.TMDB_API_KEY; const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
try { try {
@ -389,7 +389,7 @@ export async function POST(req: Request) {
description: "Get trending movies from TMDB", description: "Get trending movies from TMDB",
parameters: z.object({}), parameters: z.object({}),
execute: async () => { execute: async () => {
const TMDB_API_KEY = process.env.TMDB_API_KEY; const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
try { try {
@ -423,7 +423,7 @@ export async function POST(req: Request) {
description: "Get trending TV shows from TMDB", description: "Get trending TV shows from TMDB",
parameters: z.object({}), parameters: z.object({}),
execute: async () => { execute: async () => {
const TMDB_API_KEY = process.env.TMDB_API_KEY; const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
try { try {
@ -460,7 +460,7 @@ export async function POST(req: Request) {
}), }),
execute: async ({ query }: { query: string }) => { execute: async ({ query }: { query: string }) => {
try { try {
const exa = new Exa(process.env.EXA_API_KEY as string); const exa = new Exa(serverEnv.EXA_API_KEY as string);
// Search academic papers with content summary // Search academic papers with content summary
const result = await exa.searchAndContents( const result = await exa.searchAndContents(
@ -516,7 +516,7 @@ export async function POST(req: Request) {
}), }),
execute: async ({ query, no_of_results }: { query: string, no_of_results: number }) => { execute: async ({ query, no_of_results }: { query: string, no_of_results: number }) => {
try { try {
const exa = new Exa(process.env.EXA_API_KEY as string); const exa = new Exa(serverEnv.EXA_API_KEY as string);
// Simple search to get YouTube URLs only // Simple search to get YouTube URLs only
const searchResult = await exa.search( const searchResult = await exa.search(
@ -545,17 +545,17 @@ export async function POST(req: Request) {
try { try {
// Fetch detailed info from our endpoints // Fetch detailed info from our endpoints
const [detailsResponse, captionsResponse, timestampsResponse] = await Promise.all([ const [detailsResponse, captionsResponse, timestampsResponse] = await Promise.all([
fetch(`${process.env.YT_ENDPOINT}/video-data`, { fetch(`${serverEnv.YT_ENDPOINT}/video-data`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: result.url }) body: JSON.stringify({ url: result.url })
}).then(res => res.ok ? res.json() : null), }).then(res => res.ok ? res.json() : null),
fetch(`${process.env.YT_ENDPOINT}/video-captions`, { fetch(`${serverEnv.YT_ENDPOINT}/video-captions`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: result.url }) body: JSON.stringify({ url: result.url })
}).then(res => res.ok ? res.text() : null), }).then(res => res.ok ? res.text() : null),
fetch(`${process.env.YT_ENDPOINT}/video-timestamps`, { fetch(`${serverEnv.YT_ENDPOINT}/video-timestamps`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: result.url }) body: JSON.stringify({ url: result.url })
@ -595,7 +595,7 @@ export async function POST(req: Request) {
url: z.string().describe("The URL to retrieve the information from."), url: z.string().describe("The URL to retrieve the information from."),
}), }),
execute: async ({ url }: { url: string }) => { execute: async ({ url }: { url: string }) => {
const app = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY }); const app = new FirecrawlApp({ apiKey: serverEnv.FIRECRAWL_API_KEY });
try { try {
const content = await app.scrapeUrl(url); const content = await app.scrapeUrl(url);
if (!content.success || !content.metadata) { if (!content.success || !content.metadata) {
@ -625,7 +625,7 @@ export async function POST(req: Request) {
lon: z.number().describe("The longitude of the location."), lon: z.number().describe("The longitude of the location."),
}), }),
execute: async ({ lat, lon }: { lat: number; lon: number }) => { execute: async ({ lat, lon }: { lat: number; lon: number }) => {
const apiKey = process.env.OPENWEATHER_API_KEY; const apiKey = serverEnv.OPENWEATHER_API_KEY;
const response = await fetch( const response = await fetch(
`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}`, `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}`,
); );
@ -645,7 +645,7 @@ export async function POST(req: Request) {
console.log("Title:", title); console.log("Title:", title);
console.log("Icon:", icon); console.log("Icon:", icon);
const sandbox = await CodeInterpreter.create(process.env.SANDBOX_TEMPLATE_ID!); const sandbox = await CodeInterpreter.create(serverEnv.SANDBOX_TEMPLATE_ID!);
const execution = await sandbox.runCode(code); const execution = await sandbox.runCode(code);
let message = ""; let message = "";
let images = []; let images = [];
@ -729,14 +729,14 @@ export async function POST(req: Request) {
execute: async ({ query, coordinates }: { query: string; coordinates: number[] }) => { execute: async ({ query, coordinates }: { query: string; coordinates: number[] }) => {
try { try {
// Forward geocoding with Google Maps API // Forward geocoding with Google Maps API
const googleApiKey = process.env.GOOGLE_MAPS_API_KEY; const googleApiKey = serverEnv.GOOGLE_MAPS_API_KEY;
const googleResponse = await fetch( const googleResponse = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${googleApiKey}` `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${googleApiKey}`
); );
const googleData = await googleResponse.json(); const googleData = await googleResponse.json();
// Reverse geocoding with Mapbox // Reverse geocoding with Mapbox
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN; const mapboxToken = serverEnv.MAPBOX_ACCESS_TOKEN;
const [lat, lng] = coordinates; const [lat, lng] = coordinates;
const mapboxResponse = await fetch( const mapboxResponse = await fetch(
`https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&access_token=${mapboxToken}` `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&access_token=${mapboxToken}`
@ -805,7 +805,7 @@ export async function POST(req: Request) {
location?: string; location?: string;
radius?: number; radius?: number;
}) => { }) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN; const mapboxToken = serverEnv.MAPBOX_ACCESS_TOKEN;
let proximity = ''; let proximity = '';
if (location) { if (location) {
@ -854,9 +854,9 @@ export async function POST(req: Request) {
from: z.string().describe("The source language (optional, will be auto-detected if not provided)."), from: z.string().describe("The source language (optional, will be auto-detected if not provided)."),
}), }),
execute: async ({ text, to, from }: { text: string; to: string; from?: string }) => { execute: async ({ text, to, from }: { text: string; to: string; from?: string }) => {
const key = process.env.AZURE_TRANSLATOR_KEY; const key = serverEnv.AZURE_TRANSLATOR_KEY;
const endpoint = "https://api.cognitive.microsofttranslator.com"; const endpoint = "https://api.cognitive.microsofttranslator.com";
const location = process.env.AZURE_TRANSLATOR_LOCATION; const location = serverEnv.AZURE_TRANSLATOR_LOCATION;
const url = `${endpoint}/translate?api-version=3.0&to=${to}${from ? `&from=${from}` : ''}`; const url = `${endpoint}/translate?api-version=3.0&to=${to}${from ? `&from=${from}` : ''}`;
@ -893,14 +893,14 @@ export async function POST(req: Request) {
type: string; type: string;
radius: number; radius: number;
}) => { }) => {
const apiKey = process.env.TRIPADVISOR_API_KEY; const apiKey = serverEnv.TRIPADVISOR_API_KEY;
let finalLat = latitude; let finalLat = latitude;
let finalLng = longitude; let finalLng = longitude;
try { try {
// Try geocoding first // Try geocoding first
const geocodingData = await fetch( const geocodingData = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${process.env.GOOGLE_MAPS_API_KEY}` `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${serverEnv.GOOGLE_MAPS_API_KEY}`
); );
const geocoding = await geocodingData.json(); const geocoding = await geocodingData.json();
@ -1007,7 +1007,7 @@ export async function POST(req: Request) {
// Get timezone for the location // Get timezone for the location
const tzResponse = await fetch( const tzResponse = await fetch(
`https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}&timestamp=${Math.floor(Date.now() / 1000)}&key=${process.env.GOOGLE_MAPS_API_KEY}` `https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}&timestamp=${Math.floor(Date.now() / 1000)}&key=${serverEnv.GOOGLE_MAPS_API_KEY}`
); );
const tzData = await tzResponse.json(); const tzData = await tzResponse.json();
const timezone = tzData.timeZoneId || 'UTC'; const timezone = tzData.timeZoneId || 'UTC';
@ -1131,7 +1131,7 @@ export async function POST(req: Request) {
execute: async ({ flight_number }: { flight_number: string }) => { execute: async ({ flight_number }: { flight_number: string }) => {
try { try {
const response = await fetch( const response = await fetch(
`https://api.aviationstack.com/v1/flights?access_key=${process.env.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}` `https://api.aviationstack.com/v1/flights?access_key=${serverEnv.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}`
); );
return await response.json(); return await response.json();
} catch (error) { } catch (error) {

View File

@ -1,10 +1,10 @@
import { list, del, ListBlobResult } from '@vercel/blob'; import { del, list, ListBlobResult } from '@vercel/blob';
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'edge'; export const runtime = 'edge';
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) { if (req.headers.get('Authorization') !== `Bearer ${serverEnv.CRON_SECRET}`) {
return new NextResponse('Unauthorized', { status: 401 }); return new NextResponse('Unauthorized', { status: 401 });
} }

View File

@ -1,13 +1,14 @@
"use client"; "use client";
import { ThemeProvider } from "next-themes" import { clientEnv } from "@/env/client";
import { ReactNode } from "react" import { ThemeProvider } from "next-themes";
import posthog from 'posthog-js' import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react' import { PostHogProvider } from 'posthog-js/react';
import { ReactNode } from "react";
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { posthog.init(clientEnv.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, api_host: clientEnv.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'always', person_profiles: 'always',
}) })
} }

View File

@ -1,7 +1,8 @@
import React, { useEffect, useRef, useCallback } from 'react'; import { clientEnv } from "@/env/client";
import { cn } from "@/lib/utils";
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl/dist/mapbox-gl.css';
import { cn } from "@/lib/utils"; import React, { useCallback, useEffect, useRef } from 'react';
interface Location { interface Location {
lat: number; lat: number;
@ -38,7 +39,7 @@ interface Place {
timezone?: string; timezone?: string;
} }
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || ''; mapboxgl.accessToken = clientEnv.NEXT_PUBLIC_MAPBOX_TOKEN || '';
interface InteractiveMapProps { interface InteractiveMapProps {
center: Location; center: Location;

View File

@ -1,10 +1,11 @@
// /app/components/map-components.tsx // /app/components/map-components.tsx
import React, { useEffect, useRef } from 'react'; import { Skeleton } from "@/components/ui/skeleton";
import { clientEnv } from "@/env/client";
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl/dist/mapbox-gl.css';
import { Skeleton } from "@/components/ui/skeleton"; import React, { useEffect, useRef } from 'react';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || ''; mapboxgl.accessToken = clientEnv.NEXT_PUBLIC_MAPBOX_TOKEN || '';
interface Location { interface Location {
lat: number; lat: number;
@ -171,4 +172,4 @@ const MapContainer: React.FC<MapContainerProps> = ({
); );
}; };
export { MapComponent, MapSkeleton, MapContainer }; export { MapComponent, MapContainer, MapSkeleton };

16
env/client.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// https://env.t3.gg/docs/nextjs#create-your-schema
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'
export const clientEnv = createEnv({
client: {
NEXT_PUBLIC_MAPBOX_TOKEN: z.string().min(1),
NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1),
NEXT_PUBLIC_POSTHOG_HOST: z.string().min(1).url(),
},
runtimeEnv: {
NEXT_PUBLIC_MAPBOX_TOKEN: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
},
})

24
env/server.ts vendored Normal file
View File

@ -0,0 +1,24 @@
// https://env.t3.gg/docs/nextjs#create-your-schema
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'
export const serverEnv = createEnv({
server: {
ELEVENLABS_API_KEY: z.string().min(1),
TAVILY_API_KEY: z.string().min(1),
EXA_API_KEY: z.string().min(1),
TMDB_API_KEY: z.string().min(1),
YT_ENDPOINT: z.string().min(1),
FIRECRAWL_API_KEY: z.string().min(1),
OPENWEATHER_API_KEY: z.string().min(1),
SANDBOX_TEMPLATE_ID: z.string().min(1),
GOOGLE_MAPS_API_KEY: z.string().min(1),
MAPBOX_ACCESS_TOKEN: z.string().min(1),
AZURE_TRANSLATOR_KEY: z.string().min(1),
AZURE_TRANSLATOR_LOCATION: z.string().min(1),
TRIPADVISOR_API_KEY: z.string().min(1),
AVIATION_STACK_API_KEY: z.string().min(1),
CRON_SECRET: z.string().min(1),
},
experimental__runtimeEnv: process.env,
})

View File

@ -1,85 +1,94 @@
// https://env.t3.gg/docs/nextjs#validate-schema-on-build-(recommended)
import { createJiti } from 'jiti'
import { fileURLToPath } from 'node:url'
const jiti = createJiti(fileURLToPath(import.meta.url))
// Import env here to validate during build. Using jiti we can import .ts files :)
jiti.import('./env/server')
jiti.import('./env/client')
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
transpilePackages: ["geist"], transpilePackages: ['geist'],
async headers() { async headers() {
return [ return [
{ {
source: '/(.*)', source: '/(.*)',
headers: [ headers: [
{ {
key: 'X-Content-Type-Options', key: 'X-Content-Type-Options',
value: 'nosniff', value: 'nosniff',
}, },
{ {
key: 'X-Frame-Options', key: 'X-Frame-Options',
value: 'DENY', value: 'DENY',
}, },
{ {
key: 'Referrer-Policy', key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin', value: 'strict-origin-when-cross-origin',
}, },
], ],
}, },
] ]
}, },
images: { images: {
dangerouslyAllowSVG: true, dangerouslyAllowSVG: true,
remotePatterns: [ remotePatterns: [
{ {
protocol: 'https', protocol: 'https',
hostname: 'www.google.com', hostname: 'www.google.com',
port: '', port: '',
pathname: '/s2/favicons', pathname: '/s2/favicons',
}, },
{ {
protocol: 'https', protocol: 'https',
hostname: 'api.producthunt.com', hostname: 'api.producthunt.com',
port: '', port: '',
pathname: '/widgets/embed-image/v1/featured.svg', pathname: '/widgets/embed-image/v1/featured.svg',
}, },
{ {
protocol: 'https', protocol: 'https',
hostname: 'metwm7frkvew6tn1.public.blob.vercel-storage.com', hostname: 'metwm7frkvew6tn1.public.blob.vercel-storage.com',
port: '', port: '',
pathname: "**" pathname: '**',
}, },
// upload.wikimedia.org // upload.wikimedia.org
{ {
protocol: 'https', protocol: 'https',
hostname: 'upload.wikimedia.org', hostname: 'upload.wikimedia.org',
port: '', port: '',
pathname: '**' pathname: '**',
}, },
// media.theresanaiforthat.com // media.theresanaiforthat.com
{ {
protocol: 'https', protocol: 'https',
hostname: 'media.theresanaiforthat.com', hostname: 'media.theresanaiforthat.com',
port: '', port: '',
pathname: '**' pathname: '**',
}, },
// www.uneed.best // www.uneed.best
{ {
protocol: 'https', protocol: 'https',
hostname: 'www.uneed.best', hostname: 'www.uneed.best',
port: '', port: '',
pathname: '**' pathname: '**',
}, },
// image.tmdb.org // image.tmdb.org
{ {
protocol: 'https', protocol: 'https',
hostname: 'image.tmdb.org', hostname: 'image.tmdb.org',
port: '', port: '',
pathname: '/t/p/original/**' pathname: '/t/p/original/**',
}, },
// image.tmdb.org // image.tmdb.org
{ {
protocol: 'https', protocol: 'https',
hostname: 'image.tmdb.org', hostname: 'image.tmdb.org',
port: '', port: '',
pathname: '/**' pathname: '/**',
}, },
] ],
}, },
}; }
export default nextConfig; export default nextConfig

View File

@ -29,6 +29,7 @@
"@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@t3-oss/env-nextjs": "^0.11.1",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@tavily/core": "^0.0.2", "@tavily/core": "^0.0.2",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
@ -54,6 +55,7 @@
"geist": "^1.3.1", "geist": "^1.3.1",
"google-auth-library": "^9.14.1", "google-auth-library": "^9.14.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"jiti": "^2.4.2",
"katex": "^0.16.11", "katex": "^0.16.11",
"lucide-react": "^0.424.0", "lucide-react": "^0.424.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",

File diff suppressed because it is too large Load Diff