From 3464494b140cea398f0301ee6391182ece473e7a Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Fri, 16 Jan 2026 15:44:06 +0000 Subject: [PATCH] feat(community): Phase 2.2 - YouTube Auto-Sync und AI Reply Suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementiert automatische YouTube-Kommentar-Synchronisation und KI-gestützte Antwortvorschläge für das Community Management. Neue Features: - Cron-Endpoint für externen Scheduler (/api/cron/youtube-sync) - ClaudeReplyService für AI-generierte Antworten (3 Tonalitäten) - Sync Status API mit Live-Polling - AI Reply Suggestions UI mit Varianten-Auswahl - Job Logger für strukturiertes Logging von Background Jobs Änderungen: - ClaudeAnalysisService: Model-Update auf claude-3-5-haiku-20241022 - CommunityInbox: Sync Status Badge, AI Reply Suggestions Integration - SCSS: Styles für Sync-Indicator und Suggestion Cards Co-Authored-By: Claude Opus 4.5 --- docs/reports/phase2.2-corrections-report.md | 244 ++++++++++++++ package.json | 2 + pnpm-lock.yaml | 305 ++++++++++++++++++ .../views/community/inbox/CommunityInbox.tsx | 162 +++++++++- .../admin/views/community/inbox/inbox.scss | 215 ++++++++++++ .../api/community/generate-reply/route.ts | 138 ++++++++ src/app/(payload)/api/community/sync/route.ts | 112 +++++++ .../(payload)/api/cron/youtube-sync/route.ts | 65 ++++ .../claude/ClaudeAnalysisService.ts | 14 +- .../integrations/claude/ClaudeReplyService.ts | 243 ++++++++++++++ src/lib/jobs/JobLogger.ts | 80 +++++ src/lib/jobs/syncAllComments.ts | 172 ++++++++++ src/types/youtube.ts | 104 ++++++ 13 files changed, 1839 insertions(+), 17 deletions(-) create mode 100644 docs/reports/phase2.2-corrections-report.md create mode 100644 src/app/(payload)/api/community/generate-reply/route.ts create mode 100644 src/app/(payload)/api/community/sync/route.ts create mode 100644 src/app/(payload)/api/cron/youtube-sync/route.ts create mode 100644 src/lib/integrations/claude/ClaudeReplyService.ts create mode 100644 src/lib/jobs/JobLogger.ts create mode 100644 src/lib/jobs/syncAllComments.ts create mode 100644 src/types/youtube.ts diff --git a/docs/reports/phase2.2-corrections-report.md b/docs/reports/phase2.2-corrections-report.md new file mode 100644 index 0000000..6f40bc4 --- /dev/null +++ b/docs/reports/phase2.2-corrections-report.md @@ -0,0 +1,244 @@ +# Phase 2.2 Prompt - Korrektur-Bericht + +**Datum:** 16. Januar 2026 +**Erstellt von:** Claude Code (Opus 4.5) +**Prompt-Datei:** `/prompts/Phase2.2 youtube sync ai replies prompt.md` + +--- + +## Zusammenfassung + +Der Phase 2.2 Prompt beschreibt die Implementierung von: +1. **YouTube Auto-Sync** - Automatischer Kommentar-Import +2. **AI Reply Suggestions** - KI-Antwortvorschläge mit Claude +3. **UI Integration** - Erweiterungen für CommunityInbox +4. **Cron-Konfiguration** - Automatische Sync-Jobs + +**Ergebnis:** Der Prompt enthält mehrere Annahmen über existierende Infrastruktur, die nicht zutreffen. **Korrekturen sind erforderlich.** + +--- + +## KRITISCHE Korrektur: ClaudeAnalysisService fehlt + +### Problem + +Der Prompt geht davon aus, dass `ClaudeAnalysisService.ts` bereits existiert: + +```typescript +// Prompt Zeile 44-50: +// src/lib/integrations/claude/ClaudeAnalysisService.ts +// - analyzeSentiment(message: string): Promise +// - extractTopics(message: string): Promise +// - detectMedicalContent(message: string): Promise +``` + +**Realität:** Das Verzeichnis `src/lib/integrations/claude/` ist LEER. + +### Auswirkung + +Der existierende `CommentsSyncService.ts` importiert `ClaudeAnalysisService`: + +```typescript +// src/lib/integrations/youtube/CommentsSyncService.ts, Zeile 5: +import { ClaudeAnalysisService } from '../claude/ClaudeAnalysisService' +``` + +**Der Code kann nicht kompiliert/ausgeführt werden, solange ClaudeAnalysisService fehlt!** + +### Lösung + +`ClaudeAnalysisService.ts` muss ZUERST erstellt werden, bevor der Sync-Service funktioniert. + +Der Prompt enthält bereits den vollständigen Code dafür (Zeilen 1236-1339). + +--- + +## Bestandsaufnahme: Was existiert bereits? + +### Vorhandene Dateien + +| Datei | Status | Anmerkungen | +|-------|--------|-------------| +| `src/lib/integrations/youtube/YouTubeClient.ts` | Existiert | Leicht andere Signatur als im Prompt | +| `src/lib/integrations/youtube/CommentsSyncService.ts` | Existiert | Importiert fehlenden ClaudeAnalysisService | +| `src/lib/integrations/youtube/oauth.ts` | Existiert | Vollständig funktional | +| `src/lib/integrations/claude/` | LEER | ClaudeAnalysisService fehlt! | +| `src/collections/CommunityInteractions.ts` | Existiert | 568 Zeilen, vollständig | +| `src/collections/SocialAccounts.ts` | Existiert | Vollständig | +| `src/collections/SocialPlatforms.ts` | Existiert | Vollständig | +| `src/lib/services/RulesEngine.ts` | Existiert | 9054 Bytes | + +### Vorhandene API Routes + +| Route | Status | Anmerkungen | +|-------|--------|-------------| +| `/api/community/sync-comments/` | Existiert | POST - Manueller Sync (pro Account) | +| `/api/community/analytics/*` | Existiert | 8 Endpoints (Phase 2.1) | +| `/api/community/export/` | Existiert | Export-Funktionalität | +| `/api/community/reply/` | Existiert | Antworten senden | +| `/api/community/stats/` | Existiert | Statistiken | + +--- + +## Korrekturen am Prompt + +### 1. ClaudeAnalysisService muss erstellt werden (nicht "bereits implementiert") + +**Prompt-Text ändern von:** +```markdown +### Claude Integration (bereits implementiert) + +// src/lib/integrations/claude/ClaudeAnalysisService.ts +``` + +**Ändern zu:** +```markdown +### Claude Integration (MUSS ERSTELLT WERDEN) + +// src/lib/integrations/claude/ClaudeAnalysisService.ts +// HINWEIS: Diese Datei existiert noch nicht und muss als erstes erstellt werden! +``` + +### 2. API Route Namenskonvention anpassen + +**Prompt schlägt vor:** +``` +/api/community/sync/route.ts +``` + +**Bereits existiert:** +``` +/api/community/sync-comments/route.ts +``` + +**Empfehlung:** Den existierenden Pfad beibehalten oder Prompt aktualisieren. + +### 3. YouTubeClient Signatur-Unterschiede + +**Im Prompt:** +```typescript +class YouTubeClient { + constructor(accessToken: string, accountId: number) { ... } + static async getValidClient(accountId: number): Promise +} +``` + +**Existierende Version:** +```typescript +class YouTubeClient { + constructor(credentials: YouTubeCredentials, payload: Payload) { ... } + // Keine statische getValidClient Methode +} +``` + +**Empfehlung:** Die existierende Version erweitern, nicht ersetzen. + +### 4. Implementierungsreihenfolge aktualisieren + +**Neue empfohlene Reihenfolge:** + +1. **ClaudeAnalysisService erstellen** (KRITISCH - ohne dies funktioniert nichts!) +2. Type Definitions (`youtube.ts`) +3. YouTubeClient erweitern (getValidClient Methode hinzufügen) +4. CommentsSyncService prüfen/anpassen +5. Job Runner + Logger +6. Cron Endpoint +7. ClaudeReplyService +8. API Endpoint `/generate-reply` +9. UI Integration +10. SCSS Ergänzungen + +--- + +## Fehlende Komponenten + +### Laut Prompt zu erstellen + +| Komponente | Pfad | Priorität | +|------------|------|-----------| +| ClaudeAnalysisService | `src/lib/integrations/claude/ClaudeAnalysisService.ts` | KRITISCH | +| ClaudeReplyService | `src/lib/integrations/claude/ClaudeReplyService.ts` | Hoch | +| Type Definitions | `src/types/youtube.ts` | Mittel | +| Job Runner | `src/lib/jobs/syncAllComments.ts` | Mittel | +| Job Logger | `src/lib/jobs/JobLogger.ts` | Mittel | +| Cron Endpoint | `src/app/(payload)/api/cron/youtube-sync/route.ts` | Mittel | +| Generate Reply | `src/app/(payload)/api/community/generate-reply/route.ts` | Hoch | + +### UI-Erweiterungen + +- AI Reply Section in CommunityInbox +- Sync Status Badge +- SCSS Ergänzungen (~100 Zeilen) + +--- + +## Collection-Feldabgleich + +### CommunityInteractions - Prompt vs. Realität + +| Feld (Prompt) | Vorhanden | Anmerkungen | +|---------------|-----------|-------------| +| `externalId` | Ja | Einzigartig, indiziert | +| `platform` | Ja | Relationship | +| `socialAccount` | Ja | Relationship | +| `linkedContent` | Ja | Relationship zu youtube-content | +| `type` | Ja | comment, reply, dm, mention, review, question | +| `author.*` | Ja | Vollständig | +| `message` | Ja | Textarea | +| `analysis.*` | Ja | Sentiment, Score, Confidence, Topics | +| `flags.*` | Ja | isMedicalQuestion, requiresEscalation, isSpam, isFromInfluencer | +| `status` | Ja | new, in_review, waiting, replied, resolved, archived, spam | +| `priority` | Ja | urgent, high, normal, low | +| `response.*` | Ja | text, usedTemplate, sentAt, sentBy | + +**Ergebnis:** Collection ist vollständig kompatibel mit dem Prompt. + +--- + +## Umgebungsvariablen prüfen + +Der Prompt erfordert folgende Umgebungsvariablen: + +| Variable | Beschreibung | Status | +|----------|--------------|--------| +| `ANTHROPIC_API_KEY` | Claude API Key | Prüfen in .env | +| `YOUTUBE_CLIENT_ID` | YouTube OAuth Client ID | Prüfen in .env | +| `YOUTUBE_CLIENT_SECRET` | YouTube OAuth Client Secret | Prüfen in .env | +| `YOUTUBE_REDIRECT_URI` | OAuth Callback URL | Prüfen in .env | +| `CRON_SECRET` | Auth-Token für Cron-Endpoint | NEU - hinzufügen | + +--- + +## Empfohlene nächste Schritte + +1. **Sofort:** `ClaudeAnalysisService.ts` erstellen (Code aus Prompt Zeilen 1236-1339) +2. **Dann:** Build testen - `pnpm build` +3. **Danach:** Sync-Funktionalität manuell testen +4. **Fortfahren:** Restliche Komponenten implementieren + +--- + +## Hinweise für die Implementierung + +### Claude API Modell + +Der Prompt verwendet `claude-3-haiku-20240307`. Dieses Modell ist schnell und kostengünstig, aber veraltet. Aktuelle Alternativen: + +- `claude-3-5-haiku-20241022` - Neuere Version +- `claude-3-5-sonnet-20241022` - Bessere Qualität bei höheren Kosten + +### Rate Limiting + +Der Prompt enthält Rate-Limiting zwischen API-Calls: +- 100ms zwischen Kommentaren +- 200ms zwischen Videos + +Dies ist wichtig für YouTube API Quota und Claude API Kosten. + +### Error Handling + +Die existierende `CommentsSyncService` hat Fallback-Analyse bei Claude-Fehlern - das ist gut implementiert. + +--- + +*Generiert am 16. Januar 2026 von Claude Code (Opus 4.5)* diff --git a/package.json b/package.json index be86aa6..8fd37e8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/pdfkit": "^0.17.4", "bullmq": "^5.65.1", "cross-env": "^7.0.3", + "date-fns": "^4.1.0", "dotenv": "16.4.7", "exceljs": "^4.4.0", "googleapis": "^170.0.0", @@ -53,6 +54,7 @@ "pdfkit": "^0.17.2", "react": "19.2.3", "react-dom": "19.2.3", + "recharts": "^3.6.0", "sharp": "0.34.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd07425..5d8c309 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 dotenv: specifier: 16.4.7 version: 16.4.7 @@ -86,6 +89,9 @@ importers: react-dom: specifier: 19.2.3 version: 19.2.3(react@19.2.3) + recharts: + specifier: ^3.6.0 + version: 3.6.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1) sharp: specifier: 0.34.5 version: 0.34.5 @@ -1346,6 +1352,17 @@ packages: engines: {node: '>=18'} hasBin: true + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/pluginutils@1.0.0-beta.11': resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} @@ -1640,6 +1657,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1670,6 +1690,33 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1743,6 +1790,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -2321,6 +2371,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2376,6 +2470,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -2599,6 +2696,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.43.0: + resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -2750,6 +2850,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + exceljs@4.4.0: resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} engines: {node: '>=8.3.0'} @@ -3072,6 +3175,12 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.3: + resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} + immutable@4.3.7: resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} @@ -3094,6 +3203,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ioredis@5.8.2: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} @@ -4054,6 +4167,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -4092,6 +4217,14 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + recharts@3.6.0: + resolution: {integrity: sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -4100,6 +4233,14 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4112,6 +4253,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4435,6 +4579,9 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4629,6 +4776,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.5: resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} @@ -4657,6 +4809,9 @@ packages: vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-tsconfig-paths@6.0.0: resolution: {integrity: sha512-0lGkM62rud1ShKWLbJpbTHPoJuZIL9QW1ecCueDhqxWrStIRsyHapBQ4eV05tBqrW9z6jkp9ybBVgLSWp+Mv1A==} peerDependencies: @@ -6444,6 +6599,18 @@ snapshots: dependencies: playwright: 1.57.0 + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.3 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + '@rolldown/pluginutils@1.0.0-beta.11': {} '@rollup/rollup-android-arm-eabi@4.53.3': @@ -6792,6 +6959,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -6837,6 +7006,30 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -6911,6 +7104,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/uuid@10.0.0': {} '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': @@ -7543,6 +7738,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + damerau-levenshtein@1.0.8: {} data-uri-to-buffer@4.0.1: {} @@ -7588,6 +7821,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} decode-named-character-reference@1.2.0: @@ -7797,6 +8032,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.43.0: {} + esbuild-register@3.6.0(esbuild@0.25.12): dependencies: debug: 4.4.3 @@ -8069,6 +8306,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.1: {} + exceljs@4.4.0: dependencies: archiver: 5.3.2 @@ -8427,6 +8666,10 @@ snapshots: immediate@3.0.6: {} + immer@10.2.0: {} + + immer@11.1.3: {} + immutable@4.3.7: {} import-fresh@3.3.1: @@ -8449,6 +8692,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + ioredis@5.8.2: dependencies: '@ioredis/commands': 1.4.0 @@ -9605,6 +9850,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + redux: 5.0.1 + react-refresh@0.17.0: {} react-select@5.9.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -9661,12 +9915,38 @@ snapshots: real-require@0.2.0: {} + recharts@3.6.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.43.0 + eventemitter3: 5.0.1 + immer: 10.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.3) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -9689,6 +9969,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -10079,6 +10361,8 @@ snapshots: tiny-inflate@1.0.3: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -10314,6 +10598,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + utf8-byte-length@1.0.5: {} util-deprecate@1.0.2: {} @@ -10339,6 +10627,23 @@ snapshots: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite-tsconfig-paths@6.0.0(typescript@5.9.3)(vite@7.2.4(@types/node@22.19.1)(sass@1.77.4)(tsx@4.20.6)): dependencies: debug: 4.4.3 diff --git a/src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx b/src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx index 8431a39..7f687b4 100644 --- a/src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx +++ b/src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx @@ -96,6 +96,22 @@ export const CommunityInbox: React.FC = () => { const [exportFormat, setExportFormat] = useState<'pdf' | 'excel' | 'csv'>('excel') const [exportDateRange, setExportDateRange] = useState({ from: '', to: '' }) + // AI Reply state + const [isGeneratingReply, setIsGeneratingReply] = useState(false) + const [generatedReplies, setGeneratedReplies] = useState>([]) + + // Sync status state + const [syncStatus, setSyncStatus] = useState<{ + isRunning: boolean + lastRunAt: string | null + nextRunAt: string | null + } | null>(null) + // Refs const listRef = useRef(null) @@ -226,22 +242,52 @@ export const CommunityInbox: React.FC = () => { loadInteractions() }, [loadInteractions]) - // Sync comments + // Fetch Sync Status + useEffect(() => { + const fetchSyncStatus = async () => { + try { + const response = await fetch('/api/community/sync') + if (response.ok) { + const status = await response.json() + setSyncStatus(status) + } + } catch (error) { + console.error('Failed to fetch sync status:', error) + } + } + + fetchSyncStatus() + const interval = setInterval(fetchSyncStatus, 60000) // Jede Minute + + return () => clearInterval(interval) + }, []) + + // Sync comments (all accounts) const handleSync = async () => { setSyncing(true) try { - const response = await fetch('/api/community/sync-comments', { + const response = await fetch('/api/community/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ analyzeWithAI: true }), }) if (response.ok) { const result = await response.json() - alert(`Sync complete: ${result.created} new, ${result.updated} updated`) + if (result.success && result.results) { + const totalNew = result.results.reduce((sum: number, r: { newComments: number }) => sum + r.newComments, 0) + const totalUpdated = result.results.reduce((sum: number, r: { updatedComments: number }) => sum + r.updatedComments, 0) + alert(`Sync abgeschlossen: ${totalNew} neue, ${totalUpdated} aktualisierte Kommentare`) + } loadInteractions() + // Refresh sync status + const statusResponse = await fetch('/api/community/sync') + if (statusResponse.ok) { + const status = await statusResponse.json() + setSyncStatus(status) + } } else { - throw new Error('Sync failed') + const error = await response.json() + throw new Error(error.error || 'Sync failed') } } catch (error) { console.error('Sync error:', error) @@ -386,27 +432,48 @@ export const CommunityInbox: React.FC = () => { } // Generate AI reply - const generateAIReply = async () => { + const generateAIReply = async (variants: boolean = false) => { if (!selectedInteraction) return + setIsGeneratingReply(true) + setGeneratedReplies([]) + try { const response = await fetch('/api/community/generate-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interactionId: selectedInteraction.id, + variants, }), }) if (response.ok) { const data = await response.json() - setReplyText(data.reply) + + if (variants && data.variants) { + setGeneratedReplies(data.variants) + } else if (data.reply) { + // Einzelne Antwort direkt übernehmen + setReplyText(data.reply.text) + } + } else { + throw new Error('Generation failed') } } catch (error) { console.error('AI generation error:', error) + alert('KI-Antwortgenerierung fehlgeschlagen') + } finally { + setIsGeneratingReply(false) } } + // Use generated reply + const useGeneratedReply = (reply: { text: string }) => { + setReplyText(reply.text) + setGeneratedReplies([]) + } + // Select interaction const selectInteraction = (interaction: Interaction) => { setSelectedInteraction(interaction) @@ -736,8 +803,12 @@ export const CommunityInbox: React.FC = () => { ))} -