From c66fedea273dab22402dff6bda6a1a9ba336ac5a Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 07:51:37 +0800 Subject: [PATCH 01/33] chore: update upgrade card visibility logic to ensure data is loaded before rendering --- src/components/dashboard/upgrade-card.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/upgrade-card.tsx b/src/components/dashboard/upgrade-card.tsx index 6807aa0..d42431d 100644 --- a/src/components/dashboard/upgrade-card.tsx +++ b/src/components/dashboard/upgrade-card.tsx @@ -35,7 +35,13 @@ export function UpgradeCard() { const isMember = paymentData?.currentPlan?.isLifetime || !!paymentData?.subscription; - if (!mounted || isLoading || isMember) { + // Ensure the upgrade card is only shown when the data is loaded + if (!mounted || isLoading || !paymentData) { + return null; + } + + // If the user is a member, don't show the upgrade card + if (isMember) { return null; } From 1be26638fc867777d4db7a0bc5ba131f082ffe8f Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 08:23:54 +0800 Subject: [PATCH 02/33] chore: update billing card rendering logic to include payment data check --- src/components/settings/billing/billing-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/billing/billing-card.tsx b/src/components/settings/billing/billing-card.tsx index 494dbbc..8b2f95b 100644 --- a/src/components/settings/billing/billing-card.tsx +++ b/src/components/settings/billing/billing-card.tsx @@ -100,7 +100,7 @@ export default function BillingCard() { // Render loading skeleton if not mounted or in a loading state const isPageLoading = isLoadingPayment || isLoadingSession; - if (!mounted || isPageLoading) { + if (!mounted || isPageLoading || !paymentData) { return ( From ffe5bc4ea5ffaeb12dcb3dca3d358a1e279fcfa4 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 08:55:02 +0800 Subject: [PATCH 03/33] chore: update PasswordCardWrapper to include CardFooter with skeleton loading state --- .../settings/security/password-card-wrapper.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/settings/security/password-card-wrapper.tsx b/src/components/settings/security/password-card-wrapper.tsx index a7f5b5f..03ebda1 100644 --- a/src/components/settings/security/password-card-wrapper.tsx +++ b/src/components/settings/security/password-card-wrapper.tsx @@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, + CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'; @@ -57,7 +58,7 @@ export function PasswordCardWrapper() { function PasswordSkeletonCard() { const t = useTranslations('Dashboard.settings.security.updatePassword'); return ( - + {t('title')} {t('description')} @@ -67,9 +68,10 @@ function PasswordSkeletonCard() { - - + + + ); } From 6927f4b234195203797e216a7d0a0a9e42fdf147 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 09:00:13 +0800 Subject: [PATCH 04/33] chore: adjust skeleton component heights in billing and password cards for improved loading state visibility --- src/components/settings/billing/billing-card.tsx | 4 ++-- src/components/settings/security/password-card-wrapper.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/settings/billing/billing-card.tsx b/src/components/settings/billing/billing-card.tsx index 8b2f95b..1be005d 100644 --- a/src/components/settings/billing/billing-card.tsx +++ b/src/components/settings/billing/billing-card.tsx @@ -111,14 +111,14 @@ export default function BillingCard() {
- +
- +
); diff --git a/src/components/settings/security/password-card-wrapper.tsx b/src/components/settings/security/password-card-wrapper.tsx index 03ebda1..29f7709 100644 --- a/src/components/settings/security/password-card-wrapper.tsx +++ b/src/components/settings/security/password-card-wrapper.tsx @@ -70,7 +70,7 @@ function PasswordSkeletonCard() { - + ); From 6837c5a8d4612dd0eb382d336715e758ffeb7be4 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 09:05:12 +0800 Subject: [PATCH 05/33] chore: simplify CreditsBalanceCard layout --- .../settings/credits/credits-balance-card.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/settings/credits/credits-balance-card.tsx b/src/components/settings/credits/credits-balance-card.tsx index d05796f..a8629a1 100644 --- a/src/components/settings/credits/credits-balance-card.tsx +++ b/src/components/settings/credits/credits-balance-card.tsx @@ -146,7 +146,7 @@ export default function CreditsBalanceCard() { {t('title')} {t('description')} - + {/* Credits balance display */}
@@ -157,7 +157,8 @@ export default function CreditsBalanceCard() {
{/* available */}
- +
+ {/* Balance information */}
{/* Expiring credits warning */} @@ -177,13 +178,6 @@ export default function CreditsBalanceCard() {
)} -
- - {/* */} ); From 01f5734dd56ec785e25de3ca839425c18b189e42 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 09:52:28 +0800 Subject: [PATCH 06/33] chore: update credit expiration messaging and logic to reflect upcoming expiration in days --- messages/en.json | 3 +- messages/zh.json | 3 +- src/actions/get-credit-stats.ts | 27 ++++++----- .../settings/credits/credits-balance-card.tsx | 46 ++++++++----------- src/lib/constants.ts | 8 ++++ 5 files changed, 45 insertions(+), 42 deletions(-) diff --git a/messages/en.json b/messages/en.json index 53d5c26..1c902a0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -601,8 +601,7 @@ "creditsAdded": "Credits have been added to your account", "viewTransactions": "View Credit Transactions", "retry": "Retry", - - "expiringCredits": "{credits} credits expiring on {date}" + "expiringCredits": "{credits} credits expiring in the next {days} days" }, "packages": { "title": "Credit Packages", diff --git a/messages/zh.json b/messages/zh.json index ab1ab7f..1bbe1bd 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -601,8 +601,7 @@ "creditsAdded": "积分已添加到您的账户", "viewTransactions": "查看积分记录", "retry": "重试", - - "expiringCredits": "{credits} 积分将在 {date} 过期" + "expiringCredits": "{credits} 积分将在 {days} 天内过期" }, "packages": { "title": "积分套餐", diff --git a/src/actions/get-credit-stats.ts b/src/actions/get-credit-stats.ts index 1bb4c02..3bc1fd9 100644 --- a/src/actions/get-credit-stats.ts +++ b/src/actions/get-credit-stats.ts @@ -3,11 +3,10 @@ import { getDb } from '@/db'; import { creditTransaction } from '@/db/schema'; import type { User } from '@/lib/auth-types'; +import { CREDITS_EXPIRATION_DAYS } from '@/lib/constants'; import { userActionClient } from '@/lib/safe-action'; import { addDays } from 'date-fns'; -import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm'; - -const CREDITS_EXPIRATION_DAYS = 31; +import { and, eq, gt, gte, isNotNull, lte, sum } from 'drizzle-orm'; /** * Get credit statistics for a user @@ -18,12 +17,14 @@ export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => { const userId = currentUser.id; const db = await getDb(); - // Get credits expiring in the next CREDITS_EXPIRATION_DAYS days - const expirationDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS); - const expiringCredits = await db + const now = new Date(); + // Get credits expiring in the next 30 days + const expirationDaysFromNow = addDays(now, CREDITS_EXPIRATION_DAYS); + + // Get total credits expiring in the next 30 days + const expiringCreditsResult = await db .select({ - amount: sum(creditTransaction.remainingAmount), - earliestExpiration: sql`MIN(${creditTransaction.expirationDate})`, + totalAmount: sum(creditTransaction.remainingAmount), }) .from(creditTransaction) .where( @@ -31,18 +32,20 @@ export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => { eq(creditTransaction.userId, userId), isNotNull(creditTransaction.expirationDate), isNotNull(creditTransaction.remainingAmount), - gte(creditTransaction.remainingAmount, 1), + gt(creditTransaction.remainingAmount, 0), lte(creditTransaction.expirationDate, expirationDaysFromNow), - gte(creditTransaction.expirationDate, new Date()) + gte(creditTransaction.expirationDate, now) ) ); + const totalExpiringCredits = + Number(expiringCreditsResult[0]?.totalAmount) || 0; + return { success: true, data: { expiringCredits: { - amount: Number(expiringCredits[0]?.amount) || 0, - earliestExpiration: expiringCredits[0]?.earliestExpiration || null, + amount: totalExpiringCredits, }, }, }; diff --git a/src/components/settings/credits/credits-balance-card.tsx b/src/components/settings/credits/credits-balance-card.tsx index a8629a1..74c1b1d 100644 --- a/src/components/settings/credits/credits-balance-card.tsx +++ b/src/components/settings/credits/credits-balance-card.tsx @@ -14,7 +14,7 @@ import { websiteConfig } from '@/config/website'; import { useCreditBalance, useCreditStats } from '@/hooks/use-credits'; import { useMounted } from '@/hooks/use-mounted'; import { useLocaleRouter } from '@/i18n/navigation'; -import { formatDate } from '@/lib/formatter'; +import { CREDITS_EXPIRATION_DAYS } from '@/lib/constants'; import { cn } from '@/lib/utils'; import { Routes } from '@/routes'; import { RefreshCwIcon } from 'lucide-react'; @@ -102,13 +102,12 @@ export default function CreditsBalanceCard() {
- -
-
- +
- {/* show nothing */} + + + ); } @@ -147,7 +146,7 @@ export default function CreditsBalanceCard() { {t('description')} - {/* Credits balance display */} + {/* Credits balance */}
{/* */} @@ -159,25 +158,20 @@ export default function CreditsBalanceCard() {
- {/* Balance information */} -
- {/* Expiring credits warning */} - {!isLoadingStats && - creditStats && - creditStats.expiringCredits.amount > 0 && - creditStats.expiringCredits.earliestExpiration && ( -
- - {t('expiringCredits', { - credits: creditStats.expiringCredits.amount, - date: formatDate( - new Date(creditStats.expiringCredits.earliestExpiration) - ), - })} - -
- )} -
+ {/* Expiring credits warning */} + {!isLoadingStats && creditStats && ( +
+ {' '} +
+ + {t('expiringCredits', { + credits: creditStats.expiringCredits.amount, + days: CREDITS_EXPIRATION_DAYS, + })} + +
+
+ )}
); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 39459fd..856a240 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,2 +1,10 @@ +/** + * in next 30 days for credits expiration + */ +export const CREDITS_EXPIRATION_DAYS = 30; + +/** + * placeholder image for blog post card + */ export const PLACEHOLDER_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg=='; From d319bd8af25c69b8d431c19b5a67d9297b62e8b8 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 10:20:30 +0800 Subject: [PATCH 07/33] chore: rename enableForFreePlan to enablePackagesForFreePlan for clarity in credits configuration --- src/components/settings/credits/credit-packages.tsx | 2 +- src/config/website.tsx | 2 +- src/types/index.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx index 64a1abd..012678b 100644 --- a/src/components/settings/credits/credit-packages.tsx +++ b/src/components/settings/credits/credit-packages.tsx @@ -45,7 +45,7 @@ export function CreditPackages() { const isFreePlan = currentPlan?.isFree === true; // Check if user is on free plan and enableForFreePlan is false - if (isFreePlan && !websiteConfig.credits.enableForFreePlan) { + if (isFreePlan && !websiteConfig.credits.enablePackagesForFreePlan) { return null; } diff --git a/src/config/website.tsx b/src/config/website.tsx index eda1eba..3abe804 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -155,7 +155,7 @@ export const websiteConfig: WebsiteConfig = { }, credits: { enableCredits: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true', - enableForFreePlan: false, + enablePackagesForFreePlan: false, registerGiftCredits: { enable: true, credits: 50, diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 56fe117..0a6644b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -169,7 +169,7 @@ export interface PriceConfig { */ export interface CreditsConfig { enableCredits: boolean; // Whether to enable credits - enableForFreePlan: boolean; // Whether to enable purchase credits for free plan users + enablePackagesForFreePlan: boolean;// Whether to enable purchase credits for free plan users registerGiftCredits: { enable: boolean; // Whether to enable register gift credits credits: number; // The number of credits to give to the user From 669ac94badb39d5d9cdc114818a84597cffaaaa6 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 17:18:37 +0800 Subject: [PATCH 08/33] chore: update comments to reflect renaming of enableForFreePlan to enablePackagesForFreePlan for clarity --- src/components/settings/credits/credit-packages.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx index 012678b..9f41ce3 100644 --- a/src/components/settings/credits/credit-packages.tsx +++ b/src/components/settings/credits/credit-packages.tsx @@ -41,10 +41,10 @@ export function CreditPackages() { (pkg) => !pkg.disabled && pkg.price.priceId ); - // Check if user is on free plan and enableForFreePlan is false + // Check if user is on free plan and enablePackagesForFreePlan is false const isFreePlan = currentPlan?.isFree === true; - // Check if user is on free plan and enableForFreePlan is false + // Check if user is on free plan and enablePackagesForFreePlan is false if (isFreePlan && !websiteConfig.credits.enablePackagesForFreePlan) { return null; } From 1ff42009d879e516553a84501ce3ca0e20c31378 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 20:07:32 +0800 Subject: [PATCH 09/33] chore: update function parameters planId instead of priceId --- src/credits/credits.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/credits/credits.ts b/src/credits/credits.ts index 3d6e043..763370d 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'; import { websiteConfig } from '@/config/website'; import { getDb } from '@/db'; import { creditTransaction, userCredit } from '@/db/schema'; -import { findPlanByPriceId } from '@/lib/price-plan'; +import { findPlanByPlanId, findPlanByPriceId } from '@/lib/price-plan'; import { addDays, isAfter } from 'date-fns'; import { and, asc, eq, gt, isNull, not, or } from 'drizzle-orm'; import { CREDIT_TRANSACTION_TYPE } from './types'; @@ -442,11 +442,11 @@ export async function addRegisterGiftCredits(userId: string) { /** * Add free monthly credits * @param userId - User ID - * @param priceId - Price ID + * @param planId - Plan ID */ -export async function addMonthlyFreeCredits(userId: string, priceId: string) { +export async function addMonthlyFreeCredits(userId: string, planId: string) { // NOTICE: make sure the free plan is not disabled and has credits enabled - const pricePlan = findPlanByPriceId(priceId); + const pricePlan = findPlanByPlanId(planId); if ( !pricePlan || pricePlan.disabled || @@ -455,7 +455,7 @@ export async function addMonthlyFreeCredits(userId: string, priceId: string) { !pricePlan.credits.enable ) { console.log( - `addMonthlyFreeCredits, no credits configured for plan ${priceId}` + `addMonthlyFreeCredits, no credits configured for plan ${planId}` ); return; } From e15d76461f9fded5cd3036eb3b6ed45fe91c98f0 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 23 Aug 2025 20:21:54 +0800 Subject: [PATCH 10/33] feat: add function to check if subscription credits can be added based on last refresh time --- src/credits/credits.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/credits/credits.ts b/src/credits/credits.ts index 763370d..71f8326 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -403,6 +403,33 @@ export async function canAddMonthlyCredits(userId: string) { return canAdd; } +/** + * Check if subscription credits can be added for a user based on last refresh time + * @param userId - User ID + */ +export async function canAddSubscriptionCredits(userId: string) { + const db = await getDb(); + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const existingTransaction = await db + .select() + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + eq( + creditTransaction.type, + CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL + ), + gt(creditTransaction.createdAt, startOfMonth) + ) + ) + .limit(1); + + return existingTransaction.length === 0; +} + /** * Add register gift credits * @param userId - User ID @@ -508,7 +535,7 @@ export async function addSubscriptionCredits(userId: string, priceId: string) { return; } - const canAdd = await canAddMonthlyCredits(userId); + const canAdd = await canAddSubscriptionCredits(userId); const now = new Date(); // Add credits if it's a new month From 96d630f3aca3436490f001a9a170eaf4a511ecfb Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 00:52:06 +0800 Subject: [PATCH 11/33] chore: add new hostname configuration for service.firecrawl.dev in Next.js config --- next.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/next.config.ts b/next.config.ts index ac6ceeb..accad57 100644 --- a/next.config.ts +++ b/next.config.ts @@ -48,6 +48,10 @@ const nextConfig: NextConfig = { protocol: 'https', hostname: 'html.tailus.io', }, + { + protocol: 'https', + hostname: 'service.firecrawl.dev', + }, ], }, }; From e6bc1ea9e8794af27815b9dfc781c1d57bba70b3 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 01:16:39 +0800 Subject: [PATCH 12/33] refactor: rename 'credits' to 'amount' in credit-related configurations and components for consistency --- src/actions/create-credit-checkout-session.ts | 2 +- src/components/settings/credits/credit-packages.tsx | 2 +- src/config/website.tsx | 10 +++++----- src/credits/credits.ts | 2 +- src/credits/types.ts | 2 +- src/lib/auth.ts | 2 +- src/types/index.d.ts | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/actions/create-credit-checkout-session.ts b/src/actions/create-credit-checkout-session.ts index 7e12532..4099623 100644 --- a/src/actions/create-credit-checkout-session.ts +++ b/src/actions/create-credit-checkout-session.ts @@ -48,7 +48,7 @@ export const createCreditCheckoutSession = userActionClient ...metadata, type: 'credit_purchase', packageId, - credits: creditPackage.credits.toString(), + credits: creditPackage.amount.toString(), userId: currentUser.id, userName: currentUser.name, }; diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx index 9f41ce3..e78454c 100644 --- a/src/components/settings/credits/credit-packages.tsx +++ b/src/components/settings/credits/credit-packages.tsx @@ -84,7 +84,7 @@ export function CreditPackages() {
- {creditPackage.credits.toLocaleString()} + {creditPackage.amount.toLocaleString()}
diff --git a/src/config/website.tsx b/src/config/website.tsx index 3abe804..5bd39f0 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -158,14 +158,14 @@ export const websiteConfig: WebsiteConfig = { enablePackagesForFreePlan: false, registerGiftCredits: { enable: true, - credits: 50, + amount: 50, expireDays: 30, }, packages: { basic: { id: 'basic', popular: false, - credits: 100, + amount: 100, expireDays: 30, price: { priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC!, @@ -177,7 +177,7 @@ export const websiteConfig: WebsiteConfig = { standard: { id: 'standard', popular: true, - credits: 200, + amount: 200, expireDays: 30, price: { priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD!, @@ -189,7 +189,7 @@ export const websiteConfig: WebsiteConfig = { premium: { id: 'premium', popular: false, - credits: 500, + amount: 500, expireDays: 30, price: { priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM!, @@ -201,7 +201,7 @@ export const websiteConfig: WebsiteConfig = { enterprise: { id: 'enterprise', popular: false, - credits: 1000, + amount: 1000, expireDays: 30, price: { priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE!, diff --git a/src/credits/credits.ts b/src/credits/credits.ts index 71f8326..3bb3529 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -450,7 +450,7 @@ export async function addRegisterGiftCredits(userId: string) { // add register gift credits if user has not received them yet if (record.length === 0) { - const credits = websiteConfig.credits.registerGiftCredits.credits; + const credits = websiteConfig.credits.registerGiftCredits.amount; const expireDays = websiteConfig.credits.registerGiftCredits.expireDays; await addCredits({ userId, diff --git a/src/credits/types.ts b/src/credits/types.ts index c79a1ba..e8241d9 100644 --- a/src/credits/types.ts +++ b/src/credits/types.ts @@ -26,7 +26,7 @@ export interface CreditPackagePrice { */ export interface CreditPackage { id: string; // Unique identifier for the package - credits: number; // Number of credits in the package + amount: number; // Amount of credits in the package price: CreditPackagePrice; // Price of the package popular: boolean; // Whether the package is popular name?: string; // Display name of the package diff --git a/src/lib/auth.ts b/src/lib/auth.ts index a7a9293..4af988d 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -190,7 +190,7 @@ async function onCreateUser(user: User) { if ( websiteConfig.credits.enableCredits && websiteConfig.credits.registerGiftCredits.enable && - websiteConfig.credits.registerGiftCredits.credits > 0 + websiteConfig.credits.registerGiftCredits.amount > 0 ) { try { await addRegisterGiftCredits(user.id); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 0a6644b..beb7ac7 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -172,7 +172,7 @@ export interface CreditsConfig { enablePackagesForFreePlan: boolean;// Whether to enable purchase credits for free plan users registerGiftCredits: { enable: boolean; // Whether to enable register gift credits - credits: number; // The number of credits to give to the user + amount: number; // The amount of credits to give to the user expireDays?: number; // The number of days to expire the credits, undefined means no expire }; packages: Record; // Packages indexed by ID From 258ddad3996a9e7a502af158666c86d32b3af8f5 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 09:24:59 +0800 Subject: [PATCH 13/33] refactor: consolidate credit addition logic into a single function to improve maintainability and clarity --- src/credits/credits.ts | 69 +++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/src/credits/credits.ts b/src/credits/credits.ts index 3bb3529..6a4dd34 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -4,7 +4,7 @@ import { getDb } from '@/db'; import { creditTransaction, userCredit } from '@/db/schema'; import { findPlanByPlanId, findPlanByPriceId } from '@/lib/price-plan'; import { addDays, isAfter } from 'date-fns'; -import { and, asc, eq, gt, isNull, not, or } from 'drizzle-orm'; +import { and, asc, eq, gt, isNull, not, or, sql } from 'drizzle-orm'; import { CREDIT_TRANSACTION_TYPE } from './types'; /** @@ -375,54 +375,27 @@ export async function processExpiredCredits(userId: string) { } /** - * Check if credits can be added for a user based on last refresh time + * Check if specific type of credits can be added for a user based on transaction history * @param userId - User ID + * @param creditType - Type of credit transaction to check */ -export async function canAddMonthlyCredits(userId: string) { - const db = await getDb(); - const record = await db - .select() - .from(userCredit) - .where(eq(userCredit.userId, userId)) - .limit(1); - - const now = new Date(); - let canAdd = false; - - // Check if user has never received credits or it's a new month - if (!record[0]?.lastRefreshAt) { - canAdd = true; - } else { - // different month or year means new month - const last = new Date(record[0].lastRefreshAt); - canAdd = - now.getMonth() !== last.getMonth() || - now.getFullYear() !== last.getFullYear(); - } - - return canAdd; -} - -/** - * Check if subscription credits can be added for a user based on last refresh time - * @param userId - User ID - */ -export async function canAddSubscriptionCredits(userId: string) { +export async function canAddCreditsByType(userId: string, creditType: string) { const db = await getDb(); const now = new Date(); - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const currentMonth = now.getMonth(); + const currentYear = now.getFullYear(); + // Check if user has already received this type of credits this month const existingTransaction = await db .select() .from(creditTransaction) .where( and( eq(creditTransaction.userId, userId), - eq( - creditTransaction.type, - CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL - ), - gt(creditTransaction.createdAt, startOfMonth) + eq(creditTransaction.type, creditType), + // Check if transaction was created in the current month and year + sql`EXTRACT(MONTH FROM ${creditTransaction.createdAt}) = ${currentMonth + 1}`, + sql`EXTRACT(YEAR FROM ${creditTransaction.createdAt}) = ${currentYear}` ) ) .limit(1); @@ -430,6 +403,11 @@ export async function canAddSubscriptionCredits(userId: string) { return existingTransaction.length === 0; } +/** + * Check if subscription credits can be added for a user based on last refresh time + * @param userId - User ID + */ + /** * Add register gift credits * @param userId - User ID @@ -487,7 +465,10 @@ export async function addMonthlyFreeCredits(userId: string, planId: string) { return; } - const canAdd = await canAddMonthlyCredits(userId); + const canAdd = await canAddCreditsByType( + userId, + CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH + ); const now = new Date(); // add credits if it's a new month @@ -535,7 +516,10 @@ export async function addSubscriptionCredits(userId: string, priceId: string) { return; } - const canAdd = await canAddSubscriptionCredits(userId); + const canAdd = await canAddCreditsByType( + userId, + CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL + ); const now = new Date(); // Add credits if it's a new month @@ -588,7 +572,10 @@ export async function addLifetimeMonthlyCredits( return; } - const canAdd = await canAddMonthlyCredits(userId); + const canAdd = await canAddCreditsByType( + userId, + CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY + ); const now = new Date(); // Add credits if it's a new month From afdaeba2be0effbb8798b22df20c08fab82fa4bd Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 09:56:19 +0800 Subject: [PATCH 14/33] refactor: remove updateUserLastRefreshAt function and its calls to streamline credit update logic --- src/credits/credits.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/credits/credits.ts b/src/credits/credits.ts index 6a4dd34..c82ce45 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -49,23 +49,6 @@ export async function updateUserCredits(userId: string, credits: number) { } } -/** - * Update user's last refresh time - * @param userId - User ID - * @param date - Last refresh time - */ -export async function updateUserLastRefreshAt(userId: string, date: Date) { - try { - const db = await getDb(); - await db - .update(userCredit) - .set({ lastRefreshAt: date, updatedAt: new Date() }) - .where(eq(userCredit.userId, userId)); - } catch (error) { - console.error('updateUserLastRefreshAt, error:', error); - } -} - /** * Write a credit transaction record * @param params - Credit transaction parameters @@ -483,9 +466,6 @@ export async function addMonthlyFreeCredits(userId: string, planId: string) { expireDays, }); - // Update last refresh time for free monthly credits - await updateUserLastRefreshAt(userId, now); - console.log( `addMonthlyFreeCredits, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}` ); @@ -535,9 +515,6 @@ export async function addSubscriptionCredits(userId: string, priceId: string) { expireDays, }); - // Update last refresh time for subscription credits - await updateUserLastRefreshAt(userId, now); - console.log( `addSubscriptionCredits, ${credits} credits for user ${userId}, priceId: ${priceId}, date: ${now.getFullYear()}-${now.getMonth() + 1}` ); @@ -591,9 +568,6 @@ export async function addLifetimeMonthlyCredits( expireDays, }); - // Update last refresh time for lifetime credits - await updateUserLastRefreshAt(userId, now); - console.log( `addLifetimeMonthlyCredits, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}` ); From 2d0392db61dcfdd00648f9ebe9f2a4a8639f3a49 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 10:03:35 +0800 Subject: [PATCH 15/33] refactor: update credit eligibility checks to use canAddCreditsByType function for improved clarity and maintainability --- src/credits/distribute.ts | 76 ++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/src/credits/distribute.ts b/src/credits/distribute.ts index 9c8f13d..2d2d1e4 100644 --- a/src/credits/distribute.ts +++ b/src/credits/distribute.ts @@ -5,6 +5,7 @@ import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan'; import { PlanIntervals } from '@/payment/types'; import { addDays } from 'date-fns'; import { and, eq, gt, inArray, isNull, lt, not, or, sql } from 'drizzle-orm'; +import { canAddCreditsByType } from './credits'; import { CREDIT_TRANSACTION_TYPE } from './types'; /** @@ -218,7 +219,6 @@ export async function batchAddMonthlyFreeCredits(userIds: string[]) { const userCredits = await tx .select({ userId: userCredit.userId, - lastRefreshAt: userCredit.lastRefreshAt, currentCredits: userCredit.currentCredits, }) .from(userCredit) @@ -229,19 +229,17 @@ export async function batchAddMonthlyFreeCredits(userIds: string[]) { userCredits.map((record) => [record.userId, record]) ); - // Filter users who can receive credits - const eligibleUserIds = userIds.filter((userId) => { - const record = userCreditMap.get(userId); - if (!record?.lastRefreshAt) { - return true; // never added credits before - } - // different month or year means new month - const last = new Date(record.lastRefreshAt); - return ( - now.getMonth() !== last.getMonth() || - now.getFullYear() !== last.getFullYear() + // Check which users can receive credits based on transaction history + const eligibleUserIds: string[] = []; + for (const userId of userIds) { + const canAdd = await canAddCreditsByType( + userId, + CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH ); - }); + if (canAdd) { + eligibleUserIds.push(userId); + } + } if (eligibleUserIds.length === 0) { console.log('batchAddMonthlyFreeCredits, no eligible users'); @@ -280,7 +278,6 @@ export async function batchAddMonthlyFreeCredits(userIds: string[]) { id: randomUUID(), userId, currentCredits: credits, - lastRefreshAt: now, createdAt: now, updatedAt: now, })); @@ -297,7 +294,6 @@ export async function batchAddMonthlyFreeCredits(userIds: string[]) { .update(userCredit) .set({ currentCredits: newBalance, - lastRefreshAt: now, updatedAt: now, }) .where(eq(userCredit.userId, userId)); @@ -362,7 +358,6 @@ export async function batchAddLifetimeMonthlyCredits( const userCredits = await tx .select({ userId: userCredit.userId, - lastRefreshAt: userCredit.lastRefreshAt, currentCredits: userCredit.currentCredits, }) .from(userCredit) @@ -373,19 +368,17 @@ export async function batchAddLifetimeMonthlyCredits( userCredits.map((record) => [record.userId, record]) ); - // Filter users who can receive credits - const eligibleUserIds = userIdsForPrice.filter((userId: string) => { - const record = userCreditMap.get(userId); - if (!record?.lastRefreshAt) { - return true; // never added credits before - } - // different month or year means new month - const last = new Date(record.lastRefreshAt); - return ( - now.getMonth() !== last.getMonth() || - now.getFullYear() !== last.getFullYear() + // Check which users can receive credits based on transaction history + const eligibleUserIds: string[] = []; + for (const userId of userIdsForPrice) { + const canAdd = await canAddCreditsByType( + userId, + CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY ); - }); + if (canAdd) { + eligibleUserIds.push(userId); + } + } if (eligibleUserIds.length === 0) { console.log( @@ -426,7 +419,6 @@ export async function batchAddLifetimeMonthlyCredits( id: randomUUID(), userId, currentCredits: credits, - lastRefreshAt: now, createdAt: now, updatedAt: now, })); @@ -443,7 +435,6 @@ export async function batchAddLifetimeMonthlyCredits( .update(userCredit) .set({ currentCredits: newBalance, - lastRefreshAt: now, updatedAt: now, }) .where(eq(userCredit.userId, userId)); @@ -508,7 +499,6 @@ export async function batchAddYearlyUsersMonthlyCredits( const userCredits = await tx .select({ userId: userCredit.userId, - lastRefreshAt: userCredit.lastRefreshAt, currentCredits: userCredit.currentCredits, }) .from(userCredit) @@ -519,19 +509,17 @@ export async function batchAddYearlyUsersMonthlyCredits( userCredits.map((record) => [record.userId, record]) ); - // Filter users who can receive credits - const eligibleUserIds = userIds.filter((userId) => { - const record = userCreditMap.get(userId); - if (!record?.lastRefreshAt) { - return true; // never added credits before - } - // different month or year means new month - const last = new Date(record.lastRefreshAt); - return ( - now.getMonth() !== last.getMonth() || - now.getFullYear() !== last.getFullYear() + // Check which users can receive credits based on transaction history + const eligibleUserIds: string[] = []; + for (const userId of userIds) { + const canAdd = await canAddCreditsByType( + userId, + CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL ); - }); + if (canAdd) { + eligibleUserIds.push(userId); + } + } if (eligibleUserIds.length === 0) { console.log( @@ -572,7 +560,6 @@ export async function batchAddYearlyUsersMonthlyCredits( id: randomUUID(), userId, currentCredits: credits, - lastRefreshAt: now, createdAt: now, updatedAt: now, })); @@ -589,7 +576,6 @@ export async function batchAddYearlyUsersMonthlyCredits( .update(userCredit) .set({ currentCredits: newBalance, - lastRefreshAt: now, updatedAt: now, }) .where(eq(userCredit.userId, userId)); From 33fe00b8dc6f92c99055073476a60343459c344f Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 10:22:31 +0800 Subject: [PATCH 16/33] chore: mark lastRefreshAt field as deprecated in userCredit schema --- src/credits/credits.ts | 2 -- src/db/schema.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/credits/credits.ts b/src/credits/credits.ts index c82ce45..d78b03f 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -147,7 +147,6 @@ export async function addCredits({ .update(userCredit) .set({ currentCredits: newBalance, - // lastRefreshAt: new Date(), // NOTE: we can not update this field here updatedAt: new Date(), }) .where(eq(userCredit.userId, userId)); @@ -158,7 +157,6 @@ export async function addCredits({ id: randomUUID(), userId, currentCredits: newBalance, - // lastRefreshAt: new Date(), // NOTE: we can not update this field here createdAt: new Date(), updatedAt: new Date(), }); diff --git a/src/db/schema.ts b/src/db/schema.ts index 81e0df1..ad8c17e 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -94,7 +94,7 @@ export const userCredit = pgTable("user_credit", { id: text("id").primaryKey(), userId: text("user_id").notNull().references(() => user.id, { onDelete: 'cascade' }), currentCredits: integer("current_credits").notNull().default(0), - lastRefreshAt: timestamp("last_refresh_at"), + lastRefreshAt: timestamp("last_refresh_at"), // deprecated createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ From e626bb9af4af082d2eb6e8580cca7c0bc624192b Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 24 Aug 2025 10:37:37 +0800 Subject: [PATCH 17/33] feat: add ai-elements components --- package.json | 6 + pnpm-lock.yaml | 596 +++++++++++++++++- src/components/ai-elements/actions.tsx | 65 ++ src/components/ai-elements/branch.tsx | 212 +++++++ src/components/ai-elements/code-block.tsx | 150 +++++ src/components/ai-elements/conversation.tsx | 62 ++ src/components/ai-elements/image.tsx | 24 + .../ai-elements/inline-citation.tsx | 287 +++++++++ src/components/ai-elements/loader.tsx | 96 +++ src/components/ai-elements/message.tsx | 64 ++ src/components/ai-elements/prompt-input.tsx | 230 +++++++ src/components/ai-elements/reasoning.tsx | 180 ++++++ src/components/ai-elements/response.tsx | 22 + src/components/ai-elements/source.tsx | 74 +++ src/components/ai-elements/suggestion.tsx | 56 ++ src/components/ai-elements/task.tsx | 94 +++ src/components/ai-elements/tool.tsx | 142 +++++ src/components/ai-elements/web-preview.tsx | 252 ++++++++ 18 files changed, 2610 insertions(+), 2 deletions(-) create mode 100644 src/components/ai-elements/actions.tsx create mode 100644 src/components/ai-elements/branch.tsx create mode 100644 src/components/ai-elements/code-block.tsx create mode 100644 src/components/ai-elements/conversation.tsx create mode 100644 src/components/ai-elements/image.tsx create mode 100644 src/components/ai-elements/inline-citation.tsx create mode 100644 src/components/ai-elements/loader.tsx create mode 100644 src/components/ai-elements/message.tsx create mode 100644 src/components/ai-elements/prompt-input.tsx create mode 100644 src/components/ai-elements/reasoning.tsx create mode 100644 src/components/ai-elements/response.tsx create mode 100644 src/components/ai-elements/source.tsx create mode 100644 src/components/ai-elements/suggestion.tsx create mode 100644 src/components/ai-elements/task.tsx create mode 100644 src/components/ai-elements/tool.tsx create mode 100644 src/components/ai-elements/web-preview.tsx diff --git a/package.json b/package.json index a2aba56..2a19b20 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@ai-sdk/fireworks": "^1.0.0", "@ai-sdk/google": "^2.0.0", "@ai-sdk/openai": "^2.0.0", + "@ai-sdk/react": "^2.0.22", "@ai-sdk/replicate": "^1.0.0", "@base-ui-components/react": "1.0.0-beta.0", "@better-fetch/fetch": "^1.1.18", @@ -74,6 +75,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-use-controllable-state": "^1.2.2", "@react-email/components": "0.0.33", "@react-email/render": "1.0.5", "@stripe/stripe-js": "^5.6.0", @@ -118,6 +120,7 @@ "react-hook-form": "^7.62.0", "react-remove-scroll": "^2.6.3", "react-resizable-panels": "^2.1.7", + "react-syntax-highlighter": "^15.6.3", "react-tweet": "^3.2.2", "react-use-measure": "^2.1.7", "recharts": "^2.15.1", @@ -125,6 +128,7 @@ "s3mini": "^0.2.0", "shiki": "^2.4.2", "sonner": "^2.0.0", + "streamdown": "^1.0.12", "stripe": "^17.6.0", "swiper": "^11.2.5", "tailwind-merge": "^3.0.2", @@ -132,6 +136,7 @@ "tw-animate-css": "^1.2.4", "use-intl": "^3.26.5", "use-media": "^1.5.0", + "use-stick-to-bottom": "^1.1.1", "vaul": "^1.1.2", "zod": "^4.0.17", "zustand": "^5.0.3" @@ -145,6 +150,7 @@ "@types/pg": "^8.11.11", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", "drizzle-kit": "^0.30.4", "knip": "^5.61.2", "postcss": "^8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1296d35..969aa6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@ai-sdk/openai': specifier: ^2.0.0 version: 2.0.0(zod@4.0.17) + '@ai-sdk/react': + specifier: ^2.0.22 + version: 2.0.22(react@19.0.0)(zod@4.0.17) '@ai-sdk/replicate': specifier: ^1.0.0 version: 1.0.0(zod@4.0.17) @@ -155,6 +158,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': + specifier: ^1.2.2 + version: 1.2.2(@types/react@19.0.9)(react@19.0.0) '@react-email/components': specifier: 0.0.33 version: 0.0.33(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -287,6 +293,9 @@ importers: react-resizable-panels: specifier: ^2.1.7 version: 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-syntax-highlighter: + specifier: ^15.6.3 + version: 15.6.3(react@19.0.0) react-tweet: specifier: ^3.2.2 version: 3.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -308,6 +317,9 @@ importers: sonner: specifier: ^2.0.0 version: 2.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + streamdown: + specifier: ^1.0.12 + version: 1.0.12(@types/react@19.0.9)(react@19.0.0) stripe: specifier: ^17.6.0 version: 17.6.0 @@ -329,6 +341,9 @@ importers: use-media: specifier: ^1.5.0 version: 1.5.0(react@19.0.0) + use-stick-to-bottom: + specifier: ^1.1.1 + version: 1.1.1(react@19.0.0) vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -363,6 +378,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.0.3(@types/react@19.0.9) + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 drizzle-kit: specifier: ^0.30.4 version: 0.30.4 @@ -411,6 +429,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/gateway@1.0.11': + resolution: {integrity: sha512-ErwWS3sPOuWy42eE3AVxlKkTa1XjjKBEtNCOylVKMO5KNyz5qie8QVlLYbULOG56dtxX4zTKX3rQNJudplhcmQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/google@2.0.0': resolution: {integrity: sha512-35uWKG+aWm0QClJV/kNhcyR9IVrDkZoI1UlWvUCjwoqbCxj4/L/1LKKbpM3JSRa9u74ghHzBB0UjLHdgcIoanw==} engines: {node: '>=18'} @@ -435,10 +459,26 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.5': + resolution: {integrity: sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider@2.0.0': resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} + '@ai-sdk/react@2.0.22': + resolution: {integrity: sha512-nJt2U0ZDjpdPEIHCEWlxOixUhQyA/teQ0y9gz66mYW40OhBjSsZjcEAYhbS05mvy+NMVqzlE3sVu54DqzjR68w==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.25.76 || ^4 + peerDependenciesMeta: + zod: + optional: true + '@ai-sdk/replicate@1.0.0': resolution: {integrity: sha512-whCL8u2aKXJcD8LmxK9oZOL3I/XkLgY7PqNsqLzemP5AlchjZTn8LLvwx5LBc2W3nkEXOz4Kt1oJGv1rQRxbnA==} engines: {node: '>=18'} @@ -3326,24 +3366,36 @@ packages: '@shikijs/core@2.5.0': resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + '@shikijs/core@3.11.0': + resolution: {integrity: sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g==} + '@shikijs/core@3.9.1': resolution: {integrity: sha512-W5Vwen0KJCtR7KFRo+3JLGAqLUPsfW7e+wZ4yaRBGIogwI9ZlnkpRm9ZV8JtfzMxOkIwZwMmmN0hNErLtm3AYg==} '@shikijs/engine-javascript@2.5.0': resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + '@shikijs/engine-javascript@3.11.0': + resolution: {integrity: sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA==} + '@shikijs/engine-javascript@3.9.1': resolution: {integrity: sha512-4hGenxYpAmtALryKsdli2K58F0s7RBYpj/RSDcAAGfRM6eTEGI5cZnt86mr+d9/4BaZ5sH5s4p3VU5irIdhj9Q==} '@shikijs/engine-oniguruma@2.5.0': resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + '@shikijs/engine-oniguruma@3.11.0': + resolution: {integrity: sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw==} + '@shikijs/engine-oniguruma@3.9.1': resolution: {integrity: sha512-WPlL/xqviwS3te4unSGGGfflKsuHLMI6tPdNYvgz/IygcBT6UiwDFSzjBKyebwi5GGSlXsjjdoJLIBnAplmEZw==} '@shikijs/langs@2.5.0': resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + '@shikijs/langs@3.11.0': + resolution: {integrity: sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw==} + '@shikijs/langs@3.9.1': resolution: {integrity: sha512-Vyy2Yv9PP3Veh3VSsIvNncOR+O93wFsNYgN2B6cCCJlS7H9SKFYc55edsqernsg8WT/zam1cfB6llJsQWLnVhA==} @@ -3353,6 +3405,9 @@ packages: '@shikijs/themes@2.5.0': resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + '@shikijs/themes@3.11.0': + resolution: {integrity: sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q==} + '@shikijs/themes@3.9.1': resolution: {integrity: sha512-zAykkGECNICCMXpKeVvq04yqwaSuAIvrf8MjsU5bzskfg4XreU+O0B5wdNCYRixoB9snd3YlZ373WV5E/g5T9A==} @@ -3362,6 +3417,9 @@ packages: '@shikijs/types@2.5.0': resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + '@shikijs/types@3.11.0': + resolution: {integrity: sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==} + '@shikijs/types@3.9.1': resolution: {integrity: sha512-rqM3T7a0iM1oPKz9iaH/cVgNX9Vz1HERcUcXJ94/fulgVdwqfnhXzGxO4bLrAnh/o5CPLy3IcYedogfV+Ns0Qg==} @@ -3568,12 +3626,18 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3600,6 +3664,9 @@ packages: peerDependencies: '@types/react': ^19.0.0 + '@types/react-syntax-highlighter@15.5.13': + resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} + '@types/react@19.0.9': resolution: {integrity: sha512-FedNTYgmMwSZmD1Sru/W1gJKuiYCN/3SuBkmZkcxX+FpO5zL76B22A9YNfAKg4HQO3Neh/30AiynP6BELdU0qQ==} @@ -3731,6 +3798,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + ai@5.0.22: + resolution: {integrity: sha512-RZiYhj7Ux7hrLtXkHPcxzdiSZt4NOiC69O5AkNfMCsz3twwz/KRkl9ASptosoOsg833s5yRcTSdIu5z53Sl6Pw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -3849,12 +3922,21 @@ packages: character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -3911,6 +3993,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -3918,6 +4003,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -4230,6 +4319,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -4393,6 +4486,9 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} @@ -4443,6 +4539,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + formatly@0.2.4: resolution: {integrity: sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==} engines: {node: '>=18.3.0'} @@ -4581,6 +4681,12 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + harden-react-markdown@1.0.4: + resolution: {integrity: sha512-F9JGhMEOPIQjRLL1iwznxXQuJnXuyyhudToQ1ZDFxWz21ZKo1NoD80ymWxAsgGp1RRWunChJQXd9qmp9OMp/yQ==} + peerDependencies: + react: '>=16.8.0' + react-markdown: '>=9.0.0' + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4597,6 +4703,27 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} @@ -4609,13 +4736,31 @@ packages: hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -4661,15 +4806,24 @@ packages: intl-messageformat@10.7.15: resolution: {integrity: sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg==} + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -4685,6 +4839,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} @@ -4747,6 +4904,10 @@ packages: engines: {node: '>=6'} hasBin: true + katex@0.16.22: + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4857,6 +5018,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4872,6 +5036,11 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.539.0: + resolution: {integrity: sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -4879,6 +5048,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@16.2.0: + resolution: {integrity: sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg==} + engines: {node: '>= 20'} + hasBin: true + marked@7.0.4: resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} engines: {node: '>= 16'} @@ -4917,6 +5091,9 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -4972,6 +5149,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-extension-mdx-expression@3.0.0: resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} @@ -5272,9 +5452,15 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -5428,16 +5614,27 @@ packages: engines: {node: '>=14'} hasBin: true + prismjs@1.27.0: + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} + engines: {node: '>=6'} + prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + proj4@2.15.0: resolution: {integrity: sha512-LqCNEcPdI03BrCHxPLj29vsd5afsm+0sV1H/O3nTDKrv8/LA01ea1z4QADDMjUqxSXWnrmmQDjqFm1J/uZ5RLw==} prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} @@ -5503,6 +5700,12 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-medium-image-zoom@5.3.0: resolution: {integrity: sha512-RCIzVlsKqy3BYgGgYbolUfuvx0aSKC7YhX/IJGEp+WJxsqdIVYJHkBdj++FAj6VD7RiWj6VVmdCfa/9vJE9hZg==} peerDependencies: @@ -5564,6 +5767,11 @@ packages: '@types/react': optional: true + react-syntax-highlighter@15.6.3: + resolution: {integrity: sha512-HebdyA9r20hgmA0q8RyRJ4c/vB4E6KL2HeWb5MNjU3iJEiT2w9jfU2RJsmI6f3Cy3SGE5tm0AIkBzM/E7e9/lQ==} + peerDependencies: + react: '>= 0.14.0' + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -5619,6 +5827,9 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + refractor@3.6.0: + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -5631,12 +5842,18 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + remark-mdx@3.1.0: resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} @@ -5717,6 +5934,9 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + shiki@3.11.0: + resolution: {integrity: sha512-VgKumh/ib38I1i3QkMn6mAQA6XjjQubqaAYhfge71glAll0/4xnt8L2oSuC45Qcr/G5Kbskj4RliMQddGmy/Og==} + shiki@3.9.1: resolution: {integrity: sha512-HogZ8nMnv9VAQMrG+P7BleJFhrKHm3fi6CYyHRbUu61gJ0lpqLr6ecYEui31IYG1Cn9Bad7N2vf332iXHnn0bQ==} @@ -5782,6 +6002,9 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -5789,6 +6012,11 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + streamdown@1.0.12: + resolution: {integrity: sha512-4vea/NYZE+gRcEZYS5PSU2IBFgH1zA2yklei9FvtlwJt9/oaRFnaUDpBDuD5EgdvqtUAGt4J2p4H/fZFgRi0Ow==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5883,6 +6111,10 @@ packages: third-party-capital@1.0.20: resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -5941,6 +6173,9 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -5950,6 +6185,9 @@ packages: unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -6003,6 +6241,11 @@ packages: '@types/react': optional: true + use-stick-to-bottom@1.1.1: + resolution: {integrity: sha512-JkDp0b0tSmv7HQOOpL1hT7t7QaoUBXkq045WWWOFDTlLGRzgIIyW7vyzOIJzY7L2XVIG7j1yUxeDj2LHm9Vwng==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.4.0: resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} peerDependencies: @@ -6026,6 +6269,9 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -6042,6 +6288,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6159,6 +6408,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.0(zod@4.0.17) zod: 4.0.17 + '@ai-sdk/gateway@1.0.11(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.5(zod@4.0.17) + zod: 4.0.17 + '@ai-sdk/google@2.0.0(zod@4.0.17)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -6185,10 +6440,28 @@ snapshots: zod: 4.0.17 zod-to-json-schema: 3.24.6(zod@4.0.17) + '@ai-sdk/provider-utils@3.0.5(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 4.0.17 + zod-to-json-schema: 3.24.6(zod@4.0.17) + '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 + '@ai-sdk/react@2.0.22(react@19.0.0)(zod@4.0.17)': + dependencies: + '@ai-sdk/provider-utils': 3.0.5(zod@4.0.17) + ai: 5.0.22(zod@4.0.17) + react: 19.0.0 + swr: 2.3.2(react@19.0.0) + throttleit: 2.1.0 + optionalDependencies: + zod: 4.0.17 + '@ai-sdk/replicate@1.0.0(zod@4.0.17)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -8800,6 +9073,13 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 + '@shikijs/core@3.11.0': + dependencies: + '@shikijs/types': 3.11.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + '@shikijs/core@3.9.1': dependencies: '@shikijs/types': 3.9.1 @@ -8813,6 +9093,12 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 3.1.1 + '@shikijs/engine-javascript@3.11.0': + dependencies: + '@shikijs/types': 3.11.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + '@shikijs/engine-javascript@3.9.1': dependencies: '@shikijs/types': 3.9.1 @@ -8824,6 +9110,11 @@ snapshots: '@shikijs/types': 2.5.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.11.0': + dependencies: + '@shikijs/types': 3.11.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.9.1': dependencies: '@shikijs/types': 3.9.1 @@ -8833,6 +9124,10 @@ snapshots: dependencies: '@shikijs/types': 2.5.0 + '@shikijs/langs@3.11.0': + dependencies: + '@shikijs/types': 3.11.0 + '@shikijs/langs@3.9.1': dependencies: '@shikijs/types': 3.9.1 @@ -8850,6 +9145,10 @@ snapshots: dependencies: '@shikijs/types': 2.5.0 + '@shikijs/themes@3.11.0': + dependencies: + '@shikijs/types': 3.11.0 + '@shikijs/themes@3.9.1': dependencies: '@shikijs/types': 3.9.1 @@ -8864,6 +9163,11 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@3.11.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/types@3.9.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -9057,12 +9361,18 @@ snapshots: '@types/estree@1.0.6': {} + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 '@types/json-schema@7.0.15': {} + '@types/katex@0.16.7': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -9097,6 +9407,10 @@ snapshots: dependencies: '@types/react': 19.0.9 + '@types/react-syntax-highlighter@15.5.13': + dependencies: + '@types/react': 19.0.9 + '@types/react@19.0.9': dependencies: csstype: 3.1.3 @@ -9203,6 +9517,14 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.0.17 + ai@5.0.22(zod@4.0.17): + dependencies: + '@ai-sdk/gateway': 1.0.11(zod@4.0.17) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.5(zod@4.0.17) + '@opentelemetry/api': 1.9.0 + zod: 4.0.17 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -9341,10 +9663,16 @@ snapshots: character-entities-html4@2.1.0: {} + character-entities-legacy@1.1.4: {} + character-entities-legacy@3.0.0: {} + character-entities@1.2.4: {} + character-entities@2.0.2: {} + character-reference-invalid@1.1.4: {} + character-reference-invalid@2.0.1: {} chokidar@4.0.3: @@ -9403,10 +9731,14 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@1.0.8: {} + comma-separated-tokens@2.0.3: {} commander@11.1.0: {} + commander@8.3.0: {} + compute-scroll-into-view@3.1.1: {} concat-map@0.0.1: {} @@ -9623,6 +9955,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -9934,6 +10268,10 @@ snapshots: dependencies: reusify: 1.1.0 + fault@1.0.4: + dependencies: + format: 0.2.2 + fd-package-json@2.0.0: dependencies: walk-up-path: 4.0.0 @@ -9980,6 +10318,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + format@0.2.2: {} + formatly@0.2.4: dependencies: fd-package-json: 2.0.0 @@ -10132,6 +10472,11 @@ snapshots: graceful-fs@4.2.11: {} + harden-react-markdown@1.0.4(react-markdown@10.1.0(@types/react@19.0.9)(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-markdown: 10.1.0(@types/react@19.0.9)(react@19.0.0) + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -10144,6 +10489,49 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.0.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@2.2.5: {} + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-estree@3.1.3: dependencies: '@types/estree': 1.0.6 @@ -10203,10 +10591,37 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 + hastscript@6.0.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + + highlight.js@10.7.3: {} + + highlightjs-vue@1.0.0: {} + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -10215,6 +10630,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} htmlparser2@8.0.2: @@ -10255,8 +10672,15 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.1 tslib: 2.8.1 + is-alphabetical@1.0.4: {} + is-alphabetical@2.0.1: {} + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + is-alphanumerical@2.0.1: dependencies: is-alphabetical: 2.0.1 @@ -10265,6 +10689,8 @@ snapshots: is-arrayish@0.3.2: optional: true + is-decimal@1.0.4: {} + is-decimal@2.0.1: {} is-extglob@2.1.1: {} @@ -10275,6 +10701,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@1.0.4: {} + is-hexadecimal@2.0.1: {} is-interactive@1.0.0: {} @@ -10315,6 +10743,10 @@ snapshots: json5@2.2.3: {} + katex@0.16.22: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10416,6 +10848,11 @@ snapshots: dependencies: js-tokens: 4.0.0 + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + lru-cache@10.4.3: {} lru-cache@11.1.0: {} @@ -10428,10 +10865,16 @@ snapshots: dependencies: react: 19.0.0 + lucide-react@0.539.0(react@19.0.0): + dependencies: + react: 19.0.0 + markdown-extensions@2.0.0: {} markdown-table@3.0.4: {} + marked@16.2.0: {} + marked@7.0.4: {} math-intrinsics@1.1.0: {} @@ -10522,6 +10965,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -10685,6 +11140,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.1 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.7 + devlop: 1.1.0 + katex: 0.16.22 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + micromark-extension-mdx-expression@3.0.0: dependencies: '@types/estree': 1.0.6 @@ -11087,6 +11552,15 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -11097,6 +11571,10 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseley@0.12.1: dependencies: leac: 0.6.0 @@ -11252,8 +11730,12 @@ snapshots: prettier@3.5.3: {} + prismjs@1.27.0: {} + prismjs@1.29.0: {} + prismjs@1.30.0: {} + proj4@2.15.0: dependencies: mgrs: 1.0.0 @@ -11265,6 +11747,10 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + property-information@7.0.0: {} proxy-from-env@1.1.0: {} @@ -11392,6 +11878,24 @@ snapshots: react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@19.0.9)(react@19.0.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.0.9 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-medium-image-zoom@5.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -11452,6 +11956,16 @@ snapshots: optionalDependencies: '@types/react': 19.0.9 + react-syntax-highlighter@15.6.3(react@19.0.0): + dependencies: + '@babel/runtime': 7.27.6 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 19.0.0 + refractor: 3.6.0 + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@babel/runtime': 7.26.10 @@ -11532,6 +12046,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + refractor@3.6.0: + dependencies: + hastscript: 6.0.0 + parse-entities: 2.0.0 + prismjs: 1.27.0 + regenerator-runtime@0.14.1: {} regex-recursion@6.0.2: @@ -11544,6 +12064,16 @@ snapshots: dependencies: regex-utilities: 2.3.0 + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.7 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.22 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.6 @@ -11563,6 +12093,15 @@ snapshots: transitivePeerDependencies: - supports-color + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-mdx@3.1.0: dependencies: mdast-util-mdx: 3.0.0 @@ -11688,6 +12227,17 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shiki@3.11.0: + dependencies: + '@shikijs/core': 3.11.0 + '@shikijs/engine-javascript': 3.11.0 + '@shikijs/engine-oniguruma': 3.11.0 + '@shikijs/langs': 3.11.0 + '@shikijs/themes': 3.11.0 + '@shikijs/types': 3.11.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + shiki@3.9.1: dependencies: '@shikijs/core': 3.9.1 @@ -11784,11 +12334,31 @@ snapshots: source-map@0.7.4: {} + space-separated-tokens@1.1.5: {} + space-separated-tokens@2.0.2: {} split2@4.2.0: optional: true + streamdown@1.0.12(@types/react@19.0.9)(react@19.0.0): + dependencies: + clsx: 2.1.1 + harden-react-markdown: 1.0.4(react-markdown@10.1.0(@types/react@19.0.9)(react@19.0.0))(react@19.0.0) + katex: 0.16.22 + lucide-react: 0.539.0(react@19.0.0) + marked: 16.2.0 + react: 19.0.0 + react-markdown: 10.1.0(@types/react@19.0.9)(react@19.0.0) + rehype-katex: 7.0.1 + remark-gfm: 4.0.1 + remark-math: 6.0.0 + shiki: 3.11.0 + tailwind-merge: 3.3.1 + transitivePeerDependencies: + - '@types/react' + - supports-color + streamsearch@1.1.0: {} string-width@4.2.3: @@ -11872,6 +12442,8 @@ snapshots: third-party-capital@1.0.20: {} + throttleit@2.1.0: {} + tiny-invariant@1.3.3: {} tinyexec@1.0.1: {} @@ -11926,6 +12498,11 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -11938,6 +12515,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -11995,6 +12577,10 @@ snapshots: optionalDependencies: '@types/react': 19.0.9 + use-stick-to-bottom@1.1.1(react@19.0.0): + dependencies: + react: 19.0.0 + use-sync-external-store@1.4.0(react@19.0.0): dependencies: react: 19.0.0 @@ -12016,6 +12602,11 @@ snapshots: - '@types/react' - '@types/react-dom' + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -12049,6 +12640,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-namespaces@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -12071,8 +12664,7 @@ snapshots: ws@8.17.1: {} - xtend@4.0.2: - optional: true + xtend@4.0.2: {} yallist@3.1.1: {} diff --git a/src/components/ai-elements/actions.tsx b/src/components/ai-elements/actions.tsx new file mode 100644 index 0000000..5267d6b --- /dev/null +++ b/src/components/ai-elements/actions.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import type { ComponentProps } from 'react'; + +export type ActionsProps = ComponentProps<'div'>; + +export const Actions = ({ className, children, ...props }: ActionsProps) => ( +
+ {children} +
+); + +export type ActionProps = ComponentProps & { + tooltip?: string; + label?: string; +}; + +export const Action = ({ + tooltip, + children, + label, + className, + variant = 'ghost', + size = 'sm', + ...props +}: ActionProps) => { + const button = ( + + ); + + if (tooltip) { + return ( + + + {button} + +

{tooltip}

+
+
+
+ ); + } + + return button; +}; diff --git a/src/components/ai-elements/branch.tsx b/src/components/ai-elements/branch.tsx new file mode 100644 index 0000000..70eb6c3 --- /dev/null +++ b/src/components/ai-elements/branch.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import type { UIMessage } from 'ai'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import type { ComponentProps, HTMLAttributes, ReactElement } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; + +type BranchContextType = { + currentBranch: number; + totalBranches: number; + goToPrevious: () => void; + goToNext: () => void; + branches: ReactElement[]; + setBranches: (branches: ReactElement[]) => void; +}; + +const BranchContext = createContext(null); + +const useBranch = () => { + const context = useContext(BranchContext); + + if (!context) { + throw new Error('Branch components must be used within Branch'); + } + + return context; +}; + +export type BranchProps = HTMLAttributes & { + defaultBranch?: number; + onBranchChange?: (branchIndex: number) => void; +}; + +export const Branch = ({ + defaultBranch = 0, + onBranchChange, + className, + ...props +}: BranchProps) => { + const [currentBranch, setCurrentBranch] = useState(defaultBranch); + const [branches, setBranches] = useState([]); + + const handleBranchChange = (newBranch: number) => { + setCurrentBranch(newBranch); + onBranchChange?.(newBranch); + }; + + const goToPrevious = () => { + const newBranch = + currentBranch > 0 ? currentBranch - 1 : branches.length - 1; + handleBranchChange(newBranch); + }; + + const goToNext = () => { + const newBranch = + currentBranch < branches.length - 1 ? currentBranch + 1 : 0; + handleBranchChange(newBranch); + }; + + const contextValue: BranchContextType = { + currentBranch, + totalBranches: branches.length, + goToPrevious, + goToNext, + branches, + setBranches, + }; + + return ( + +
div]:pb-0', className)} + {...props} + /> + + ); +}; + +export type BranchMessagesProps = HTMLAttributes; + +export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => { + const { currentBranch, setBranches, branches } = useBranch(); + const childrenArray = Array.isArray(children) ? children : [children]; + + // Use useEffect to update branches when they change + useEffect(() => { + if (branches.length !== childrenArray.length) { + setBranches(childrenArray); + } + }, [childrenArray, branches, setBranches]); + + return childrenArray.map((branch, index) => ( +
div]:pb-0', + index === currentBranch ? 'block' : 'hidden' + )} + key={branch.key} + {...props} + > + {branch} +
+ )); +}; + +export type BranchSelectorProps = HTMLAttributes & { + from: UIMessage['role']; +}; + +export const BranchSelector = ({ + className, + from, + ...props +}: BranchSelectorProps) => { + const { totalBranches } = useBranch(); + + // Don't render if there's only one branch + if (totalBranches <= 1) { + return null; + } + + return ( +
+ ); +}; + +export type BranchPreviousProps = ComponentProps; + +export const BranchPrevious = ({ + className, + children, + ...props +}: BranchPreviousProps) => { + const { goToPrevious, totalBranches } = useBranch(); + + return ( + + ); +}; + +export type BranchNextProps = ComponentProps; + +export const BranchNext = ({ + className, + children, + ...props +}: BranchNextProps) => { + const { goToNext, totalBranches } = useBranch(); + + return ( + + ); +}; + +export type BranchPageProps = HTMLAttributes; + +export const BranchPage = ({ className, ...props }: BranchPageProps) => { + const { currentBranch, totalBranches } = useBranch(); + + return ( + + {currentBranch + 1} of {totalBranches} + + ); +}; diff --git a/src/components/ai-elements/code-block.tsx b/src/components/ai-elements/code-block.tsx new file mode 100644 index 0000000..dcbccbd --- /dev/null +++ b/src/components/ai-elements/code-block.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { CheckIcon, CopyIcon } from 'lucide-react'; +import type { ComponentProps, HTMLAttributes, ReactNode } from 'react'; +import { createContext, useContext, useState } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { + oneDark, + oneLight, +} from 'react-syntax-highlighter/dist/esm/styles/prism'; + +type CodeBlockContextType = { + code: string; +}; + +const CodeBlockContext = createContext({ + code: '', +}); + +export type CodeBlockProps = HTMLAttributes & { + code: string; + language: string; + showLineNumbers?: boolean; + children?: ReactNode; +}; + +export const CodeBlock = ({ + code, + language, + showLineNumbers = false, + className, + children, + ...props +}: CodeBlockProps) => ( + +
+
+ {/* @ts-expect-error - SyntaxHighlighter is not a valid JSX component */} + + {code} + + {/* @ts-expect-error - SyntaxHighlighter is not a valid JSX component */} + + {code} + + {children && ( +
+ {children} +
+ )} +
+
+
+); + +export type CodeBlockCopyButtonProps = ComponentProps & { + onCopy?: () => void; + onError?: (error: Error) => void; + timeout?: number; +}; + +export const CodeBlockCopyButton = ({ + onCopy, + onError, + timeout = 2000, + children, + className, + ...props +}: CodeBlockCopyButtonProps) => { + const [isCopied, setIsCopied] = useState(false); + const { code } = useContext(CodeBlockContext); + + const copyToClipboard = async () => { + if (typeof window === 'undefined' || !navigator.clipboard.writeText) { + onError?.(new Error('Clipboard API not available')); + return; + } + + try { + await navigator.clipboard.writeText(code); + setIsCopied(true); + onCopy?.(); + setTimeout(() => setIsCopied(false), timeout); + } catch (error) { + onError?.(error as Error); + } + }; + + const Icon = isCopied ? CheckIcon : CopyIcon; + + return ( + + ); +}; diff --git a/src/components/ai-elements/conversation.tsx b/src/components/ai-elements/conversation.tsx new file mode 100644 index 0000000..1fb6b55 --- /dev/null +++ b/src/components/ai-elements/conversation.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { ArrowDownIcon } from 'lucide-react'; +import type { ComponentProps } from 'react'; +import { useCallback } from 'react'; +import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'; + +export type ConversationProps = ComponentProps; + +export const Conversation = ({ className, ...props }: ConversationProps) => ( + +); + +export type ConversationContentProps = ComponentProps< + typeof StickToBottom.Content +>; + +export const ConversationContent = ({ + className, + ...props +}: ConversationContentProps) => ( + +); + +export type ConversationScrollButtonProps = ComponentProps; + +export const ConversationScrollButton = ({ + className, + ...props +}: ConversationScrollButtonProps) => { + const { isAtBottom, scrollToBottom } = useStickToBottomContext(); + + const handleScrollToBottom = useCallback(() => { + scrollToBottom(); + }, [scrollToBottom]); + + return ( + !isAtBottom && ( + + ) + ); +}; diff --git a/src/components/ai-elements/image.tsx b/src/components/ai-elements/image.tsx new file mode 100644 index 0000000..405e1ee --- /dev/null +++ b/src/components/ai-elements/image.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/lib/utils'; +import type { Experimental_GeneratedImage } from 'ai'; + +export type ImageProps = Experimental_GeneratedImage & { + className?: string; + alt?: string; +}; + +export const Image = ({ + base64, + uint8Array, + mediaType, + ...props +}: ImageProps) => ( + {props.alt} +); diff --git a/src/components/ai-elements/inline-citation.tsx b/src/components/ai-elements/inline-citation.tsx new file mode 100644 index 0000000..10e951c --- /dev/null +++ b/src/components/ai-elements/inline-citation.tsx @@ -0,0 +1,287 @@ +'use client'; + +import { Badge } from '@/components/ui/badge'; +import { + Carousel, + CarouselContent, + CarouselItem, + type CarouselApi, +} from '@/components/ui/carousel'; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from '@/components/ui/hover-card'; +import { cn } from '@/lib/utils'; +import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react'; +import { + type ComponentProps, + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; + +export type InlineCitationProps = ComponentProps<'span'>; + +export const InlineCitation = ({ + className, + ...props +}: InlineCitationProps) => ( + +); + +export type InlineCitationTextProps = ComponentProps<'span'>; + +export const InlineCitationText = ({ + className, + ...props +}: InlineCitationTextProps) => ( + +); + +export type InlineCitationCardProps = ComponentProps; + +export const InlineCitationCard = (props: InlineCitationCardProps) => ( + +); + +export type InlineCitationCardTriggerProps = ComponentProps & { + sources: string[]; +}; + +export const InlineCitationCardTrigger = ({ + sources, + className, + ...props +}: InlineCitationCardTriggerProps) => ( + + + {sources.length ? ( + <> + {new URL(sources[0]).hostname}{' '} + {sources.length > 1 && `+${sources.length - 1}`} + + ) : ( + 'unknown' + )} + + +); + +export type InlineCitationCardBodyProps = ComponentProps<'div'>; + +export const InlineCitationCardBody = ({ + className, + ...props +}: InlineCitationCardBodyProps) => ( + +); + +const CarouselApiContext = createContext(undefined); + +const useCarouselApi = () => { + const context = useContext(CarouselApiContext); + return context; +}; + +export type InlineCitationCarouselProps = ComponentProps; + +export const InlineCitationCarousel = ({ + className, + children, + ...props +}: InlineCitationCarouselProps) => { + const [api, setApi] = useState(); + + return ( + + + {children} + + + ); +}; + +export type InlineCitationCarouselContentProps = ComponentProps<'div'>; + +export const InlineCitationCarouselContent = ( + props: InlineCitationCarouselContentProps +) => ; + +export type InlineCitationCarouselItemProps = ComponentProps<'div'>; + +export const InlineCitationCarouselItem = ({ + className, + ...props +}: InlineCitationCarouselItemProps) => ( + +); + +export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>; + +export const InlineCitationCarouselHeader = ({ + className, + ...props +}: InlineCitationCarouselHeaderProps) => ( +
+); + +export type InlineCitationCarouselIndexProps = ComponentProps<'div'>; + +export const InlineCitationCarouselIndex = ({ + children, + className, + ...props +}: InlineCitationCarouselIndexProps) => { + const api = useCarouselApi(); + const [current, setCurrent] = useState(0); + const [count, setCount] = useState(0); + + useEffect(() => { + if (!api) { + return; + } + + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); + + api.on('select', () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); + + return ( +
+ {children ?? `${current}/${count}`} +
+ ); +}; + +export type InlineCitationCarouselPrevProps = ComponentProps<'button'>; + +export const InlineCitationCarouselPrev = ({ + className, + ...props +}: InlineCitationCarouselPrevProps) => { + const api = useCarouselApi(); + + const handleClick = useCallback(() => { + if (api) { + api.scrollPrev(); + } + }, [api]); + + return ( + + ); +}; + +export type InlineCitationCarouselNextProps = ComponentProps<'button'>; + +export const InlineCitationCarouselNext = ({ + className, + ...props +}: InlineCitationCarouselNextProps) => { + const api = useCarouselApi(); + + const handleClick = useCallback(() => { + if (api) { + api.scrollNext(); + } + }, [api]); + + return ( + + ); +}; + +export type InlineCitationSourceProps = ComponentProps<'div'> & { + title?: string; + url?: string; + description?: string; +}; + +export const InlineCitationSource = ({ + title, + url, + description, + className, + children, + ...props +}: InlineCitationSourceProps) => ( +
+ {title && ( +

{title}

+ )} + {url && ( +

{url}

+ )} + {description && ( +

+ {description} +

+ )} + {children} +
+); + +export type InlineCitationQuoteProps = ComponentProps<'blockquote'>; + +export const InlineCitationQuote = ({ + children, + className, + ...props +}: InlineCitationQuoteProps) => ( +
+ {children} +
+); diff --git a/src/components/ai-elements/loader.tsx b/src/components/ai-elements/loader.tsx new file mode 100644 index 0000000..be469aa --- /dev/null +++ b/src/components/ai-elements/loader.tsx @@ -0,0 +1,96 @@ +import { cn } from '@/lib/utils'; +import type { HTMLAttributes } from 'react'; + +type LoaderIconProps = { + size?: number; +}; + +const LoaderIcon = ({ size = 16 }: LoaderIconProps) => ( + + Loader + + + + + + + + + + + + + + + + + + +); + +export type LoaderProps = HTMLAttributes & { + size?: number; +}; + +export const Loader = ({ className, size = 16, ...props }: LoaderProps) => ( +
+ +
+); diff --git a/src/components/ai-elements/message.tsx b/src/components/ai-elements/message.tsx new file mode 100644 index 0000000..8eaf6f8 --- /dev/null +++ b/src/components/ai-elements/message.tsx @@ -0,0 +1,64 @@ +import { + Avatar, + AvatarFallback, + AvatarImage, +} from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; +import type { UIMessage } from 'ai'; +import type { ComponentProps, HTMLAttributes } from 'react'; + +export type MessageProps = HTMLAttributes & { + from: UIMessage['role']; +}; + +export const Message = ({ className, from, ...props }: MessageProps) => ( +
div]:max-w-[80%]', + className + )} + {...props} + /> +); + +export type MessageContentProps = HTMLAttributes; + +export const MessageContent = ({ + children, + className, + ...props +}: MessageContentProps) => ( +
+
{children}
+
+); + +export type MessageAvatarProps = ComponentProps & { + src: string; + name?: string; +}; + +export const MessageAvatar = ({ + src, + name, + className, + ...props +}: MessageAvatarProps) => ( + + + {name?.slice(0, 2) || 'ME'} + +); diff --git a/src/components/ai-elements/prompt-input.tsx b/src/components/ai-elements/prompt-input.tsx new file mode 100644 index 0000000..72f6994 --- /dev/null +++ b/src/components/ai-elements/prompt-input.tsx @@ -0,0 +1,230 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import type { ChatStatus } from 'ai'; +import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react'; +import type { + ComponentProps, + HTMLAttributes, + KeyboardEventHandler, +} from 'react'; +import { Children } from 'react'; + +export type PromptInputProps = HTMLAttributes; + +export const PromptInput = ({ className, ...props }: PromptInputProps) => ( +
+); + +export type PromptInputTextareaProps = ComponentProps & { + minHeight?: number; + maxHeight?: number; +}; + +export const PromptInputTextarea = ({ + onChange, + className, + placeholder = 'What would you like to know?', + minHeight = 48, + maxHeight = 164, + ...props +}: PromptInputTextareaProps) => { + const handleKeyDown: KeyboardEventHandler = (e) => { + if (e.key === 'Enter') { + // Don't submit if IME composition is in progress + if (e.nativeEvent.isComposing) { + return; + } + + if (e.shiftKey) { + // Allow newline + return; + } + + // Submit on Enter (without Shift) + e.preventDefault(); + const form = e.currentTarget.form; + if (form) { + form.requestSubmit(); + } + } + }; + + return ( +