@adatechnology/conversations-ui 0.1.0-rc.3 → 0.1.0-rc.30
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/chunk-DXPSPUWF.js +110 -0
- package/dist/chunk-GY472G6E.js +2316 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +311 -4
- package/dist/flows/index.js +1322 -55
- package/dist/index.d.ts +1074 -42
- package/dist/index.js +3571 -742
- package/dist/preview/index.d.ts +328 -8
- package/dist/preview/index.js +902 -115
- package/dist/styles.css +819 -0
- package/dist/types-De5aN-E_.d.ts +502 -0
- package/package.json +3 -3
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +14 -3
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +347 -24
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +163 -45
- package/src/ConversationListItem.tsx +19 -2
- package/src/ConversationLocalesProvider.tsx +42 -0
- package/src/ConversationRow.tsx +31 -7
- package/src/DocumentsLibrary.tsx +382 -0
- package/src/EmojiPicker.tsx +70 -55
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/InteractiveMessage.test.tsx +41 -0
- package/src/InteractiveMessage.tsx +146 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +98 -22
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +75 -6
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +155 -19
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +164 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +28 -4
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +543 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +35 -8
- package/src/flows/FlowNodePanel.tsx +149 -38
- package/src/flows/FlowPalette.tsx +13 -3
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1003 -0
- package/src/flows/flowCanvasModel.test.ts +293 -0
- package/src/flows/flowCanvasModel.ts +342 -0
- package/src/flows/flowEditorOps.test.ts +241 -0
- package/src/flows/flowEditorOps.ts +177 -0
- package/src/flows/flowGraph.ts +6 -6
- package/src/flows/index.ts +40 -1
- package/src/flows/labels.ts +141 -0
- package/src/flows/workspaceContract.test.ts +95 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +114 -13
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +199 -13
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +89 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +127 -4
- package/src/preview/index.ts +33 -3
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +185 -10
- package/src/quickReply.test.ts +58 -0
- package/src/settings/MessagesWorkspace.tsx +468 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.tsx +9 -1
- package/src/styles.css +783 -0
- package/src/types.ts +64 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +346 -0
- package/src/workspace/index.ts +12 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-N7B24WYD.js +0 -719
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-C0PtaO7S.d.ts +0 -207
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulário da biblioteca de arquivos. Sobrescrevível campo a campo: o produto troca "cliente"
|
|
3
|
+
* por "lead" ou o idioma inteiro sem manter uma cópia da tela para isso.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DocumentsWorkspaceLabels {
|
|
7
|
+
readonly title: string
|
|
8
|
+
readonly subtitle: (total: number) => string
|
|
9
|
+
readonly searchPlaceholder: string
|
|
10
|
+
readonly empty: string
|
|
11
|
+
readonly noResults: string
|
|
12
|
+
readonly loading: string
|
|
13
|
+
readonly failure: string
|
|
14
|
+
readonly view: string
|
|
15
|
+
readonly download: string
|
|
16
|
+
readonly remove: string
|
|
17
|
+
readonly removeConfirm: (filename: string) => string
|
|
18
|
+
readonly openConversation: string
|
|
19
|
+
readonly sourceFilter: string
|
|
20
|
+
readonly categoryFilter: string
|
|
21
|
+
readonly startDate: string
|
|
22
|
+
readonly endDate: string
|
|
23
|
+
readonly clearFilters: string
|
|
24
|
+
readonly selectAllPage: string
|
|
25
|
+
readonly selectRow: string
|
|
26
|
+
readonly bulkSelected: (count: number) => string
|
|
27
|
+
readonly bulkClear: string
|
|
28
|
+
readonly bulkDownloadZip: string
|
|
29
|
+
readonly bulkRemove: (count: number) => string
|
|
30
|
+
readonly bulkRemoveConfirm: (count: number) => string
|
|
31
|
+
readonly upload: string
|
|
32
|
+
readonly uploadError: string
|
|
33
|
+
readonly columnFilename: string
|
|
34
|
+
readonly columnContact: string
|
|
35
|
+
readonly columnType: string
|
|
36
|
+
readonly columnSize: string
|
|
37
|
+
readonly columnSource: string
|
|
38
|
+
readonly columnDate: string
|
|
39
|
+
readonly columnActions: string
|
|
40
|
+
readonly sourceLabels: Readonly<Record<string, string>>
|
|
41
|
+
readonly categoryLabels: Readonly<Record<string, string>>
|
|
42
|
+
readonly show: string
|
|
43
|
+
readonly perPage: string
|
|
44
|
+
readonly total: (count: number) => string
|
|
45
|
+
readonly page: (current: number, last: number) => string
|
|
46
|
+
readonly previousPage: string
|
|
47
|
+
readonly nextPage: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_DOCUMENTS_WORKSPACE_LABELS: DocumentsWorkspaceLabels = {
|
|
51
|
+
title: 'Documentos',
|
|
52
|
+
subtitle: (total) => `${total} arquivo${total === 1 ? '' : 's'} trocado${total === 1 ? '' : 's'}`,
|
|
53
|
+
searchPlaceholder: 'Buscar por nome do arquivo ou telefone',
|
|
54
|
+
empty: 'Nenhum arquivo trocado ainda.',
|
|
55
|
+
noResults: 'Nenhum arquivo encontrado para os filtros aplicados.',
|
|
56
|
+
loading: 'Carregando arquivos…',
|
|
57
|
+
failure: 'Não foi possível carregar os arquivos.',
|
|
58
|
+
view: 'Visualizar',
|
|
59
|
+
download: 'Baixar',
|
|
60
|
+
remove: 'Excluir',
|
|
61
|
+
removeConfirm: (filename) => `Excluir "${filename}"?`,
|
|
62
|
+
openConversation: 'Abrir conversa',
|
|
63
|
+
sourceFilter: 'Origem',
|
|
64
|
+
categoryFilter: 'Tipo',
|
|
65
|
+
startDate: 'De',
|
|
66
|
+
endDate: 'Até',
|
|
67
|
+
clearFilters: 'Limpar filtros',
|
|
68
|
+
selectAllPage: 'Selecionar todos desta página',
|
|
69
|
+
selectRow: 'Selecionar arquivo',
|
|
70
|
+
bulkSelected: (count) => `${count} selecionado${count === 1 ? '' : 's'}`,
|
|
71
|
+
bulkClear: 'Limpar seleção',
|
|
72
|
+
bulkDownloadZip: 'Baixar em zip',
|
|
73
|
+
bulkRemove: (count) => `Excluir ${count}`,
|
|
74
|
+
bulkRemoveConfirm: (count) => `Excluir ${count} arquivo${count === 1 ? '' : 's'}?`,
|
|
75
|
+
upload: 'Enviar documento',
|
|
76
|
+
uploadError: 'Não foi possível enviar o arquivo.',
|
|
77
|
+
columnFilename: 'Arquivo',
|
|
78
|
+
columnContact: 'Contato',
|
|
79
|
+
columnType: 'Tipo',
|
|
80
|
+
columnSize: 'Tamanho',
|
|
81
|
+
columnSource: 'Origem',
|
|
82
|
+
columnDate: 'Recebido em',
|
|
83
|
+
columnActions: 'Ações',
|
|
84
|
+
sourceLabels: { customer: 'Cliente', agent: 'Atendente', bot: 'Bot' },
|
|
85
|
+
categoryLabels: { document: 'Documento', image: 'Imagem', audio: 'Áudio', video: 'Vídeo' },
|
|
86
|
+
show: 'Mostrar',
|
|
87
|
+
perPage: 'por página',
|
|
88
|
+
total: (count) => `${count} no total`,
|
|
89
|
+
page: (current, last) => `${current} / ${last}`,
|
|
90
|
+
previousPage: 'Página anterior',
|
|
91
|
+
nextPage: 'Próxima página',
|
|
92
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { EMOJI_CATEGORIES, searchEmojis } from './emojiCatalog'
|
|
3
|
+
|
|
4
|
+
describe('searchEmojis', () => {
|
|
5
|
+
it('devolve o catálogo inteiro quando não há termo', () => {
|
|
6
|
+
const total = EMOJI_CATEGORIES.reduce((sum, category) => sum + category.entries.length, 0)
|
|
7
|
+
expect(searchEmojis('').length).toBe(total)
|
|
8
|
+
expect(searchEmojis(' ').length).toBe(total)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('acha pela palavra sem acento e com acento', () => {
|
|
12
|
+
const semAcento = searchEmojis('coracao').map((entry) => entry.emoji)
|
|
13
|
+
const comAcento = searchEmojis('coração').map((entry) => entry.emoji)
|
|
14
|
+
|
|
15
|
+
expect(semAcento).toContain('❤️')
|
|
16
|
+
expect(comAcento).toContain('❤️')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('ignora caixa e casa por prefixo, não pelo meio da palavra', () => {
|
|
20
|
+
expect(searchEmojis('CASA').map((entry) => entry.emoji)).toContain('🏠')
|
|
21
|
+
// "sa" está dentro de "casa", mas não é prefixo de nenhuma palavra-chave dela.
|
|
22
|
+
expect(searchEmojis('sa').map((entry) => entry.emoji)).not.toContain('🏠')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('atravessa categorias, não só a aberta', () => {
|
|
26
|
+
const resultados = searchEmojis('pagamento').map((entry) => entry.emoji)
|
|
27
|
+
|
|
28
|
+
expect(resultados).toContain('💳')
|
|
29
|
+
expect(resultados.length).toBeGreaterThan(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('devolve vazio quando nada casa, em vez de cair no catálogo inteiro', () => {
|
|
33
|
+
expect(searchEmojis('xyzabc')).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo de emojis com palavras-chave em português, usado pela busca do seletor.
|
|
3
|
+
*
|
|
4
|
+
* Fica em módulo próprio (e não dentro do `EmojiPicker`) porque a busca precisa varrer TODAS as
|
|
5
|
+
* categorias, não só a aberta — o índice é montado uma vez, no carregamento, e não a cada tecla.
|
|
6
|
+
*
|
|
7
|
+
* As palavras-chave são de busca, não legendas: incluem sinônimos e formas sem acento, porque quem
|
|
8
|
+
* digita rápido escreve "coracao" e "polegar" com a mesma frequência que a forma correta.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type EmojiEntry = {
|
|
12
|
+
readonly emoji: string
|
|
13
|
+
readonly keywords: readonly string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type EmojiCategory = {
|
|
17
|
+
readonly name: string
|
|
18
|
+
readonly entries: readonly EmojiEntry[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const EMOJI_CATEGORIES: readonly EmojiCategory[] = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Smileys',
|
|
24
|
+
entries: [
|
|
25
|
+
{ emoji: '😀', keywords: ['sorriso', 'feliz', 'alegre'] },
|
|
26
|
+
{ emoji: '😃', keywords: ['sorriso', 'feliz', 'animado'] },
|
|
27
|
+
{ emoji: '😄', keywords: ['sorriso', 'feliz', 'risada'] },
|
|
28
|
+
{ emoji: '😁', keywords: ['sorriso', 'dentes', 'feliz'] },
|
|
29
|
+
{ emoji: '😅', keywords: ['alivio', 'alívio', 'suor', 'nervoso'] },
|
|
30
|
+
{ emoji: '😂', keywords: ['risada', 'chorando', 'engracado', 'engraçado'] },
|
|
31
|
+
{ emoji: '🤣', keywords: ['risada', 'rolando', 'engracado', 'engraçado'] },
|
|
32
|
+
{ emoji: '😊', keywords: ['sorriso', 'timido', 'tímido', 'feliz'] },
|
|
33
|
+
{ emoji: '😇', keywords: ['anjo', 'inocente', 'santo'] },
|
|
34
|
+
{ emoji: '🙂', keywords: ['sorriso', 'leve', 'ok'] },
|
|
35
|
+
{ emoji: '😉', keywords: ['piscada', 'piscar', 'flerte'] },
|
|
36
|
+
{ emoji: '😌', keywords: ['aliviado', 'calmo', 'tranquilo'] },
|
|
37
|
+
{ emoji: '😍', keywords: ['amor', 'apaixonado', 'coracao', 'coração', 'olhos'] },
|
|
38
|
+
{ emoji: '🥰', keywords: ['amor', 'apaixonado', 'carinho'] },
|
|
39
|
+
{ emoji: '😘', keywords: ['beijo', 'amor', 'carinho'] },
|
|
40
|
+
{ emoji: '😋', keywords: ['gostoso', 'delicia', 'delícia', 'lingua', 'língua'] },
|
|
41
|
+
{ emoji: '😜', keywords: ['lingua', 'língua', 'brincadeira', 'piscada'] },
|
|
42
|
+
{ emoji: '🤪', keywords: ['maluco', 'doido', 'brincadeira'] },
|
|
43
|
+
{ emoji: '🤔', keywords: ['pensando', 'duvida', 'dúvida', 'hmm'] },
|
|
44
|
+
{ emoji: '🤗', keywords: ['abraco', 'abraço', 'carinho'] },
|
|
45
|
+
{ emoji: '😐', keywords: ['neutro', 'serio', 'sério', 'indiferente'] },
|
|
46
|
+
{ emoji: '😴', keywords: ['dormindo', 'sono', 'cansado'] },
|
|
47
|
+
{ emoji: '😭', keywords: ['chorando', 'triste', 'lagrima', 'lágrima'] },
|
|
48
|
+
{ emoji: '😢', keywords: ['triste', 'chorando', 'lagrima', 'lágrima'] },
|
|
49
|
+
{ emoji: '😡', keywords: ['raiva', 'bravo', 'irritado'] },
|
|
50
|
+
{ emoji: '😱', keywords: ['susto', 'medo', 'assustado'] },
|
|
51
|
+
{ emoji: '🤯', keywords: ['explodindo', 'chocado', 'surpresa'] },
|
|
52
|
+
{ emoji: '😎', keywords: ['oculos', 'óculos', 'legal', 'estiloso'] },
|
|
53
|
+
{ emoji: '🥳', keywords: ['festa', 'comemorar', 'aniversario', 'aniversário'] },
|
|
54
|
+
{ emoji: '😷', keywords: ['mascara', 'máscara', 'doente', 'saude', 'saúde'] },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Gestos',
|
|
59
|
+
entries: [
|
|
60
|
+
{ emoji: '👍', keywords: ['joia', 'jóia', 'polegar', 'ok', 'positivo', 'curtir'] },
|
|
61
|
+
{ emoji: '👎', keywords: ['polegar', 'negativo', 'ruim', 'nao', 'não'] },
|
|
62
|
+
{ emoji: '👌', keywords: ['ok', 'certo', 'perfeito'] },
|
|
63
|
+
{ emoji: '✌️', keywords: ['paz', 'vitoria', 'vitória', 'dois'] },
|
|
64
|
+
{ emoji: '🤞', keywords: ['sorte', 'dedos', 'cruzados', 'torcendo'] },
|
|
65
|
+
{ emoji: '🤙', keywords: ['chama', 'ligar', 'shaka'] },
|
|
66
|
+
{ emoji: '👋', keywords: ['tchau', 'ola', 'olá', 'aceno', 'oi'] },
|
|
67
|
+
{ emoji: '✋', keywords: ['mao', 'mão', 'parar', 'pare'] },
|
|
68
|
+
{ emoji: '👏', keywords: ['palmas', 'aplauso', 'parabens', 'parabéns'] },
|
|
69
|
+
{ emoji: '🙌', keywords: ['comemorar', 'maos', 'mãos', 'sucesso'] },
|
|
70
|
+
{ emoji: '🤝', keywords: ['acordo', 'aperto', 'mao', 'mão', 'negocio', 'negócio', 'parceria'] },
|
|
71
|
+
{ emoji: '🙏', keywords: ['obrigado', 'reza', 'oracao', 'oração', 'por favor'] },
|
|
72
|
+
{ emoji: '✍️', keywords: ['escrever', 'assinar', 'assinatura'] },
|
|
73
|
+
{ emoji: '💪', keywords: ['forca', 'força', 'musculo', 'músculo', 'braco', 'braço'] },
|
|
74
|
+
{ emoji: '👇', keywords: ['abaixo', 'baixo', 'apontar', 'seta'] },
|
|
75
|
+
{ emoji: '👉', keywords: ['direita', 'apontar', 'seta'] },
|
|
76
|
+
{ emoji: '☝️', keywords: ['acima', 'cima', 'apontar', 'atencao', 'atenção'] },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Corações',
|
|
81
|
+
entries: [
|
|
82
|
+
{ emoji: '❤️', keywords: ['coracao', 'coração', 'amor', 'vermelho'] },
|
|
83
|
+
{ emoji: '🧡', keywords: ['coracao', 'coração', 'laranja'] },
|
|
84
|
+
{ emoji: '💛', keywords: ['coracao', 'coração', 'amarelo'] },
|
|
85
|
+
{ emoji: '💚', keywords: ['coracao', 'coração', 'verde'] },
|
|
86
|
+
{ emoji: '💙', keywords: ['coracao', 'coração', 'azul'] },
|
|
87
|
+
{ emoji: '💜', keywords: ['coracao', 'coração', 'roxo'] },
|
|
88
|
+
{ emoji: '🖤', keywords: ['coracao', 'coração', 'preto'] },
|
|
89
|
+
{ emoji: '🤍', keywords: ['coracao', 'coração', 'branco'] },
|
|
90
|
+
{ emoji: '💔', keywords: ['coracao', 'coração', 'partido', 'triste'] },
|
|
91
|
+
{ emoji: '💕', keywords: ['coracao', 'coração', 'amor', 'casal'] },
|
|
92
|
+
{ emoji: '💖', keywords: ['coracao', 'coração', 'brilho', 'amor'] },
|
|
93
|
+
{ emoji: '💝', keywords: ['coracao', 'coração', 'presente', 'laco', 'laço'] },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Negócios',
|
|
98
|
+
entries: [
|
|
99
|
+
{ emoji: '🏠', keywords: ['casa', 'imovel', 'imóvel', 'residencia', 'residência', 'moradia'] },
|
|
100
|
+
{ emoji: '🏡', keywords: ['casa', 'imovel', 'imóvel', 'jardim', 'moradia'] },
|
|
101
|
+
{ emoji: '🏢', keywords: ['predio', 'prédio', 'empresa', 'escritorio', 'escritório'] },
|
|
102
|
+
{ emoji: '🏦', keywords: ['banco', 'financiamento', 'agencia', 'agência'] },
|
|
103
|
+
{ emoji: '🔑', keywords: ['chave', 'casa', 'entrega', 'imovel', 'imóvel'] },
|
|
104
|
+
{ emoji: '📄', keywords: ['documento', 'papel', 'contrato', 'arquivo'] },
|
|
105
|
+
{ emoji: '📋', keywords: ['prancheta', 'lista', 'documento', 'checklist'] },
|
|
106
|
+
{ emoji: '📝', keywords: ['anotar', 'escrever', 'nota', 'formulario', 'formulário'] },
|
|
107
|
+
{ emoji: '✅', keywords: ['ok', 'certo', 'aprovado', 'concluido', 'concluído', 'check'] },
|
|
108
|
+
{ emoji: '❌', keywords: ['errado', 'negado', 'recusado', 'cancelar'] },
|
|
109
|
+
{ emoji: '⚠️', keywords: ['atencao', 'atenção', 'alerta', 'cuidado'] },
|
|
110
|
+
{ emoji: '💰', keywords: ['dinheiro', 'valor', 'saco', 'grana', 'pagamento'] },
|
|
111
|
+
{ emoji: '💵', keywords: ['dinheiro', 'nota', 'valor', 'pagamento'] },
|
|
112
|
+
{ emoji: '💳', keywords: ['cartao', 'cartão', 'credito', 'crédito', 'pagamento'] },
|
|
113
|
+
{ emoji: '🧾', keywords: ['recibo', 'nota', 'fiscal', 'comprovante'] },
|
|
114
|
+
{ emoji: '📊', keywords: ['grafico', 'gráfico', 'relatorio', 'relatório', 'dados'] },
|
|
115
|
+
{ emoji: '📈', keywords: ['grafico', 'gráfico', 'subindo', 'crescimento', 'alta'] },
|
|
116
|
+
{ emoji: '📉', keywords: ['grafico', 'gráfico', 'caindo', 'queda', 'baixa'] },
|
|
117
|
+
{ emoji: '🗓️', keywords: ['calendario', 'calendário', 'data', 'agenda', 'prazo'] },
|
|
118
|
+
{ emoji: '⏰', keywords: ['relogio', 'relógio', 'hora', 'prazo', 'alarme'] },
|
|
119
|
+
{ emoji: '📞', keywords: ['telefone', 'ligar', 'contato', 'chamada'] },
|
|
120
|
+
{ emoji: '📱', keywords: ['celular', 'telefone', 'whatsapp', 'contato'] },
|
|
121
|
+
{ emoji: '📧', keywords: ['email', 'e-mail', 'mensagem', 'contato'] },
|
|
122
|
+
{ emoji: '📎', keywords: ['anexo', 'clipe', 'arquivo'] },
|
|
123
|
+
{ emoji: '🔍', keywords: ['buscar', 'procurar', 'lupa', 'pesquisa', 'consulta'] },
|
|
124
|
+
{ emoji: '🤖', keywords: ['robo', 'robô', 'bot', 'assistente', 'automatico', 'automático'] },
|
|
125
|
+
{ emoji: '💬', keywords: ['mensagem', 'conversa', 'balao', 'balão', 'chat'] },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'Objetos',
|
|
130
|
+
entries: [
|
|
131
|
+
{ emoji: '🎁', keywords: ['presente', 'brinde', 'surpresa'] },
|
|
132
|
+
{ emoji: '🎉', keywords: ['festa', 'comemorar', 'parabens', 'parabéns'] },
|
|
133
|
+
{ emoji: '🎊', keywords: ['festa', 'confete', 'comemorar'] },
|
|
134
|
+
{ emoji: '🎂', keywords: ['bolo', 'aniversario', 'aniversário', 'festa'] },
|
|
135
|
+
{ emoji: '💡', keywords: ['ideia', 'ideía', 'lampada', 'lâmpada', 'dica'] },
|
|
136
|
+
{ emoji: '🔔', keywords: ['sino', 'aviso', 'notificacao', 'notificação', 'lembrete'] },
|
|
137
|
+
{ emoji: '⭐', keywords: ['estrela', 'favorito', 'avaliacao', 'avaliação'] },
|
|
138
|
+
{ emoji: '🔥', keywords: ['fogo', 'quente', 'destaque', 'top'] },
|
|
139
|
+
{ emoji: '🚀', keywords: ['foguete', 'rapido', 'rápido', 'lancamento', 'lançamento'] },
|
|
140
|
+
{ emoji: '💻', keywords: ['computador', 'notebook', 'trabalho'] },
|
|
141
|
+
{ emoji: '📷', keywords: ['foto', 'camera', 'câmera', 'imagem'] },
|
|
142
|
+
{ emoji: '🚗', keywords: ['carro', 'veiculo', 'veículo', 'automovel', 'automóvel'] },
|
|
143
|
+
{ emoji: '✈️', keywords: ['aviao', 'avião', 'viagem', 'voo'] },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Comida',
|
|
148
|
+
entries: [
|
|
149
|
+
{ emoji: '🍔', keywords: ['hamburguer', 'hambúrguer', 'lanche', 'comida'] },
|
|
150
|
+
{ emoji: '🍕', keywords: ['pizza', 'comida', 'lanche'] },
|
|
151
|
+
{ emoji: '🍟', keywords: ['batata', 'frita', 'lanche'] },
|
|
152
|
+
{ emoji: '🍿', keywords: ['pipoca', 'cinema', 'filme'] },
|
|
153
|
+
{ emoji: '🍞', keywords: ['pao', 'pão', 'padaria'] },
|
|
154
|
+
{ emoji: '🧀', keywords: ['queijo', 'comida'] },
|
|
155
|
+
{ emoji: '🥗', keywords: ['salada', 'saudavel', 'saudável', 'comida'] },
|
|
156
|
+
{ emoji: '☕', keywords: ['cafe', 'café', 'bebida', 'quente'] },
|
|
157
|
+
{ emoji: '🍺', keywords: ['cerveja', 'bebida', 'chopp'] },
|
|
158
|
+
{ emoji: '🍷', keywords: ['vinho', 'bebida', 'taca', 'taça'] },
|
|
159
|
+
{ emoji: '🥂', keywords: ['brinde', 'comemorar', 'champanhe'] },
|
|
160
|
+
{ emoji: '🥤', keywords: ['refrigerante', 'bebida', 'copo'] },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
] as const
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Índice achatado: a busca ignora categoria, porque quem digita "casa" quer o resultado venha de
|
|
167
|
+
* onde vier. A ordem preserva a das categorias, então o resultado sai agrupado por afinidade sem
|
|
168
|
+
* precisar ordenar.
|
|
169
|
+
*/
|
|
170
|
+
const ALL_ENTRIES: readonly EmojiEntry[] = EMOJI_CATEGORIES.flatMap((category) => category.entries)
|
|
171
|
+
|
|
172
|
+
function normalize(value: string): string {
|
|
173
|
+
return value
|
|
174
|
+
.toLowerCase()
|
|
175
|
+
.normalize('NFD')
|
|
176
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
177
|
+
.trim()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Busca por prefixo de palavra-chave, não por substring: "ca" traz "casa" e "cartão", mas "sa" não
|
|
182
|
+
* traz "casa" — casar no meio da palavra devolvia resultado que ninguém consegue explicar.
|
|
183
|
+
*/
|
|
184
|
+
export function searchEmojis(query: string): readonly EmojiEntry[] {
|
|
185
|
+
const term = normalize(query)
|
|
186
|
+
if (!term) return ALL_ENTRIES
|
|
187
|
+
|
|
188
|
+
return ALL_ENTRIES.filter((entry) => entry.keywords.some((keyword) => normalize(keyword).startsWith(term)))
|
|
189
|
+
}
|
|
@@ -18,10 +18,20 @@ export function FlowGroupHeader({ data }: NodeProps) {
|
|
|
18
18
|
return (
|
|
19
19
|
<div className="flex items-center gap-2 rounded-full border border-cyan-300 dark:border-cyan-700 bg-cyan-50 dark:bg-cyan-950/50 px-3 py-1 text-xs font-medium text-cyan-800 dark:text-cyan-200 shadow-sm whitespace-nowrap">
|
|
20
20
|
<span>{label}</span>
|
|
21
|
-
<button
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onFocus}
|
|
24
|
+
data-cv-tooltip={labels.flowGroup.focus} aria-label={labels.flowGroup.focus}
|
|
25
|
+
className="hover:text-blue-600 dark:hover:text-blue-400"
|
|
26
|
+
>
|
|
22
27
|
<Maximize2 size={12} />
|
|
23
28
|
</button>
|
|
24
|
-
<button
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={onClose}
|
|
32
|
+
data-cv-tooltip={labels.flowGroup.close} aria-label={labels.flowGroup.close}
|
|
33
|
+
className="hover:text-red-600 dark:hover:text-red-400"
|
|
34
|
+
>
|
|
25
35
|
<X size={12} />
|
|
26
36
|
</button>
|
|
27
37
|
</div>
|
|
@@ -27,18 +27,21 @@ export function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverr
|
|
|
27
27
|
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey])
|
|
28
28
|
|
|
29
29
|
const nodes = useMemo<Node[]>(
|
|
30
|
-
() =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
() =>
|
|
31
|
+
Object.values(graphs).map(
|
|
32
|
+
(g): Node => ({
|
|
33
|
+
id: g.key,
|
|
34
|
+
type: 'flowMapNode',
|
|
35
|
+
position: positions[g.key] ?? { x: 0, y: 0 },
|
|
36
|
+
data: {
|
|
37
|
+
label: g.label,
|
|
38
|
+
nodeCount: Object.keys(g.nodes).length,
|
|
39
|
+
isRoot: g.key === rootKey,
|
|
40
|
+
labels,
|
|
41
|
+
onOpen: () => onOpenFlow(g.key),
|
|
42
|
+
} satisfies FlowMapNodeData,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
42
45
|
[graphs, positions, rootKey, onOpenFlow, labels],
|
|
43
46
|
)
|
|
44
47
|
|
|
@@ -21,10 +21,13 @@ export function FlowMapNode({ data }: NodeProps) {
|
|
|
21
21
|
<div className="flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300">
|
|
22
22
|
<GitBranch size={14} />
|
|
23
23
|
<span className="text-sm font-semibold truncate">{label}</span>
|
|
24
|
-
{isRoot &&
|
|
24
|
+
{isRoot && (
|
|
25
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0" data-cv-tooltip={labels.startNodeTooltip} />
|
|
26
|
+
)}
|
|
25
27
|
</div>
|
|
26
28
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{labels.flowMap.nodeCount(nodeCount)}</p>
|
|
27
29
|
<button
|
|
30
|
+
data-cv-tooltip={labels.flowMap.openFlow} aria-label={labels.flowMap.openFlow}
|
|
28
31
|
type="button"
|
|
29
32
|
onClick={onOpen}
|
|
30
33
|
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline"
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { Handle, Position, type NodeProps } from '@xyflow/react'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
MessageCircleQuestion,
|
|
4
|
+
GitBranch,
|
|
5
|
+
Zap,
|
|
6
|
+
ListTree,
|
|
7
|
+
Diamond,
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
AlertCircle,
|
|
10
|
+
Headset,
|
|
11
|
+
Clock3,
|
|
12
|
+
ShoppingBag,
|
|
13
|
+
type LucideIcon,
|
|
14
|
+
} from 'lucide-react'
|
|
3
15
|
import type { FlowEditorLabels } from './labels'
|
|
4
16
|
import type { FlowNodeData, GraphIssue } from './flowGraph'
|
|
5
17
|
|
|
@@ -31,6 +43,9 @@ const DEFAULT_ACTION_KIND_ICON: Record<string, LucideIcon> = {
|
|
|
31
43
|
// que só faz sentido para quem escreveu o interpretador do fluxo, não para quem está editando.
|
|
32
44
|
export function nodeLabel(node: FlowNodeData | undefined, labels: FlowEditorLabels): string {
|
|
33
45
|
if (!node) return '—'
|
|
46
|
+
// Apelido dado no editor manda sobre qualquer texto derivado: dois nós de mesma ação ficam
|
|
47
|
+
// idênticos no card sem ele, e é exatamente para desempatá-los que o campo existe.
|
|
48
|
+
if (node.label) return node.label
|
|
34
49
|
if (node.type === 'action') {
|
|
35
50
|
const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : undefined
|
|
36
51
|
// Sempre retorna string: um actionKind futuro sem label mapeado cai no próprio valor bruto
|
|
@@ -50,6 +65,12 @@ export type FlowNodeCardData = {
|
|
|
50
65
|
liveCount: number
|
|
51
66
|
isStart: boolean
|
|
52
67
|
isSelected: boolean
|
|
68
|
+
/**
|
|
69
|
+
* Nó sem nenhuma ligação de entrada — o bot nunca chega nele. Contorno tracejado e pulso: sem
|
|
70
|
+
* isso, desconectar um fio ou criar um card solto passa despercebido até o fluxo quebrar em
|
|
71
|
+
* produção.
|
|
72
|
+
*/
|
|
73
|
+
isDetached?: boolean
|
|
53
74
|
issues: GraphIssue[]
|
|
54
75
|
labels: FlowEditorLabels
|
|
55
76
|
actionKindIcons?: Record<string, LucideIcon>
|
|
@@ -83,7 +104,9 @@ function sourceRows(node: FlowNodeData, labels: FlowEditorLabels): { id: string;
|
|
|
83
104
|
function SourceRow({ label, isDefault, handleId }: { label: string; isDefault: boolean; handleId: string }) {
|
|
84
105
|
return (
|
|
85
106
|
<div className="relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3">
|
|
86
|
-
<span
|
|
107
|
+
<span
|
|
108
|
+
className={`text-xs truncate flex-1 ${isDefault ? 'italic text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-200'}`}
|
|
109
|
+
>
|
|
87
110
|
{label}
|
|
88
111
|
</span>
|
|
89
112
|
<Handle
|
|
@@ -98,18 +121,22 @@ function SourceRow({ label, isDefault, handleId }: { label: string; isDefault: b
|
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
export function FlowNodeCard({ data }: NodeProps) {
|
|
101
|
-
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } =
|
|
124
|
+
const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect } =
|
|
125
|
+
data as unknown as FlowNodeCardData
|
|
102
126
|
const label = nodeLabel(node, labels)
|
|
103
127
|
const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons }
|
|
104
|
-
const Icon =
|
|
128
|
+
const Icon =
|
|
129
|
+
node.type === 'action' && node.actionKind
|
|
130
|
+
? (iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type])
|
|
131
|
+
: NODE_TYPE_ICON[node.type]
|
|
105
132
|
const rows = sourceRows(node, labels)
|
|
106
133
|
const hasError = issues.some((i) => i.severity === 'error')
|
|
107
134
|
const hasWarning = !hasError && issues.some((i) => i.severity === 'warning')
|
|
108
135
|
|
|
109
136
|
return (
|
|
110
137
|
<div
|
|
111
|
-
|
|
112
|
-
className={`relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''}`}
|
|
138
|
+
data-cv-tooltip={isDetached ? labels.detachedNodeTooltip : label}
|
|
139
|
+
className={`relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''} ${isDetached ? 'border-dashed !border-amber-400 animate-pulse' : ''}`}
|
|
113
140
|
onClick={() => onSelect(node.id)}
|
|
114
141
|
>
|
|
115
142
|
<Handle type="target" position={Position.Top} id="target" className="!bg-gray-400 dark:!bg-gray-500" />
|
|
@@ -117,7 +144,7 @@ export function FlowNodeCard({ data }: NodeProps) {
|
|
|
117
144
|
<div className="flex items-center justify-between gap-2">
|
|
118
145
|
<span className="flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs">
|
|
119
146
|
<Icon size={12} strokeWidth={2.5} />
|
|
120
|
-
{isStart && <span className="h-1.5 w-1.5 rounded-full bg-emerald-500"
|
|
147
|
+
{isStart && <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" data-cv-tooltip={labels.startNodeTooltip} />}
|
|
121
148
|
{labels.legend[node.type]}
|
|
122
149
|
</span>
|
|
123
150
|
<div className="flex items-center gap-1">
|
|
@@ -125,7 +152,7 @@ export function FlowNodeCard({ data }: NodeProps) {
|
|
|
125
152
|
{hasWarning && <AlertTriangle size={13} className="text-amber-500 dark:text-amber-400" />}
|
|
126
153
|
{liveCount > 0 && (
|
|
127
154
|
<span
|
|
128
|
-
|
|
155
|
+
data-cv-tooltip={labels.liveCountTooltip(liveCount)}
|
|
129
156
|
className="font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs"
|
|
130
157
|
>
|
|
131
158
|
{liveCount}
|