@adatechnology/conversations-ui 0.1.0-rc.21 → 0.1.0-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/preview/index.d.ts +23 -1
- package/dist/preview/index.js +48 -14
- package/dist/styles.css +82 -0
- package/package.json +1 -1
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +89 -0
- package/src/preview/index.ts +3 -0
- package/src/styles.css +85 -0
package/dist/preview/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { G as MessagePayload, o as ConversationSummary, m as ConversationEventSource, q as ConversationsApi, L as ListConversationsParams, n as ConversationPage, S as SSEProvider, k as ConversationDocument, N as ResolveMediaUrl } from '../types-C6A_9edv.js';
|
|
2
2
|
export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../types-C6A_9edv.js';
|
|
3
3
|
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
4
5
|
import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
|
|
5
6
|
export { PREVIEW_MEDIA_ID_PREFIX } from '@adatechnology/meta-whatsapp-contracts';
|
|
6
7
|
|
|
@@ -296,6 +297,27 @@ type PreviewUploadedMedia = {
|
|
|
296
297
|
declare function mediaTypeOf(mimeType: string): SendPreviewMediaParams['mediaType'];
|
|
297
298
|
declare function ConversationPreview({ client, sse, conversationId, loadMessages, placeholder, pollIntervalMs, uploadMedia, }: ConversationPreviewProps): react.JSX.Element;
|
|
298
299
|
|
|
300
|
+
type ConversationSimulatorPanelLabels = {
|
|
301
|
+
readonly title: string;
|
|
302
|
+
/** Complementa o telefone no subtítulo, explicando para onde a mensagem realmente vai. */
|
|
303
|
+
readonly destinationHint: string;
|
|
304
|
+
readonly close: string;
|
|
305
|
+
readonly placeholder: string;
|
|
306
|
+
};
|
|
307
|
+
declare const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels;
|
|
308
|
+
type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
|
|
309
|
+
readonly onClose: () => void;
|
|
310
|
+
/**
|
|
311
|
+
* Telefone já formatado para leitura. É o host que formata: máscara de telefone é convenção
|
|
312
|
+
* regional, e o pacote não tem como saber a do produto.
|
|
313
|
+
*/
|
|
314
|
+
readonly displayNumber?: string;
|
|
315
|
+
readonly labels?: Partial<ConversationSimulatorPanelLabels>;
|
|
316
|
+
/** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
|
|
317
|
+
readonly headerActions?: ReactNode;
|
|
318
|
+
};
|
|
319
|
+
declare function ConversationSimulatorPanel({ onClose, displayNumber, labels, headerActions, ...previewProps }: ConversationSimulatorPanelProps): react.JSX.Element;
|
|
320
|
+
|
|
299
321
|
/**
|
|
300
322
|
* Cliente do preview que NÃO carrega segredo: em vez de montar e assinar o payload da Meta no
|
|
301
323
|
* navegador, manda um comando semântico (`{ kind: 'text', text }`) para uma rota do próprio host,
|
|
@@ -467,4 +489,4 @@ type MediaTypesPreviewProps = {
|
|
|
467
489
|
};
|
|
468
490
|
declare function MediaTypesPreview({ conversationId, className, }: MediaTypesPreviewProps): react.JSX.Element;
|
|
469
491
|
|
|
470
|
-
export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewBridgeClientParams, type CreatePreviewMediaUploaderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_MEDIA_UPLOAD_PATH, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, MEDIA_TYPES_CONVERSATION_ID, MediaTypesPreview, type MediaTypesPreviewProps, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_DOCUMENTS, PREVIEW_FILE_SAMPLES, PREVIEW_MESSAGES, PreviewBridgeRejectedError, type PreviewEmission, PreviewInProductionError, type PreviewInboundCommand, PreviewMediaUploadRejectedError, type PreviewMediaUploadRequest, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewUploadedMedia, type PreviewWebhookClient, PreviewWebhookRejectedError, type SendPreviewInboundCommand, type SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewBridgeClient, createPreviewMediaPoster, createPreviewMediaResolver, createPreviewMediaUploader, createPreviewStore, createPreviewWebhookClient, mediaTypeOf, previewFileBase64, previewFileUrl, resolvePreviewFileSample, signPreviewPayload, startPreviewScript };
|
|
492
|
+
export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, ConversationSimulatorPanel, type ConversationSimulatorPanelLabels, type ConversationSimulatorPanelProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewBridgeClientParams, type CreatePreviewMediaUploaderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, DEFAULT_MEDIA_UPLOAD_PATH, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, MEDIA_TYPES_CONVERSATION_ID, MediaTypesPreview, type MediaTypesPreviewProps, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_DOCUMENTS, PREVIEW_FILE_SAMPLES, PREVIEW_MESSAGES, PreviewBridgeRejectedError, type PreviewEmission, PreviewInProductionError, type PreviewInboundCommand, PreviewMediaUploadRejectedError, type PreviewMediaUploadRequest, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewUploadedMedia, type PreviewWebhookClient, PreviewWebhookRejectedError, type SendPreviewInboundCommand, type SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewBridgeClient, createPreviewMediaPoster, createPreviewMediaResolver, createPreviewMediaUploader, createPreviewStore, createPreviewWebhookClient, mediaTypeOf, previewFileBase64, previewFileUrl, resolvePreviewFileSample, signPreviewPayload, startPreviewScript };
|
package/dist/preview/index.js
CHANGED
|
@@ -1034,6 +1034,38 @@ function ConversationPreview({
|
|
|
1034
1034
|
] });
|
|
1035
1035
|
}
|
|
1036
1036
|
|
|
1037
|
+
// src/preview/ConversationSimulatorPanel.tsx
|
|
1038
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1039
|
+
var DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS = {
|
|
1040
|
+
title: "Simulador do cliente",
|
|
1041
|
+
destinationHint: "entrega no webhook real",
|
|
1042
|
+
close: "Fechar simulador",
|
|
1043
|
+
placeholder: "Escreva como o cliente\u2026"
|
|
1044
|
+
};
|
|
1045
|
+
function ConversationSimulatorPanel({
|
|
1046
|
+
onClose,
|
|
1047
|
+
displayNumber,
|
|
1048
|
+
labels,
|
|
1049
|
+
headerActions,
|
|
1050
|
+
...previewProps
|
|
1051
|
+
}) {
|
|
1052
|
+
const text = { ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, ...labels };
|
|
1053
|
+
const subtitle = [displayNumber ?? previewProps.conversationId, text.destinationHint].join(" \xB7 ");
|
|
1054
|
+
return /* @__PURE__ */ jsxs2("aside", { className: "cv-simulator-panel", "aria-label": text.title, children: [
|
|
1055
|
+
/* @__PURE__ */ jsxs2("header", { className: "cv-simulator-panel__header", children: [
|
|
1056
|
+
/* @__PURE__ */ jsxs2("div", { className: "cv-simulator-panel__heading", children: [
|
|
1057
|
+
/* @__PURE__ */ jsx2("h2", { className: "cv-simulator-panel__title", children: text.title }),
|
|
1058
|
+
/* @__PURE__ */ jsx2("p", { className: "cv-simulator-panel__subtitle", children: subtitle })
|
|
1059
|
+
] }),
|
|
1060
|
+
/* @__PURE__ */ jsxs2("div", { className: "cv-simulator-panel__actions", children: [
|
|
1061
|
+
headerActions,
|
|
1062
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose, title: text.close, "aria-label": text.close, className: "cv-simulator-panel__close", children: /* @__PURE__ */ jsx2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
|
|
1063
|
+
] })
|
|
1064
|
+
] }),
|
|
1065
|
+
/* @__PURE__ */ jsx2("div", { className: "cv-simulator-panel__body", children: /* @__PURE__ */ jsx2(ConversationPreview, { ...previewProps, placeholder: text.placeholder }) })
|
|
1066
|
+
] });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1037
1069
|
// src/preview/createPreviewWebhookClient.ts
|
|
1038
1070
|
import {
|
|
1039
1071
|
buildInboundAudioPayload,
|
|
@@ -1244,7 +1276,7 @@ function startPreviewScript(params) {
|
|
|
1244
1276
|
|
|
1245
1277
|
// src/preview/MediaTypesPreview.tsx
|
|
1246
1278
|
import { useMemo as useMemo2 } from "react";
|
|
1247
|
-
import { jsx as
|
|
1279
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1248
1280
|
var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
|
|
1249
1281
|
function MediaTypesPreview({
|
|
1250
1282
|
conversationId = MEDIA_TYPES_CONVERSATION_ID,
|
|
@@ -1259,26 +1291,26 @@ function MediaTypesPreview({
|
|
|
1259
1291
|
const messages = PREVIEW_MESSAGES[conversationId] ?? [];
|
|
1260
1292
|
const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
|
|
1261
1293
|
const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
|
|
1262
|
-
return /* @__PURE__ */
|
|
1263
|
-
/* @__PURE__ */
|
|
1264
|
-
/* @__PURE__ */
|
|
1265
|
-
/* @__PURE__ */
|
|
1294
|
+
return /* @__PURE__ */ jsx3(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs3("div", { className, children: [
|
|
1295
|
+
/* @__PURE__ */ jsxs3("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
|
|
1296
|
+
/* @__PURE__ */ jsx3("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
|
|
1297
|
+
/* @__PURE__ */ jsxs3("p", { className: "text-sm text-gray-500", children: [
|
|
1266
1298
|
documents.length,
|
|
1267
1299
|
" arquivos, ",
|
|
1268
1300
|
mimeTypes.length,
|
|
1269
1301
|
" tipos. Clique no olho para abrir em aba nova e no bot\xE3o da bolha para carregar a m\xEDdia na thread \u2014 \xE9 o que teste automatizado n\xE3o v\xEA."
|
|
1270
1302
|
] })
|
|
1271
1303
|
] }),
|
|
1272
|
-
/* @__PURE__ */
|
|
1273
|
-
/* @__PURE__ */
|
|
1274
|
-
/* @__PURE__ */
|
|
1275
|
-
/* @__PURE__ */
|
|
1304
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
|
|
1305
|
+
/* @__PURE__ */ jsxs3("section", { className: "space-y-3", children: [
|
|
1306
|
+
/* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
|
|
1307
|
+
/* @__PURE__ */ jsx3(DocumentsLibrary, { perPage: documents.length || 20 })
|
|
1276
1308
|
] }),
|
|
1277
|
-
/* @__PURE__ */
|
|
1278
|
-
/* @__PURE__ */
|
|
1279
|
-
/* @__PURE__ */
|
|
1280
|
-
/* @__PURE__ */
|
|
1281
|
-
/* @__PURE__ */
|
|
1309
|
+
/* @__PURE__ */ jsxs3("section", { className: "space-y-3", children: [
|
|
1310
|
+
/* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
|
|
1311
|
+
/* @__PURE__ */ jsx3(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
|
|
1312
|
+
/* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
|
|
1313
|
+
/* @__PURE__ */ jsx3(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx3(
|
|
1282
1314
|
MessageBubble,
|
|
1283
1315
|
{
|
|
1284
1316
|
message,
|
|
@@ -1294,7 +1326,9 @@ function MediaTypesPreview({
|
|
|
1294
1326
|
export {
|
|
1295
1327
|
AudioRecorderButton,
|
|
1296
1328
|
ConversationPreview,
|
|
1329
|
+
ConversationSimulatorPanel,
|
|
1297
1330
|
DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
|
|
1331
|
+
DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
|
|
1298
1332
|
DEFAULT_MEDIA_UPLOAD_PATH,
|
|
1299
1333
|
DEFAULT_PREVIEW_SCRIPT,
|
|
1300
1334
|
GLOBAL_CHANNEL,
|
package/dist/styles.css
CHANGED
|
@@ -237,3 +237,85 @@
|
|
|
237
237
|
:where(button:not(:disabled), [role=button]:not(:disabled), summary) {
|
|
238
238
|
cursor: pointer;
|
|
239
239
|
}
|
|
240
|
+
.cv-simulator-panel {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
width: 24rem;
|
|
244
|
+
flex-shrink: 0;
|
|
245
|
+
border-left: 1px solid rgb(229 231 235);
|
|
246
|
+
background: #fff;
|
|
247
|
+
}
|
|
248
|
+
.cv-simulator-panel__header {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
justify-content: space-between;
|
|
252
|
+
gap: 0.5rem;
|
|
253
|
+
padding: 0.75rem 1rem;
|
|
254
|
+
border-bottom: 1px solid rgb(229 231 235);
|
|
255
|
+
}
|
|
256
|
+
.cv-simulator-panel__heading {
|
|
257
|
+
min-width: 0;
|
|
258
|
+
}
|
|
259
|
+
.cv-simulator-panel__title {
|
|
260
|
+
font-size: 0.875rem;
|
|
261
|
+
font-weight: 600;
|
|
262
|
+
color: rgb(17 24 39);
|
|
263
|
+
overflow: hidden;
|
|
264
|
+
text-overflow: ellipsis;
|
|
265
|
+
white-space: nowrap;
|
|
266
|
+
}
|
|
267
|
+
.cv-simulator-panel__subtitle {
|
|
268
|
+
font-size: 0.75rem;
|
|
269
|
+
color: rgb(156 163 175);
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
text-overflow: ellipsis;
|
|
272
|
+
white-space: nowrap;
|
|
273
|
+
}
|
|
274
|
+
.cv-simulator-panel__actions {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
gap: 0.25rem;
|
|
278
|
+
}
|
|
279
|
+
.cv-simulator-panel__close {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
padding: 0.375rem;
|
|
282
|
+
border: 0;
|
|
283
|
+
border-radius: 0.5rem;
|
|
284
|
+
background: transparent;
|
|
285
|
+
color: rgb(156 163 175);
|
|
286
|
+
transition: color 150ms, background-color 150ms;
|
|
287
|
+
}
|
|
288
|
+
.cv-simulator-panel__close:hover {
|
|
289
|
+
color: rgb(55 65 81);
|
|
290
|
+
background: rgb(243 244 246);
|
|
291
|
+
}
|
|
292
|
+
.cv-simulator-panel__body {
|
|
293
|
+
flex: 1;
|
|
294
|
+
min-height: 0;
|
|
295
|
+
}
|
|
296
|
+
.dark .cv-simulator-panel {
|
|
297
|
+
border-left-color: rgb(31 41 55);
|
|
298
|
+
background: rgb(17 24 39);
|
|
299
|
+
}
|
|
300
|
+
.dark .cv-simulator-panel__header {
|
|
301
|
+
border-bottom-color: rgb(31 41 55);
|
|
302
|
+
}
|
|
303
|
+
.dark .cv-simulator-panel__title {
|
|
304
|
+
color: rgb(243 244 246);
|
|
305
|
+
}
|
|
306
|
+
.dark .cv-simulator-panel__subtitle {
|
|
307
|
+
color: rgb(107 114 128);
|
|
308
|
+
}
|
|
309
|
+
.dark .cv-simulator-panel__close:hover {
|
|
310
|
+
color: rgb(229 231 235);
|
|
311
|
+
background: rgb(31 41 55);
|
|
312
|
+
}
|
|
313
|
+
@media (max-width: 1023px) {
|
|
314
|
+
.cv-simulator-panel {
|
|
315
|
+
position: fixed;
|
|
316
|
+
inset: 0;
|
|
317
|
+
width: 100%;
|
|
318
|
+
z-index: 40;
|
|
319
|
+
border-left: 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { ConversationSimulatorPanel, type ConversationSimulatorPanelProps } from './ConversationSimulatorPanel'
|
|
5
|
+
import { createMockSSEProvider } from './createMockSSEProvider'
|
|
6
|
+
import { createPreviewStore } from './previewStore'
|
|
7
|
+
|
|
8
|
+
function markupOf(overrides: Partial<ConversationSimulatorPanelProps> = {}): string {
|
|
9
|
+
const store = createPreviewStore({ conversations: [], messages: {} })
|
|
10
|
+
|
|
11
|
+
return renderToStaticMarkup(
|
|
12
|
+
<ConversationSimulatorPanel
|
|
13
|
+
conversationId="5511900000042"
|
|
14
|
+
onClose={() => {}}
|
|
15
|
+
client={{ sendText: async () => {}, sendInteractiveReply: async () => {} } as never}
|
|
16
|
+
sse={createMockSSEProvider({ store })}
|
|
17
|
+
loadMessages={async () => []}
|
|
18
|
+
{...overrides}
|
|
19
|
+
/>,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('ConversationSimulatorPanel', () => {
|
|
24
|
+
it('mostra o telefone formatado pelo host, e não o id cru', () => {
|
|
25
|
+
expect(markupOf({ displayNumber: '+55 (11) 90000-0042' })).toContain('+55 (11) 90000-0042')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('cai no id da conversa quando o host não formata', () => {
|
|
29
|
+
expect(markupOf()).toContain('5511900000042')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('avisa para onde a mensagem vai, para ninguém achar que é conversa de mentira', () => {
|
|
33
|
+
expect(markupOf()).toContain('entrega no webhook real')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('dá nome acessível ao botão de fechar, que só tem ícone', () => {
|
|
37
|
+
expect(markupOf()).toContain('aria-label="Fechar simulador"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('aceita rótulos parciais do host sem exigir o conjunto inteiro', () => {
|
|
41
|
+
const markup = markupOf({ labels: { title: 'Testar fluxo' } })
|
|
42
|
+
|
|
43
|
+
expect(markup).toContain('Testar fluxo')
|
|
44
|
+
// O que não foi sobrescrito continua no default.
|
|
45
|
+
expect(markup).toContain('aria-label="Fechar simulador"')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('renderiza ação extra do host no cabeçalho', () => {
|
|
49
|
+
expect(markupOf({ headerActions: <button type="button">Rodar roteiro</button> })).toContain('Rodar roteiro')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('nomeia o próprio aside, para o leitor de tela distinguir do transcript ao lado', () => {
|
|
53
|
+
expect(markupOf()).toContain('aria-label="Simulador do cliente"')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ada Technology. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* This source code is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
* modification, distribution, or use of this file, via any medium, is
|
|
6
|
+
* strictly prohibited without prior written permission from Ada Technology.
|
|
7
|
+
*
|
|
8
|
+
* Author: Anderson Filho <andersonfrfilho@gmail.com>
|
|
9
|
+
*
|
|
10
|
+
* Painel lateral do simulador de cliente: a moldura em volta do `ConversationPreview`.
|
|
11
|
+
*
|
|
12
|
+
* Existe como componente do pacote porque cada produto tinha reescrito a mesma moldura —
|
|
13
|
+
* `<aside>`, cabeçalho com título e telefone, botão de fechar — e as cópias divergiram. Uma delas
|
|
14
|
+
* migrou para o cliente-ponte (assinatura no servidor) e a outra ficou assinando no navegador, com
|
|
15
|
+
* o app secret publicado no bundle. É o tipo de correção que precisa chegar a todo mundo de uma vez.
|
|
16
|
+
*
|
|
17
|
+
* Mora DENTRO da tela de conversas, e não numa aba própria, por um motivo prático: o token do
|
|
18
|
+
* atendente costuma viver em `sessionStorage`, que é por aba. Numa aba separada o simulador nascia
|
|
19
|
+
* sem sessão, o transcript nunca carregava, e o sintoma era "mandei e não aconteceu nada".
|
|
20
|
+
*
|
|
21
|
+
* O efeito de cada envio aparece na thread ao lado pelo mesmo stream que a inbox já assina — este
|
|
22
|
+
* painel não abre conexão própria.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ReactNode } from 'react'
|
|
26
|
+
|
|
27
|
+
import { ConversationPreview, type ConversationPreviewProps } from './ConversationPreview'
|
|
28
|
+
|
|
29
|
+
export type ConversationSimulatorPanelLabels = {
|
|
30
|
+
readonly title: string
|
|
31
|
+
/** Complementa o telefone no subtítulo, explicando para onde a mensagem realmente vai. */
|
|
32
|
+
readonly destinationHint: string
|
|
33
|
+
readonly close: string
|
|
34
|
+
readonly placeholder: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels = {
|
|
38
|
+
title: 'Simulador do cliente',
|
|
39
|
+
destinationHint: 'entrega no webhook real',
|
|
40
|
+
close: 'Fechar simulador',
|
|
41
|
+
placeholder: 'Escreva como o cliente…',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
|
|
45
|
+
readonly onClose: () => void
|
|
46
|
+
/**
|
|
47
|
+
* Telefone já formatado para leitura. É o host que formata: máscara de telefone é convenção
|
|
48
|
+
* regional, e o pacote não tem como saber a do produto.
|
|
49
|
+
*/
|
|
50
|
+
readonly displayNumber?: string
|
|
51
|
+
readonly labels?: Partial<ConversationSimulatorPanelLabels>
|
|
52
|
+
/** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
|
|
53
|
+
readonly headerActions?: ReactNode
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ConversationSimulatorPanel({
|
|
57
|
+
onClose,
|
|
58
|
+
displayNumber,
|
|
59
|
+
labels,
|
|
60
|
+
headerActions,
|
|
61
|
+
...previewProps
|
|
62
|
+
}: ConversationSimulatorPanelProps) {
|
|
63
|
+
const text = { ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, ...labels }
|
|
64
|
+
const subtitle = [displayNumber ?? previewProps.conversationId, text.destinationHint].join(' · ')
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<aside className="cv-simulator-panel" aria-label={text.title}>
|
|
68
|
+
<header className="cv-simulator-panel__header">
|
|
69
|
+
<div className="cv-simulator-panel__heading">
|
|
70
|
+
<h2 className="cv-simulator-panel__title">{text.title}</h2>
|
|
71
|
+
<p className="cv-simulator-panel__subtitle">{subtitle}</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="cv-simulator-panel__actions">
|
|
74
|
+
{headerActions}
|
|
75
|
+
<button type="button" onClick={onClose} title={text.close} aria-label={text.close} className="cv-simulator-panel__close">
|
|
76
|
+
{/* SVG inline em vez de lucide-react: o pacote não impõe biblioteca de ícone ao host. */}
|
|
77
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
78
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<div className="cv-simulator-panel__body">
|
|
85
|
+
<ConversationPreview {...previewProps} placeholder={text.placeholder} />
|
|
86
|
+
</div>
|
|
87
|
+
</aside>
|
|
88
|
+
)
|
|
89
|
+
}
|
package/src/preview/index.ts
CHANGED
|
@@ -25,6 +25,9 @@ export type { CreateMockSSEProviderParams } from './createMockSSEProvider'
|
|
|
25
25
|
|
|
26
26
|
export { PREVIEW_CONVERSATIONS, PREVIEW_MESSAGES, PREVIEW_DOCUMENTS } from './previewFixtures'
|
|
27
27
|
|
|
28
|
+
export { ConversationSimulatorPanel, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS } from './ConversationSimulatorPanel'
|
|
29
|
+
export type { ConversationSimulatorPanelProps, ConversationSimulatorPanelLabels } from './ConversationSimulatorPanel'
|
|
30
|
+
|
|
28
31
|
export { ConversationPreview } from './ConversationPreview'
|
|
29
32
|
export { mediaTypeOf } from './ConversationPreview'
|
|
30
33
|
export type { ConversationPreviewProps, PreviewUploadedMedia } from './ConversationPreview'
|
package/src/styles.css
CHANGED
|
@@ -192,3 +192,88 @@
|
|
|
192
192
|
:where(button:not(:disabled), [role='button']:not(:disabled), summary) {
|
|
193
193
|
cursor: pointer;
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Simulador de cliente. Largura fixa porque é painel auxiliar: a thread ao lado é que deve
|
|
198
|
+
* ganhar o espaço restante quando a janela encolhe.
|
|
199
|
+
*/
|
|
200
|
+
.cv-simulator-panel {
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
width: 24rem;
|
|
204
|
+
flex-shrink: 0;
|
|
205
|
+
border-left: 1px solid rgb(229 231 235);
|
|
206
|
+
background: #fff;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.cv-simulator-panel__header {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: space-between;
|
|
213
|
+
gap: 0.5rem;
|
|
214
|
+
padding: 0.75rem 1rem;
|
|
215
|
+
border-bottom: 1px solid rgb(229 231 235);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.cv-simulator-panel__heading { min-width: 0; }
|
|
219
|
+
|
|
220
|
+
.cv-simulator-panel__title {
|
|
221
|
+
font-size: 0.875rem;
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
color: rgb(17 24 39);
|
|
224
|
+
overflow: hidden;
|
|
225
|
+
text-overflow: ellipsis;
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.cv-simulator-panel__subtitle {
|
|
230
|
+
font-size: 0.75rem;
|
|
231
|
+
color: rgb(156 163 175);
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
text-overflow: ellipsis;
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.cv-simulator-panel__actions {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 0.25rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.cv-simulator-panel__close {
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
padding: 0.375rem;
|
|
246
|
+
border: 0;
|
|
247
|
+
border-radius: 0.5rem;
|
|
248
|
+
background: transparent;
|
|
249
|
+
color: rgb(156 163 175);
|
|
250
|
+
transition: color 150ms, background-color 150ms;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.cv-simulator-panel__close:hover {
|
|
254
|
+
color: rgb(55 65 81);
|
|
255
|
+
background: rgb(243 244 246);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* `min-height: 0` para o transcript rolar dentro do painel em vez de esticar o `<aside>`. */
|
|
259
|
+
.cv-simulator-panel__body { flex: 1; min-height: 0; }
|
|
260
|
+
|
|
261
|
+
.dark .cv-simulator-panel {
|
|
262
|
+
border-left-color: rgb(31 41 55);
|
|
263
|
+
background: rgb(17 24 39);
|
|
264
|
+
}
|
|
265
|
+
.dark .cv-simulator-panel__header { border-bottom-color: rgb(31 41 55); }
|
|
266
|
+
.dark .cv-simulator-panel__title { color: rgb(243 244 246); }
|
|
267
|
+
.dark .cv-simulator-panel__subtitle { color: rgb(107 114 128); }
|
|
268
|
+
.dark .cv-simulator-panel__close:hover { color: rgb(229 231 235); background: rgb(31 41 55); }
|
|
269
|
+
|
|
270
|
+
/* Painel auxiliar não cabe ao lado da thread em tela estreita: vira sobreposição de tela cheia. */
|
|
271
|
+
@media (max-width: 1023px) {
|
|
272
|
+
.cv-simulator-panel {
|
|
273
|
+
position: fixed;
|
|
274
|
+
inset: 0;
|
|
275
|
+
width: 100%;
|
|
276
|
+
z-index: 40;
|
|
277
|
+
border-left: 0;
|
|
278
|
+
}
|
|
279
|
+
}
|