Refactor dependencies and add new features
This commit is contained in:
parent
be141ead3a
commit
d2c299350e
@ -1,16 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { createAzure } from '@ai-sdk/azure';
|
||||
import { anthropic } from '@ai-sdk/anthropic'
|
||||
import {
|
||||
convertToCoreMessages,
|
||||
streamText,
|
||||
tool,
|
||||
experimental_createProviderRegistry
|
||||
} from "ai";
|
||||
import { createAnthropicVertex } from 'anthropic-vertex-ai';
|
||||
import { BlobRequestAbortedError, put } from '@vercel/blob';
|
||||
import { CodeInterpreter } from "@e2b/code-interpreter";
|
||||
import FirecrawlApp from '@mendable/firecrawl-js';
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
|
||||
// Allow streaming responses up to 60 seconds
|
||||
export const maxDuration = 120;
|
||||
@ -21,33 +20,9 @@ const azure = createAzure({
|
||||
apiKey: process.env.AZURE_API_KEY,
|
||||
});
|
||||
|
||||
// Helper function to get Google credentials
|
||||
// You can encode your service account key using the following command:
|
||||
// base64 -i /path/to/your-service-account-key.json | tr -d '\n' > encoded_credentials.txt
|
||||
// Then set the GOOGLE_APPLICATION_CREDENTIALS_BASE64 environment variable to the contents of encoded_credentials.txt
|
||||
function getCredentials() {
|
||||
const credentialsBase64 = process.env.GOOGLE_APPLICATION_CREDENTIALS_BASE64;
|
||||
if (!credentialsBase64) {
|
||||
throw new Error('GOOGLE_APPLICATION_CREDENTIALS_BASE64 environment variable is not set');
|
||||
}
|
||||
return JSON.parse(Buffer.from(credentialsBase64, 'base64').toString());
|
||||
}
|
||||
|
||||
// Google Vertex setup for Anthropic
|
||||
const auth = new GoogleAuth({
|
||||
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
credentials: getCredentials(),
|
||||
});
|
||||
|
||||
const anthropicVertex = createAnthropicVertex({
|
||||
region: process.env.GOOGLE_VERTEX_REGION,
|
||||
projectId: process.env.GOOGLE_VERTEX_PROJECT_ID,
|
||||
googleAuth: auth,
|
||||
});
|
||||
|
||||
// Provider registry
|
||||
const registry = experimental_createProviderRegistry({
|
||||
anthropicVertex,
|
||||
anthropic,
|
||||
azure,
|
||||
});
|
||||
|
||||
@ -75,6 +50,7 @@ export async function POST(req: Request) {
|
||||
topP: 0.5,
|
||||
frequencyPenalty: 0,
|
||||
presencePenalty: 0,
|
||||
experimental_activeTools: ["get_weather_data","programming", "web_search", "text_translate"],
|
||||
system: `
|
||||
You are an expert AI web search engine called MiniPerplx, that helps users find information on the internet with no bullshit talks.
|
||||
Always start with running the tool(s) and then and then only write your response AT ALL COSTS!!
|
||||
|
||||
@ -123,6 +123,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import FormComponent from '@/components/ui/form-component';
|
||||
import WeatherChart from '@/components/weather-chart';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
@ -320,117 +321,6 @@ GPT-4o has been re-enabled! You can use it by selecting the model from the dropd
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<Card className="my-4 shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-neutral-800 dark:text-neutral-100">Weather Forecast for {result.city.name}</CardTitle>
|
||||
<CardDescription className="text-neutral-600 dark:text-neutral-400">
|
||||
Showing min and max temperatures for the next 5 days
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
|
||||
stroke="#9CA3AF"
|
||||
/>
|
||||
<YAxis
|
||||
domain={[Math.floor(minTemp) - 2, Math.ceil(maxTemp) + 2]}
|
||||
tickFormatter={(value) => `${value}°C`}
|
||||
stroke="#9CA3AF"
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="minTemp"
|
||||
stroke="var(--color-minTemp)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Min Temp."
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="maxTemp"
|
||||
stroke="var(--color-maxTemp)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Max Temp."
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none text-neutral-800 dark:text-neutral-100">
|
||||
{result.city.name}, {result.city.country}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Next 5 days forecast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
WeatherChart.displayName = 'WeatherChart';
|
||||
|
||||
|
||||
// Google Maps components
|
||||
@ -1144,20 +1034,6 @@ GPT-4o has been re-enabled! You can use it by selecting the model from the dropd
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="my-4 shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="h-6 w-3/4 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px] bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return <WeatherChart result={result} />;
|
||||
}
|
||||
|
||||
|
||||
207
components/map-components.tsx
Normal file
207
components/map-components.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import React, { useRef, useState, useEffect, useCallback, memo } from 'react';
|
||||
import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { MapPin, Star } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
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<HTMLDivElement>(null);
|
||||
const [mapError, setMapError] = useState<string | null>(null);
|
||||
const googleMapRef = useRef<google.maps.Map | null>(null);
|
||||
const markersRef = useRef<google.maps.marker.AdvancedMarkerElement[]>([]);
|
||||
|
||||
const initializeMap = useCallback(async () => {
|
||||
if (mapRef.current && isValidCoordinate(center.lat) && isValidCoordinate(center.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: center,
|
||||
zoom: 14,
|
||||
mapId: "347ff92e0c7225cf",
|
||||
});
|
||||
} else {
|
||||
googleMapRef.current.setCenter(center);
|
||||
}
|
||||
|
||||
markersRef.current.forEach(marker => marker.map = null);
|
||||
markersRef.current = [];
|
||||
|
||||
places.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');
|
||||
}
|
||||
}, [center, places]);
|
||||
|
||||
useEffect(() => {
|
||||
loadGoogleMapsScript(() => {
|
||||
try {
|
||||
initializeMap();
|
||||
} catch (error) {
|
||||
console.error('Error initializing map:', error);
|
||||
setMapError('Failed to initialize Google Maps');
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
markersRef.current.forEach(marker => marker.map = null);
|
||||
};
|
||||
}, [initializeMap]);
|
||||
|
||||
if (mapError) {
|
||||
return <div className="h-64 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 text-neutral-800 dark:text-neutral-200">{mapError}</div>;
|
||||
}
|
||||
|
||||
return <div ref={mapRef} className="w-full h-64" />;
|
||||
});
|
||||
|
||||
MapComponent.displayName = 'MapComponent';
|
||||
|
||||
const MapSkeleton = () => (
|
||||
<Skeleton className="w-full h-64 bg-neutral-200 dark:bg-neutral-700" />
|
||||
);
|
||||
|
||||
const PlaceDetails = ({ place }: { place: any }) => (
|
||||
<div className="flex justify-between items-start py-2">
|
||||
<div>
|
||||
<h4 className="font-semibold text-neutral-800 dark:text-neutral-200">{place.name}</h4>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 max-w-[200px]" title={place.vicinity}>
|
||||
{place.vicinity}
|
||||
</p>
|
||||
</div>
|
||||
{place.rating && (
|
||||
<Badge variant="secondary" className="flex items-center bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">
|
||||
<Star className="h-3 w-3 mr-1 text-yellow-400" />
|
||||
{place.rating} ({place.user_ratings_total})
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="aspect-video w-full">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
loading="lazy"
|
||||
allowFullScreen
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={mapUrl}
|
||||
className='rounded-xl'
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Card className="w-full my-4 overflow-hidden shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-neutral-800 dark:text-neutral-100">
|
||||
<MapPin className="h-5 w-5 text-primary" />
|
||||
<span>{place.name}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<MapEmbed location={location} />
|
||||
<div className="mt-4 space-y-2 text-neutral-800 dark:text-neutral-200">
|
||||
<p><strong>Address:</strong> {place.formatted_address}</p>
|
||||
{place.rating && (
|
||||
<div className="flex items-center">
|
||||
<strong className="mr-2">Rating:</strong>
|
||||
<Badge variant="secondary" className="flex items-center bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">
|
||||
<Star className="h-3 w-3 mr-1 text-yellow-400" />
|
||||
{place.rating}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{place.opening_hours && (
|
||||
<p><strong>Open now:</strong> {place.opening_hours.open_now ? 'Yes' : 'No'}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
FindPlaceResult.displayName = 'FindPlaceResult';
|
||||
|
||||
const TextSearchResult = React.memo(({ result }: { result: any }) => {
|
||||
const centerLocation = result.results[0]?.geometry?.location;
|
||||
const mapLocation = centerLocation ? `${centerLocation.lat},${centerLocation.lng}` : '';
|
||||
|
||||
return (
|
||||
<Card className="w-full my-4 overflow-hidden shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-neutral-800 dark:text-neutral-100">
|
||||
<MapPin className="h-5 w-5 text-primary" />
|
||||
<span>Text Search Results</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{mapLocation && <MapComponent center={centerLocation} places={result.results} />}
|
||||
<Accordion type="single" collapsible className="w-full mt-4">
|
||||
<AccordionItem value="place-details">
|
||||
<AccordionTrigger className="text-neutral-800 dark:text-neutral-200">Place Details</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 max-h-64 overflow-y-auto">
|
||||
{result.results.map((place: any, index: number) => (
|
||||
<PlaceDetails key={index} place={place} />
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
TextSearchResult.displayName = 'TextSearchResult';
|
||||
|
||||
export { MapComponent, MapSkeleton, FindPlaceResult, TextSearchResult };
|
||||
208
components/markdown-render.tsx
Normal file
208
components/markdown-render.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import Marked, { ReactRenderer } from 'marked-react';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { fetchMetadata } from '@/app/actions';
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface CitationLink {
|
||||
text: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface LinkMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const isValidUrl = (str: string) => {
|
||||
try {
|
||||
new URL(str);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => {
|
||||
const [metadataCache, setMetadataCache] = useState<Record<string, LinkMetadata>>({});
|
||||
|
||||
const citationLinks = useMemo<CitationLink[]>(() => {
|
||||
return Array.from(content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)).map(([_, text, link]) => ({
|
||||
text,
|
||||
link,
|
||||
}));
|
||||
}, [content]);
|
||||
|
||||
const fetchMetadataWithCache = useCallback(async (url: string) => {
|
||||
if (metadataCache[url]) {
|
||||
return metadataCache[url];
|
||||
}
|
||||
|
||||
const metadata = await fetchMetadata(url);
|
||||
if (metadata) {
|
||||
setMetadataCache(prev => ({ ...prev, [url]: metadata }));
|
||||
}
|
||||
return metadata;
|
||||
}, [metadataCache]);
|
||||
|
||||
const CodeBlock = ({ language, children }: { language: string | undefined; children: string }) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(children);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<SyntaxHighlighter
|
||||
language={language || 'text'}
|
||||
style={oneDark}
|
||||
showLineNumbers
|
||||
wrapLines
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighter>
|
||||
<Button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-2 bg-neutral-700 dark:bg-neutral-600 bg-opacity-80 rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
{isCopied ? <Check size={16} className="text-green-500" /> : <Copy size={16} className="text-neutral-200" />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkPreview = ({ href }: { href: string }) => {
|
||||
const [metadata, setMetadata] = useState<LinkMetadata | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetchMetadataWithCache(href).then((data) => {
|
||||
setMetadata(data);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [href]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<div className="animate-spin h-5 w-5 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const domain = new URL(href).hostname;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-2 bg-white dark:bg-neutral-800 rounded-md shadow-md overflow-hidden">
|
||||
<div className="flex items-center space-x-2 p-3 bg-neutral-100 dark:bg-neutral-700">
|
||||
<Image
|
||||
src={`https://www.google.com/s2/favicons?domain=${domain}&sz=256`}
|
||||
alt="Favicon"
|
||||
width={20}
|
||||
height={20}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300 truncate">{domain}</span>
|
||||
</div>
|
||||
<div className="px-3 pb-3">
|
||||
<h3 className="text-base font-semibold text-neutral-800 dark:text-neutral-200 line-clamp-2">
|
||||
{metadata?.title || "Untitled"}
|
||||
</h3>
|
||||
{metadata?.description && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1 line-clamp-2">
|
||||
{metadata.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHoverCard = (href: string, text: React.ReactNode, isCitation: boolean = false) => {
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={isCitation ? "cursor-help text-sm text-primary py-0.5 px-1.5 m-0 bg-neutral-200 dark:bg-neutral-700 rounded-full no-underline" : "text-teal-600 dark:text-teal-400 no-underline hover:underline"}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="w-80 p-0 shadow-lg"
|
||||
>
|
||||
<LinkPreview href={href} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
};
|
||||
|
||||
const renderer: Partial<ReactRenderer> = {
|
||||
paragraph(children) {
|
||||
return <p className="my-4 text-neutral-800 dark:text-neutral-200">{children}</p>;
|
||||
},
|
||||
code(children, language) {
|
||||
return <CodeBlock language={language}>{String(children)}</CodeBlock>;
|
||||
},
|
||||
link(href, text) {
|
||||
const citationIndex = citationLinks.findIndex(link => link.link === href);
|
||||
if (citationIndex !== -1) {
|
||||
return (
|
||||
<sup>
|
||||
{renderHoverCard(href, citationIndex + 1, true)}
|
||||
</sup>
|
||||
);
|
||||
}
|
||||
return isValidUrl(href) ? renderHoverCard(href, text) : <a href={href} className="text-blue-600 dark:text-blue-400 hover:underline">{text}</a>;
|
||||
},
|
||||
heading(children, level) {
|
||||
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
|
||||
const className = `text-${4 - level}xl font-bold my-4 text-neutral-800 dark:text-neutral-100`;
|
||||
return <HeadingTag className={className}>{children}</HeadingTag>;
|
||||
},
|
||||
list(children, ordered) {
|
||||
const ListTag = ordered ? 'ol' : 'ul';
|
||||
return <ListTag className="list-inside list-disc my-4 pl-4 text-neutral-800 dark:text-neutral-200">{children}</ListTag>;
|
||||
},
|
||||
listItem(children) {
|
||||
return <li className="my-2 text-neutral-800 dark:text-neutral-200">{children}</li>;
|
||||
},
|
||||
blockquote(children) {
|
||||
return <blockquote className="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic my-4 text-neutral-700 dark:text-neutral-300">{children}</blockquote>;
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="markdown-body dark:text-neutral-200">
|
||||
<Marked renderer={renderer}>{content}</Marked>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownRenderer;
|
||||
@ -24,7 +24,7 @@ interface ModelSwitcherProps {
|
||||
|
||||
const models = [
|
||||
{ value: "azure:gpt4o-mini", label: "GPT-4o Mini", icon: Zap, description: "High speed, good quality", color: "emerald" },
|
||||
{ value: "anthropicVertex:claude-3-5-sonnet@20240620", label: "Claude", icon: Sparkles, description: "High quality, lower speed", color: "indigo" },
|
||||
{ value: "anthropic:claude-3-5-sonnet-latest", label: "Claude", icon: Sparkles, description: "High quality, lower speed", color: "indigo" },
|
||||
{ value: "azure:gpt-4o", label: "GPT-4o", icon: Cpu, description: "Higher quality, normal speed", color: "blue" },
|
||||
];
|
||||
|
||||
|
||||
118
components/weather-chart.tsx
Normal file
118
components/weather-chart.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Line, LineChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts";
|
||||
import { Card, CardHeader, CardContent, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
|
||||
interface WeatherDataPoint {
|
||||
date: string;
|
||||
minTemp: number;
|
||||
maxTemp: number;
|
||||
}
|
||||
|
||||
interface WeatherChartProps {
|
||||
result: any;
|
||||
}
|
||||
|
||||
const WeatherChart: React.FC<WeatherChartProps> = 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)),
|
||||
}));
|
||||
|
||||
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);
|
||||
|
||||
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 (
|
||||
<Card className="my-4 shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-neutral-800 dark:text-neutral-100">Weather Forecast for {result.city.name}</CardTitle>
|
||||
<CardDescription className="text-neutral-600 dark:text-neutral-400">
|
||||
Showing min and max temperatures for the next 5 days
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
|
||||
stroke="#9CA3AF"
|
||||
/>
|
||||
<YAxis
|
||||
domain={[Math.floor(minTemp) - 2, Math.ceil(maxTemp) + 2]}
|
||||
tickFormatter={(value) => `${value}°C`}
|
||||
stroke="#9CA3AF"
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="minTemp"
|
||||
stroke="var(--color-minTemp)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Min Temp."
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="maxTemp"
|
||||
stroke="var(--color-maxTemp)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Max Temp."
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none text-neutral-800 dark:text-neutral-100">
|
||||
{result.city.name}, {result.city.country}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Next 5 days forecast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
WeatherChart.displayName = 'WeatherChart';
|
||||
|
||||
export default WeatherChart;
|
||||
@ -9,9 +9,11 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^0.0.51",
|
||||
"@ai-sdk/azure": "^0.0.31",
|
||||
"@ai-sdk/cohere": "latest",
|
||||
"@ai-sdk/google": "^0.0.46",
|
||||
"@ai-sdk/google": "^0.0.52",
|
||||
"@ai-sdk/groq": "^0.0.1",
|
||||
"@ai-sdk/mistral": "^0.0.41",
|
||||
"@ai-sdk/openai": "^0.0.58",
|
||||
"@e2b/code-interpreter": "^0.0.8",
|
||||
|
||||
@ -5,6 +5,9 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@ai-sdk/anthropic':
|
||||
specifier: ^0.0.51
|
||||
version: 0.0.51(zod@3.23.8)
|
||||
'@ai-sdk/azure':
|
||||
specifier: ^0.0.31
|
||||
version: 0.0.31(zod@3.23.8)
|
||||
@ -12,8 +15,11 @@ dependencies:
|
||||
specifier: latest
|
||||
version: 0.0.25(zod@3.23.8)
|
||||
'@ai-sdk/google':
|
||||
specifier: ^0.0.46
|
||||
version: 0.0.46(zod@3.23.8)
|
||||
specifier: ^0.0.52
|
||||
version: 0.0.52(zod@3.23.8)
|
||||
'@ai-sdk/groq':
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1(zod@3.23.8)
|
||||
'@ai-sdk/mistral':
|
||||
specifier: ^0.0.41
|
||||
version: 0.0.41(zod@3.23.8)
|
||||
@ -91,7 +97,7 @@ dependencies:
|
||||
version: 1.4.2
|
||||
ai:
|
||||
specifier: latest
|
||||
version: 3.4.9(openai@4.67.2)(react@18.3.1)(svelte@4.2.19)(vue@3.5.11)(zod@3.23.8)
|
||||
version: 3.4.18(openai@4.67.2)(react@18.3.1)(svelte@4.2.19)(vue@3.5.11)(zod@3.23.8)
|
||||
anthropic-vertex-ai:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(zod@3.23.8)
|
||||
@ -226,6 +232,17 @@ devDependencies:
|
||||
|
||||
packages:
|
||||
|
||||
/@ai-sdk/anthropic@0.0.51(zod@3.23.8):
|
||||
resolution: {integrity: sha512-XPLBvdwdMlNAvGMyfsDgrCDXN2Wz7M+wfCJthqiwdiKHmq2jDLGdt0ZCAozgxxW28HVzMfJlFjuyECiA5Le3YA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 0.0.24
|
||||
'@ai-sdk/provider-utils': 1.0.20(zod@3.23.8)
|
||||
zod: 3.23.8
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/azure@0.0.31(zod@3.23.8):
|
||||
resolution: {integrity: sha512-LTiv890qHcw3w87l+OOuYqW1HM9+7olS5mpSOriRY2uZxJWr3MGz8MYqJu2jGNajNKi4j64GsaOuNK69k8KXjw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -249,18 +266,29 @@ packages:
|
||||
zod: 3.23.8
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/google@0.0.46(zod@3.23.8):
|
||||
resolution: {integrity: sha512-rsc3Wh54EfSt3l/7IqPdTeuxA7xvFk2p8/HxxyoHfcwvQYmQ/bpgxmadId862sVsK79L8k3iRxvVwGVkkaEeaA==}
|
||||
/@ai-sdk/google@0.0.52(zod@3.23.8):
|
||||
resolution: {integrity: sha512-bfsA/1Ae0SQ6NfLwWKs5SU4MBwlzJjVhK6bTVBicYFjUxg9liK/W76P1Tq/qK9OlrODACz3i1STOIWsFPpIOuQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 0.0.22
|
||||
'@ai-sdk/provider-utils': 1.0.17(zod@3.23.8)
|
||||
'@ai-sdk/provider': 0.0.24
|
||||
'@ai-sdk/provider-utils': 1.0.20(zod@3.23.8)
|
||||
json-schema: 0.4.0
|
||||
zod: 3.23.8
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/groq@0.0.1(zod@3.23.8):
|
||||
resolution: {integrity: sha512-M8XHUovs2UqOx6xlhABXXCGlzbgeErSyIwvH1LQeDl3Z2CbSSgvttc0k6irm4J7ViuULE5XcIDQurXijIePWqQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 0.0.24
|
||||
'@ai-sdk/provider-utils': 1.0.20(zod@3.23.8)
|
||||
zod: 3.23.8
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/mistral@0.0.41(zod@3.23.8):
|
||||
resolution: {integrity: sha512-UTVtdC61AF4KQWnM3VAoo6/gi7G1frL3qVlKyW5toiRAUjCdeqLJUF2ho2iO8yqf+qIT6j57jWT3o6pqREy3Wg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -402,8 +430,8 @@ packages:
|
||||
json-schema: 0.4.0
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/react@0.0.62(react@18.3.1)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-1asDpxgmeHWL0/EZPCLENxfOHT+0jce0z/zasRhascodm2S6f6/KZn5doLG9jdmarcb+GjMjFmmwyOVXz3W1xg==}
|
||||
/@ai-sdk/react@0.0.64(react@18.3.1)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-4LN2vleyA6rYHZ4Rk9CdxnJgaVkNPJDD4Wx1brUhc5RvUxj3TODcm2UwGOR/mxv4pcydtZGELfQQs/i/tkAUCw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
@ -421,8 +449,8 @@ packages:
|
||||
zod: 3.23.8
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/solid@0.0.49(zod@3.23.8):
|
||||
resolution: {integrity: sha512-KnfWTt640cS1hM2fFIba8KHSPLpOIWXtEm28pNCHTvqasVKlh2y/zMQANTwE18pF2nuXL9P9F5/dKWaPsaEzQw==}
|
||||
/@ai-sdk/solid@0.0.50(zod@3.23.8):
|
||||
resolution: {integrity: sha512-JF+KKOgGAgcROgae6FU+hAtxMRhR896SzwI3H1h5hFOZrjqYeYzemJoKzA5MR5IBnPSK4FzEjunc8G5L67TyzQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
solid-js: ^1.7.7
|
||||
@ -436,8 +464,8 @@ packages:
|
||||
- zod
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/svelte@0.0.51(svelte@4.2.19)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-aIZJaIds+KpCt19yUDCRDWebzF/17GCY7gN9KkcA2QM6IKRO5UmMcqEYja0ZmwFQPm1kBZkF2njhr8VXis2mAw==}
|
||||
/@ai-sdk/svelte@0.0.52(svelte@4.2.19)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-ZGd81ruVuqpOh1Suma+HwBMBywcOV0IUzi96Q3knIoZIz99sVwebSKH8ExMofXm49bQdCTRa73Wn8sTs6QDIYg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
svelte: ^3.0.0 || ^4.0.0
|
||||
@ -470,8 +498,8 @@ packages:
|
||||
zod-to-json-schema: 3.23.2(zod@3.23.8)
|
||||
dev: false
|
||||
|
||||
/@ai-sdk/vue@0.0.54(vue@3.5.11)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-Ltu6gbuii8Qlp3gg7zdwdnHdS4M8nqKDij2VVO1223VOtIFwORFJzKqpfx44U11FW8z2TPVBYN+FjkyVIcN2hg==}
|
||||
/@ai-sdk/vue@0.0.55(vue@3.5.11)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-NZ89CeRPO3D9GjI7GmK3vC+YXjsaWi3iCIvxlGqfQYt0JFKcjgM6dfeq8Nkk+qWI9OoxoOhV/yQdqWQKPv3RRg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
vue: ^3.3.4
|
||||
@ -2021,8 +2049,8 @@ packages:
|
||||
humanize-ms: 1.2.1
|
||||
dev: false
|
||||
|
||||
/ai@3.4.9(openai@4.67.2)(react@18.3.1)(svelte@4.2.19)(vue@3.5.11)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-wmVzpIHNGjCEjIJ/3945a/DIkz+gwObjC767ZRgO8AmtIZMO5KqvqNr7n2KF+gQrCPCMC8fM1ICQFXSvBZnBlA==}
|
||||
/ai@3.4.18(openai@4.67.2)(react@18.3.1)(svelte@4.2.19)(vue@3.5.11)(zod@3.23.8):
|
||||
resolution: {integrity: sha512-dc6rSBDgaRMX4VgTBsUZwEN5tBWMpJd+MJxB05E2cL4ft9mOmQEZNS6yeu4Ci5rUDj4ZhnmvANHrP7td8Ko9Og==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
openai: ^4.42.0
|
||||
@ -2044,11 +2072,11 @@ packages:
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 0.0.24
|
||||
'@ai-sdk/provider-utils': 1.0.20(zod@3.23.8)
|
||||
'@ai-sdk/react': 0.0.62(react@18.3.1)(zod@3.23.8)
|
||||
'@ai-sdk/solid': 0.0.49(zod@3.23.8)
|
||||
'@ai-sdk/svelte': 0.0.51(svelte@4.2.19)(zod@3.23.8)
|
||||
'@ai-sdk/react': 0.0.64(react@18.3.1)(zod@3.23.8)
|
||||
'@ai-sdk/solid': 0.0.50(zod@3.23.8)
|
||||
'@ai-sdk/svelte': 0.0.52(svelte@4.2.19)(zod@3.23.8)
|
||||
'@ai-sdk/ui-utils': 0.0.46(zod@3.23.8)
|
||||
'@ai-sdk/vue': 0.0.54(vue@3.5.11)(zod@3.23.8)
|
||||
'@ai-sdk/vue': 0.0.55(vue@3.5.11)(zod@3.23.8)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
eventsource-parser: 1.1.2
|
||||
json-schema: 0.4.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user