diff --git a/.env.example b/.env.example index f291b5f9..ab85e436 100644 --- a/.env.example +++ b/.env.example @@ -52,7 +52,7 @@ SENTRY_DSN= # AI Agent (Payload CMS Agent via OpenRouter) # ──────────────────────────────────────────────────────────────────────────── # Required for the Payload CMS AI Chat Agent -OPENROUTER_API_KEY= +MISTRAL_API_KEY= # ──────────────────────────────────────────────────────────────────────────── # Payload Infrastructure (Dockerized) diff --git a/.npmrc b/.npmrc index 70ea8b5b..ea651854 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1 @@ @mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/ -//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN} diff --git a/Dockerfile.dev b/Dockerfile.dev index 4b0dfa7a..bd6326c7 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,7 +1,7 @@ FROM node:20-alpine # Install essential build tools if needed (e.g., for node-gyp) -RUN apk add --no-cache libc6-compat python3 make g++ +RUN apk add --no-cache libc6-compat python3 make g++ curl WORKDIR /app diff --git a/app/(payload)/admin/importMap.js b/app/(payload)/admin/importMap.js index aecd3639..10c6ec49 100644 --- a/app/(payload)/admin/importMap.js +++ b/app/(payload)/admin/importMap.js @@ -1,57 +1,84 @@ -import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { default as default_9ed509b5e5f7d08a16335393f27586cc } from '../../../src/payload/components/Icon' -import { default as default_5470ea90f7a8fd882c2fe59ff2b1c5b9 } from '../../../src/payload/components/Logo' -import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' +import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'; +import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'; +import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'; +import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'; +import { default as default_9ed509b5e5f7d08a16335393f27586cc } from '../../../src/payload/components/Icon'; +import { default as default_5470ea90f7a8fd882c2fe59ff2b1c5b9 } from '../../../src/payload/components/Logo'; +import { ChatWindowProvider as ChatWindowProvider_d32a660df96f186e48bfc5b31626ccf5 } from '@mintel/payload-ai/components/ChatWindow'; +import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'; export const importMap = { - "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "/src/payload/components/Icon#default": default_9ed509b5e5f7d08a16335393f27586cc, - "/src/payload/components/Logo#default": default_5470ea90f7a8fd882c2fe59ff2b1c5b9, - "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 -} + '@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell': + RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, + '@payloadcms/richtext-lexical/rsc#RscEntryLexicalField': + RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, + '@payloadcms/richtext-lexical/rsc#LexicalDiffComponent': + LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, + '@payloadcms/richtext-lexical/client#BlocksFeatureClient': + BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': + InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient': + HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#UploadFeatureClient': + UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#BlockquoteFeatureClient': + BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#RelationshipFeatureClient': + RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#LinkFeatureClient': + LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#ChecklistFeatureClient': + ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#OrderedListFeatureClient': + OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#UnorderedListFeatureClient': + UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#IndentFeatureClient': + IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#AlignFeatureClient': + AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#HeadingFeatureClient': + HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#ParagraphFeatureClient': + ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#InlineCodeFeatureClient': + InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#SuperscriptFeatureClient': + SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#SubscriptFeatureClient': + SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#StrikethroughFeatureClient': + StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#UnderlineFeatureClient': + UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#BoldFeatureClient': + BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '@payloadcms/richtext-lexical/client#ItalicFeatureClient': + ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + '/src/payload/components/Icon#default': default_9ed509b5e5f7d08a16335393f27586cc, + '/src/payload/components/Logo#default': default_5470ea90f7a8fd882c2fe59ff2b1c5b9, + '@mintel/payload-ai/components/ChatWindow#ChatWindowProvider': + ChatWindowProvider_d32a660df96f186e48bfc5b31626ccf5, + '@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1, +}; diff --git a/app/api/ai-search/route.ts b/app/api/ai-search/route.ts index f5ed898f..6158e820 100644 --- a/app/api/ai-search/route.ts +++ b/app/api/ai-search/route.ts @@ -3,16 +3,33 @@ import { searchProducts } from '../../../src/lib/qdrant'; import redis from '../../../src/lib/redis'; import { z } from 'zod'; import * as Sentry from '@sentry/nextjs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; // Max allowed duration (Vercel) + // Config and constants -const RATE_LIMIT_POINTS = 5; // 5 requests -const RATE_LIMIT_DURATION = 60 * 1; // per 1 minute +const RATE_LIMIT_POINTS = 20; // 20 requests per minute +const RATE_LIMIT_DURATION = 60; // 1 minute window +const DAILY_BUDGET_LIMIT = 200; // max 200 requests per IP per day +const DAILY_BUDGET_DURATION = 60 * 60 * 24; // 24h +const MAX_CONVERSATION_MESSAGES = 20; // max messages in context +const MAX_RESPONSE_TOKENS = 300; // cap AI response length — keeps it chat-like // Removed requestSchema as it's replaced by direct parsing export async function POST(req: NextRequest) { // Changed req type to NextRequest try { - const { messages, visitorId, honeypot } = await req.json(); + let body: any; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + const { messages, honeypot } = body; + + // Get client IP for rate limiting + const forwarded = req.headers.get('x-forwarded-for'); + const clientIp = forwarded?.split(',')[0]?.trim() || req.headers.get('x-real-ip') || 'unknown'; // 1. Basic Validation if (!messages || !Array.isArray(messages) || messages.length === 0) { @@ -38,35 +55,50 @@ export async function POST(req: NextRequest) { }); } - // 3. Rate Limiting via Redis + // 3. Rate Limiting via Redis (IP-based) try { - if (visitorId) { - const requestCount = await redis.incr(`ai_search_rate_limit:${visitorId}`); - if (requestCount === 1) { - await redis.expire(`ai_search_rate_limit:${visitorId}`, RATE_LIMIT_DURATION); // Use constant - } + // Per-minute burst limit + const minuteKey = `ai_rate:${clientIp}:min`; + const minuteCount = await redis.incr(minuteKey); + if (minuteCount === 1) await redis.expire(minuteKey, RATE_LIMIT_DURATION); - if (requestCount > RATE_LIMIT_POINTS) { - // Use constant - return NextResponse.json( - { - error: 'Rate limit exceeded. Please try again later.', - }, - { status: 429 }, - ); - } + if (minuteCount > RATE_LIMIT_POINTS) { + return NextResponse.json( + { error: 'Zu viele Anfragen. Bitte warte einen Moment.' }, + { status: 429 }, + ); + } + + // Daily budget limit + const dayKey = `ai_rate:${clientIp}:day`; + const dayCount = await redis.incr(dayKey); + if (dayCount === 1) await redis.expire(dayKey, DAILY_BUDGET_DURATION); + + if (dayCount > DAILY_BUDGET_LIMIT) { + return NextResponse.json( + { error: 'Tägliches Limit erreicht. Bitte versuche es morgen erneut.' }, + { status: 429 }, + ); } } catch (redisError) { - // Renamed variable for clarity - console.error('Redis Rate Limiting Error:', redisError); // Changed to error for consistency + console.error('Redis Rate Limiting Error:', redisError); Sentry.captureException(redisError, { tags: { context: 'ai-search-rate-limit' } }); // Fail open if Redis is down } + // 4. Cap conversation length to limit token usage + const cappedMessages = messages.slice(-MAX_CONVERSATION_MESSAGES); + // 4. Fetch Context from Qdrant based on the latest message let contextStr = ''; let foundProducts: any[] = []; + // Team context — hardcoded from translation data (no Payload collection for team) + const teamContextStr = ` +Das ECHTE KLZ Team: +- Michael Bodemer (Geschäftsführer) — Der Macher, packt an wenn es kompliziert wird, kennt Kabelnetze in- und auswendig +- Klaus Mintel (Geschäftsführer) — Der Fels in der Brandung, jahrzehntelange Erfahrung, stabiles Netzwerk`; + try { const searchResults = await searchProducts(latestMessage, 5); @@ -85,50 +117,69 @@ export async function POST(req: NextRequest) { foundProducts = searchResults .filter((p) => (p.payload?.type === 'product' || !p.payload?.type) && p.payload?.data) - .map((p: any) => p.payload?.data); + .map((p: any) => ({ + id: p.id as string, + title: p.payload?.data?.title as string, + sku: p.payload?.data?.sku as string, + slug: p.payload?.data?.slug as string, + })); } - } catch (e) { - console.error('Qdrant Search Error:', e); - Sentry.captureException(e, { tags: { context: 'ai-search-qdrant' } }); + } catch (searchError) { + console.error('Qdrant Search Error:', searchError); + Sentry.captureException(searchError, { tags: { context: 'ai-search-qdrant' } }); // We can still proceed without context if Qdrant fails } // 5. Generate AI Response via OpenRouter (Mistral for DSGVO) - const systemPrompt = `Du bist ein professioneller und extrem kompetenter Sales-Engineer / Consultant der Firma "KLZ Cables". -Deine Aufgabe ist es, Kunden und Interessenten bei der Auswahl von Mittelspannungskabeln, Starkstromkabeln und Infrastrukturausrüstung beratend zur Seite zu stehen. + const systemPrompt = `Du bist "Ohm" — der digitale KI-Berater von KLZ Cables. Dein Name ist eine Anspielung auf die Einheit des elektrischen Widerstands. -WICHTIGE REGELN: -1. ANTWORTE IMMER IN DER SPRACHE DES BENUTZERS. Wenn der Benutzer Deutsch spricht, antworte auf Deutsch. -2. Wenn der Kunde vage ist (z.B. "Ich will einen Windpark bauen"), würge ihn NICHT ab. Stelle stattdessen gezielte, professionelle Rückfragen als Berater (z.B. "Für einen Windpark benötigen wir einige Rahmendaten: Reden wir über die Parkverkabelung (Mittelspannung, z.B. 20kV oder 33kV) oder die Netzanbindung? Welche Querschnitte oder Ströme erwarten Sie?"). -3. Nutze das bereitgestellte KABELWISSEN und KATALOG-Gedächtnis unten, um deine Antworten zu fundieren. -4. Bleibe stets professionell, lösungsorientiert und leicht technisch (Industrial Aesthetic). Du kannst humorvoll sein, wenn der Nutzer offensichtlich Quatsch fragt, aber lenke es immer elegant zurück zu Kabeln oder Energieinfrastruktur. -5. Antworte in reinem Text (kein Markdown für die Antwort, es sei denn es sind einfache Absätze oder Listen). -6. Wenn genügend Informationen vorhanden sind, präsentiere passende Kabel aus dem Katalog. -7. Oute dich als Berater von KLZ Cables. +STIL & PERSÖNLICHKEIT: +- Antworte KURZ, KNAPP und PROFESSIONELL (maximal 2-3 Sätze). +- Schreibe wie in einem lockeren, aber kompetenten B2B-Chat (Du-Form ist okay, aber fachlich top). +- Kein Markdown, nur Fließtext. +- NIEMALS Platzhalter wie [Ihr Name], [Name], [Firma] verwenden. -VERFÜGBARER KONTEXT: -${contextStr ? contextStr : 'Keine spezifischen Katalogdaten für diese Anfrage gefunden.'} +DEINE HAUPTAUFGABE: BERATEN, NICHT AUSFRAGEN! +- Wenn der Kunde ein Projekt nennt (z.B. "Windpark 30kV"), dann lies im KONTEXT nach, welche Kabel passen, und EMPFIEHL SIE DIREKT! (z.B. "Für 30kV Windparks nehmen wir meistens NA2XS(F)2Y."). +- Stelle NIEMALS mehr als EINE Rückfrage pro Nachricht. +- FRAGE NICHT nach abstrakten Dingen wie "Welchen Kabeltyp brauchst du?" -> DAS IST DEIN JOB, IHM DAS ZU SAGEN! +- FRAGE NICHT nach Längen oder genauen Trassen, es sei denn, der Kunde hat schon ganz klar gesagt, was er kaufen will. +- Biete aktiv Hilfe an: "Ich kann dir die passenden Querschnitte raussuchen, wenn du willst." + +VORGEHEN: +1. Prüfe den KONTEXT auf passende Kabel für das Kundenprojekt. +2. Nenne direkt 1-2 passende Produktserien aus dem Kontext, die für diesen Fall Sinn machen. +3. Biete eine konkrete Hilfestellung an (z.B. Leitungsberechnung, Verfügbarkeitsprüfung) ODER stelle EINE einzige fachliche Rückfrage, um das Kabel weiter einzugrenzen (z.B. Alu oder Kupfer?). +4. Wenn das Projekt klar ist und die Kabeltypen besprochen sind, frag nach, ob ein Kollege (z.B. Micha) ein konkretes Angebot machen soll. + +GRENZEN: +- PRIVAT-ANFRAGEN: B2B only. Private Hausinstallationen lehnen wir freundlich ab. +- Keine Preise oder genauen Lieferzeiten versprechen. Immer auf die menschlichen Kollegen verweisen für finale Angebote. + +KONTEXT KABEL & TEAM: +${contextStr || 'Kein Katalogkontext verfügbar.'} +${teamContextStr} `; - const openRouterKey = process.env.OPENROUTER_API_KEY; - if (!openRouterKey) { - throw new Error('OPENROUTER_API_KEY is not set'); + const mistralKey = process.env.MISTRAL_API_KEY; + if (!mistralKey) { + throw new Error('MISTRAL_API_KEY is not set'); } - const fetchRes = await fetch('https://openrouter.ai/api/v1/chat/completions', { + // DSGVO: Mistral AI API direkt (EU/Frankreich) statt OpenRouter (US) + const fetchRes = await fetch('https://api.mistral.ai/v1/chat/completions', { method: 'POST', headers: { - Authorization: `Bearer ${openRouterKey}`, + Authorization: `Bearer ${mistralKey}`, 'Content-Type': 'application/json', - 'HTTP-Referer': process.env.NEXT_PUBLIC_BASE_URL || 'https://klz-cables.com', - 'X-Title': 'KLZ Cables Search AI', }, body: JSON.stringify({ - model: 'mistralai/mistral-large-2407', + model: 'ministral-8b-latest', temperature: 0.3, + max_tokens: MAX_RESPONSE_TOKENS, messages: [ { role: 'system', content: systemPrompt }, - ...messages.map((m: any) => ({ + ...cappedMessages.map((m: any) => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content), })), @@ -138,7 +189,19 @@ ${contextStr ? contextStr : 'Keine spezifischen Katalogdaten für diese Anfrage if (!fetchRes.ok) { const errBody = await fetchRes.text(); - throw new Error(`OpenRouter API Error: ${errBody}`); + console.error('Mistral API Error:', errBody); + Sentry.captureException(new Error(`Mistral ${fetchRes.status}: ${errBody}`), { + tags: { context: 'ai-search-mistral' }, + }); + + // Return user-friendly error based on status + const userMsg = + fetchRes.status === 429 + ? 'Der KI-Service ist gerade überlastet. Bitte versuche es in ein paar Sekunden erneut.' + : fetchRes.status >= 500 + ? 'Der KI-Service ist vorübergehend nicht erreichbar. Bitte versuche es gleich nochmal.' + : 'Es gab ein Problem mit der KI-Anfrage. Bitte versuche es erneut.'; + return NextResponse.json({ error: userMsg }, { status: 502 }); } const data = await fetchRes.json(); @@ -152,6 +215,9 @@ ${contextStr ? contextStr : 'Keine spezifischen Katalogdaten für diese Anfrage } catch (error) { console.error('AI Search API Error:', error); Sentry.captureException(error, { tags: { context: 'ai-search-api' } }); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + return NextResponse.json( + { error: 'Ein interner Fehler ist aufgetreten. Bitte versuche es erneut.' }, + { status: 500 }, + ); } } diff --git a/app/api/sync-qdrant/route.ts b/app/api/sync-qdrant/route.ts new file mode 100644 index 00000000..bed2dbda --- /dev/null +++ b/app/api/sync-qdrant/route.ts @@ -0,0 +1,126 @@ +import { NextResponse } from 'next/server'; +import { getPayload } from 'payload'; +import configPromise from '../../../payload.config'; +import { upsertProductVector } from '../../../src/lib/qdrant'; + +export const dynamic = 'force-dynamic'; +export const maxDuration = 120; + +/** + * Internal endpoint called by the warmup script on every dev boot. + * Syncs posts, pages, and products from Payload CMS into Qdrant. + * NOT for form entries, media, or users. + */ +export async function GET() { + const results = { products: 0, posts: 0, pages: 0, errors: [] as string[] }; + + try { + const payload = await getPayload({ config: configPromise }); + + // ── Products ── + const { docs: products } = await payload.find({ + collection: 'products', + limit: 1000, + depth: 0, + where: { _status: { equals: 'published' } }, + }); + + for (const product of products) { + try { + const contentText = `${product.title} - SKU: ${product.sku}\n${product.description || ''}`; + await upsertProductVector(String(product.id), contentText, { + type: 'product', + data: { + title: product.title, + sku: product.sku, + slug: product.slug, + description: product.description, + }, + }); + results.products++; + } catch (e: any) { + results.errors.push(`product:${product.sku}: ${e.message}`); + } + } + + // ── Posts ── + const { docs: posts } = await payload.find({ + collection: 'posts', + limit: 1000, + depth: 0, + where: { _status: { equals: 'published' } }, + }); + + for (const post of posts) { + try { + const contentText = [ + `Blog-Artikel: ${post.title}`, + post.excerpt ? `Zusammenfassung: ${post.excerpt}` : '', + post.category ? `Kategorie: ${post.category}` : '', + ] + .filter(Boolean) + .join('\n'); + + await upsertProductVector(`post_${post.id}`, contentText, { + type: 'knowledge', + content: contentText, + data: { + title: post.title, + slug: post.slug, + }, + }); + results.posts++; + } catch (e: any) { + results.errors.push(`post:${post.slug}: ${e.message}`); + } + } + + // ── Pages ── + const { docs: pages } = await payload.find({ + collection: 'pages', + limit: 1000, + depth: 0, + where: { _status: { equals: 'published' } }, + }); + + for (const page of pages) { + try { + const contentText = [ + `Seite: ${page.title}`, + page.excerpt ? `Beschreibung: ${page.excerpt}` : '', + ] + .filter(Boolean) + .join('\n'); + + await upsertProductVector(`page_${page.id}`, contentText, { + type: 'knowledge', + content: contentText, + data: { + title: page.title, + slug: page.slug, + }, + }); + results.pages++; + } catch (e: any) { + results.errors.push(`page:${page.slug}: ${e.message}`); + } + } + + console.log( + `[Qdrant Sync] ✅ ${results.products} products, ${results.posts} posts, ${results.pages} pages synced`, + ); + + return NextResponse.json({ + success: true, + synced: { + products: results.products, + posts: results.posts, + pages: results.pages, + }, + errors: results.errors.length > 0 ? results.errors : undefined, + }); + } catch (error: any) { + console.error('[Qdrant Sync] ❌ Fatal error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index 998c4a94..7f117db2 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -6,11 +6,11 @@ import { useTranslations, useLocale } from 'next-intl'; import dynamic from 'next/dynamic'; import { useAnalytics } from '../analytics/useAnalytics'; import { AnalyticsEvents } from '../analytics/analytics-events'; -import AIOrb from '../search/AIOrb'; -import { useState } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { ChevronRight } from 'lucide-react'; import { AISearchResults } from '../search/AISearchResults'; const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false }); +const AIOrb = dynamic(() => import('../search/AIOrb'), { ssr: false }); export default function Hero({ data }: { data?: any }) { const t = useTranslations('Home.hero'); @@ -19,6 +19,62 @@ export default function Hero({ data }: { data?: any }) { const [searchQuery, setSearchQuery] = useState(''); const [isSearchOpen, setIsSearchOpen] = useState(false); + const [heroPlaceholder, setHeroPlaceholder] = useState( + 'Projekt beschreiben oder Kabel suchen...', + ); + const typingRef = useRef | null>(null); + + const HERO_PLACEHOLDERS = [ + 'Querschnittsberechnung für 110kV Trasse', // Hochspannung + 'Wie schwer ist NAYY 4x150?', + 'Ich plane einen Solarpark, was brauche ich?', // Projekt Solar + 'Unterschied zwischen N2XSY und NAY2XSY?', // Fach + 'Mittelspannungskabel für Windkraftanlage', // Windpark + 'Welches Aluminiumkabel für 20kV?', // Mittelspannung + ]; + + // Typing animation for the hero search placeholder + useEffect(() => { + if (searchQuery) { + setHeroPlaceholder('Projekt beschreiben oder Kabel suchen...'); + return; + } + + let textIdx = 0; + let charIdx = 0; + let deleting = false; + + const tick = () => { + const fullText = HERO_PLACEHOLDERS[textIdx]; + + if (deleting) { + charIdx--; + setHeroPlaceholder(fullText.substring(0, charIdx)); + } else { + charIdx++; + setHeroPlaceholder(fullText.substring(0, charIdx)); + } + + let delay = deleting ? 30 : 70; + + if (!deleting && charIdx === fullText.length) { + delay = 2500; + deleting = true; + } else if (deleting && charIdx === 0) { + deleting = false; + textIdx = (textIdx + 1) % HERO_PLACEHOLDERS.length; + delay = 400; + } + + typingRef.current = setTimeout(tick, delay); + }; + + typingRef.current = setTimeout(tick, 1500); + + return () => { + if (typingRef.current) clearTimeout(typingRef.current); + }; + }, [searchQuery]); const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -79,15 +135,16 @@ export default function Hero({ data }: { data?: any }) { onSubmit={handleSearchSubmit} className="w-full max-w-2xl bg-white/10 backdrop-blur-md border border-white/20 rounded-2xl p-2 flex items-center mt-8 mb-10 transition-all focus-within:bg-white/15 focus-within:border-accent shadow-lg relative" > -
+
setSearchQuery(e.target.value)} - placeholder="Projekt beschreiben oder Kabel suchen..." - className="flex-1 bg-transparent border-none text-white pl-12 pr-2 py-4 placeholder:text-white/50 focus:outline-none text-lg lg:text-xl" + placeholder={heroPlaceholder} + className="flex-1 bg-transparent border-none text-white pl-20 pr-2 py-4 placeholder:text-white/50 focus:outline-none text-lg lg:text-xl" + autoFocus /> -
- - {/* Chat History Area */} -
- {messages.length === 0 && !isLoading && !error && ( -
- -

I am your technical consultant.

-

- Describe your project, ask for specific cables, or tell me your requirements. -

+ {/* ── Glassmorphism container ── */} +
+ {/* ── Header ── */} +
+
+
+ +
+
+

Ohm

+

+ {isLoading ? 'Denkt nach...' : error ? 'Fehler aufgetreten' : 'Online'} +

+
- )} - - {messages.map((msg, index) => ( -
-
- {msg.role === 'assistant' && ( -

- - AI Assistant -

- )} -
- {msg.role === 'assistant' ? ( - {msg.content} +
+ {messages.length > 0 && ( + + )} + +
+
+ + {/* ── Chat Area ── */} +
+ {/* Empty state */} + {messages.length === 0 && !isLoading && !error && ( +
+
+ +
+
+

+ Wie kann ich helfen? +

+

+ Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine + Anforderungen. +

+
+ {/* Quick prompts */} +
+ {['Windpark 33kV Verkabelung', 'NYCWY 4x185', 'Erdkabel für Solarpark'].map( + (prompt) => ( + + ), )}
+
+ )} - {/* Product Matches inside Assistant Message */} - {msg.role === 'assistant' && msg.products && msg.products.length > 0 && ( -
-

- Empfohlene Produkte -

-
- {msg.products.map((product, idx) => ( - { - onClose(); - trackEvent(AnalyticsEvents.BUTTON_CLICK, { - target: product.slug, - location: 'ai_search_results', - }); + {/* Messages */} + {messages.map((msg, index) => ( +
+
+ {/* Copy Button */} + + + {msg.role === 'assistant' && ( +
+ + + Ohm + +
+ )} +
+ {msg.role === 'assistant' ? ( + {msg.content} + ) : ( +

{msg.content}

+ )} +
+ + {/* Timestamp */} + {!msg.products?.length && ( +

+ {new Date(msg.timestamp).toLocaleTimeString('de', { + hour: '2-digit', + minute: '2-digit', + })} +

+ )} + + {/* Product cards */} + {msg.role === 'assistant' && msg.products && msg.products.length > 0 && ( +
+

+ Empfohlene Produkte +

+
+ {msg.products.map((product, idx) => ( + { + onClose(); + trackEvent(AnalyticsEvents.BUTTON_CLICK, { + target: product.slug, + location: 'ai_chat', + }); + }} + className="group flex items-center justify-between bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.06] hover:border-accent/30 rounded-xl px-4 py-3 transition-all duration-300" + style={{ animation: `chatFadeIn 0.3s ease-out ${idx * 0.1}s both` }} + > +
+

+ {product.sku} +

+
+ {product.title} +
+
+ + + ))} +
+
+ )} +
+
+ ))} + + {/* Loading indicator */} + {isLoading && ( +
+
+
+ +
+
+

+ {loadingText} +

+
+ {[0, 1, 2].map((i) => ( +
-
-

- {product.sku} -

-
- {product.title} -
-
-
- - Details - - -
- + /> ))}
- )} +
-
- ))} + )} - {isLoading && ( -
-
- + {/* Error */} + {error && ( +
+
+
+ +
+
+

Da ist was schiefgelaufen 😬

+

{error}

+ +
+
-
- )} + )} - {error && ( -
- -
-

System Error

-

{error}

-
-
- )} - -
-
- - {/* Input Area */} -
-
- setQuery(e.target.value)} - onKeyDown={onKeyDown} - placeholder="Type your question or requirements..." - className="flex-1 bg-transparent border-none text-white text-base md:text-lg p-4 focus:outline-none placeholder:text-white/30" - disabled={isLoading} - /> - setHoneypot(e.target.value)} - tabIndex={-1} - autoComplete="off" - aria-hidden="true" - /> - +
-
- - Press Enter to send • Esc to close - + + {/* ── Input Area ── */} +
+
+ setQuery(e.target.value)} + onKeyDown={onKeyDown} + placeholder="Nachricht eingeben..." + className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20" + disabled={isLoading} + tabIndex={1} + autoFocus + /> + setHoneypot(e.target.value)} + tabIndex={-1} + autoComplete="off" + aria-hidden="true" + /> + +
+
+ + Enter zum Senden · Esc zum Schließen + + + · + + + 🛡️ DSGVO-konform · EU-Server + +
+ + {/* ── Keyframe animations ── */} +
); } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3ad559d1..4f8610d3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,7 +8,7 @@ services: - infra labels: - "caddy=http://${TRAEFIK_HOST:-klz.localhost}" - - "caddy.reverse_proxy=host.docker.internal:3100" + - "caddy.reverse_proxy=http://klz-app:3000" # Full Docker dev (use with `pnpm run dev:docker`) klz-app: @@ -26,13 +26,20 @@ services: - ${ENV_FILE:-.env} environment: NODE_ENV: development + # Force Garbage Collection before Docker kills the container (OOM) + NODE_OPTIONS: "--max-old-space-size=6144" NEXT_TELEMETRY_DISABLED: "1" POSTGRES_URI: postgres://${PAYLOAD_DB_USER:-payload}:${PAYLOAD_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${PAYLOAD_DB_NAME:-payload} PAYLOAD_SECRET: ${PAYLOAD_SECRET:-fallback-secret-for-dev} - NODE_OPTIONS: "--max-old-space-size=8192" - UV_THREADPOOL_SIZE: "4" + UV_THREADPOOL_SIZE: "1" + RAYON_NUM_THREADS: "1" + NEXT_PRIVATE_WORKER_THREADS: "false" NPM_TOKEN: ${NPM_TOKEN:-} CI: "true" + QDRANT_URL: "http://klz-qdrant:6333" + REDIS_URL: "redis://klz-redis:6379" + MISTRAL_API_KEY: ${MISTRAL_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} volumes: - .:/app - klz_node_modules:/app/node_modules @@ -42,19 +49,34 @@ services: - /app/.git - /app/reference - /app/data + deploy: resources: limits: - cpus: '4' memory: 8G command: > - sh -c "pnpm install --no-frozen-lockfile && pnpm next dev --webpack --hostname 0.0.0.0" + sh -c "pnpm install --no-frozen-lockfile && + while true; do + ( + echo '[warmup] Waiting for Next.js to be reachable...' + until curl -sf http://localhost:3000 > /dev/null; do sleep 2; done + echo '[warmup] Server is up! Pre-compiling routes...' + curl -sf http://localhost:3000/de > /dev/null 2>&1 && echo '[warmup] /de ready' + curl -sf http://localhost:3000/api/health/cms > /dev/null 2>&1 && echo '[warmup] /api/health/cms ready' + curl -sf -X POST -H 'Content-Type: application/json' -d '{\"messages\":[{\"role\":\"user\",\"content\":\"warmup\"}]}' http://localhost:3000/api/ai-search > /dev/null 2>&1 && echo '[warmup] /api/ai-search ready' + echo '[warmup] Syncing CMS data to Qdrant...' + SYNC_RESULT=$(curl -sf http://localhost:3000/api/sync-qdrant 2>&1) + echo \"[warmup] Qdrant sync: $SYNC_RESULT\" + echo '[warmup] All routes pre-compiled + Qdrant synced ✓' + ) & + pnpm next dev --webpack --hostname 0.0.0.0; + echo '[klz-app] next dev exited, restarting in 2s...'; + sleep 2; + done" labels: - "traefik.enable=true" - "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.port=3000" - "traefik.docker.network=infra" - - "caddy=http://${TRAEFIK_HOST:-klz.localhost}" - - "caddy.reverse_proxy={{upstreams 3000}}" klz-db: image: postgres:15-alpine @@ -81,7 +103,7 @@ services: networks: - default ports: - - "6379:6379" + - "16379:6379" klz-qdrant: image: qdrant/qdrant:v1.13.2 @@ -91,7 +113,7 @@ services: networks: - default ports: - - "6333:6333" + - "16333:6333" networks: default: diff --git a/next.config.mjs b/next.config.mjs index fc8425db..5296df46 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,18 +7,28 @@ import { withPayload } from '@payloadcms/next/withPayload'; const isProd = process.env.NODE_ENV === 'production'; /** @type {import('next').NextConfig} */ const nextConfig = { - serverExternalPackages: ['@mintel/payload-ai'], + transpilePackages: ['react-image-crop', '@react-three/fiber'], onDemandEntries: { - // Make sure entries are not disposed too quickly - maxInactiveAge: 60 * 1000, + // Keep compiled pages/routes in memory for 5 minutes (reduced from 25m to prevent OOM) + maxInactiveAge: 5 * 60 * 1000, + // Keep up to 2 pages in the dev buffer (reduced from 10 to prevent OOM) + pagesBufferLength: 2, }, experimental: { - optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'], - cpus: 3, + optimizePackageImports: [ + 'lucide-react', + 'framer-motion', + '@/components/ui', + '@sentry/nextjs', + '@payloadcms/richtext-lexical', + 'react-hook-form', + 'zod', + 'date-fns', + ], workerThreads: false, + memoryBasedWorkersCount: true, }, reactStrictMode: false, - swcMinify: true, productionBrowserSourceMaps: false, logging: { fetches: { @@ -26,6 +36,21 @@ const nextConfig = { }, }, ...(isProd ? { output: 'standalone' } : {}), + // Prevent webpack from restarting when .env files are touched via Docker volume mount + webpack: (config, { dev }) => { + if (dev) { + config.watchOptions = { + ...config.watchOptions, + ignored: /node_modules|\.env/, + // Reduce poll frequency to lower CPU churn from VirtioFS + poll: 1000, + aggregateTimeout: 300, + }; + // Reduce source map quality in dev for faster rebuilds + config.devtool = 'eval'; + } + return config; + }, async headers() { const isProd = process.env.NODE_ENV === 'production'; const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin; @@ -48,7 +73,7 @@ const nextConfig = { style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: ${extraImgDomains}; - connect-src 'self' ${umamiDomain} ${glitchtipDomain}; + connect-src 'self' ${umamiDomain} ${glitchtipDomain} https://raw.githack.com https://raw.githubusercontent.com; frame-src 'self'; object-src 'none'; base-uri 'self'; @@ -394,6 +419,7 @@ const nextConfig = { ]; }, images: { + qualities: [25, 50, 75, 100], formats: ['image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], remotePatterns: [ diff --git a/package.json b/package.json index 149db1cd..2058def1 100644 --- a/package.json +++ b/package.json @@ -105,8 +105,8 @@ "vitest": "^4.0.16" }, "scripts": { - "dev": "bash -c '[ -f .env ] || (cp .env.example .env && sed -i.bak \"s/TRAEFIK_HOST=klz-cables.com/TRAEFIK_HOST=klz.localhost/\" .env && rm -f .env.bak && echo \"✅ Created .env from .env.example\"); trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db klz-proxy --remove-orphans'", - "dev:local": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy && POSTGRES_URI=NODE_ENV=development next dev --webpack --port 3100 --hostname 0.0.0.0'", + "dev": "bash -c '[ -f .env ] || (cp .env.example .env && sed -i.bak \"s/TRAEFIK_HOST=klz-cables.com/TRAEFIK_HOST=klz.localhost/\" .env && rm -f .env.bak && echo \"✅ Created .env from .env.example\"); trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db klz-proxy klz-qdrant klz-redis --remove-orphans'", + "dev:local": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy klz-qdrant klz-redis && POSTGRES_URI=NODE_ENV=development next dev --webpack --port 3100 --hostname 0.0.0.0'", "dev:infra": "COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy", "build": "next build", "start": "next start", diff --git a/payload.config.ts b/payload.config.ts index 1007f153..23de3294 100644 --- a/payload.config.ts +++ b/payload.config.ts @@ -22,7 +22,13 @@ import { FormSubmissions } from './src/payload/collections/FormSubmissions'; import { Products } from './src/payload/collections/Products'; import { Pages } from './src/payload/collections/Pages'; import { seedDatabase } from './src/payload/seed'; -import { payloadChatPlugin } from '@mintel/payload-ai'; + +const isMigrate = process.argv.includes('migrate'); +let chatPlugin: any = null; +if (!isMigrate) { + const mod = await import('@mintel/payload-ai'); + chatPlugin = mod.payloadChatPlugin; +} const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -100,9 +106,13 @@ export default buildConfig({ : undefined, sharp, plugins: [ - payloadChatPlugin({ - enabled: true, - mcpServers: [], - }), + ...(chatPlugin + ? [ + chatPlugin({ + enabled: true, + mcpServers: [{ name: 'klz-qdrant-mcp' }], + }), + ] + : []), ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f1a6f4e..0497a26c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@mintel/next-utils': specifier: ^1.9.0 version: 1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3) + '@mintel/payload-ai': + specifier: ^1.9.15 + version: 1.9.15(@payloadcms/next@3.77.0(@types/react@19.2.13)(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@payloadcms/ui@3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(ws@8.19.0) '@payloadcms/db-postgres': specifier: ^3.77.0 version: 3.77.0(@opentelemetry/api@1.9.0)(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3)) @@ -313,6 +316,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/gateway@3.0.66': + resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/google@3.0.31': resolution: {integrity: sha512-RVNz8WFSIRbXbYDBE6JvlE2escWPJimBCs22LzKEYH7DNfl/X7cHNa1LFho4PsY6Ib0JmbzB8s2+i0wHs/wNCg==} engines: {node: '>=18'} @@ -325,16 +334,34 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/openai@3.0.41': + resolution: {integrity: sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.15': resolution: {integrity: sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.19': + resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@3.0.8': resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} + '@ai-sdk/react@3.0.118': + resolution: {integrity: sha512-fBAix8Jftxse6/2YJnOFkwW1/O6EQK4DK68M9DlFmZGAzBmsaHXEPVS77sVIlkaOWCy11bE7434NAVXRY+3OsQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1480,6 +1507,12 @@ packages: '@hapi/topo@6.0.2': resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@html-validate/stylish@4.3.0': resolution: {integrity: sha512-eUfvKpRJg5TvzSfTf2EovrQoTKjkRnPUOUnXVJ2cQ4GbC/bQw98oxN+DdSf+HxOBK00YOhsP52xWdJPV1o4n5w==} engines: {node: '>= 18'} @@ -1757,15 +1790,24 @@ packages: '@medv/finder@4.0.2': resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==} + '@mintel/content-engine@1.9.10': + resolution: {integrity: sha512-rv5vJ1bQkW713q14tPLOgRt5Y7+t4tu76i3H3tJkCADAqGvLrM/QuI9SF6aR9H8/KHzzF7BjRHDFsHPYqzQhpg==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fcontent-engine/-/1.9.10/content-engine-1.9.10.tgz} + '@mintel/eslint-config@1.9.5': resolution: {integrity: sha512-nZylW/99gnzkU/oCQNR5Muj6/gGYsf1EJSM8LSRAU3sU6wR3kUu4cba7LVQPb6uTrfW8CzD2JsL8pzanDqSZzA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Feslint-config/-/1.9.5/eslint-config-1.9.5.tgz} + '@mintel/journaling@1.9.10': + resolution: {integrity: sha512-wuOz6bhloVgw1yiA3OhRMxak7pqB5kft62QjxPz1w+LSRaRR4nDXRR1qhSdKlcqLhEJL5+kljJnVMZ3N2UhJnw==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fjournaling/-/1.9.10/journaling-1.9.10.tgz} + '@mintel/mail@1.9.5': resolution: {integrity: sha512-g7+pL2/NFrmjAgMUHHj4GTpWestabdIZBR0UkMiJBsEFpxu7TFJXorFw418w/z+G21JQDKLyQrT/NpNLaCFW+Q==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fmail/-/1.9.5/mail-1.9.5.tgz} peerDependencies: react: ^19.0.0 react-dom: ^19.0.0 + '@mintel/meme-generator@1.9.10': + resolution: {integrity: sha512-Mg89RH4KgKOmpFF1G6DNu/LPzzQg2CgV15lazpsLa8ce/XydcSXOv2QvS2jk3kytGHOgqnu3j3xAn10Yi4KgvA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fmeme-generator/-/1.9.10/meme-generator-1.9.10.tgz} + '@mintel/next-config@1.9.5': resolution: {integrity: sha512-gnEtpoGXHjFwPIccU5GUoqqkAL1vni1uNtxSJ1XkG4z8HVQ9C3dH4IxxmUqMJaLL0fTSDOZpZKZIV0C1C28iyg==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fnext-config/-/1.9.5/next-config-1.9.5.tgz} @@ -1778,9 +1820,31 @@ packages: '@mintel/next-utils@1.9.5': resolution: {integrity: sha512-Ndcf2AONTccw8zMbsyFudubBsnoXfYuYoRPGq3d5OLcKfzlz+2g0ee8xcsNFwa9fpnAAMlvauYu0mIRGA/ydVA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fnext-utils/-/1.9.5/next-utils-1.9.5.tgz} + '@mintel/payload-ai@1.9.15': + resolution: {integrity: sha512-8TZ1imcLgOjXZsBt6fQMVLfkhsIRKz7tJqq5aTnOO28kwCb+TvTh+5AgZWKWxxTSXPYcpc3Zt97kNraOT18dzg==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fpayload-ai/-/1.9.15/payload-ai-1.9.15.tgz} + peerDependencies: + '@payloadcms/next': '>=3.0.0' + '@payloadcms/ui': '>=3.0.0' + payload: '>=3.0.0' + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@mintel/thumbnail-generator@1.9.10': + resolution: {integrity: sha512-3YcM4w7ysuffsxUJenx7RRSyTFhirDXDTgQ1KbRhNxkQH+9fCQUrH72w9lDNCZmP2776TSmwDPK46KD5CAGKzw==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Fthumbnail-generator/-/1.9.10/thumbnail-generator-1.9.10.tgz} + '@mintel/tsconfig@1.9.5': resolution: {integrity: sha512-dxYJoGAE+9vwaPIpQL3NCzyEKMFYuJyDwWmCuUeP0lFo91brgSpHfl4cw98sK8UsrncIP6r4YvAgncooVnovaQ==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Ftsconfig/-/1.9.5/tsconfig-1.9.5.tgz} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -3347,6 +3411,12 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.19.10': resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==} @@ -3702,6 +3772,10 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + abs-svg-path@0.1.1: resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} @@ -3709,6 +3783,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -3752,12 +3830,22 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + ai@6.0.101: resolution: {integrity: sha512-Ur/NgbgOp1rdhyDiKDk6EOpSgd1g5ADlbcD1cjQJtQsnmhEngz3Rf8nK5JetDh0vnbLy2aEBpaQeL+zvLRWuaA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + ai@6.0.116: + resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -4049,6 +4137,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + body-scroll-lock@4.0.0-beta.0: resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} @@ -4357,6 +4449,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -4383,6 +4479,10 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -5180,6 +5280,10 @@ packages: event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -5194,6 +5298,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -5202,10 +5310,20 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.0: + resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -5305,6 +5423,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -5346,10 +5468,17 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -5392,6 +5521,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} @@ -5626,6 +5759,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + engines: {node: '>=16.9.0'} + hoopy@0.1.4: resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} engines: {node: '>= 6.0.0'} @@ -5716,6 +5853,9 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -5743,6 +5883,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + icu-minify@4.8.2: resolution: {integrity: sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==} @@ -5983,6 +6127,9 @@ packages: is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -6102,6 +6249,9 @@ packages: jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + jose@6.2.0: + resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -6545,6 +6695,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -6559,6 +6713,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6858,6 +7016,11 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -6986,6 +7149,18 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -7126,6 +7301,9 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -7263,6 +7441,10 @@ packages: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -7361,6 +7543,10 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -7449,6 +7635,10 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-datepicker@7.6.0: resolution: {integrity: sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==} peerDependencies: @@ -7557,6 +7747,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -7616,6 +7810,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + replicate@1.4.0: + resolution: {integrity: sha512-1ufKejfUVz/azy+5TnzQP7U1+MHVWZ6psnQ06az8byUUnRhT+DZ/MvewzB1NQYBVMgNKR7xPDtTwlcP5nv/5+w==} + engines: {git: '>=2.11.0', node: '>=18.0.0', npm: '>=7.19.0', yarn: '>=1.7.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -7696,6 +7894,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -7792,6 +7994,10 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -7799,6 +8005,10 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -8147,6 +8357,11 @@ packages: svg-to-pdfkit@0.1.8: resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -8229,6 +8444,10 @@ packages: three@0.183.1: resolution: {integrity: sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ==} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -8424,6 +8643,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -8466,6 +8689,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -8709,6 +8935,10 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + webdriver-bidi-protocol@0.4.1: resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} @@ -8951,6 +9181,11 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -9007,6 +9242,13 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 3.25.76 + '@ai-sdk/gateway@3.0.66(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + '@ai-sdk/google@3.0.31(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -9019,6 +9261,12 @@ snapshots: '@ai-sdk/provider-utils': 4.0.15(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/openai@3.0.41(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.15(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -9026,10 +9274,27 @@ snapshots: eventsource-parser: 3.0.6 zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.19(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 + '@ai-sdk/react@3.0.118(react@19.2.4)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + ai: 6.0.116(zod@3.25.76) + react: 19.2.4 + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@apidevtools/json-schema-ref-parser@11.9.3': @@ -10071,6 +10336,10 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 + '@hono/node-server@1.19.11(hono@4.12.5)': + dependencies: + hono: 4.12.5 + '@html-validate/stylish@4.3.0': dependencies: kleur: 4.1.5 @@ -10417,6 +10686,19 @@ snapshots: '@medv/finder@4.0.2': {} + '@mintel/content-engine@1.9.10(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@mintel/journaling': 1.9.10(ws@8.19.0)(zod@3.25.76) + '@mintel/meme-generator': 1.9.10(ws@8.19.0)(zod@3.25.76) + '@mintel/thumbnail-generator': 1.9.10 + dotenv: 17.3.1 + openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + transitivePeerDependencies: + - debug + - encoding + - ws + - zod + '@mintel/eslint-config@1.9.5(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint/eslintrc': 3.3.4 @@ -10434,12 +10716,30 @@ snapshots: - supports-color - typescript + '@mintel/journaling@1.9.10(ws@8.19.0)(zod@3.25.76)': + dependencies: + axios: 1.13.5(debug@4.4.3) + openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + transitivePeerDependencies: + - debug + - encoding + - ws + - zod + '@mintel/mail@1.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@mintel/meme-generator@1.9.10(ws@8.19.0)(zod@3.25.76)': + dependencies: + openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + transitivePeerDependencies: + - encoding + - ws + - zod + '@mintel/next-config@1.9.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))': dependencies: '@sentry/nextjs': 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0(esbuild@0.25.12)) @@ -10500,8 +10800,58 @@ snapshots: - sass - typescript + '@mintel/payload-ai@1.9.15(@payloadcms/next@3.77.0(@types/react@19.2.13)(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@payloadcms/ui@3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(ws@8.19.0)': + dependencies: + '@ai-sdk/openai': 3.0.41(zod@3.25.76) + '@ai-sdk/react': 3.0.118(react@19.2.4)(zod@3.25.76) + '@mintel/content-engine': 1.9.10(ws@8.19.0)(zod@3.25.76) + '@mintel/thumbnail-generator': 1.9.10 + '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76) + '@payloadcms/next': 3.77.0(@types/react@19.2.13)(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@payloadcms/ui': 3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@qdrant/js-client-rest': 1.17.0(typescript@5.9.3) + ai: 6.0.116(zod@3.25.76) + payload: 3.77.0(graphql@16.12.0)(typescript@5.9.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + replicate: 1.4.0 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - debug + - encoding + - supports-color + - typescript + - ws + + '@mintel/thumbnail-generator@1.9.10': + dependencies: + replicate: 1.4.0 + '@mintel/tsconfig@1.9.5': {} + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.0(express@5.2.1) + hono: 4.12.5 + jose: 6.2.0 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 @@ -12254,6 +12604,15 @@ snapshots: dependencies: '@types/node': 22.19.10 + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.19.13 + form-data: 4.0.5 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.19.10': dependencies: undici-types: 6.21.0 @@ -12667,6 +13026,10 @@ snapshots: abbrev@2.0.0: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + abs-svg-path@0.1.1: {} accepts@1.3.8: @@ -12674,6 +13037,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -12708,6 +13076,10 @@ snapshots: agent-base@7.1.4: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + ai@6.0.101(zod@3.25.76): dependencies: '@ai-sdk/gateway': 3.0.55(zod@3.25.76) @@ -12716,6 +13088,14 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.76 + ai@6.0.116(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.66(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -12724,6 +13104,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: ajv: 8.18.0 @@ -13017,6 +13401,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + body-scroll-lock@4.0.0-beta.0: {} boolbase@1.0.0: {} @@ -13365,6 +13763,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} conventional-changelog-angular@8.1.0: @@ -13385,6 +13785,8 @@ snapshots: cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} core-js@3.48.0: {} @@ -14332,6 +14734,8 @@ snapshots: stream-combiner: 0.0.4 through: 2.3.8 + event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} events-universal@1.0.1: @@ -14344,6 +14748,10 @@ snapshots: eventsource-parser@3.0.6: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -14358,6 +14766,11 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@8.3.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@4.22.1: dependencies: accepts: 1.3.8 @@ -14394,6 +14807,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} extend-shallow@2.0.1: @@ -14494,6 +14940,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-root@1.1.0: {} find-up@4.1.0: @@ -14542,6 +14999,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -14550,6 +15009,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -14576,6 +15040,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + from@0.1.7: {} fs.realpath@1.0.0: {} @@ -14877,6 +15343,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.12.5: {} + hoopy@0.1.4: {} hsl-to-hex@1.0.0: @@ -14983,6 +15451,10 @@ snapshots: human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + husky@9.1.7: {} hyphen@1.14.1: {} @@ -15003,6 +15475,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + icu-minify@4.8.2: dependencies: '@formatjs/icu-messageformat-parser': 3.5.1 @@ -15239,6 +15715,8 @@ snapshots: is-promise@2.2.2: {} + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -15365,6 +15843,8 @@ snapshots: jose@5.9.6: {} + jose@6.2.0: {} + joycon@3.1.1: {} jpeg-exif@1.1.4: {} @@ -15914,6 +16394,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memoize-one@6.0.0: {} meow@12.1.1: {} @@ -15922,6 +16404,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -16318,6 +16802,8 @@ snapshots: node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -16449,6 +16935,21 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@4.104.0(ws@8.19.0)(zod@3.25.76): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.19.0 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + opener@1.5.2: {} optionator@0.9.4: @@ -16646,6 +17147,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -16856,6 +17359,8 @@ snapshots: sonic-boom: 4.2.0 thread-stream: 3.1.0 + pkce-challenge@5.0.1: {} + pkg-types@2.3.0: dependencies: confbox: 0.2.4 @@ -16924,6 +17429,9 @@ snapshots: process-warning@5.0.0: {} + process@0.11.10: + optional: true + progress@2.0.3: {} promise-worker-transferable@1.0.4: @@ -17060,6 +17568,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-datepicker@7.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@floating-ui/react': 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -17198,6 +17713,15 @@ snapshots: react@19.2.4: {} + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + optional: true + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -17298,6 +17822,10 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + replicate@1.4.0: + optionalDependencies: + readable-stream: 4.7.0 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -17398,6 +17926,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-async@2.4.1: {} run-parallel@1.2.0: @@ -17509,6 +18047,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -17522,6 +18076,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -17952,6 +18515,12 @@ snapshots: dependencies: pdfkit: 0.17.2 + swr@2.4.1(react@19.2.4): + dependencies: + dequal: 2.0.3 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + symbol-tree@3.2.4: {} tabbable@6.4.0: {} @@ -18043,6 +18612,8 @@ snapshots: three@0.183.1: {} + throttleit@2.1.0: {} + through@2.3.8: {} tiny-inflate@1.0.3: {} @@ -18222,6 +18793,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -18283,6 +18860,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@5.26.5: {} + undici-types@6.21.0: {} undici@6.23.0: {} @@ -18554,6 +19133,8 @@ snapshots: web-namespaces@2.0.1: {} + web-streams-polyfill@4.0.0-beta.3: {} + webdriver-bidi-protocol@0.4.1: {} webgl-constants@1.1.1: {} @@ -18814,6 +19395,10 @@ snapshots: yoga-layout@3.2.1: {} + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@4.0.2(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/src/lib/qdrant.ts b/src/lib/qdrant.ts index 8762d82f..6ed12b75 100644 --- a/src/lib/qdrant.ts +++ b/src/lib/qdrant.ts @@ -1,4 +1,5 @@ import { QdrantClient } from '@qdrant/js-client-rest'; +import redis from './redis'; const isDockerContainer = process.env.IS_DOCKER === 'true' || process.env.HOSTNAME?.includes('klz-app'); @@ -10,15 +11,26 @@ const qdrantApiKey = process.env.QDRANT_API_KEY || ''; export const qdrant = new QdrantClient({ url: qdrantUrl, apiKey: qdrantApiKey || undefined, + // Disable qdrant client's own version check to avoid the warning spam + checkCompatibility: false, }); export const COLLECTION_NAME = 'klz_products'; export const VECTOR_SIZE = 1536; // OpenAI text-embedding-3-small +// Cache TTLs +const EMBEDDING_CACHE_TTL = 60 * 60 * 24; // 24h — embeddings are deterministic +const SEARCH_CACHE_TTL = 60 * 30; // 30 min — product data could change + +// Track collection existence in-memory (don't re-check every request) +let collectionVerified = false; + /** - * Ensure the collection exists in Qdrant. + * Ensure the collection exists in Qdrant (only checks once per process lifetime). */ export async function ensureCollection() { + if (collectionVerified) return; + try { const collections = await qdrant.getCollections(); const exists = collections.collections.some((c) => c.name === COLLECTION_NAME); @@ -31,15 +43,47 @@ export async function ensureCollection() { }); console.log(`Successfully created Qdrant collection: ${COLLECTION_NAME}`); } + collectionVerified = true; } catch (error) { console.error('Error ensuring Qdrant collection:', error); } } /** - * Generate an embedding for a given text using OpenRouter (OpenAI embedding proxy) + * Simple hash for cache keys + */ +function hashKey(text: string): string { + let hash = 0; + for (let i = 0; i < text.length; i++) { + const chr = text.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return hash.toString(36); +} + +/** + * Generate an embedding for a given text using OpenRouter (OpenAI embedding proxy). + * Results are cached in Redis for 24h since embeddings are deterministic. + * + * NOTE: We keep OpenRouter for embeddings because the Qdrant collection uses 1536-dim + * vectors (OpenAI text-embedding-3-small). Switching to Mistral embed (1024-dim) would + * require re-indexing the entire product catalog. + * User-facing chat uses Mistral AI directly for DSGVO compliance. */ export async function generateEmbedding(text: string): Promise { + const cacheKey = `emb:${hashKey(text.toLowerCase().trim())}`; + + // Try Redis cache first + try { + const cached = await redis.get(cacheKey); + if (cached) { + return JSON.parse(cached); + } + } catch { + // Redis down — proceed without cache + } + const openRouterKey = process.env.OPENROUTER_API_KEY; if (!openRouterKey) { throw new Error('OPENROUTER_API_KEY is not set'); @@ -67,7 +111,16 @@ export async function generateEmbedding(text: string): Promise { } const data = await response.json(); - return data.data[0].embedding; + const embedding = data.data[0].embedding; + + // Cache the embedding in Redis + try { + await redis.set(cacheKey, JSON.stringify(embedding), 'EX', EMBEDDING_CACHE_TTL); + } catch { + // Redis down — proceed without caching + } + + return embedding; } /** @@ -113,9 +166,23 @@ export async function deleteProductVector(id: string | number) { } /** - * Search products in Qdrant + * Search products in Qdrant. + * Results are cached in Redis for 30 minutes keyed by query text. */ export async function searchProducts(query: string, limit = 5) { + const cacheKey = `search:${hashKey(query.toLowerCase().trim())}:${limit}`; + + // Try Redis cache first + try { + const cached = await redis.get(cacheKey); + if (cached) { + console.log(`[Qdrant] Cache HIT for query: "${query.substring(0, 50)}"`); + return JSON.parse(cached); + } + } catch { + // Redis down — proceed without cache + } + try { await ensureCollection(); const vector = await generateEmbedding(query); @@ -126,6 +193,13 @@ export async function searchProducts(query: string, limit = 5) { with_payload: true, }); + // Cache results in Redis + try { + await redis.set(cacheKey, JSON.stringify(results), 'EX', SEARCH_CACHE_TTL); + } catch { + // Redis down — proceed without caching + } + return results; } catch (error) { console.error('Error searching in Qdrant:', error);