Changes:
* trending queries UI improved * Modern look on Flight Tracker tool * added Latex rendering support
This commit is contained in:
parent
cd965a895a
commit
326e385993
@ -89,10 +89,14 @@ export async function fetchMetadata(url: string) {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url, { next: { revalidate: 3600 } }); // Cache for 1 hour
|
const response = await fetch(url, { next: { revalidate: 3600 } }); // Cache for 1 hour
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = load(html);
|
|
||||||
|
|
||||||
const title = $('head title').text() || $('meta[property="og:title"]').attr('content') || '';
|
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||||
const description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '';
|
const descMatch = html.match(
|
||||||
|
/<meta\s+name=["']description["']\s+content=["'](.*?)["']/i
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = titleMatch ? titleMatch[1] : '';
|
||||||
|
const description = descMatch ? descMatch[1] : '';
|
||||||
|
|
||||||
return { title, description };
|
return { title, description };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -101,6 +105,7 @@ export async function fetchMetadata(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type SearchGroupId = 'web' | 'academic' | 'shopping' | 'youtube' | 'x' | 'writing';
|
type SearchGroupId = 'web' | 'academic' | 'shopping' | 'youtube' | 'x' | 'writing';
|
||||||
|
|
||||||
const groupTools = {
|
const groupTools = {
|
||||||
@ -216,6 +221,10 @@ When asked a "What is" question, maintain the same format as the question and an
|
|||||||
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 unpresentable.
|
Do not talk in bullet points or lists at all costs as it 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.
|
||||||
|
No matter what happens, always provide the citations at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided.
|
||||||
|
Citation format: [Author et al. (Year) Title](URL)
|
||||||
|
Always run the tools first and then write the response.
|
||||||
`,
|
`,
|
||||||
shopping: `You are a shopping assistant that helps users find and compare products.
|
shopping: `You are a shopping assistant that helps users find and compare products.
|
||||||
The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}.
|
The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "2-digit", weekday: "short" })}.
|
||||||
@ -230,13 +239,19 @@ When asked a "What is" question, maintain the same format as the question and an
|
|||||||
Do not talk in bullet points or lists at all costs.
|
Do not talk in bullet points or lists at all costs.
|
||||||
Provide important details and summaries of the videos in paragraphs.
|
Provide important details and summaries of the videos in paragraphs.
|
||||||
Give citations with timestamps and video links to insightful content. Don't just put timestamp at 0:00.
|
Give citations with timestamps and video links to insightful content. Don't just put timestamp at 0:00.
|
||||||
|
Citation format: [Title](URL ending with parameter t=<no_of_seconds>)
|
||||||
Do not provide the video thumbnail in the response at all costs.`,
|
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.
|
||||||
writing: `You are a writing assistant that helps users with writing, conversation, coding, poems, haikus, long essays or intellectual topics.`,
|
Always provide the citations at the end of each paragraph and in the end of sentences where you use it in which they are referred to with the given format to the information provided.
|
||||||
|
Citation format: [Post Title](URL)
|
||||||
|
`,
|
||||||
|
writing: `You are a writing assistant that helps users with writing, conversation, coding, poems, haikus, long essays or intellectual topics.
|
||||||
|
Latex should be wrapped with $ symbol for inline and $$ for block equations as they are supported in the response.
|
||||||
|
Do not use the \( and \) for inline equations, use the $ symbol instead at all costs!!`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
import { generateObject } from 'ai';
|
||||||
|
import { groq } from '@ai-sdk/groq'
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export interface TrendingQuery {
|
export interface TrendingQuery {
|
||||||
icon: string;
|
icon: string;
|
||||||
@ -28,11 +31,30 @@ async function fetchGoogleTrends(): Promise<TrendingQuery[]> {
|
|||||||
const xmlText = await response.text();
|
const xmlText = await response.text();
|
||||||
const items = xmlText.match(/<title>(?!Daily Search Trends)(.*?)<\/title>/g) || [];
|
const items = xmlText.match(/<title>(?!Daily Search Trends)(.*?)<\/title>/g) || [];
|
||||||
|
|
||||||
return items.map(item => ({
|
const categories = ['trending', 'community', 'science', 'tech', 'travel', 'politics', 'health', 'sports', 'finance', 'football'] as const;
|
||||||
icon: 'trending',
|
|
||||||
text: item.replace(/<\/?title>/g, ''),
|
const schema = z.object({
|
||||||
category: 'trending' // TODO: add category based on the query results
|
category: z.enum(categories),
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemsWithCategoryAndIcon = await Promise.all(items.map(async item => {
|
||||||
|
const { object } = await generateObject({
|
||||||
|
model: groq("llama-3.2-3b-preview"),
|
||||||
|
prompt: `Give the category for the topic from the existing values only in lowercase only: ${item.replace(/<\/?title>/g, '')}
|
||||||
|
|
||||||
|
- if the topic category isn't present in the list, please select 'trending' only!`,
|
||||||
|
schema,
|
||||||
|
temperature: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: object.category,
|
||||||
|
text: item.replace(/<\/?title>/g, ''),
|
||||||
|
category: object.category
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return itemsWithCategoryAndIcon;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch Google Trends for geo: ${geo}`, error);
|
console.error(`Failed to fetch Google Trends for geo: ${geo}`, error);
|
||||||
return [];
|
return [];
|
||||||
@ -55,7 +77,7 @@ async function fetchRedditQuestions(): Promise<TrendingQuery[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const maxLength = 50;
|
const maxLength = 50;
|
||||||
|
|
||||||
@ -74,16 +96,16 @@ async function fetchRedditQuestions(): Promise<TrendingQuery[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFromMultipleSources() {
|
async function fetchFromMultipleSources() {
|
||||||
const [googleTrends,
|
const [googleTrends,
|
||||||
// redditQuestions
|
// redditQuestions
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchGoogleTrends(),
|
fetchGoogleTrends(),
|
||||||
// fetchRedditQuestions(),
|
// fetchRedditQuestions(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const allQueries = [...googleTrends,
|
const allQueries = [...googleTrends,
|
||||||
// ...redditQuestions
|
// ...redditQuestions
|
||||||
];
|
];
|
||||||
return allQueries
|
return allQueries
|
||||||
.sort(() => Math.random() - 0.5);
|
.sort(() => Math.random() - 0.5);
|
||||||
}
|
}
|
||||||
@ -91,7 +113,7 @@ async function fetchFromMultipleSources() {
|
|||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const trends = await fetchFromMultipleSources();
|
const trends = await fetchFromMultipleSources();
|
||||||
|
|
||||||
if (trends.length === 0) {
|
if (trends.length === 0) {
|
||||||
// Fallback queries if both sources fail
|
// Fallback queries if both sources fail
|
||||||
return NextResponse.json([
|
return NextResponse.json([
|
||||||
@ -112,7 +134,7 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(trends);
|
return NextResponse.json(trends);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch trends:', error);
|
console.error('Failed to fetch trends:', error);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ React,
|
|||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import Marked, { ReactRenderer } from 'marked-react';
|
import Marked, { ReactRenderer } from 'marked-react';
|
||||||
|
import Latex from 'react-latex-next';
|
||||||
import { track } from '@vercel/analytics';
|
import { track } from '@vercel/analytics';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useChat } from 'ai/react';
|
import { useChat } from 'ai/react';
|
||||||
@ -101,7 +102,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
import { Sheet, SheetContent, SheetHeader, SheetPortal, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
import { GitHubLogoIcon } from '@radix-ui/react-icons';
|
import { GitHubLogoIcon } from '@radix-ui/react-icons';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -120,7 +121,7 @@ import WeatherChart from '@/components/weather-chart';
|
|||||||
import InteractiveChart from '@/components/interactive-charts';
|
import InteractiveChart from '@/components/interactive-charts';
|
||||||
import { MapComponent, MapContainer, MapSkeleton } from '@/components/map-components';
|
import { MapComponent, MapContainer, MapSkeleton } from '@/components/map-components';
|
||||||
import MultiSearch from '@/components/multi-search';
|
import MultiSearch from '@/components/multi-search';
|
||||||
import { RedditLogo, RoadHorizon, XLogo } from '@phosphor-icons/react';
|
import { CurrencyDollar, Flag, RedditLogo, RoadHorizon, SoccerBall, TennisBall, XLogo } from '@phosphor-icons/react';
|
||||||
import { BorderTrail } from '@/components/core/border-trail';
|
import { BorderTrail } from '@/components/core/border-trail';
|
||||||
import { TextShimmer } from '@/components/core/text-shimmer';
|
import { TextShimmer } from '@/components/core/text-shimmer';
|
||||||
import { Tweet } from 'react-tweet';
|
import { Tweet } from 'react-tweet';
|
||||||
@ -587,6 +588,32 @@ const HomeContent = () => {
|
|||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
const [selectedGroup, setSelectedGroup] = useState<SearchGroupId>('web');
|
const [selectedGroup, setSelectedGroup] = useState<SearchGroupId>('web');
|
||||||
|
|
||||||
|
const CACHE_KEY = 'trendingQueriesCache';
|
||||||
|
const CACHE_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
|
||||||
|
|
||||||
|
// Add this type definition
|
||||||
|
interface TrendingQueriesCache {
|
||||||
|
data: TrendingQuery[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTrendingQueriesFromCache = (): TrendingQueriesCache | null => {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
|
const cached = localStorage.getItem(CACHE_KEY);
|
||||||
|
if (!cached) return null;
|
||||||
|
|
||||||
|
const parsedCache = JSON.parse(cached) as TrendingQueriesCache;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - parsedCache.timestamp > CACHE_DURATION) {
|
||||||
|
localStorage.removeItem(CACHE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedCache;
|
||||||
|
};
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
const [openChangelog, setOpenChangelog] = useState(false);
|
const [openChangelog, setOpenChangelog] = useState(false);
|
||||||
@ -633,10 +660,25 @@ const HomeContent = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTrending = async () => {
|
const fetchTrending = async () => {
|
||||||
|
// Check cache first
|
||||||
|
const cached = getTrendingQueriesFromCache();
|
||||||
|
if (cached) {
|
||||||
|
setTrendingQueries(cached.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/trending');
|
const res = await fetch('/api/trending');
|
||||||
if (!res.ok) throw new Error('Failed to fetch trending queries');
|
if (!res.ok) throw new Error('Failed to fetch trending queries');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
const cacheData: TrendingQueriesCache = {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
||||||
|
|
||||||
setTrendingQueries(data);
|
setTrendingQueries(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching trending queries:', error);
|
console.error('Error fetching trending queries:', error);
|
||||||
@ -1918,6 +1960,48 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
}));
|
}));
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
|
const inlineMathRegex = /\$([^\$]+)\$/g;
|
||||||
|
const blockMathRegex = /\$\$([^\$]+)\$\$/g;
|
||||||
|
|
||||||
|
const isValidLatex = (text: string): boolean => {
|
||||||
|
// Basic validation - checks for balanced delimiters
|
||||||
|
return !(text.includes('\\') && !text.match(/\\[a-zA-Z{}\[\]]+/));
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderLatexString = (text: string) => {
|
||||||
|
let parts = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// Try to match inline math first ($...$)
|
||||||
|
while ((match = /\$([^\$]+)\$/g.exec(text.slice(lastIndex))) !== null) {
|
||||||
|
const mathText = match[1];
|
||||||
|
const fullMatch = match[0];
|
||||||
|
const matchIndex = lastIndex + match.index;
|
||||||
|
|
||||||
|
// Add text before math
|
||||||
|
if (matchIndex > lastIndex) {
|
||||||
|
parts.push(text.slice(lastIndex, matchIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only render as LaTeX if valid
|
||||||
|
if (isValidLatex(mathText)) {
|
||||||
|
parts.push(<Latex key={matchIndex}>{fullMatch}</Latex>);
|
||||||
|
} else {
|
||||||
|
parts.push(fullMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = matchIndex + fullMatch.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining text
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(text.slice(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts : text;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchMetadataWithCache = useCallback(async (url: string) => {
|
const fetchMetadataWithCache = useCallback(async (url: string) => {
|
||||||
if (metadataCache[url]) {
|
if (metadataCache[url]) {
|
||||||
return metadataCache[url];
|
return metadataCache[url];
|
||||||
@ -2039,8 +2123,36 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderer: Partial<ReactRenderer> = {
|
const renderer: Partial<ReactRenderer> = {
|
||||||
|
text(text: string) {
|
||||||
|
if (!text.includes('$')) return text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Latex
|
||||||
|
delimiters={[
|
||||||
|
{ left: '$$', right: '$$', display: true },
|
||||||
|
{ left: '$', right: '$', display: false }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Latex>
|
||||||
|
);
|
||||||
|
},
|
||||||
paragraph(children) {
|
paragraph(children) {
|
||||||
return <p className="my-4 text-neutral-800 dark:text-neutral-200">{children}</p>;
|
if (typeof children === 'string' && children.includes('$')) {
|
||||||
|
return (
|
||||||
|
<p className="my-4">
|
||||||
|
<Latex
|
||||||
|
delimiters={[
|
||||||
|
{ left: '$$', right: '$$', display: true },
|
||||||
|
{ left: '$', right: '$', display: false }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Latex>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <p className="my-4">{children}</p>;
|
||||||
},
|
},
|
||||||
code(children, language) {
|
code(children, language) {
|
||||||
return <CodeBlock language={language}>{String(children)}</CodeBlock>;
|
return <CodeBlock language={language}>{String(children)}</CodeBlock>;
|
||||||
@ -2195,58 +2307,71 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SuggestionCards: React.FC<{
|
const SuggestionCards: React.FC<{
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
trendingQueries: TrendingQuery[];
|
trendingQueries: TrendingQuery[];
|
||||||
}> = ({ selectedModel, trendingQueries }) => {
|
}> = ({ selectedModel, trendingQueries }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const scrollIntervalRef = useRef<NodeJS.Timeout>();
|
const scrollIntervalRef = useRef<NodeJS.Timeout>();
|
||||||
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
setIsTouchDevice('ontouchstart' in window);
|
||||||
}, [trendingQueries]);
|
}, [trendingQueries]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isTouchDevice) return; // Disable auto-scroll on touch devices
|
||||||
|
|
||||||
const startScrolling = () => {
|
const startScrolling = () => {
|
||||||
if (!scrollRef.current || isPaused) return;
|
if (!scrollRef.current || isPaused) return;
|
||||||
scrollRef.current.scrollLeft += 2;
|
scrollRef.current.scrollLeft += 1; // Reduced speed
|
||||||
|
|
||||||
|
// Reset scroll when reaching end
|
||||||
|
if (scrollRef.current.scrollLeft >=
|
||||||
|
(scrollRef.current.scrollWidth - scrollRef.current.clientWidth)) {
|
||||||
|
scrollRef.current.scrollLeft = 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollIntervalRef.current = setInterval(startScrolling, 20);
|
scrollIntervalRef.current = setInterval(startScrolling, 30);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (scrollIntervalRef.current) {
|
if (scrollIntervalRef.current) {
|
||||||
clearInterval(scrollIntervalRef.current);
|
clearInterval(scrollIntervalRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isPaused]);
|
}, [isPaused, isTouchDevice]);
|
||||||
|
|
||||||
const getCardWidth = (text: string) => {
|
|
||||||
const charWidth = 8;
|
|
||||||
const padding = 32;
|
|
||||||
const iconWidth = 28;
|
|
||||||
return Math.min(
|
|
||||||
padding + iconWidth + (text.length * charWidth),
|
|
||||||
400
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading || trendingQueries.length === 0) {
|
if (isLoading || trendingQueries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 mt-4">
|
<div className="relative mt-4 px-0">
|
||||||
{[1, 2, 3].map((_, index) => (
|
{/* Overlay with Loading Text */}
|
||||||
<div
|
<div className="absolute inset-0 z-10 flex items-center justify-center">
|
||||||
key={index}
|
<div className="backdrop-blur-sm bg-white/30 dark:bg-black/30 rounded-2xl px-6 py-3 shadow-lg">
|
||||||
className="flex-shrink-0 w-[200px] bg-neutral-100 dark:bg-neutral-800 rounded-xl p-4 animate-pulse"
|
<div className="flex items-center gap-2 text-sm font-medium text-neutral-600 dark:text-neutral-300">
|
||||||
>
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
<div className="flex items-center space-x-2">
|
<span>Loading trending queries</span>
|
||||||
<div className="w-5 h-5 bg-neutral-200 dark:bg-neutral-700 rounded-full" />
|
|
||||||
<div className="h-4 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
|
{/* Background Cards */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[1, 2, 3].map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex-shrink-0 w-[140px] md:w-[220px] bg-neutral-100 dark:bg-neutral-800 rounded-xl p-4 animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-5 h-5 bg-neutral-200 dark:bg-neutral-700 rounded-full" />
|
||||||
|
<div className="h-4 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2258,28 +2383,53 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
science: <Brain className="w-5 h-5" />,
|
science: <Brain className="w-5 h-5" />,
|
||||||
tech: <Code className="w-5 h-5" />,
|
tech: <Code className="w-5 h-5" />,
|
||||||
travel: <Globe className="w-5 h-5" />,
|
travel: <Globe className="w-5 h-5" />,
|
||||||
|
politics: <Flag className="w-5 h-5" />,
|
||||||
|
health: <Heart className="w-5 h-5" />,
|
||||||
|
sports: <TennisBall className="w-5 h-5" />,
|
||||||
|
finance: <CurrencyDollar className="w-5 h-5" />,
|
||||||
|
football: <SoccerBall className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
return iconMap[category as keyof typeof iconMap] || <Sparkles className="w-5 h-5" />;
|
return iconMap[category as keyof typeof iconMap] || <Sparkles className="w-5 h-5" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
{/* Gradient Fades */}
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-12 bg-gradient-to-r from-background to-transparent z-10" />
|
||||||
|
<div className="absolute right-0 top-0 bottom-0 w-12 bg-gradient-to-l from-background to-transparent z-10" />
|
||||||
|
|
||||||
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="flex gap-2 mt-4 overflow-x-auto pb-3 relative scroll-smooth no-scrollbar"
|
className="flex gap-4 mt-4 overflow-x-auto pb-4 px-4 md:px-0 relative scroll-smooth no-scrollbar"
|
||||||
onMouseEnter={() => setIsPaused(true)}
|
onMouseEnter={() => !isTouchDevice && setIsPaused(true)}
|
||||||
onMouseLeave={() => setIsPaused(false)}
|
onMouseLeave={() => !isTouchDevice && setIsPaused(false)}
|
||||||
|
onTouchStart={() => setIsPaused(true)}
|
||||||
|
onTouchEnd={() => setIsPaused(false)}
|
||||||
>
|
>
|
||||||
{Array(20).fill(trendingQueries).flat().map((query, index) => (
|
{Array(20).fill(trendingQueries).flat().map((query, index) => (
|
||||||
<button
|
<button
|
||||||
key={`${index}-${query.text}`}
|
key={`${index}-${query.text}`}
|
||||||
onClick={() => handleExampleClick(query)}
|
onClick={() => handleExampleClick(query)}
|
||||||
className="flex-shrink-0 bg-neutral-100 dark:bg-neutral-800 rounded-xl p-3 text-left hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors duration-200"
|
className="group flex-shrink-0 bg-neutral-50/50 dark:bg-neutral-800/50
|
||||||
style={{ width: `${getCardWidth(query.text)}px` }}
|
backdrop-blur-sm rounded-xl p-3.5 text-left
|
||||||
|
hover:bg-neutral-100 dark:hover:bg-neutral-700/70
|
||||||
|
transition-all duration-200 ease-out
|
||||||
|
hover:scale-102 origin-center
|
||||||
|
h-[52px] min-w-fit
|
||||||
|
hover:shadow-lg
|
||||||
|
border border-neutral-200/50 dark:border-neutral-700/50
|
||||||
|
hover:border-neutral-300 dark:hover:border-neutral-600"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-neutral-700 dark:text-neutral-300">
|
|
||||||
<span className="flex-shrink-0">{getIconForCategory(query.category)}</span>
|
<div className="flex items-center gap-3 text-neutral-700 dark:text-neutral-300">
|
||||||
<span className="text-sm font-medium whitespace-nowrap pr-1">
|
<span
|
||||||
|
className="flex-shrink-0 transition-transform duration-200 group-hover:scale-110 group-hover:rotate-3"
|
||||||
|
>
|
||||||
|
{getIconForCategory(query.category)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-sm font-medium truncate max-w-[180px] group-hover:text-neutral-900 dark:group-hover:text-neutral-100"
|
||||||
|
>
|
||||||
{query.text}
|
{query.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -2301,11 +2451,11 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// const memoizedMessages = useMemo(() => messages, [messages]);
|
const memoizedMessages = useMemo(() => messages, [messages]);
|
||||||
|
|
||||||
const memoizedSuggestionCards = useMemo(() => (
|
const memoizedSuggestionCards = useMemo(() => (
|
||||||
<SuggestionCards
|
<SuggestionCards
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
trendingQueries={trendingQueries}
|
trendingQueries={trendingQueries}
|
||||||
/>
|
/>
|
||||||
), [selectedModel, trendingQueries]);
|
), [selectedModel, trendingQueries]);
|
||||||
@ -2349,7 +2499,7 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
fileInputRef={fileInputRef}
|
fileInputRef={fileInputRef}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
stop={stop}
|
stop={stop}
|
||||||
messages={messages}
|
messages={memoizedMessages}
|
||||||
append={append}
|
append={append}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
setSelectedModel={handleModelChange}
|
setSelectedModel={handleModelChange}
|
||||||
@ -2365,7 +2515,7 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
|
|
||||||
|
|
||||||
<div className="space-y-4 sm:space-y-6 mb-32">
|
<div className="space-y-4 sm:space-y-6 mb-32">
|
||||||
{messages.map((message, index) => (
|
{memoizedMessages.map((message, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@ -50,8 +50,8 @@ export function FlightTracker({ data }: FlightTrackerProps) {
|
|||||||
|
|
||||||
const formatTime = (timestamp: string) => {
|
const formatTime = (timestamp: string) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toLocaleTimeString('en-US', {
|
return date.toLocaleTimeString('en-US', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: false,
|
hour12: false,
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
@ -82,16 +82,38 @@ export function FlightTracker({ data }: FlightTrackerProps) {
|
|||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "LANDED":
|
case "LANDED":
|
||||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
|
return "bg-green-100 hover:bg-green-200 text-green-800 dark:bg-green-900 dark:hover:bg-green-800 dark:text-green-200";
|
||||||
case "DEPARTING ON TIME":
|
case "DEPARTING ON TIME":
|
||||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
|
return "bg-green-100 hover:bg-green-200 text-green-800 dark:bg-green-900 dark:hover:bg-green-800 dark:text-green-200";
|
||||||
case "DELAYED":
|
case "DELAYED":
|
||||||
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
|
return "bg-red-100 hover:bg-red-200 text-red-800 dark:bg-red-900 dark:hover:bg-red-800 dark:text-red-200";
|
||||||
default:
|
default:
|
||||||
return "bg-neutral-100 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200";
|
return "bg-neutral-100 hover:bg-neutral-200 text-neutral-800 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPlanePosition = (status: string) => {
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case 'landed': return 'right-0';
|
||||||
|
case 'active': return 'left-1/2 -translate-x-1/2';
|
||||||
|
default: return 'left-0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDuration = (departureTime: string, arrivalTime: string): string => {
|
||||||
|
const departure = new Date(departureTime);
|
||||||
|
const arrival = new Date(arrivalTime);
|
||||||
|
const durationInMinutes = Math.floor((arrival.getTime() - departure.getTime()) / (1000 * 60));
|
||||||
|
|
||||||
|
if (durationInMinutes < 0) return 'N/A';
|
||||||
|
|
||||||
|
const hours = Math.floor(durationInMinutes / 60);
|
||||||
|
const minutes = durationInMinutes % 60;
|
||||||
|
|
||||||
|
if (hours === 0) return `${minutes}m`;
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
};
|
||||||
|
|
||||||
const flightInfo = {
|
const flightInfo = {
|
||||||
flightNumber: flight.flight.iata,
|
flightNumber: flight.flight.iata,
|
||||||
status: mapStatus(flight.flight_status),
|
status: mapStatus(flight.flight_status),
|
||||||
@ -111,7 +133,7 @@ export function FlightTracker({ data }: FlightTrackerProps) {
|
|||||||
terminal: flight.arrival.terminal || undefined,
|
terminal: flight.arrival.terminal || undefined,
|
||||||
gate: flight.arrival.gate || undefined
|
gate: flight.arrival.gate || undefined
|
||||||
},
|
},
|
||||||
duration: flight.flight.duration ? `${flight.flight.duration} minutes` : 'N/A',
|
duration: calculateDuration(flight.departure.scheduled, flight.arrival.scheduled),
|
||||||
lastUpdated: new Date().toLocaleTimeString('en-US', {
|
lastUpdated: new Date().toLocaleTimeString('en-US', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@ -121,76 +143,135 @@ export function FlightTracker({ data }: FlightTrackerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl bg-card dark:bg-card">
|
<Card className="w-full max-w-3xl bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm border-neutral-200/50 dark:border-neutral-800/50 shadow-none">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-4 md:p-8">
|
||||||
<div className="flex justify-between items-center mb-6">
|
{/* Header */}
|
||||||
<div>
|
<div className="flex flex-col md:flex-row md:items-center gap-4 md:gap-0 md:justify-between mb-6 pb-6 border-b border-neutral-200/50 dark:border-neutral-800/50">
|
||||||
<h2 className="text-2xl font-bold">{flightInfo.flightNumber}</h2>
|
<div className="flex items-center gap-3">
|
||||||
<Badge className={getStatusColor(flightInfo.status)}>
|
<div className="w-10 h-10 md:w-12 md:h-12 rounded-full bg-blue-500/10 dark:bg-blue-500/20 flex items-center justify-center">
|
||||||
{flightInfo.status}
|
<Plane className="h-5 w-5 md:h-6 md:w-6 text-blue-600 dark:text-blue-400" />
|
||||||
</Badge>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{flightInfo.flightNumber}</h2>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">{flight.airline.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className={`${getStatusColor(flightInfo.status)} px-3 py-1 md:px-4 md:py-1.5 text-sm font-medium self-start md:self-aut shadow-none`}>
|
||||||
|
{flightInfo.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flight Route */}
|
||||||
|
<div className="py-4 md:py-8">
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 md:gap-8 md:items-center">
|
||||||
|
{/* Departure */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-2xl md:text-3xl font-mono font-bold mb-2 truncate">
|
||||||
|
{flightInfo.departure.code}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5 md:space-y-1">
|
||||||
|
<p className="font-medium text-sm truncate">{flightInfo.departure.airport}</p>
|
||||||
|
<p className="text-lg md:text-xl font-bold">{flightInfo.departure.time}</p>
|
||||||
|
<p className="text-xs text-neutral-500">{flightInfo.departure.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flight Path - Hidden on mobile */}
|
||||||
|
<div className="hidden md:block flex-1 relative h-[2px] mx-4">
|
||||||
|
<div className="absolute left-0 top-1/2 w-2 h-2 bg-blue-600 dark:bg-blue-400 rounded-full -translate-y-1/2" />
|
||||||
|
<div className="w-full h-[2px] border-t-2 border-dotted border-blue-500/50 dark:border-blue-400/50" />
|
||||||
|
<div className="absolute right-0 top-1/2 w-2 h-2 bg-blue-600 dark:bg-blue-400 rounded-full -translate-y-1/2" />
|
||||||
|
<div className={`absolute top-1/2 -translate-y-1/2 ${getPlanePosition(flightInfo.status)} transition-all duration-1000`}>
|
||||||
|
<div className="bg-white dark:bg-neutral-800 p-2 rounded-full border">
|
||||||
|
<Plane className="h-5 w-5 text-blue-600 dark:text-blue-400 transform rotate-45" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Flight Progress */}
|
||||||
|
<div className="md:hidden relative w-[97%] h-8 flex items-center">
|
||||||
|
{/* Background Track */}
|
||||||
|
<div className="absolute inset-0 h-1 top-1/2 -translate-y-1/2 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div
|
||||||
|
className={`absolute h-1 top-1/2 -translate-y-1/2 bg-blue-500 rounded-full transition-all duration-1000 ${flightInfo.status === 'LANDED' ? 'w-full' :
|
||||||
|
flightInfo.status === 'DEPARTING ON TIME' ? 'w-[5%]' : 'w-1/2'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Animated Plane */}
|
||||||
|
<div
|
||||||
|
className={`absolute top-1/2 -translate-y-1/2 transition-all duration-1000`}
|
||||||
|
style={{
|
||||||
|
left: flightInfo.status === 'LANDED' ? '100%' :
|
||||||
|
flightInfo.status === 'DEPARTING ON TIME' ? '5%' : '50%',
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="bg-white dark:bg-neutral-800 p-1.5 rounded-full border">
|
||||||
|
<Plane className="h-4 w-4 text-blue-600 dark:text-blue-400 transform rotate-45" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arrival */}
|
||||||
|
<div className="flex-1 min-w-0 md:text-right">
|
||||||
|
<div className="text-2xl md:text-3xl font-mono font-bold mb-2 truncate">
|
||||||
|
{flightInfo.arrival.code}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5 md:space-y-1">
|
||||||
|
<p className="font-medium text-sm truncate">{flightInfo.arrival.airport}</p>
|
||||||
|
<p className="text-lg md:text-xl font-bold">{flightInfo.arrival.time}</p>
|
||||||
|
<p className="text-xs text-neutral-500">{flightInfo.arrival.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
{/* Flight Details */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="mt-6 md:mt-8 grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 bg-neutral-50/50 dark:bg-neutral-800/50 rounded-xl p-4">
|
||||||
<div className="text-4xl font-mono">{flightInfo.departure.code}</div>
|
{/* Departure Details */}
|
||||||
<motion.div
|
<div className="space-y-3 max-w-full">
|
||||||
className="text-primary"
|
{flightInfo.departure.terminal && (
|
||||||
initial={{ x: -50 }}
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
animate={{ x: 50 }}
|
<Terminal className="h-4 w-4 flex-shrink-0 text-neutral-500" />
|
||||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
<span className="text-sm truncate">Terminal {flightInfo.departure.terminal}</span>
|
||||||
>
|
</div>
|
||||||
<Plane className="h-6 w-6" />
|
)}
|
||||||
</motion.div>
|
{flightInfo.departure.gate && (
|
||||||
<div className="text-4xl font-mono">{flightInfo.arrival.code}</div>
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
</div>
|
<div className="h-4 w-4 flex-shrink-0 rounded bg-blue-500/10 flex items-center justify-center text-[10px] text-blue-600">G</div>
|
||||||
<div className="mt-8 grid grid-cols-2 gap-8">
|
<span className="text-sm truncate">Gate {flightInfo.departure.gate}</span>
|
||||||
<div>
|
</div>
|
||||||
<p className="text-lg font-medium">{flightInfo.departure.airport}</p>
|
)}
|
||||||
<p className="text-3xl font-bold mt-1">{flightInfo.departure.time}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">{flightInfo.departure.date}</p>
|
|
||||||
{(flightInfo.departure.terminal || flightInfo.departure.gate) && (
|
|
||||||
<div className="mt-2 flex items-center gap-4">
|
|
||||||
{flightInfo.departure.terminal && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Terminal className="h-4 w-4" />
|
|
||||||
<span>Terminal {flightInfo.departure.terminal}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{flightInfo.departure.gate && (
|
|
||||||
<div>Gate {flightInfo.departure.gate}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-lg font-medium">{flightInfo.arrival.airport}</p>
|
|
||||||
<p className="text-3xl font-bold mt-1">{flightInfo.arrival.time}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">{flightInfo.arrival.date}</p>
|
|
||||||
{(flightInfo.arrival.terminal || flightInfo.arrival.gate) && (
|
|
||||||
<div className="mt-2 flex items-center gap-4">
|
|
||||||
{flightInfo.arrival.terminal && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Terminal className="h-4 w-4" />
|
|
||||||
<span>Terminal {flightInfo.arrival.terminal}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{flightInfo.arrival.gate && (
|
|
||||||
<div>Gate {flightInfo.arrival.gate}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex items-center gap-2 text-sm text-muted-foreground">
|
{/* Arrival Details */}
|
||||||
|
<div className="space-y-3 max-w-full">
|
||||||
|
{flightInfo.arrival.terminal && (
|
||||||
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
|
<Terminal className="h-4 w-4 flex-shrink-0 text-neutral-500" />
|
||||||
|
<span className="text-sm truncate">Terminal {flightInfo.arrival.terminal}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{flightInfo.arrival.gate && (
|
||||||
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
|
<div className="h-4 w-4 flex-shrink-0 rounded bg-blue-500/10 flex items-center justify-center text-[10px] text-blue-600">G</div>
|
||||||
|
<span className="text-sm truncate">Gate {flightInfo.arrival.gate}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="mt-4 md:mt-6 flex flex-col md:flex-row items-start md:items-center gap-2 md:gap-3 text-sm text-neutral-500">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-4 w-4" />
|
||||||
<span>Duration: {flightInfo.duration}</span>
|
<span>Flight duration: {flightInfo.duration}</span>
|
||||||
<span className="mx-2">•</span>
|
|
||||||
<span>Last updated: {flightInfo.lastUpdated}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="hidden md:inline text-neutral-300">•</span>
|
||||||
|
<span>Last updated: {flightInfo.lastUpdated}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"@ai-sdk/anthropic": "^1.0.5",
|
"@ai-sdk/anthropic": "^1.0.5",
|
||||||
"@ai-sdk/azure": "^1.0.10",
|
"@ai-sdk/azure": "^1.0.10",
|
||||||
"@ai-sdk/cohere": "^1.0.3",
|
"@ai-sdk/cohere": "^1.0.3",
|
||||||
"@ai-sdk/google": "^1.0.7",
|
"@ai-sdk/google": "^1.0.11",
|
||||||
"@ai-sdk/groq": "^0.0.1",
|
"@ai-sdk/groq": "^0.0.1",
|
||||||
"@ai-sdk/mistral": "^0.0.41",
|
"@ai-sdk/mistral": "^0.0.41",
|
||||||
"@ai-sdk/openai": "^0.0.58",
|
"@ai-sdk/openai": "^0.0.58",
|
||||||
|
|||||||
@ -15,8 +15,8 @@ dependencies:
|
|||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(zod@3.24.1)
|
version: 1.0.3(zod@3.24.1)
|
||||||
'@ai-sdk/google':
|
'@ai-sdk/google':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.11
|
||||||
version: 1.0.7(zod@3.24.1)
|
version: 1.0.11(zod@3.24.1)
|
||||||
'@ai-sdk/groq':
|
'@ai-sdk/groq':
|
||||||
specifier: ^0.0.1
|
specifier: ^0.0.1
|
||||||
version: 0.0.1(zod@3.24.1)
|
version: 0.0.1(zod@3.24.1)
|
||||||
@ -311,8 +311,8 @@ packages:
|
|||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@ai-sdk/google@1.0.7(zod@3.24.1):
|
/@ai-sdk/google@1.0.11(zod@3.24.1):
|
||||||
resolution: {integrity: sha512-D2R/VFA0zpcWYnAoqYGaZn7XqHb8ASt1hZJ86u7BOVoBnQTRPRUYHb4lSXjrMcj/QYMXIJkKojfY4isenkku5Q==}
|
resolution: {integrity: sha512-snp66p4BurhOmy2QUTlkZR8nFizx+F60t9v/2ld/fhxTK4G+QMHBUZpBujkW1gQEfE13fEOd43wCE1SQgP46Tw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.0.0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user