diff --git a/apps/builder/__tests__/coexist-messenger-api.test.ts b/apps/builder/__tests__/coexist-messenger-api.test.ts index 17dced017..e365ef312 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 aiReadsSyncedHistory 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", + aiReadsSyncedHistory: false, }) expect(mockDisable).not.toHaveBeenCalled() }) + test("Messenger enabled:true, aiReadsSyncedHistory:true delegates it through to coexistService.enable", async () => { + const result = await call( + integrationMessengerCoexistAPIs.setCoexistMessengerAPI, + { + workspaceId: "ws-1", + integrationId: "int-1", + enabled: true, + aiReadsSyncedHistory: true, + }, + { context: stubContext }, + ) + + expect(result).toEqual({ success: true }) + expect(mockEnable).toHaveBeenCalledWith({ + workspaceId: "ws-1", + integrationId: "int-1", + channel: "messenger", + aiReadsSyncedHistory: 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 aiReadsSyncedHistory 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", + aiReadsSyncedHistory: 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..c3aa8654f --- /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 AI-reads-synced-history switch defaulted OFF (AI ignores synced history by default)", () => { + expect(switchEl()).not.toBeNull() + expect(switchEl()?.getAttribute("aria-checked")).toBe("false") + }) + + test("POSTs aiReadsSyncedHistory: 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, + aiReadsSyncedHistory: false, + }), + }), + ) + }) + + test("POSTs aiReadsSyncedHistory: 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({ aiReadsSyncedHistory: true }), + }), + ) + }) + + test("POSTs aiReadsSyncedHistory 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, + aiReadsSyncedHistory: true, + }), + }), + ) + }) +}) diff --git a/apps/builder/__tests__/coexist-whatsapp-api.test.ts b/apps/builder/__tests__/coexist-whatsapp-api.test.ts index 989999b86..f4446596b 100644 --- a/apps/builder/__tests__/coexist-whatsapp-api.test.ts +++ b/apps/builder/__tests__/coexist-whatsapp-api.test.ts @@ -167,7 +167,12 @@ 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 }) + // aiReadsSyncedHistory defaults to false (zod default) and is written on + // enable — the default advances the AI marker (AI ignores synced history). + expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ + coexistEnabled: true, + coexistAiReadsSyncedHistory: false, + }) // CoexistSyncRun insert with the init-row values. expect(db.insert).toHaveBeenCalledTimes(1) @@ -188,6 +193,25 @@ describe("setCoexistWhatsappAPI", () => { expect(mockQueueAdd).not.toHaveBeenCalled() }) + test("enabled:true, aiReadsSyncedHistory:true — writes coexistAiReadsSyncedHistory alongside coexistEnabled", async () => { + const result = await call( + procedure, + { + workspaceId: "ws-1", + integrationId: "int-1", + enabled: true, + aiReadsSyncedHistory: true, + }, + { context: stubContext }, + ) + + expect(result).toEqual({ success: true }) + expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ + coexistEnabled: true, + coexistAiReadsSyncedHistory: true, + }) + }) + test("enabled:false — flag-only: flips coexistEnabled, no run insert, no delete, no job", async () => { const result = await call( procedure, @@ -208,6 +232,24 @@ describe("setCoexistWhatsappAPI", () => { expect(mockQueueAdd).not.toHaveBeenCalled() }) + test("enabled:false (decline) — never writes coexistAiReadsSyncedHistory, even when the popup sent aiReadsSyncedHistory:true", async () => { + const result = await call( + procedure, + { + workspaceId: "ws-1", + integrationId: "int-1", + enabled: false, + aiReadsSyncedHistory: true, + }, + { context: stubContext }, + ) + + expect(result).toEqual({ success: true }) + // The UPDATE runs BEFORE the `if (enabled)` branch — decline must never + // write coexistAiReadsSyncedHistory, 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 +336,40 @@ describe("i18n key presence (H12)", () => { ).toBeDefined() }) + test("coexist.aiReadsSyncedHistoryLabel is defined in en.json and vi.json", () => { + expect( + ( + enMessages as unknown as Record & { + coexist: Record + } + ).coexist.aiReadsSyncedHistoryLabel, + ).toBeDefined() + expect( + ( + viMessages as unknown as Record & { + coexist: Record + } + ).coexist.aiReadsSyncedHistoryLabel, + ).toBeDefined() + }) + + test("coexist.aiReadsSyncedHistoryHelper is defined in en.json and vi.json", () => { + expect( + ( + enMessages as unknown as Record & { + coexist: Record + } + ).coexist.aiReadsSyncedHistoryHelper, + ).toBeDefined() + expect( + ( + viMessages as unknown as Record & { + coexist: Record + } + ).coexist.aiReadsSyncedHistoryHelper, + ).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..f26166af0 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 ثم أعده.", + "aiReadsSyncedHistoryLabel": "يقرأ الذكاء الاصطناعي السجل المتزامن", + "aiReadsSyncedHistoryHelper": "عند الإيقاف، لا يرى الذكاء الاصطناعي سوى الرسائل الجديدة.", "success": { "enabled": "بدأت المزامنة. ستظهر جهات الاتصال والرسائل السابقة قريبًا.", "disabled": "تم تعطيل المزامنة." diff --git a/apps/builder/messages/da.json b/apps/builder/messages/da.json index 49638c773..eeee3a853 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.", + "aiReadsSyncedHistoryLabel": "AI læser synket historik", + "aiReadsSyncedHistoryHelper": "Når slået fra, ser AI 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..a36f78b88 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.", + "aiReadsSyncedHistoryLabel": "KI liest Synchronverlauf", + "aiReadsSyncedHistoryHelper": "Wenn aus, sieht die KI 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..a2425edec 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.", + "aiReadsSyncedHistoryLabel": "AI reads synced history", + "aiReadsSyncedHistoryHelper": "When off, AI only sees 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..0e8fb24a8 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.", + "aiReadsSyncedHistoryLabel": "La IA lee el historial sincronizado", + "aiReadsSyncedHistoryHelper": "Si está desactivado, la IA solo ve 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..b35071050 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.", + "aiReadsSyncedHistoryLabel": "Tekoäly lukee synkronoidun historian", + "aiReadsSyncedHistoryHelper": "Pois päältä: tekoäly näkee 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..65a86cfec 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.", + "aiReadsSyncedHistoryLabel": "L'IA lit l'historique synchronisé", + "aiReadsSyncedHistoryHelper": "Si désactivé, l'IA ne voit 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..74ba696af 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.", + "aiReadsSyncedHistoryLabel": "ה-AI קורא היסטוריה מסונכרנת", + "aiReadsSyncedHistoryHelper": "כשכבוי, ה-AI רואה רק הודעות חדשות.", "success": { "enabled": "הסנכרון התחיל. אנשי קשר והודעות היסטוריים יופיעו בקרוב.", "disabled": "הסנכרון הושבת." diff --git a/apps/builder/messages/id.json b/apps/builder/messages/id.json index 5598f79a4..1015ca3c1 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.", + "aiReadsSyncedHistoryLabel": "AI membaca riwayat sinkronisasi", + "aiReadsSyncedHistoryHelper": "Saat nonaktif, AI hanya melihat 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..8bfcad5b1 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.", + "aiReadsSyncedHistoryLabel": "L'IA legge la cronologia sincronizzata", + "aiReadsSyncedHistoryHelper": "Se disattivo, l'IA vede 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..3b4766959 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番号を切断して再接続してください。", + "aiReadsSyncedHistoryLabel": "AIが同期履歴を読む", + "aiReadsSyncedHistoryHelper": "オフの場合、AIは新しいメッセージのみを見ます。", "success": { "enabled": "同期を開始しました。過去の連絡先とメッセージはまもなく表示されます。", "disabled": "同期を無効にしました。" diff --git a/apps/builder/messages/nl.json b/apps/builder/messages/nl.json index f53ca23bc..bead16b10 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.", + "aiReadsSyncedHistoryLabel": "AI leest gesynchroniseerde geschiedenis", + "aiReadsSyncedHistoryHelper": "Indien uit, ziet de AI alleen 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..37e118777 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.", + "aiReadsSyncedHistoryLabel": "A IA lê o histórico sincronizado", + "aiReadsSyncedHistoryHelper": "Quando desativado, a IA vê apenas mensagens novas.", "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..74d9b7256 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.", + "aiReadsSyncedHistoryLabel": "A IA lê o histórico sincronizado", + "aiReadsSyncedHistoryHelper": "Quando desativado, a IA vê apenas 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..3558c4e67 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.", + "aiReadsSyncedHistoryLabel": "AI citește istoricul sincronizat", + "aiReadsSyncedHistoryHelper": "Când este oprit, AI vede 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..923f22f8c 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.", + "aiReadsSyncedHistoryLabel": "AI läser synkad historik", + "aiReadsSyncedHistoryHelper": "När av ser AI endast 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..b2dbde64b 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.", + "aiReadsSyncedHistoryLabel": "AI eşitlenen geçmişi okur", + "aiReadsSyncedHistoryHelper": "Kapalıyken AI yalnızca yeni mesajları görü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..d907d3674 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.", + "aiReadsSyncedHistoryLabel": "AI đọc lịch sử đồng bộ", + "aiReadsSyncedHistoryHelper": "Khi tắt, AI 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..b4e0abe68 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号码。", + "aiReadsSyncedHistoryLabel": "AI 读取同步历史", + "aiReadsSyncedHistoryHelper": "关闭时,AI 仅查看新消息。", "toggleLabel": "同步现有联系人与聊天记录" }, "condition": { diff --git a/apps/builder/messages/zh-TW.json b/apps/builder/messages/zh-TW.json index 22229e7ab..a9fd4a3dd 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號碼。", + "aiReadsSyncedHistoryLabel": "AI 讀取同步歷史", + "aiReadsSyncedHistoryHelper": "關閉時,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..8b7eab0ee 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(), + aiReadsSyncedHistory: 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", + aiReadsSyncedHistory: input.aiReadsSyncedHistory, }) : 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..8b54b5df8 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(), + aiReadsSyncedHistory: z.boolean().optional().default(false), }) export type SetCoexistMessengerRequest = z.infer< typeof setCoexistMessengerRequest @@ -36,12 +37,14 @@ export const integrationMessengerCoexistAPIs = { .output(setCoexistMessengerResponse) .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) .handler(async ({ input }) => { - const { workspaceId, integrationId, enabled } = input + const { workspaceId, integrationId, enabled, aiReadsSyncedHistory } = + input return await (enabled ? coexistService.enable({ workspaceId, integrationId, channel: "messenger", + aiReadsSyncedHistory, }) : 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..8e1747fa7 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(), + aiReadsSyncedHistory: z.boolean().optional().default(false), }) export type SetCoexistWhatsappRequest = z.infer< typeof setCoexistWhatsappRequest @@ -52,14 +53,24 @@ export const integrationWhatsappCoexistAPIs = { .output(setCoexistWhatsappResponse) .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) .handler(async ({ input }) => { - const { workspaceId, integrationId, enabled } = input + const { workspaceId, integrationId, enabled, aiReadsSyncedHistory } = + 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. + // + // `coexistAiReadsSyncedHistory` 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 + ? { coexistAiReadsSyncedHistory: aiReadsSyncedHistory } + : {}), + }) .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..2dd778e40 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: the AI ignores coexist-synced history (the marker advances) + // unless the user explicitly opts in. + const [aiReadsSyncedHistory, setAiReadsSyncedHistory] = 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, aiReadsSyncedHistory }, }) .json() @@ -125,6 +129,22 @@ export function CoexistPopup({ {t("coexist.billingNote")}

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

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

+
+