Compare commits
20 Commits
3acf0c3740
...
feature/ai
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ba1ddf2a6 | |||
| a558b18cd8 | |||
| 482460b336 | |||
| 943c6ed09d | |||
| c45a90082b | |||
| fb98c8237e | |||
| b84e0d782d | |||
| ef4f0e7ace | |||
| d6bdd28b30 | |||
| 289e41a040 | |||
| 5144378c7e | |||
| c804b051e0 | |||
| 1dc52da677 | |||
| 7f1aeaee7e | |||
| 590c542b73 | |||
| ab477e8d8e | |||
| d83ab182db | |||
| 159ee66f55 | |||
| 45c385d62e | |||
| 8e99c9d121 |
@@ -14,8 +14,8 @@ on:
|
||||
default: 'false'
|
||||
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com'
|
||||
|
||||
concurrency:
|
||||
group: deploy-pipeline
|
||||
@@ -29,16 +29,16 @@ jobs:
|
||||
name: 🔍 Prepare
|
||||
runs-on: docker
|
||||
outputs:
|
||||
target: ${{ steps.determine.outputs.target }}
|
||||
image_tag: ${{ steps.determine.outputs.image_tag }}
|
||||
env_file: ${{ steps.determine.outputs.env_file }}
|
||||
traefik_host: ${{ steps.determine.outputs.traefik_host }}
|
||||
traefik_rule: ${{ steps.determine.outputs.traefik_rule }}
|
||||
next_public_url: ${{ steps.determine.outputs.next_public_url }}
|
||||
project_name: ${{ steps.determine.outputs.project_name }}
|
||||
short_sha: ${{ steps.determine.outputs.short_sha }}
|
||||
slug: ${{ steps.determine.outputs.slug }}
|
||||
gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }}
|
||||
target: ${{ steps.determine.outputs.target }}
|
||||
image_tag: ${{ steps.determine.outputs.image_tag }}
|
||||
env_file: ${{ steps.determine.outputs.env_file }}
|
||||
traefik_host: ${{ steps.determine.outputs.traefik_host }}
|
||||
traefik_rule: ${{ steps.determine.outputs.traefik_rule }}
|
||||
next_public_url: ${{ steps.determine.outputs.next_public_url }}
|
||||
project_name: ${{ steps.determine.outputs.project_name }}
|
||||
short_sha: ${{ steps.determine.outputs.short_sha }}
|
||||
slug: ${{ steps.determine.outputs.slug }}
|
||||
gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }}
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
TRAEFIK_RULE='Host(`'"$TRAEFIK_HOST"'`)'
|
||||
PRIMARY_HOST="$TRAEFIK_HOST"
|
||||
fi
|
||||
|
||||
|
||||
GATEKEEPER_HOST="gatekeeper.$PRIMARY_HOST"
|
||||
|
||||
{
|
||||
@@ -187,10 +187,13 @@ jobs:
|
||||
- name: 🔒 Security Audit
|
||||
run: pnpm audit --audit-level high || echo "⚠️ Audit found vulnerabilities (non-blocking)"
|
||||
|
||||
- name: 🧹 Clean Workspace
|
||||
run: rm -rf .next .turbo || true
|
||||
|
||||
- name: 🧪 QA Checks
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
env:
|
||||
TURBO_TELEMETRY_DISABLED: "1"
|
||||
TURBO_TELEMETRY_DISABLED: '1'
|
||||
run: npx turbo run lint typecheck test --cache-dir=".turbo"
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 3: Build & Push
|
||||
@@ -252,54 +255,56 @@ jobs:
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
TARGET: ${{ needs.prepare.outputs.target }}
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
|
||||
TARGET: ${{ needs.prepare.outputs.target }}
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
|
||||
GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }}
|
||||
SLUG: ${{ needs.prepare.outputs.slug }}
|
||||
|
||||
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
|
||||
GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }}
|
||||
SLUG: ${{ needs.prepare.outputs.slug }}
|
||||
|
||||
# Secrets mapping (Payload CMS)
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
||||
PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }}
|
||||
PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }}
|
||||
PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }}
|
||||
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
||||
PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }}
|
||||
PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }}
|
||||
PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }}
|
||||
|
||||
# Secrets mapping (Mail)
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
||||
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
|
||||
MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
|
||||
MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
|
||||
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
|
||||
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
||||
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
|
||||
MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
|
||||
MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
|
||||
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
||||
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
||||
|
||||
# Gatekeeper
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||
|
||||
# Analytics
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
|
||||
# Search & AI
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || vars.OPENROUTER_API_KEY }}
|
||||
QDRANT_URL: ${{ secrets.QDRANT_URL || vars.QDRANT_URL || 'http://klz-qdrant:6333' }}
|
||||
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY || vars.QDRANT_API_KEY }}
|
||||
REDIS_URL: ${{ secrets.REDIS_URL || vars.REDIS_URL || 'redis://klz-redis:6379' }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || vars.OPENROUTER_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY || vars.MISTRAL_API_KEY }}
|
||||
QDRANT_URL: ${{ secrets.QDRANT_URL || vars.QDRANT_URL || 'http://klz-qdrant:6333' }}
|
||||
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY || vars.QDRANT_API_KEY }}
|
||||
REDIS_URL: ${{ secrets.REDIS_URL || vars.REDIS_URL || 'redis://klz-redis:6379' }}
|
||||
KABELFACHMANN_MCP_URL: ${{ secrets.KABELFACHMANN_MCP_URL || vars.KABELFACHMANN_MCP_URL || 'http://klz-kabelfachmann:3007/sse' }}
|
||||
# Container Registry (standalone)
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: 📝 Generate Environment
|
||||
shell: bash
|
||||
env:
|
||||
TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }}
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }}
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
run: |
|
||||
# Middleware Selection Logic
|
||||
# Regular app routes get auth on non-production
|
||||
@@ -307,7 +312,7 @@ jobs:
|
||||
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
|
||||
COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||')
|
||||
STD_MW="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-compress"
|
||||
|
||||
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
AUTH_MIDDLEWARE="$STD_MW"
|
||||
COMPOSE_PROFILES=""
|
||||
@@ -352,9 +357,11 @@ jobs:
|
||||
echo ""
|
||||
echo "# Search & AI"
|
||||
echo "OPENROUTER_API_KEY=$OPENROUTER_API_KEY"
|
||||
echo "MISTRAL_API_KEY=$MISTRAL_API_KEY"
|
||||
echo "QDRANT_URL=$QDRANT_URL"
|
||||
echo "QDRANT_API_KEY=$QDRANT_API_KEY"
|
||||
echo "REDIS_URL=$REDIS_URL"
|
||||
echo "KABELFACHMANN_MCP_URL=$KABELFACHMANN_MCP_URL"
|
||||
echo ""
|
||||
echo "TARGET=$TARGET"
|
||||
echo "SENTRY_ENVIRONMENT=$TARGET"
|
||||
@@ -404,7 +411,7 @@ jobs:
|
||||
- name: 🚀 SSH Deploy
|
||||
shell: bash
|
||||
env:
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
@@ -428,10 +435,10 @@ jobs:
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR"
|
||||
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
|
||||
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
||||
|
||||
|
||||
# Execute remote commands — alpha is pre-logged into registry.infra.mintel.me
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans"
|
||||
|
||||
|
||||
# Sanitize Payload Migrations: Replace 'dev' push entries with proper migration names.
|
||||
# Without this, Payload prompts interactively for confirmation and blocks forever in Docker.
|
||||
DB_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-db-1"
|
||||
@@ -444,7 +451,7 @@ jobs:
|
||||
echo " Attempt $i/15..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
|
||||
echo "🔧 Sanitizing payload_migrations table (if exists)..."
|
||||
REMOTE_DB_USER=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_USER=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
|
||||
REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
|
||||
@@ -455,7 +462,7 @@ jobs:
|
||||
# This ensures fresh branch deployments (empty DBs) get their schema on first deploy.
|
||||
echo "🔄 Running Payload migrations..."
|
||||
MIGRATOR_IMAGE="registry.infra.mintel.me/mintel/klz-2026:migrate-$IMAGE_TAG"
|
||||
|
||||
|
||||
ssh root@alpha.mintel.me "
|
||||
echo '${{ steps.auth.outputs.token }}' | docker login registry.infra.mintel.me -u '${{ steps.auth.outputs.user }}' --password-stdin 2>/dev/null || true
|
||||
docker pull $MIGRATOR_IMAGE
|
||||
@@ -466,7 +473,7 @@ jobs:
|
||||
&& echo '✅ Migrations complete.' \
|
||||
|| echo '⚠️ Migrations failed or already up-to-date — continuing.'
|
||||
"
|
||||
|
||||
|
||||
# Restart app to pick up clean migration state
|
||||
APP_CONTAINER="${PROJECT_NAME}-klz-app-1"
|
||||
ssh root@alpha.mintel.me "docker restart $APP_CONTAINER"
|
||||
@@ -518,7 +525,7 @@ jobs:
|
||||
with:
|
||||
path: /usr/bin/chromium
|
||||
key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }}
|
||||
|
||||
|
||||
- name: 🔍 Install Chromium (Native & ARM64)
|
||||
if: steps.cache-chromium.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,4 +32,5 @@ backups/
|
||||
.env
|
||||
|
||||
# Payload CMS auto-generated
|
||||
app/(payload)/admin/importMap.js
|
||||
# Knowledge base source files
|
||||
kabelhandbuch.txt
|
||||
@@ -18,6 +18,7 @@ ENV CI=true
|
||||
|
||||
# Copy lockfile and manifest for dependency installation caching
|
||||
COPY pnpm-lock.yaml package.json .npmrc* ./
|
||||
COPY patches* ./patches/
|
||||
|
||||
# Configure private registry and install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
@@ -37,6 +38,11 @@ FROM base AS development
|
||||
ENV NODE_ENV=development
|
||||
CMD ["pnpm", "dev:local"]
|
||||
|
||||
# Stage: Migrator
|
||||
FROM base AS migrator
|
||||
ENV NODE_ENV=production
|
||||
CMD ["pnpm", "cms:migrate"]
|
||||
|
||||
# Build application
|
||||
# Stage 3: Builder (Production)
|
||||
FROM base AS builder
|
||||
|
||||
84
app/(payload)/admin/importMap.js
Normal file
84
app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +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 { 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,
|
||||
'@mintel/payload-ai/components/ChatWindow#ChatWindowProvider':
|
||||
ChatWindowProvider_d32a660df96f186e48bfc5b31626ccf5,
|
||||
'@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||
};
|
||||
@@ -3,6 +3,11 @@ import { searchProducts } from '../../../src/lib/qdrant';
|
||||
import redis from '../../../src/lib/redis';
|
||||
import { z } from 'zod';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { generateText } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
// @ts-expect-error - Local version of @mintel/payload-ai/tools/mcpAdapter might not have types published yet
|
||||
import { createMcpTools } from '@mintel/payload-ai/tools/mcpAdapter';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60; // Max allowed duration (Vercel)
|
||||
|
||||
@@ -108,12 +113,9 @@ Das ECHTE KLZ Team:
|
||||
.map((p: any) => p.payload?.content)
|
||||
.join('\n\n');
|
||||
|
||||
const knowledgeDescriptions = searchResults
|
||||
.filter((p) => p.payload?.type === 'knowledge')
|
||||
.map((p: any) => p.payload?.content)
|
||||
.join('\n\n');
|
||||
|
||||
contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}\n\nKABELWISSEN (Handbuch):\n${knowledgeDescriptions}`;
|
||||
if (productDescriptions) {
|
||||
contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}`;
|
||||
}
|
||||
|
||||
foundProducts = searchResults
|
||||
.filter((p) => (p.payload?.type === 'product' || !p.payload?.type) && p.payload?.data)
|
||||
@@ -145,12 +147,14 @@ DEINE HAUPTAUFGABE: BERATEN, NICHT AUSFRAGEN!
|
||||
- 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."
|
||||
- Wenn technisches Wissen aus dem Kabelhandbuch benötigt wird, NUTZE UNBEDINGT eines der "kabelfachmann_*" Tools, anstatt zu raten oder zu behaupten du wüsstest es nicht! Das Tool weiss alles.
|
||||
|
||||
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.
|
||||
1. Prüfe den KONTEXT auf passende Katalog-Kabel für das Kundenprojekt.
|
||||
2. Wenn du tiefgehendes Wissen zu einem Kabeltyp brauchst (z.B. Biegeradius, Normen, Querschnitte), rufe das Kabelfachmann-Tool auf.
|
||||
3. Nenne direkt 1-2 passende Produktserien aus dem Kontext oder der Tool-Abfrage, die für diesen Fall Sinn machen.
|
||||
4. 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?).
|
||||
5. 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.
|
||||
@@ -161,51 +165,42 @@ ${contextStr || 'Kein Katalogkontext verfügbar.'}
|
||||
${teamContextStr}
|
||||
`;
|
||||
|
||||
const mistralKey = process.env.MISTRAL_API_KEY;
|
||||
if (!mistralKey) {
|
||||
throw new Error('MISTRAL_API_KEY is not set');
|
||||
const openrouterApiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!openrouterApiKey) {
|
||||
throw new Error('OPENROUTER_API_KEY is not set');
|
||||
}
|
||||
|
||||
// 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 ${mistralKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'ministral-8b-latest',
|
||||
temperature: 0.3,
|
||||
max_tokens: MAX_RESPONSE_TOKENS,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...cappedMessages.map((m: any) => ({
|
||||
role: m.role,
|
||||
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
||||
})),
|
||||
],
|
||||
}),
|
||||
const openrouter = createOpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: openrouterApiKey,
|
||||
});
|
||||
|
||||
if (!fetchRes.ok) {
|
||||
const errBody = await fetchRes.text();
|
||||
console.error('Mistral API Error:', errBody);
|
||||
Sentry.captureException(new Error(`Mistral ${fetchRes.status}: ${errBody}`), {
|
||||
tags: { context: 'ai-search-mistral' },
|
||||
let mcpTools: Record<string, any> = {};
|
||||
const mcpUrl = process.env.KABELFACHMANN_MCP_URL || 'http://host.docker.internal:3007/sse';
|
||||
try {
|
||||
const { tools } = await createMcpTools({
|
||||
name: 'kabelfachmann',
|
||||
url: mcpUrl,
|
||||
});
|
||||
|
||||
// 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 });
|
||||
mcpTools = tools;
|
||||
} catch (e) {
|
||||
console.warn('Failed to load MCP tools', e);
|
||||
Sentry.captureException(e, { tags: { context: 'ai-search-mcp' } });
|
||||
}
|
||||
|
||||
const data = await fetchRes.json();
|
||||
const text = data.choices[0].message.content;
|
||||
const { text } = await generateText({
|
||||
model: openrouter('openai/gpt-4o-mini'),
|
||||
system: systemPrompt,
|
||||
messages: cappedMessages.map((m: any) => ({
|
||||
role: m.role,
|
||||
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
||||
})),
|
||||
tools: mcpTools,
|
||||
// @ts-expect-error - maxSteps might be missing in some versions of generateText types
|
||||
maxSteps: 5,
|
||||
temperature: 0.3,
|
||||
maxTokens: MAX_RESPONSE_TOKENS,
|
||||
});
|
||||
|
||||
// Return the AI's answer along with any found products
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -106,8 +106,47 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kabelhandbuch (Static Text) ──
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = await import('crypto');
|
||||
|
||||
const txtPath = path.join(process.cwd(), 'kabelhandbuch.txt');
|
||||
let manualChunks = 0;
|
||||
|
||||
if (fs.existsSync(txtPath)) {
|
||||
try {
|
||||
const text = fs.readFileSync(txtPath, 'utf8');
|
||||
const chunks = text
|
||||
.split(/\n\s*\n/)
|
||||
.map((c: string) => c.trim())
|
||||
.filter((c: string) => c.length > 50);
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunkText = chunks[i];
|
||||
const syntheticId = crypto.randomUUID();
|
||||
|
||||
await upsertProductVector(syntheticId, chunkText, {
|
||||
type: 'knowledge',
|
||||
content: chunkText,
|
||||
data: {
|
||||
title: `Kabelhandbuch Wissen - Bereich ${i + 1}`,
|
||||
source: 'Kabelhandbuch KLZ.pdf',
|
||||
},
|
||||
});
|
||||
manualChunks++;
|
||||
}
|
||||
console.log(`[Qdrant Sync] ✅ ${manualChunks} Kabelhandbuch-Chunks synced`);
|
||||
} catch (e: any) {
|
||||
results.errors.push(`kabelhandbuch: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[Qdrant Sync] ⚠️ skipped Kabelhandbuch: ${txtPath} not found`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Qdrant Sync] ✅ ${results.products} products, ${results.posts} posts, ${results.pages} pages synced`,
|
||||
`[Qdrant Sync] ✅ ${results.products} products, ${results.posts} posts, ${results.pages} pages synced, ${manualChunks} manual chunks synced`,
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -112,9 +112,20 @@ services:
|
||||
- klz_qdrant_data:/qdrant/storage
|
||||
networks:
|
||||
- default
|
||||
ports:
|
||||
- "16333:6333"
|
||||
|
||||
klz-kabelfachmann:
|
||||
image: registry.infra.mintel.me/mintel/kabelfachmann-mcp:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
env_file:
|
||||
- ${ENV_FILE:-.env}
|
||||
environment:
|
||||
QDRANT_URL: http://klz-qdrant:6333
|
||||
depends_on:
|
||||
- klz-qdrant
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${PROJECT_NAME:-klz-cables}-internal
|
||||
|
||||
@@ -119,6 +119,18 @@ services:
|
||||
networks:
|
||||
- default
|
||||
|
||||
klz-kabelfachmann:
|
||||
image: registry.infra.mintel.me/mintel/kabelfachmann-mcp:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
env_file:
|
||||
- ${ENV_FILE:-.env}
|
||||
environment:
|
||||
QDRANT_URL: http://klz-qdrant:6333
|
||||
depends_on:
|
||||
- klz-qdrant
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${PROJECT_NAME:-klz-cables}-internal
|
||||
|
||||
@@ -23,7 +23,9 @@ export default [
|
||||
"tests/**",
|
||||
"next-env.d.ts",
|
||||
"reference/**",
|
||||
"data/**"
|
||||
"data/**",
|
||||
"remotion/**",
|
||||
"components/record-mode/**"
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
@@ -138,6 +138,10 @@
|
||||
"assets:pull:prod": "bash ./scripts/assets-sync.sh prod local",
|
||||
"assets:sync:testing-to-staging": "bash ./scripts/assets-sync.sh testing staging",
|
||||
"assets:sync:staging-to-prod": "bash ./scripts/assets-sync.sh staging prod",
|
||||
"qdrant:push:testing": "bash ./scripts/qdrant-sync.sh testing",
|
||||
"qdrant:push:staging": "bash ./scripts/qdrant-sync.sh staging",
|
||||
"qdrant:push:prod": "bash ./scripts/qdrant-sync.sh prod",
|
||||
"qdrant:push:branch": "bash ./scripts/qdrant-sync.sh",
|
||||
"pagespeed:test": "tsx ./scripts/pagespeed-sitemap.ts",
|
||||
"pagespeed:audit": "./scripts/audit-local.sh",
|
||||
"pagespeed:urls": "tsx -e \"import sitemap from './app/sitemap'; sitemap().then(urls => console.log(urls.map(u => u.url).join('\\n')))\"",
|
||||
@@ -164,6 +168,9 @@
|
||||
"overrides": {
|
||||
"next": "16.1.6",
|
||||
"minimatch": ">=10.2.2"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@mintel/payload-ai@1.9.15": "patches/@mintel__payload-ai@1.9.15.patch"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
131
patches/@mintel__payload-ai@1.9.15.patch
Normal file
131
patches/@mintel__payload-ai@1.9.15.patch
Normal file
@@ -0,0 +1,131 @@
|
||||
diff --git a/dist/components/ChatWindow/index.js b/dist/components/ChatWindow/index.js
|
||||
index 90c65bae4abb78beec98d8308e808e8ba341dcc2..f675dbc69ff82b64438288f53599c93a56391b64 100644
|
||||
--- a/dist/components/ChatWindow/index.js
|
||||
+++ b/dist/components/ChatWindow/index.js
|
||||
@@ -2,7 +2,6 @@
|
||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
-import './ChatWindow.scss';
|
||||
export const ChatWindowProvider = ({ children }) => {
|
||||
return (_jsxs(_Fragment, { children: [children, _jsx(ChatWindow, {})] }));
|
||||
};
|
||||
@@ -14,47 +13,63 @@ const ChatWindow = () => {
|
||||
initialMessages: []
|
||||
});
|
||||
// Basic implementation to toggle chat window and submit messages
|
||||
- return (_jsxs("div", { className: "payload-mcp-chat-container", children: [_jsx("button", { className: "payload-mcp-chat-toggle", onClick: () => setIsOpen(!isOpen), style: {
|
||||
- position: 'fixed',
|
||||
- bottom: '20px',
|
||||
- right: '20px',
|
||||
- zIndex: 9999,
|
||||
- padding: '12px 24px',
|
||||
- backgroundColor: '#000',
|
||||
- color: '#fff',
|
||||
- borderRadius: '8px',
|
||||
- border: 'none',
|
||||
- cursor: 'pointer',
|
||||
- fontWeight: 'bold'
|
||||
- }, children: isOpen ? 'Close AI Chat' : 'Ask AI' }), isOpen && (_jsxs("div", { className: "payload-mcp-chat-window", style: {
|
||||
- position: 'fixed',
|
||||
- bottom: '80px',
|
||||
- right: '20px',
|
||||
- width: '400px',
|
||||
- height: '600px',
|
||||
- backgroundColor: '#fff',
|
||||
- border: '1px solid #eaeaea',
|
||||
- borderRadius: '12px',
|
||||
- zIndex: 9999,
|
||||
- display: 'flex',
|
||||
- flexDirection: 'column',
|
||||
- boxShadow: '0 10px 40px rgba(0,0,0,0.1)'
|
||||
- }, children: [_jsx("div", { className: "chat-header", style: { padding: '16px', borderBottom: '1px solid #eaeaea', backgroundColor: '#f9f9f9', borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }, children: _jsx("h3", { style: { margin: 0, fontSize: '16px' }, children: "Payload MCP Chat" }) }), _jsx("div", { className: "chat-messages", style: { flex: 1, padding: '16px', overflowY: 'auto' }, children: messages.map((m) => (_jsx("div", { style: {
|
||||
- marginBottom: '12px',
|
||||
- textAlign: m.role === 'user' ? 'right' : 'left'
|
||||
- }, children: _jsxs("div", { style: {
|
||||
- display: 'inline-block',
|
||||
- padding: '8px 12px',
|
||||
- borderRadius: '8px',
|
||||
- backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0',
|
||||
- color: m.role === 'user' ? '#fff' : '#000',
|
||||
- maxWidth: '80%'
|
||||
- }, children: [m.role === 'user' ? 'G: ' : 'AI: ', m.content] }) }, m.id))) }), _jsx("form", { onSubmit: handleSubmit, style: { padding: '16px', borderTop: '1px solid #eaeaea' }, children: _jsx("input", { value: input, placeholder: "Ask me anything or use /commands...", onChange: handleInputChange, style: {
|
||||
- width: '100%',
|
||||
- padding: '12px',
|
||||
- borderRadius: '8px',
|
||||
- border: '1px solid #eaeaea',
|
||||
- boxSizing: 'border-box'
|
||||
- } }) })] }))] }));
|
||||
+ return (_jsxs("div", {
|
||||
+ className: "payload-mcp-chat-container", children: [_jsx("button", {
|
||||
+ className: "payload-mcp-chat-toggle", onClick: () => setIsOpen(!isOpen), style: {
|
||||
+ position: 'fixed',
|
||||
+ bottom: '20px',
|
||||
+ right: '20px',
|
||||
+ zIndex: 9999,
|
||||
+ padding: '12px 24px',
|
||||
+ backgroundColor: '#000',
|
||||
+ color: '#fff',
|
||||
+ borderRadius: '8px',
|
||||
+ border: 'none',
|
||||
+ cursor: 'pointer',
|
||||
+ fontWeight: 'bold'
|
||||
+ }, children: isOpen ? 'Close AI Chat' : 'Ask AI'
|
||||
+ }), isOpen && (_jsxs("div", {
|
||||
+ className: "payload-mcp-chat-window", style: {
|
||||
+ position: 'fixed',
|
||||
+ bottom: '80px',
|
||||
+ right: '20px',
|
||||
+ width: '400px',
|
||||
+ height: '600px',
|
||||
+ backgroundColor: '#fff',
|
||||
+ border: '1px solid #eaeaea',
|
||||
+ borderRadius: '12px',
|
||||
+ zIndex: 9999,
|
||||
+ display: 'flex',
|
||||
+ flexDirection: 'column',
|
||||
+ boxShadow: '0 10px 40px rgba(0,0,0,0.1)'
|
||||
+ }, children: [_jsx("div", { className: "chat-header", style: { padding: '16px', borderBottom: '1px solid #eaeaea', backgroundColor: '#f9f9f9', borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }, children: _jsx("h3", { style: { margin: 0, fontSize: '16px' }, children: "Payload MCP Chat" }) }), _jsx("div", {
|
||||
+ className: "chat-messages", style: { flex: 1, padding: '16px', overflowY: 'auto' }, children: messages.map((m) => (_jsx("div", {
|
||||
+ style: {
|
||||
+ marginBottom: '12px',
|
||||
+ textAlign: m.role === 'user' ? 'right' : 'left'
|
||||
+ }, children: _jsxs("div", {
|
||||
+ style: {
|
||||
+ display: 'inline-block',
|
||||
+ padding: '8px 12px',
|
||||
+ borderRadius: '8px',
|
||||
+ backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0',
|
||||
+ color: m.role === 'user' ? '#fff' : '#000',
|
||||
+ maxWidth: '80%'
|
||||
+ }, children: [m.role === 'user' ? 'G: ' : 'AI: ', m.content]
|
||||
+ })
|
||||
+ }, m.id)))
|
||||
+ }), _jsx("form", {
|
||||
+ onSubmit: handleSubmit, style: { padding: '16px', borderTop: '1px solid #eaeaea' }, children: _jsx("input", {
|
||||
+ value: input, placeholder: "Ask me anything or use /commands...", onChange: handleInputChange, style: {
|
||||
+ width: '100%',
|
||||
+ padding: '12px',
|
||||
+ borderRadius: '8px',
|
||||
+ border: '1px solid #eaeaea',
|
||||
+ boxSizing: 'border-box'
|
||||
+ }
|
||||
+ })
|
||||
+ })]
|
||||
+ }))]
|
||||
+ }));
|
||||
};
|
||||
//# sourceMappingURL=index.js.map
|
||||
\ No newline at end of file
|
||||
diff --git a/src/components/ChatWindow/index.tsx b/src/components/ChatWindow/index.tsx
|
||||
index 9081ae77d4eae53ce660e285c1a6babde99ceaab..f262f1dd0fd1199734024cc27905d956e31900a2 100644
|
||||
--- a/src/components/ChatWindow/index.tsx
|
||||
+++ b/src/components/ChatWindow/index.tsx
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useChat } from '@ai-sdk/react'
|
||||
-import './ChatWindow.scss'
|
||||
|
||||
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -8,6 +8,11 @@ overrides:
|
||||
next: 16.1.6
|
||||
minimatch: '>=10.2.2'
|
||||
|
||||
patchedDependencies:
|
||||
'@mintel/payload-ai@1.9.15':
|
||||
hash: 934315d3f15180552789a94bf76c34624501f601cc7409c24da3dc27af96c8fb
|
||||
path: patches/@mintel__payload-ai@1.9.15.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -32,7 +37,7 @@ importers:
|
||||
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)
|
||||
version: 1.9.15(patch_hash=934315d3f15180552789a94bf76c34624501f601cc7409c24da3dc27af96c8fb)(@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))
|
||||
@@ -10800,7 +10805,7 @@ 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)':
|
||||
'@mintel/payload-ai@1.9.15(patch_hash=934315d3f15180552789a94bf76c34624501f601cc7409c24da3dc27af96c8fb)(@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)
|
||||
|
||||
132
scripts/qdrant-sync.sh
Executable file
132
scripts/qdrant-sync.sh
Executable file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Qdrant Snapshot Sync Tool
|
||||
# Syncs a Qdrant collection from the local machine to a remote environment
|
||||
# using the safe Snapshot API to avoid RocksDB corruption.
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
# Load environment variables
|
||||
if [ -f .env ]; then
|
||||
set -a; source .env; set +a
|
||||
fi
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
TARGET_ENV="${1:-}" # testing | staging | branch_slug | prod
|
||||
COLLECTION="${2:-kabelfachmann}"
|
||||
SSH_HOST="root@alpha.mintel.me"
|
||||
|
||||
if [[ -z "$TARGET_ENV" ]]; then
|
||||
echo "Usage: pnpm run qdrant:push <target_env> [collection]"
|
||||
echo "Example: pnpm run qdrant:push testing kabelfachmann"
|
||||
echo "Example: pnpm run qdrant:push mein-feature-slug kabelfachmann"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCAL_QDRANT_URL=${QDRANT_URL:-"http://localhost:6337"}
|
||||
TIMEOUT=300 # 5 minutes for large snapshots
|
||||
|
||||
get_target_path() {
|
||||
case "$1" in
|
||||
testing) echo "/home/deploy/sites/testing.klz-cables.com" ;;
|
||||
staging) echo "/home/deploy/sites/staging.klz-cables.com" ;;
|
||||
prod|production) echo "/home/deploy/sites/klz-cables.com" ;;
|
||||
*) echo "/home/deploy/sites/branch.klz-cables.com/$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_project_name() {
|
||||
case "$1" in
|
||||
testing) echo "klz-testing" ;;
|
||||
staging) echo "klz-staging" ;;
|
||||
prod|production) echo "klz-cablescom" ;;
|
||||
*) echo "klz-branch-$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
TGT_PATH=$(get_target_path "$TARGET_ENV")
|
||||
PROJECT_NAME=$(get_project_name "$TARGET_ENV")
|
||||
QDRANT_CONTAINER="${PROJECT_NAME}-klz-qdrant-1"
|
||||
WORK_DIR=$(mktemp -d)
|
||||
|
||||
echo "🚀 Syncing Qdrant Collection '$COLLECTION' to: $TARGET_ENV"
|
||||
|
||||
# 1. Create Snapshot Locally
|
||||
echo "📸 1/5 Creating snapshot on local Qdrant ($LOCAL_QDRANT_URL)..."
|
||||
SNAPSHOT_INFO=$(curl --max-time $TIMEOUT -s -X POST "$LOCAL_QDRANT_URL/collections/$COLLECTION/snapshots")
|
||||
|
||||
if ! echo "$SNAPSHOT_INFO" | grep -q '"status":"ok"'; then
|
||||
echo "❌ Failed to create snapshot."
|
||||
echo "Response: $SNAPSHOT_INFO"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SNAPSHOT_NAME=$(echo "$SNAPSHOT_INFO" | grep -o '"name":"[^"]*' | cut -d'"' -f4)
|
||||
echo " ✅ Snapshot created: $SNAPSHOT_NAME"
|
||||
|
||||
# 2. Download Snapshot
|
||||
echo "⬇️ 2/5 Downloading snapshot..."
|
||||
curl --max-time $TIMEOUT -s -o "$WORK_DIR/$SNAPSHOT_NAME" "$LOCAL_QDRANT_URL/collections/$COLLECTION/snapshots/$SNAPSHOT_NAME"
|
||||
echo " ✅ Downloaded to $WORK_DIR/$SNAPSHOT_NAME"
|
||||
|
||||
# 3. Compress and Transfer Snapshot
|
||||
echo "📦 3/6 Compressing snapshot to save bandwidth..."
|
||||
gzip -c "$WORK_DIR/$SNAPSHOT_NAME" > "$WORK_DIR/$SNAPSHOT_NAME.gz"
|
||||
echo " ✅ Compressed $SNAPSHOT_NAME.gz"
|
||||
|
||||
echo "📤 4/6 Uploading compressed snapshot to Alpha ($SSH_HOST)..."
|
||||
SSH_OPTS="-o ServerAliveInterval=60 -o ServerAliveCountMax=10 -o ConnectTimeout=30"
|
||||
ssh $SSH_OPTS "$SSH_HOST" "mkdir -p $TGT_PATH/qdrant_tmp"
|
||||
rsync --partial --progress --timeout=600 -e "ssh $SSH_OPTS" \
|
||||
"$WORK_DIR/$SNAPSHOT_NAME.gz" "$SSH_HOST:$TGT_PATH/qdrant_tmp/$SNAPSHOT_NAME.gz"
|
||||
echo " ✅ Upload complete."
|
||||
|
||||
# 4. Restore Snapshot on Remote Server
|
||||
echo "🔄 5/6 Restoring snapshot on target container ($QDRANT_CONTAINER)..."
|
||||
|
||||
# Qdrant restore process:
|
||||
# - Extract snapshot on server
|
||||
# - Recreate collection (so it is clean)
|
||||
# - Download snapshot to container
|
||||
# - Recover from snapshot file
|
||||
|
||||
ssh $SSH_OPTS "$SSH_HOST" << EOF
|
||||
set -e
|
||||
# Step A: Extract the compressed file
|
||||
echo " [Remote] Extracting snapshot..."
|
||||
gunzip -f "$TGT_PATH/qdrant_tmp/$SNAPSHOT_NAME.gz"
|
||||
|
||||
# Step B: Copy file into the container
|
||||
docker cp "$TGT_PATH/qdrant_tmp/$SNAPSHOT_NAME" $QDRANT_CONTAINER:/qdrant/$SNAPSHOT_NAME
|
||||
|
||||
# Step C: Delete existing collection
|
||||
curl -s -X DELETE "http://127.0.0.1:6333/collections/$COLLECTION" > /dev/null
|
||||
|
||||
# Step D: Re-create empty collection (required before recovery)
|
||||
# wir nutzen die standard vector config vom Kabelfachmann (Cosine, 384 dim für all-MiniLM-L6-v2)
|
||||
curl -s -X PUT "http://127.0.0.1:6333/collections/$COLLECTION" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "vectors": { "size": 384, "distance": "Cosine" } }' > /dev/null
|
||||
|
||||
# Step E: Recover
|
||||
echo " [Remote] Triggering recover API..."
|
||||
curl -s -X PUT "http://127.0.0.1:6333/collections/$COLLECTION/snapshots/recover" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "location": "file:///qdrant/'$SNAPSHOT_NAME'" }' > /dev/null
|
||||
|
||||
# Step F: Cleanup
|
||||
docker exec $QDRANT_CONTAINER rm /qdrant/$SNAPSHOT_NAME
|
||||
rm -rf "$TGT_PATH/qdrant_tmp"
|
||||
EOF
|
||||
|
||||
echo " ✅ Restore complete."
|
||||
|
||||
# 5. Local Cleanup
|
||||
echo "🧹 6/6 Cleaning up..."
|
||||
rm -rf "$WORK_DIR"
|
||||
# Delete snapshot from local Qdrant server to save space
|
||||
curl -s -X DELETE "$LOCAL_QDRANT_URL/collections/$COLLECTION/snapshots/$SNAPSHOT_NAME" > /dev/null
|
||||
echo " ✅ Local cleanup done."
|
||||
|
||||
echo ""
|
||||
echo "🎉 Successfully synced Qdrant collection '$COLLECTION' to $TARGET_ENV!"
|
||||
@@ -16,7 +16,7 @@ export const qdrant = new QdrantClient({
|
||||
});
|
||||
|
||||
export const COLLECTION_NAME = 'klz_products';
|
||||
export const VECTOR_SIZE = 1536; // OpenAI text-embedding-3-small
|
||||
export const VECTOR_SIZE = 1024; // Mistral mistral-embed
|
||||
|
||||
// Cache TTLs
|
||||
const EMBEDDING_CACHE_TTL = 60 * 60 * 24; // 24h — embeddings are deterministic
|
||||
@@ -50,26 +50,15 @@ export async function ensureCollection() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple hash for cache keys
|
||||
* Hash text for cache key
|
||||
*/
|
||||
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);
|
||||
const { createHash } = require('crypto');
|
||||
return createHash('sha256').update(text).digest('hex').slice(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Generate embedding using Mistral API (EU/DSGVO-compliant)
|
||||
*/
|
||||
export async function generateEmbedding(text: string): Promise<number[]> {
|
||||
const cacheKey = `emb:${hashKey(text.toLowerCase().trim())}`;
|
||||
@@ -84,22 +73,20 @@ export async function generateEmbedding(text: string): Promise<number[]> {
|
||||
// Redis down — proceed without cache
|
||||
}
|
||||
|
||||
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 response = await fetch('https://openrouter.ai/api/v1/embeddings', {
|
||||
const response = await fetch('https://api.mistral.ai/v1/embeddings', {
|
||||
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: 'openai/text-embedding-3-small',
|
||||
input: text,
|
||||
model: 'mistral-embed',
|
||||
input: [text],
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,66 @@ export const Pages: CollectionConfig = {
|
||||
};
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, req }) => {
|
||||
// Run index sync asynchronously to not block the CMS save operation
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const { upsertProductVector, deleteProductVector } = await import('../../lib/qdrant');
|
||||
|
||||
// Check if page is published
|
||||
if (doc._status !== 'published') {
|
||||
await deleteProductVector(`page_${doc.id}`);
|
||||
req.payload.logger.info(`Removed drafted page ${doc.slug} from Qdrant`);
|
||||
} else {
|
||||
// Serialize payload
|
||||
const contentText = [
|
||||
`Seite: ${doc.title}`,
|
||||
doc.excerpt ? `Beschreibung: ${doc.excerpt}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
const payload = {
|
||||
type: 'knowledge',
|
||||
content: contentText,
|
||||
data: {
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
},
|
||||
};
|
||||
|
||||
await upsertProductVector(`page_${doc.id}`, contentText, payload);
|
||||
req.payload.logger.info(`Upserted page ${doc.slug} to Qdrant`);
|
||||
}
|
||||
} catch (error) {
|
||||
req.payload.logger.error({
|
||||
msg: 'Error syncing page to Qdrant',
|
||||
err: error,
|
||||
pageId: doc.id,
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
return doc;
|
||||
},
|
||||
],
|
||||
afterDelete: [
|
||||
async ({ id, req }) => {
|
||||
try {
|
||||
const { deleteProductVector } = await import('../../lib/qdrant');
|
||||
await deleteProductVector(`page_${id}`);
|
||||
req.payload.logger.info(`Deleted page ${id} from Qdrant`);
|
||||
} catch (error) {
|
||||
req.payload.logger.error({
|
||||
msg: 'Error deleting page from Qdrant',
|
||||
err: error,
|
||||
pageId: id,
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
|
||||
@@ -45,6 +45,67 @@ export const Posts: CollectionConfig = {
|
||||
};
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, req }) => {
|
||||
// Run index sync asynchronously to not block the CMS save operation
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const { upsertProductVector, deleteProductVector } = await import('../../lib/qdrant');
|
||||
|
||||
// Check if post is published
|
||||
if (doc._status !== 'published') {
|
||||
await deleteProductVector(`post_${doc.id}`);
|
||||
req.payload.logger.info(`Removed drafted post ${doc.slug} from Qdrant`);
|
||||
} else {
|
||||
// Serialize payload
|
||||
const contentText = [
|
||||
`Blog-Artikel: ${doc.title}`,
|
||||
doc.excerpt ? `Zusammenfassung: ${doc.excerpt}` : '',
|
||||
doc.category ? `Kategorie: ${doc.category}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
const payload = {
|
||||
type: 'knowledge',
|
||||
content: contentText,
|
||||
data: {
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
},
|
||||
};
|
||||
|
||||
await upsertProductVector(`post_${doc.id}`, contentText, payload);
|
||||
req.payload.logger.info(`Upserted post ${doc.slug} to Qdrant`);
|
||||
}
|
||||
} catch (error) {
|
||||
req.payload.logger.error({
|
||||
msg: 'Error syncing post to Qdrant',
|
||||
err: error,
|
||||
postId: doc.id,
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
return doc;
|
||||
},
|
||||
],
|
||||
afterDelete: [
|
||||
async ({ id, req }) => {
|
||||
try {
|
||||
const { deleteProductVector } = await import('../../lib/qdrant');
|
||||
await deleteProductVector(`post_${id}`);
|
||||
req.payload.logger.info(`Deleted post ${id} from Qdrant`);
|
||||
} catch (error) {
|
||||
req.payload.logger.error({
|
||||
msg: 'Error deleting post from Qdrant',
|
||||
err: error,
|
||||
postId: id,
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
|
||||
BIN
src/scripts/error.png
Normal file
BIN
src/scripts/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import 'dotenv/config';
|
||||
|
||||
// Override Qdrant URL for local script execution outside docker
|
||||
process.env.QDRANT_URL = process.env.QDRANT_URL || 'http://localhost:6333';
|
||||
|
||||
Reference in New Issue
Block a user