From 0a60905587a5de96728f46fa62049eb05d0c498c Mon Sep 17 00:00:00 2001 From: Hung Phan Viet Date: Tue, 18 Aug 2026 13:11:14 +0700 Subject: [PATCH 1/3] feat(coexist): add option to skip synced history for ai context --- .../__tests__/coexist-messenger-api.test.ts | 27 +- apps/builder/__tests__/coexist-popup.test.tsx | 182 + .../__tests__/coexist-whatsapp-api.test.ts | 77 +- apps/builder/messages/ar.json | 2 + apps/builder/messages/da.json | 2 + apps/builder/messages/de.json | 2 + apps/builder/messages/en.json | 2 + apps/builder/messages/es.json | 2 + apps/builder/messages/fi.json | 2 + apps/builder/messages/fr.json | 2 + apps/builder/messages/he.json | 2 + apps/builder/messages/id.json | 2 + apps/builder/messages/it.json | 2 + apps/builder/messages/ja.json | 2 + apps/builder/messages/nl.json | 2 + apps/builder/messages/pt-BR.json | 2 + apps/builder/messages/pt-PT.json | 2 + apps/builder/messages/ro.json | 2 + apps/builder/messages/sv.json | 2 + apps/builder/messages/tr.json | 2 + apps/builder/messages/vi.json | 2 + apps/builder/messages/zh-CN.json | 2 + apps/builder/messages/zh-TW.json | 2 + .../integration-instagram/api/coexist.ts | 2 + .../integration-messenger/api/coexist.ts | 4 +- .../integration-whatsapp/api/coexist.ts | 12 +- .../src/features/shared/coexist-popup.tsx | 22 +- .../__tests__/bulk-historical-import.test.ts | 105 +- .../__tests__/bulk-import-messages.test.ts | 345 +- .../__tests__/coexist-instagram-sync.test.ts | 53 + .../__tests__/coexist-messenger-sync.test.ts | 94 +- .../__tests__/coexist-whatsapp-flush.test.ts | 30 + .../coexist/bulk-historical-import.ts | 267 +- .../handlers/coexist/instagram-sync.ts | 18 +- .../handlers/coexist/messenger-sync.ts | 26 +- .../handlers/coexist/whatsapp-flush.ts | 1 + .../__tests__/conversation-service.test.ts | 213 +- packages/business/src/coexist/service.ts | 4 + packages/business/src/conversation/service.ts | 55 + .../migration.sql | 3 + .../snapshot.json | 36221 ++++++++++++++++ .../coexist-sync-run/repository.ts | 33 +- .../src/schema/integration-instagram.ts | 1 + .../src/schema/integration-messenger.ts | 1 + .../src/schema/integration-whatsapp.ts | 1 + 45 files changed, 37623 insertions(+), 214 deletions(-) create mode 100644 apps/builder/__tests__/coexist-popup.test.tsx create mode 100644 packages/database/drizzle/20260818035559_coexist-skip-ai-context/migration.sql create mode 100644 packages/database/drizzle/20260818035559_coexist-skip-ai-context/snapshot.json diff --git a/apps/builder/__tests__/coexist-messenger-api.test.ts b/apps/builder/__tests__/coexist-messenger-api.test.ts index 17dced017..42a59e401 100644 --- a/apps/builder/__tests__/coexist-messenger-api.test.ts +++ b/apps/builder/__tests__/coexist-messenger-api.test.ts @@ -74,7 +74,7 @@ describe("coexist APIs", () => { mockDisable.mockResolvedValue({ success: true }) }) - test("Messenger enabled:true delegates to coexistService.enable", async () => { + test("Messenger enabled:true delegates to coexistService.enable with skipAiContext defaulted to false", async () => { const result = await call( integrationMessengerCoexistAPIs.setCoexistMessengerAPI, { workspaceId: "ws-1", integrationId: "int-1", enabled: true }, @@ -86,10 +86,32 @@ describe("coexist APIs", () => { workspaceId: "ws-1", integrationId: "int-1", channel: "messenger", + skipAiContext: false, }) expect(mockDisable).not.toHaveBeenCalled() }) + test("Messenger enabled:true, skipAiContext:true delegates it through to coexistService.enable", async () => { + const result = await call( + integrationMessengerCoexistAPIs.setCoexistMessengerAPI, + { + workspaceId: "ws-1", + integrationId: "int-1", + enabled: true, + skipAiContext: true, + }, + { context: stubContext }, + ) + + expect(result).toEqual({ success: true }) + expect(mockEnable).toHaveBeenCalledWith({ + workspaceId: "ws-1", + integrationId: "int-1", + channel: "messenger", + skipAiContext: true, + }) + }) + test("Messenger enabled:false delegates to coexistService.disable", async () => { const result = await call( integrationMessengerCoexistAPIs.setCoexistMessengerAPI, @@ -105,7 +127,7 @@ describe("coexist APIs", () => { }) }) - test("Instagram endpoint delegates with channel instagram", async () => { + test("Instagram endpoint delegates with channel instagram and skipAiContext defaulted to false", async () => { const result = await call( integrationInstagramCoexistAPIs.setCoexistInstagramAPI, { workspaceId: "ws-1", integrationId: "ig-1", enabled: true }, @@ -117,6 +139,7 @@ describe("coexist APIs", () => { workspaceId: "ws-1", integrationId: "ig-1", channel: "instagram", + skipAiContext: false, }) }) diff --git a/apps/builder/__tests__/coexist-popup.test.tsx b/apps/builder/__tests__/coexist-popup.test.tsx new file mode 100644 index 000000000..c39b501e2 --- /dev/null +++ b/apps/builder/__tests__/coexist-popup.test.tsx @@ -0,0 +1,182 @@ +import type { ReactNode } from "react" +import { act } from "react" +import { createRoot, type Root } from "react-dom/client" +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" + +// --------------------------------------------------------------------------- +// Mocks +// --------------------------------------------------------------------------- + +const { mockKyPost } = vi.hoisted(() => ({ + mockKyPost: vi.fn(), +})) + +/** Echoes the key back so assertions never depend on the English copy. */ +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})) + +vi.mock("ky", () => ({ + default: { post: mockKyPost }, +})) + +vi.mock("sonner", () => ({ + toast: { error: vi.fn(), success: vi.fn() }, +})) + +vi.mock("@/lib/errors/client-handler", () => ({ + clientErrorHandler: vi.fn(), +})) + +vi.mock("@chatbotx.io/ui/components/ui/dialog", () => ({ + Dialog: ({ children }: { children: ReactNode }) =>
{children}
, + DialogContent: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + DialogDescription: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + DialogFooter: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + DialogHeader: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

, +})) + +// jsdom ships no ResizeObserver, and Radix measures the switch thumb through it. +Object.assign(globalThis, { + ResizeObserver: class { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() + }, +}) + +// jsdom ships no PointerEvent constructor; the Switch's click handler +// re-dispatches one to drive its underlying . +if (typeof globalThis.PointerEvent === "undefined") { + class PointerEventPolyfill extends MouseEvent { + constructor(type: string, params: MouseEventInit = {}) { + super(type, params) + } + } + Object.assign(globalThis, { PointerEvent: PointerEventPolyfill }) +} + +const { CoexistPopup } = await import("@/features/shared/coexist-popup") + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("CoexistPopup", () => { + let container: HTMLDivElement + let root: Root + const onDone = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) + container = document.createElement("div") + document.body.append(container) + root = createRoot(container) + mockKyPost.mockReturnValue({ + json: vi.fn().mockResolvedValue({ success: true }), + }) + + act(() => { + root.render( + , + ) + }) + }) + + afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + }) + + const switchEl = () => + container.querySelector('[role="switch"]') + const enableButton = () => + Array.from(container.querySelectorAll("button")).find((button) => + button.textContent?.includes("coexist.enable"), + ) + const declineButton = () => + Array.from(container.querySelectorAll("button")).find((button) => + button.textContent?.includes("coexist.decline"), + ) + + test("renders the skip-AI-context switch defaulted OFF", () => { + expect(switchEl()).not.toBeNull() + expect(switchEl()?.getAttribute("aria-checked")).toBe("false") + }) + + test("POSTs skipAiContext: false by default when confirming enable", async () => { + await act(async () => { + enableButton()?.click() + await Promise.resolve() + }) + + expect(mockKyPost).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + json: expect.objectContaining({ + workspaceId: "ws-1", + integrationId: "int-1", + enabled: true, + skipAiContext: false, + }), + }), + ) + }) + + test("POSTs skipAiContext: true after toggling the switch on", async () => { + act(() => { + switchEl()?.click() + }) + expect(switchEl()?.getAttribute("aria-checked")).toBe("true") + + await act(async () => { + enableButton()?.click() + await Promise.resolve() + }) + + expect(mockKyPost).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + json: expect.objectContaining({ skipAiContext: true }), + }), + ) + }) + + test("POSTs skipAiContext in the decline body too (enabled: false)", async () => { + act(() => { + switchEl()?.click() + }) + + await act(async () => { + declineButton()?.click() + await Promise.resolve() + }) + + expect(mockKyPost).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + json: expect.objectContaining({ + enabled: false, + skipAiContext: true, + }), + }), + ) + }) +}) diff --git a/apps/builder/__tests__/coexist-whatsapp-api.test.ts b/apps/builder/__tests__/coexist-whatsapp-api.test.ts index 989999b86..9b1c3e03a 100644 --- a/apps/builder/__tests__/coexist-whatsapp-api.test.ts +++ b/apps/builder/__tests__/coexist-whatsapp-api.test.ts @@ -167,7 +167,11 @@ describe("setCoexistWhatsappAPI", () => { // Flag flipped on via the atomic UPDATE … RETURNING (one update on success). expect(db.update).toHaveBeenCalledTimes(1) - expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ coexistEnabled: true }) + // skipAiContext defaults to false (zod default) and is written on enable. + expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ + coexistEnabled: true, + coexistSkipAiContext: false, + }) // CoexistSyncRun insert with the init-row values. expect(db.insert).toHaveBeenCalledTimes(1) @@ -188,6 +192,25 @@ describe("setCoexistWhatsappAPI", () => { expect(mockQueueAdd).not.toHaveBeenCalled() }) + test("enabled:true, skipAiContext:true — writes coexistSkipAiContext alongside coexistEnabled", async () => { + const result = await call( + procedure, + { + workspaceId: "ws-1", + integrationId: "int-1", + enabled: true, + skipAiContext: true, + }, + { context: stubContext }, + ) + + expect(result).toEqual({ success: true }) + expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ + coexistEnabled: true, + coexistSkipAiContext: true, + }) + }) + test("enabled:false — flag-only: flips coexistEnabled, no run insert, no delete, no job", async () => { const result = await call( procedure, @@ -208,6 +231,24 @@ describe("setCoexistWhatsappAPI", () => { expect(mockQueueAdd).not.toHaveBeenCalled() }) + test("enabled:false (decline) — never writes coexistSkipAiContext, even when the popup sent skipAiContext:true", async () => { + const result = await call( + procedure, + { + workspaceId: "ws-1", + integrationId: "int-1", + enabled: false, + skipAiContext: true, + }, + { context: stubContext }, + ) + + expect(result).toEqual({ success: true }) + // The UPDATE runs BEFORE the `if (enabled)` branch — decline must never + // write coexistSkipAiContext, regardless of what the popup sent. + expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ coexistEnabled: false }) + }) + test("enabled:true — surfaces failure reason and marks the run failed when smb_app_data fails", async () => { // smb_app_state_sync fails → handler returns the reason and marks the run. mockTriggerSmbAppDataSync @@ -294,6 +335,40 @@ describe("i18n key presence (H12)", () => { ).toBeDefined() }) + test("coexist.skipAiContextLabel is defined in en.json and vi.json", () => { + expect( + ( + enMessages as unknown as Record & { + coexist: Record + } + ).coexist.skipAiContextLabel, + ).toBeDefined() + expect( + ( + viMessages as unknown as Record & { + coexist: Record + } + ).coexist.skipAiContextLabel, + ).toBeDefined() + }) + + test("coexist.skipAiContextHelper is defined in en.json and vi.json", () => { + expect( + ( + enMessages as unknown as Record & { + coexist: Record + } + ).coexist.skipAiContextHelper, + ).toBeDefined() + expect( + ( + viMessages as unknown as Record & { + coexist: Record + } + ).coexist.skipAiContextHelper, + ).toBeDefined() + }) + test("whatsapp.fillRequiredFields is defined in en.json and vi.json", () => { expect( ( diff --git a/apps/builder/messages/ar.json b/apps/builder/messages/ar.json index a4b24d0d1..cc46a29c4 100644 --- a/apps/builder/messages/ar.json +++ b/apps/builder/messages/ar.json @@ -2945,6 +2945,8 @@ "toggleHelper": "لا تنجح إعادة التفعيل إلا إذا كانت Meta لا تزال تحتفظ بالبيانات مؤقتًا (خلال 24 ساعة من الإعداد). وإلا، فاقطع الاتصال ثم أعده.", "toggleHelperMessenger": "لا تنجح إعادة التفعيل إلا إذا كانت Meta لا تزال تحتفظ بالبيانات مؤقتًا (خلال 24 ساعة من الإعداد). وإلا، فاقطع اتصال صفحة Messenger ثم أعده.", "toggleHelperWhatsapp": "لا تنجح إعادة التفعيل إلا إذا كانت Meta لا تزال تحتفظ بالبيانات مؤقتًا (خلال 24 ساعة من الإعداد). وإلا، فاقطع اتصال رقم WhatsApp ثم أعده.", + "skipAiContextLabel": "يتجاهل الذكاء الاصطناعي السجل المتزامن", + "skipAiContextHelper": "عند التفعيل، يتجاهل الذكاء الاصطناعي الرسائل المستوردة من هذه المزامنة ويأخذ في الاعتبار الرسائل الجديدة فقط.", "success": { "enabled": "بدأت المزامنة. ستظهر جهات الاتصال والرسائل السابقة قريبًا.", "disabled": "تم تعطيل المزامنة." diff --git a/apps/builder/messages/da.json b/apps/builder/messages/da.json index 49638c773..6a7e48459 100644 --- a/apps/builder/messages/da.json +++ b/apps/builder/messages/da.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Re-enabling kun works hvis Meta har buffered data (within 24 timer af onboarding). Otherwise afbryd og forbind igen.", "toggleHelperMessenger": "Re-enabling kun works hvis Meta har buffered data (within 24 timer af onboarding). Otherwise afbryd og forbind igen den Messenger page.", "toggleHelperWhatsapp": "Re-enabling kun works hvis Meta har buffered data (within 24 timer af onboarding). Otherwise afbryd og forbind igen den WhatsApp nummer.", + "skipAiContextLabel": "AI springer synket historik over", + "skipAiContextHelper": "Når dette er slået til, ignorerer AI'en beskeder importeret af denne synkronisering og overvejer kun nye beskeder.", "success": { "enabled": "Synkroniser started. Historical kontakter og beskeder vil appear shortly.", "disabled": "Synkroniser deaktiveret." diff --git a/apps/builder/messages/de.json b/apps/builder/messages/de.json index b559606eb..93f15e2d6 100644 --- a/apps/builder/messages/de.json +++ b/apps/builder/messages/de.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Eine erneute Aktivierung funktioniert nur, wenn Meta Daten gepuffert hat (innerhalb von 24 Stunden nach dem Onboarding). Andernfalls trennen und erneut verbinden.", "toggleHelperMessenger": "Eine erneute Aktivierung funktioniert nur, wenn Meta Daten gepuffert hat (innerhalb von 24 Stunden nach dem Onboarding). Andernfalls die Messenger-Seite trennen und erneut verbinden.", "toggleHelperWhatsapp": "Eine erneute Aktivierung funktioniert nur, wenn Meta Daten gepuffert hat (innerhalb von 24 Stunden nach dem Onboarding). Andernfalls die WhatsApp-Nummer trennen und erneut verbinden.", + "skipAiContextLabel": "KI ignoriert Synchronverlauf", + "skipAiContextHelper": "Wenn aktiviert, ignoriert die KI die durch diese Synchronisierung importierten Nachrichten und berücksichtigt nur neue Nachrichten.", "success": { "enabled": "Synchronisierung gestartet. Historische Kontakte und Nachrichten werden in Kürze angezeigt.", "disabled": "Synchronisierung deaktiviert." diff --git a/apps/builder/messages/en.json b/apps/builder/messages/en.json index 637f4f244..7ae0e3433 100644 --- a/apps/builder/messages/en.json +++ b/apps/builder/messages/en.json @@ -3041,6 +3041,8 @@ "toggleHelper": "Re-enabling only works if Meta has buffered data (within 24 hours of onboarding). Otherwise disconnect and reconnect.", "toggleHelperMessenger": "Re-enabling only works if Meta has buffered data (within 24 hours of onboarding). Otherwise disconnect and reconnect the Messenger page.", "toggleHelperWhatsapp": "Re-enabling only works if Meta has buffered data (within 24 hours of onboarding). Otherwise disconnect and reconnect the WhatsApp number.", + "skipAiContextLabel": "AI skips synced history", + "skipAiContextHelper": "When on, the AI ignores messages imported by this sync and only considers new messages.", "success": { "enabled": "Sync started. Historical contacts and messages will appear shortly.", "disabled": "Sync disabled." diff --git a/apps/builder/messages/es.json b/apps/builder/messages/es.json index 85ef11e6d..97207abbd 100644 --- a/apps/builder/messages/es.json +++ b/apps/builder/messages/es.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Re-enabling only works si Meta tiene buffered datos (within 24 horas de onboarding). Otherwise Desconectar y reconnect.", "toggleHelperMessenger": "Re-enabling only works si Meta tiene buffered datos (within 24 horas de onboarding). Otherwise Desconectar y reConectar el Messenger página.", "toggleHelperWhatsapp": "Re-enabling only works si Meta tiene buffered datos (within 24 horas de onboarding). Otherwise Desconectar y reConectar el WhatsApp number.", + "skipAiContextLabel": "La IA omite el historial sincronizado", + "skipAiContextHelper": "Cuando está activado, la IA ignora los mensajes importados por esta sincronización y solo considera los mensajes nuevos.", "success": { "enabled": "Sync started. Historical contactoos y mensajes se appear shortly.", "disabled": "Sync desactivado." diff --git a/apps/builder/messages/fi.json b/apps/builder/messages/fi.json index a02cd6336..bc318806f 100644 --- a/apps/builder/messages/fi.json +++ b/apps/builder/messages/fi.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Uudelleen käyttöönotto toimii vain, jos Metalla on puskuroidut tiedot (24 tunnin kuluessa käyttöönotosta). Muussa tapauksessa katkaise yhteys ja yhdistä uudelleen.", "toggleHelperMessenger": "Uudelleen käyttöönotto toimii vain, jos Metalla on puskuroidut tiedot (24 tunnin kuluessa käyttöönotosta). Muussa tapauksessa katkaise Messenger-sivun yhteys ja yhdistä uudelleen.", "toggleHelperWhatsapp": "Uudelleen käyttöönotto toimii vain, jos Metalla on puskuroidut tiedot (24 tunnin kuluessa käyttöönotosta). Muussa tapauksessa katkaise WhatsApp-numeron yhteys ja yhdistä uudelleen.", + "skipAiContextLabel": "Tekoäly ohittaa synkronoidun historian", + "skipAiContextHelper": "Kun tämä on käytössä, tekoäly jättää huomiotta tämän synkronoinnin tuomat viestit ja ottaa huomioon vain uudet viestit.", "success": { "enabled": "Synkronointi aloitettu. Historialliset yhteystiedot ja viestit tulevat pian näkyviin.", "disabled": "Synkronointi poistettu käytöstä." diff --git a/apps/builder/messages/fr.json b/apps/builder/messages/fr.json index 2428b2005..cc4c481a5 100644 --- a/apps/builder/messages/fr.json +++ b/apps/builder/messages/fr.json @@ -2945,6 +2945,8 @@ "toggleHelper": "La réactivation ne fonctionne que si Meta dispose encore de données en mémoire tampon, dans les 24 heures suivant l’intégration. Sinon, déconnectez et reconnectez.", "toggleHelperMessenger": "La réactivation ne fonctionne que si Meta dispose encore de données en mémoire tampon, dans les 24 heures suivant l’intégration. Sinon, déconnectez et reconnectez la page Messenger.", "toggleHelperWhatsapp": "La réactivation ne fonctionne que si Meta dispose encore de données en mémoire tampon, dans les 24 heures suivant l’intégration. Sinon, déconnectez et reconnectez le numéro WhatsApp.", + "skipAiContextLabel": "L'IA ignore l'historique synchronisé", + "skipAiContextHelper": "Lorsque cette option est activée, l'IA ignore les messages importés par cette synchronisation et ne prend en compte que les nouveaux messages.", "success": { "enabled": "Synchronisation démarrée. Les anciens contacts et messages apparaîtront prochainement.", "disabled": "Synchronisation désactivée." diff --git a/apps/builder/messages/he.json b/apps/builder/messages/he.json index 5bef2bb0f..9a0d84d92 100644 --- a/apps/builder/messages/he.json +++ b/apps/builder/messages/he.json @@ -1319,6 +1319,8 @@ "toggleHelper": "הפעלה מחדש פועלת רק אם Meta שמרה נתונים במאגר (בתוך 24 שעות מההצטרפות). אחרת יש לנתק ולחבר מחדש.", "toggleHelperMessenger": "הפעלה מחדש פועלת רק אם Meta שמרה נתונים במאגר (בתוך 24 שעות מההצטרפות). אחרת יש לנתק ולחבר מחדש את דף Messenger.", "toggleHelperWhatsapp": "הפעלה מחדש פועלת רק אם Meta שמרה נתונים במאגר (בתוך 24 שעות מההצטרפות). אחרת יש לנתק ולחבר מחדש את מספר WhatsApp.", + "skipAiContextLabel": "ה-AI מדלג על היסטוריה מסונכרנת", + "skipAiContextHelper": "כאשר האפשרות פעילה, ה-AI מתעלם מהודעות שיובאו על ידי סנכרון זה ומתחשב רק בהודעות חדשות.", "success": { "enabled": "הסנכרון התחיל. אנשי קשר והודעות היסטוריים יופיעו בקרוב.", "disabled": "הסנכרון הושבת." diff --git a/apps/builder/messages/id.json b/apps/builder/messages/id.json index 5598f79a4..ed5c6d539 100644 --- a/apps/builder/messages/id.json +++ b/apps/builder/messages/id.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Pengaktifan kembali hanya berfungsi jika Meta masih menyimpan data (dalam 24 jam sejak orientasi). Jika tidak, putuskan lalu hubungkan kembali.", "toggleHelperMessenger": "Pengaktifan kembali hanya berfungsi jika Meta masih menyimpan data (dalam 24 jam sejak orientasi). Jika tidak, putuskan lalu hubungkan kembali halaman Messenger.", "toggleHelperWhatsapp": "Pengaktifan kembali hanya berfungsi jika Meta masih menyimpan data (dalam 24 jam sejak orientasi). Jika tidak, putuskan lalu hubungkan kembali nomor WhatsApp.", + "skipAiContextLabel": "AI melewati riwayat sinkronisasi", + "skipAiContextHelper": "Jika diaktifkan, AI akan mengabaikan pesan yang diimpor oleh sinkronisasi ini dan hanya mempertimbangkan pesan baru.", "success": { "enabled": "Sinkronisasi dimulai. Kontak dan pesan historis akan segera muncul.", "disabled": "Sinkronisasi dinonaktifkan." diff --git a/apps/builder/messages/it.json b/apps/builder/messages/it.json index 442a5a5b5..cc83b9b86 100644 --- a/apps/builder/messages/it.json +++ b/apps/builder/messages/it.json @@ -2945,6 +2945,8 @@ "toggleHelper": "La riattivazione funziona solo se Meta dispone ancora dei dati nel buffer (entro 24 ore dall'onboarding). In caso contrario, disconnetti e riconnetti.", "toggleHelperMessenger": "La riattivazione funziona solo se Meta dispone ancora dei dati nel buffer (entro 24 ore dall'onboarding). In caso contrario, disconnetti e riconnetti la Pagina Messenger.", "toggleHelperWhatsapp": "La riattivazione funziona solo se Meta dispone ancora dei dati nel buffer (entro 24 ore dall'onboarding). In caso contrario, disconnetti e riconnetti il numero WhatsApp.", + "skipAiContextLabel": "L'IA ignora la cronologia sincronizzata", + "skipAiContextHelper": "Se attivo, l'IA ignora i messaggi importati da questa sincronizzazione e considera solo i nuovi messaggi.", "success": { "enabled": "Sincronizzazione avviata. I contatti e i messaggi storici appariranno a breve.", "disabled": "Sincronizzazione disattivata." diff --git a/apps/builder/messages/ja.json b/apps/builder/messages/ja.json index 553002ff0..51d0d88f4 100644 --- a/apps/builder/messages/ja.json +++ b/apps/builder/messages/ja.json @@ -2929,6 +2929,8 @@ "toggleHelper": "再有効化できるのは、Metaにバッファーデータが残っている場合(オンボーディング後24時間以内)のみです。それ以外の場合は切断して再接続してください。", "toggleHelperMessenger": "再有効化できるのは、Metaにバッファーデータが残っている場合(オンボーディング後24時間以内)のみです。それ以外の場合はMessengerページを切断して再接続してください。", "toggleHelperWhatsapp": "再有効化できるのは、Metaにバッファーデータが残っている場合(オンボーディング後24時間以内)のみです。それ以外の場合はWhatsApp番号を切断して再接続してください。", + "skipAiContextLabel": "AIは同期履歴をスキップ", + "skipAiContextHelper": "オンにすると、AIはこの同期でインポートされたメッセージを無視し、新しいメッセージのみを考慮します。", "success": { "enabled": "同期を開始しました。過去の連絡先とメッセージはまもなく表示されます。", "disabled": "同期を無効にしました。" diff --git a/apps/builder/messages/nl.json b/apps/builder/messages/nl.json index f53ca23bc..1acd7a3c4 100644 --- a/apps/builder/messages/nl.json +++ b/apps/builder/messages/nl.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Opnieuw inschakelen werkt alleen als Meta gegevens heeft gebufferd (binnen 24 uur na onboarding). Koppel anders los en opnieuw.", "toggleHelperMessenger": "Opnieuw inschakelen werkt alleen als Meta gegevens heeft gebufferd (binnen 24 uur na onboarding). Koppel anders de Messenger-pagina los en opnieuw.", "toggleHelperWhatsapp": "Opnieuw inschakelen werkt alleen als Meta gegevens heeft gebufferd (binnen 24 uur na onboarding). Koppel anders het WhatsApp-nummer los en opnieuw.", + "skipAiContextLabel": "AI slaat gesynchroniseerde geschiedenis over", + "skipAiContextHelper": "Indien ingeschakeld, negeert de AI berichten die door deze synchronisatie zijn geïmporteerd en houdt alleen rekening met nieuwe berichten.", "success": { "enabled": "Synchronisatie gestart. Historische contacten en berichten verschijnen binnenkort.", "disabled": "Synchronisatie uitgeschakeld." diff --git a/apps/builder/messages/pt-BR.json b/apps/builder/messages/pt-BR.json index 22bf314b1..30403f1c4 100644 --- a/apps/builder/messages/pt-BR.json +++ b/apps/builder/messages/pt-BR.json @@ -2929,6 +2929,8 @@ "toggleHelper": "A reativação só funciona se a Meta tiver dados armazenados em buffer (dentro de 24 horas após a integração). Caso contrário, desconecte e reconecte.", "toggleHelperMessenger": "A reativação só funciona se a Meta tiver dados armazenados em buffer (dentro de 24 horas após a integração). Caso contrário, desconecte e reconecte a página do Messenger.", "toggleHelperWhatsapp": "A reativação só funciona se a Meta tiver dados armazenados em buffer (dentro de 24 horas após a integração). Caso contrário, desconecte e reconecte o número do WhatsApp.", + "skipAiContextLabel": "A IA ignora o histórico sincronizado", + "skipAiContextHelper": "Quando ativado, a IA ignora as mensagens importadas por esta sincronização e considera apenas as novas mensagens.", "success": { "enabled": "Sincronização iniciada. Os contatos e as mensagens do histórico aparecerão em breve.", "disabled": "Sincronização desativada." diff --git a/apps/builder/messages/pt-PT.json b/apps/builder/messages/pt-PT.json index e2b1605ae..f42ccadcb 100644 --- a/apps/builder/messages/pt-PT.json +++ b/apps/builder/messages/pt-PT.json @@ -2945,6 +2945,8 @@ "toggleHelper": "A reativação só funciona se a Meta tiver dados em memória intermédia (nas 24 horas após a integração). Caso contrário, desligue e volte a ligar.", "toggleHelperMessenger": "A reativação só funciona se a Meta tiver dados em memória intermédia (nas 24 horas após a integração). Caso contrário, desligue e volte a ligar a página do Messenger.", "toggleHelperWhatsapp": "A reativação só funciona se a Meta tiver dados em memória intermédia (nas 24 horas após a integração). Caso contrário, desligue e volte a ligar o número do WhatsApp.", + "skipAiContextLabel": "A IA ignora o histórico sincronizado", + "skipAiContextHelper": "Quando ativado, a IA ignora as mensagens importadas por esta sincronização e considera apenas as mensagens novas.", "success": { "enabled": "Sincronização iniciada. Os contactos e as mensagens do histórico aparecerão em breve.", "disabled": "Sincronização desativada." diff --git a/apps/builder/messages/ro.json b/apps/builder/messages/ro.json index 218995afc..d68a04f85 100644 --- a/apps/builder/messages/ro.json +++ b/apps/builder/messages/ro.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Reactivarea funcționează doar dacă Meta are date puse în tampon (în 24 de ore de la conectare). În caz contrar, deconectează și reconectează.", "toggleHelperMessenger": "Reactivarea funcționează doar dacă Meta are date puse în tampon (în 24 de ore de la conectare). În caz contrar, deconectează și reconectează pagina Messenger.", "toggleHelperWhatsapp": "Reactivarea funcționează doar dacă Meta are date puse în tampon (în 24 de ore de la conectare). În caz contrar, deconectează și reconectează numărul WhatsApp.", + "skipAiContextLabel": "AI ignoră istoricul sincronizat", + "skipAiContextHelper": "Când este activat, AI-ul ignoră mesajele importate prin această sincronizare și ia în considerare doar mesajele noi.", "success": { "enabled": "Sincronizarea a început. Contactele și mesajele istorice vor apărea în curând.", "disabled": "Sincronizare dezactivată." diff --git a/apps/builder/messages/sv.json b/apps/builder/messages/sv.json index ed3f4b918..8cb14d65f 100644 --- a/apps/builder/messages/sv.json +++ b/apps/builder/messages/sv.json @@ -610,6 +610,8 @@ "toggleHelper": "Återaktivering fungerar endast om Meta har buffrad data (inom 24 timmar efter introduktionen). Annars behöver du koppla från och återansluta.", "toggleHelperMessenger": "Återaktivering fungerar endast om Meta har buffrad data (inom 24 timmar efter introduktionen). Annars behöver du koppla från och återansluta Messenger-sidan.", "toggleHelperWhatsapp": "Återaktivering fungerar endast om Meta har buffrad data (inom 24 timmar efter introduktionen). Annars behöver du koppla från och återansluta WhatsApp-numret.", + "skipAiContextLabel": "AI hoppar över synkad historik", + "skipAiContextHelper": "När det är aktiverat ignorerar AI:n meddelanden som importerats av denna synkronisering och tar endast hänsyn till nya meddelanden.", "toggleLabel": "Synkronisera befintliga kontakter och chatthistorik" }, "condition": { diff --git a/apps/builder/messages/tr.json b/apps/builder/messages/tr.json index b79bcd3db..a6b97def6 100644 --- a/apps/builder/messages/tr.json +++ b/apps/builder/messages/tr.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Yeniden etkinleştirme yalnızca Meta'da arabelleğe alınmış veri varsa (katılımdan itibaren 24 saat içinde) çalışır. Aksi halde bağlantıyı kesip yeniden bağlayın.", "toggleHelperMessenger": "Yeniden etkinleştirme yalnızca Meta'da arabelleğe alınmış veri varsa (katılımdan itibaren 24 saat içinde) çalışır. Aksi halde Messenger sayfasının bağlantısını kesip yeniden bağlayın.", "toggleHelperWhatsapp": "Yeniden etkinleştirme yalnızca Meta'da arabelleğe alınmış veri varsa (katılımdan itibaren 24 saat içinde) çalışır. Aksi halde WhatsApp numarasının bağlantısını kesip yeniden bağlayın.", + "skipAiContextLabel": "AI senkronize geçmişi atlar", + "skipAiContextHelper": "Açıkken, yapay zeka bu senkronizasyonla içe aktarılan mesajları yok sayar ve yalnızca yeni mesajları dikkate alır.", "success": { "enabled": "Senkronizasyon başlatıldı. Geçmiş kişiler ve mesajlar kısa süre içinde görünecek.", "disabled": "Senkronizasyon devre dışı bırakıldı." diff --git a/apps/builder/messages/vi.json b/apps/builder/messages/vi.json index 2a7c959ee..acc55f6f1 100644 --- a/apps/builder/messages/vi.json +++ b/apps/builder/messages/vi.json @@ -2945,6 +2945,8 @@ "toggleHelper": "Bật lại chỉ hoạt động nếu Meta còn đệm dữ liệu (trong vòng 24 giờ kể từ khi onboarding). Nếu không, hãy ngắt kết nối và kết nối lại.", "toggleHelperMessenger": "Bật lại chỉ hoạt động nếu Meta còn đệm dữ liệu (trong vòng 24 giờ kể từ khi onboarding). Nếu không, hãy ngắt kết nối và kết nối lại trang Messenger.", "toggleHelperWhatsapp": "Bật lại chỉ hoạt động nếu Meta còn đệm dữ liệu (trong vòng 24 giờ kể từ khi onboarding). Nếu không, hãy ngắt kết nối và kết nối lại số WhatsApp.", + "skipAiContextLabel": "AI bỏ qua lịch sử đồng bộ", + "skipAiContextHelper": "Khi bật, AI bỏ qua tin nhắn được nhập từ lần đồng bộ này và chỉ xét tin nhắn mới.", "success": { "enabled": "Đã bắt đầu đồng bộ. Liên hệ và tin nhắn lịch sử sẽ xuất hiện sớm.", "disabled": "Đã tắt đồng bộ." diff --git a/apps/builder/messages/zh-CN.json b/apps/builder/messages/zh-CN.json index a875cf8eb..3685bfd35 100644 --- a/apps/builder/messages/zh-CN.json +++ b/apps/builder/messages/zh-CN.json @@ -775,6 +775,8 @@ "toggleHelper": "仅当Meta 有缓冲数据时(启动后 24 小时内),重新激活才有效。否则断开并重新连接。", "toggleHelperMessenger": "仅当Meta 有缓冲数据时(启动后 24 小时内),重新激活才有效。否则断开并重新连接Messenger页面。", "toggleHelperWhatsapp": "仅当Meta 有缓冲数据时(启动后 24 小时内),重新激活才有效。否则,请断开并重新连接WhatsApp号码。", + "skipAiContextLabel": "AI 跳过同步历史", + "skipAiContextHelper": "开启后,AI 将忽略此次同步导入的消息,仅考虑新消息。", "toggleLabel": "同步现有联系人与聊天记录" }, "condition": { diff --git a/apps/builder/messages/zh-TW.json b/apps/builder/messages/zh-TW.json index 22229e7ab..aaebb82d0 100644 --- a/apps/builder/messages/zh-TW.json +++ b/apps/builder/messages/zh-TW.json @@ -2978,6 +2978,8 @@ "toggleHelper": "僅當Meta 有緩衝資料時(啟動後 24 小時內),重新啟用才有效。否則斷開並重新連接。", "toggleHelperMessenger": "僅當Meta 有緩衝資料時(啟動後 24 小時內),重新啟用才有效。否則斷開並重新連接Messenger頁面。", "toggleHelperWhatsapp": "僅當Meta 有緩衝資料時(啟動後 24 小時內),重新啟用才有效。否則,請斷開並重新連接WhatsApp號碼。", + "skipAiContextLabel": "AI 略過同步歷史", + "skipAiContextHelper": "開啟後,AI 將忽略此次同步匯入的訊息,僅考慮新訊息。", "success": { "enabled": "同步已開始。歷史聯絡人和訊息很快就會出現。", "disabled": "同步已停用。" diff --git a/apps/builder/src/features/integration-instagram/api/coexist.ts b/apps/builder/src/features/integration-instagram/api/coexist.ts index 067666d50..e3fe37aea 100644 --- a/apps/builder/src/features/integration-instagram/api/coexist.ts +++ b/apps/builder/src/features/integration-instagram/api/coexist.ts @@ -7,6 +7,7 @@ const setCoexistInstagramRequest = z.object({ workspaceId: z.string(), integrationId: z.string(), enabled: z.boolean(), + skipAiContext: z.boolean().optional().default(false), }) export type SetCoexistInstagramRequest = z.infer< typeof setCoexistInstagramRequest @@ -41,6 +42,7 @@ export const integrationInstagramCoexistAPIs = { workspaceId: input.workspaceId, integrationId: input.integrationId, channel: "instagram", + skipAiContext: input.skipAiContext, }) : coexistService.disable({ workspaceId: input.workspaceId, diff --git a/apps/builder/src/features/integration-messenger/api/coexist.ts b/apps/builder/src/features/integration-messenger/api/coexist.ts index 280e83c0f..dade1b08f 100644 --- a/apps/builder/src/features/integration-messenger/api/coexist.ts +++ b/apps/builder/src/features/integration-messenger/api/coexist.ts @@ -7,6 +7,7 @@ const setCoexistMessengerRequest = z.object({ workspaceId: z.string(), integrationId: z.string(), enabled: z.boolean(), + skipAiContext: z.boolean().optional().default(false), }) export type SetCoexistMessengerRequest = z.infer< typeof setCoexistMessengerRequest @@ -36,12 +37,13 @@ export const integrationMessengerCoexistAPIs = { .output(setCoexistMessengerResponse) .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) .handler(async ({ input }) => { - const { workspaceId, integrationId, enabled } = input + const { workspaceId, integrationId, enabled, skipAiContext } = input return await (enabled ? coexistService.enable({ workspaceId, integrationId, channel: "messenger", + skipAiContext, }) : coexistService.disable({ workspaceId, diff --git a/apps/builder/src/features/integration-whatsapp/api/coexist.ts b/apps/builder/src/features/integration-whatsapp/api/coexist.ts index dddf05ba8..925393893 100644 --- a/apps/builder/src/features/integration-whatsapp/api/coexist.ts +++ b/apps/builder/src/features/integration-whatsapp/api/coexist.ts @@ -15,6 +15,7 @@ const setCoexistWhatsappRequest = z.object({ workspaceId: z.string(), integrationId: z.string(), enabled: z.boolean(), + skipAiContext: z.boolean().optional().default(false), }) export type SetCoexistWhatsappRequest = z.infer< typeof setCoexistWhatsappRequest @@ -52,14 +53,21 @@ export const integrationWhatsappCoexistAPIs = { .output(setCoexistWhatsappResponse) .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) .handler(async ({ input }) => { - const { workspaceId, integrationId, enabled } = input + const { workspaceId, integrationId, enabled, skipAiContext } = input // Atomic UPDATE + RETURNING gates on workspaceId (tenancy) and yields // phoneNumberId + auth in one round trip — no separate findFirst, no // race window between flag write and queue enqueue. + // + // `coexistSkipAiContext` is set ONLY when enabling — the popup also + // POSTs on decline (`enabled: false`), and this UPDATE runs before the + // `if (enabled)` branch below, so a decline must never write it. const [updated] = await db .update(integrationWhatsappModel) - .set({ coexistEnabled: enabled }) + .set({ + coexistEnabled: enabled, + ...(enabled ? { coexistSkipAiContext: skipAiContext } : {}), + }) .where( and( eq(integrationWhatsappModel.id, integrationId), diff --git a/apps/builder/src/features/shared/coexist-popup.tsx b/apps/builder/src/features/shared/coexist-popup.tsx index 86f601a96..530ca943b 100644 --- a/apps/builder/src/features/shared/coexist-popup.tsx +++ b/apps/builder/src/features/shared/coexist-popup.tsx @@ -9,6 +9,7 @@ import { DialogHeader, DialogTitle, } from "@chatbotx.io/ui/components/ui/dialog" +import { Switch } from "@chatbotx.io/ui/components/ui/switch" import ky from "ky" import { Loader2Icon } from "lucide-react" import { useTranslations } from "next-intl" @@ -65,6 +66,9 @@ export function CoexistPopup({ }: CoexistPopupProps) { const t = useTranslations() const [pending, setPending] = useState<"enable" | "decline" | null>(null) + // Default OFF: coexist-synced history feeds the AI unless explicitly opted + // out. + const [skipAiContext, setSkipAiContext] = useState(false) const handleChoice = async (enabled: boolean) => { setPending(enabled ? "enable" : "decline") @@ -72,7 +76,7 @@ export function CoexistPopup({ const endpoint = `/api/workspaces/${workspaceId}/integrations/${channel}/${integrationId}/coexist` const result = await ky .post(endpoint, { - json: { workspaceId, integrationId, enabled }, + json: { workspaceId, integrationId, enabled, skipAiContext }, }) .json() @@ -125,6 +129,22 @@ export function CoexistPopup({ {t("coexist.billingNote")}

+
+
+ + + {t("coexist.skipAiContextLabel")} + +
+

+ {t("coexist.skipAiContextHelper")} +

+
+