@meistrari/chat-nuxt 3.9.1 → 3.10.0

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.
Files changed (87) hide show
  1. package/README.md +50 -440
  2. package/dist/module.d.mts +1 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +56 -3
  5. package/dist/runtime/assets/css/markstream.css +1 -1
  6. package/dist/runtime/conversations/components/chat-conversation-creator-filter.vue +11 -67
  7. package/dist/runtime/conversations/components/chat-conversation-item.d.vue.ts +2 -1
  8. package/dist/runtime/conversations/components/chat-conversation-item.vue +29 -33
  9. package/dist/runtime/conversations/components/chat-conversation-item.vue.d.ts +2 -1
  10. package/dist/runtime/conversations/components/chat-conversation-list.d.vue.ts +0 -1
  11. package/dist/runtime/conversations/components/chat-conversation-list.vue +117 -158
  12. package/dist/runtime/conversations/components/chat-conversation-list.vue.d.ts +0 -1
  13. package/dist/runtime/conversations/components/chat-conversation-new-group-button.vue +34 -73
  14. package/dist/runtime/conversations/components/chat-conversation-search.vue +1 -21
  15. package/dist/runtime/conversations/components/chat-widget-history-list.d.vue.ts +12 -0
  16. package/dist/runtime/conversations/components/chat-widget-history-list.vue +27 -0
  17. package/dist/runtime/conversations/components/chat-widget-history-list.vue.d.ts +12 -0
  18. package/dist/runtime/conversations/composables/conversation-groups.d.ts +1 -0
  19. package/dist/runtime/conversations/composables/conversation-groups.js +2 -0
  20. package/dist/runtime/conversations/composables/conversation-item.js +4 -4
  21. package/dist/runtime/conversations/composables/conversation-list.js +14 -5
  22. package/dist/runtime/embed/components/chat-configuration-modal.d.vue.ts +3 -3
  23. package/dist/runtime/embed/components/chat-configuration-modal.vue.d.ts +3 -3
  24. package/dist/runtime/embed/components/chat-embed-inner.d.vue.ts +6 -3
  25. package/dist/runtime/embed/components/chat-embed-inner.vue +365 -152
  26. package/dist/runtime/embed/components/chat-embed-inner.vue.d.ts +6 -3
  27. package/dist/runtime/embed/components/chat-embed.d.vue.ts +31 -4
  28. package/dist/runtime/embed/components/chat-embed.vue +10 -4
  29. package/dist/runtime/embed/components/chat-embed.vue.d.ts +31 -4
  30. package/dist/runtime/embed/components/chat-topbar.vue +83 -85
  31. package/dist/runtime/embed/components/meistrari-chat-embed.d.vue.ts +5 -3
  32. package/dist/runtime/embed/components/meistrari-chat-embed.vue +118 -57
  33. package/dist/runtime/embed/components/meistrari-chat-embed.vue.d.ts +5 -3
  34. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.d.vue.ts +0 -1
  35. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue +2 -45
  36. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue.d.ts +0 -1
  37. package/dist/runtime/embed/components/mobile/chat-mobile-home-suggestions.d.vue.ts +9 -5
  38. package/dist/runtime/embed/components/mobile/chat-mobile-home-suggestions.vue +14 -25
  39. package/dist/runtime/embed/components/mobile/chat-mobile-home-suggestions.vue.d.ts +9 -5
  40. package/dist/runtime/embed/components/mobile/chat-mobile-shell.d.vue.ts +6 -6
  41. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue +2 -3
  42. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +6 -6
  43. package/dist/runtime/embed/feature-config.d.ts +7 -0
  44. package/dist/runtime/embed/types.d.ts +47 -8
  45. package/dist/runtime/files/icons.d.ts +1 -1
  46. package/dist/runtime/files/icons.js +1 -1
  47. package/dist/runtime/messages/components/chat-mention-dropdown.d.vue.ts +6 -1
  48. package/dist/runtime/messages/components/chat-mention-dropdown.vue +46 -22
  49. package/dist/runtime/messages/components/chat-mention-dropdown.vue.d.ts +6 -1
  50. package/dist/runtime/messages/components/chat-message-bubble.vue +53 -71
  51. package/dist/runtime/messages/components/chat-message-feedback.vue +25 -229
  52. package/dist/runtime/messages/components/chat-message-input.vue +176 -134
  53. package/dist/runtime/messages/components/chat-message-list.d.vue.ts +6 -0
  54. package/dist/runtime/messages/components/chat-message-list.vue +27 -23
  55. package/dist/runtime/messages/components/chat-message-list.vue.d.ts +6 -0
  56. package/dist/runtime/messages/components/chat-model-selector.vue +10 -22
  57. package/dist/runtime/messages/components/reasoning/chat-reasoning-message.vue +2 -3
  58. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +60 -69
  59. package/dist/runtime/messages/components/reasoning/chat-thinking-indicator.vue +3 -9
  60. package/dist/runtime/messages/components/widgets/chat-bash-widget.vue +3 -12
  61. package/dist/runtime/messages/components/widgets/chat-doc-sdk-widget.vue +3 -12
  62. package/dist/runtime/messages/components/widgets/chat-generic-tool-widget.vue +3 -12
  63. package/dist/runtime/messages/components/widgets/chat-grep-widget.vue +7 -17
  64. package/dist/runtime/messages/components/widgets/chat-read-widget.vue +7 -17
  65. package/dist/runtime/messages/components/widgets/chat-tela-skill-widget.vue +4 -13
  66. package/dist/runtime/messages/components/widgets/chat-thinking-widget.vue +11 -22
  67. package/dist/runtime/messages/components/widgets/chat-web-search-widget.vue +13 -40
  68. package/dist/runtime/messages/components/widgets/chat-workstation-query-widget.vue +52 -65
  69. package/dist/runtime/messages/components/widgets/chat-write-widget.vue +8 -18
  70. package/dist/runtime/messages/composables/mention-autocomplete.js +4 -0
  71. package/dist/runtime/messages/message-reasoning.js +4 -0
  72. package/dist/runtime/messages/message-state.js +1 -0
  73. package/dist/runtime/public/files/generic.svg +2 -5
  74. package/dist/runtime/public/files/image.svg +2 -1
  75. package/dist/runtime/types/embed.d.ts +1 -1
  76. package/dist/runtime/types/schemas/chat/conversations.d.ts +2 -0
  77. package/dist/runtime/types/schemas/chat/conversations.js +3 -0
  78. package/dist/runtime/types/schemas/chat/messages.d.ts +4 -0
  79. package/dist/runtime/types/schemas/chat/messages.js +7 -0
  80. package/dist/types.d.mts +1 -1
  81. package/package.json +5 -4
  82. package/dist/runtime/conversations/components/chat-conversation-new-group-popover-content.d.vue.ts +0 -23
  83. package/dist/runtime/conversations/components/chat-conversation-new-group-popover-content.vue +0 -75
  84. package/dist/runtime/conversations/components/chat-conversation-new-group-popover-content.vue.d.ts +0 -23
  85. package/dist/runtime/messages/components/reasoning/chat-reasoning-step-item.d.vue.ts +0 -37
  86. package/dist/runtime/messages/components/reasoning/chat-reasoning-step-item.vue +0 -125
  87. package/dist/runtime/messages/components/reasoning/chat-reasoning-step-item.vue.d.ts +0 -37
@@ -1,4 +1,4 @@
1
1
  export declare const FILE_ICONS: Record<string, string>;
2
- export declare function getFileIcon(mimeType: string): string;
2
+ export declare function getFileIcon(mimeType?: string): string;
3
3
  export declare function getMimeIcon(mimeType?: string): string;
4
4
  export declare function getMimeLabel(mimeType?: string): string;
@@ -42,7 +42,7 @@ export const FILE_ICONS = {
42
42
  "unknown": "/chat-files/generic.svg"
43
43
  };
44
44
  export function getFileIcon(mimeType) {
45
- return FILE_ICONS[mimeType] ?? "/chat-files/generic.svg";
45
+ return FILE_ICONS[mimeType ?? "unknown"] ?? "/chat-files/generic.svg";
46
46
  }
47
47
  export function getMimeIcon(mimeType) {
48
48
  if (!mimeType)
@@ -1,9 +1,14 @@
1
1
  import type { MentionItem } from '../../types/schemas/chat/ui.js';
2
+ type MentionDropdownItem = MentionItem & {
3
+ iconUrl?: string;
4
+ };
2
5
  type __VLS_Props = {
3
- items: MentionItem[];
6
+ items: MentionDropdownItem[];
4
7
  highlightedIndex: number;
5
8
  trigger: '@' | '/';
6
9
  query: string;
10
+ /** Viewport rect of the trigger character ('@' / '/') the panel anchors to. */
11
+ caretRect?: DOMRect | null;
7
12
  };
8
13
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
14
  select: (item: MentionItem) => any;
@@ -3,10 +3,21 @@ const props = defineProps({
3
3
  items: { type: Array, required: true },
4
4
  highlightedIndex: { type: Number, required: true },
5
5
  trigger: { type: String, required: true },
6
- query: { type: String, required: true }
6
+ query: { type: String, required: true },
7
+ caretRect: { type: null, required: false }
7
8
  });
8
9
  const emit = defineEmits(["select"]);
9
10
  const listRef = ref(null);
11
+ const panelStyle = computed(() => {
12
+ const rect = props.caretRect;
13
+ if (!rect)
14
+ return {};
15
+ return {
16
+ position: "fixed",
17
+ left: `${Math.max(8, Math.min(rect.left, window.innerWidth - 416))}px`,
18
+ bottom: `${window.innerHeight - rect.top + 4}px`
19
+ };
20
+ });
10
21
  const sections = computed(() => {
11
22
  if (props.trigger === "@") {
12
23
  return [{
@@ -43,41 +54,54 @@ watch(() => props.highlightedIndex, () => {
43
54
  </script>
44
55
 
45
56
  <template>
57
+ <!-- Custom listbox (not a reka menu: it must stay open and keep focus in
58
+ the contenteditable while the user types). Surface and item styling
59
+ mirror TelaMenubarContent / TelaMenubarItem exactly. -->
46
60
  <div
47
- absolute bottom="100%" left-0 right-0 mb-4px
48
- z-10
49
- bg-white
50
- rounded-12px
51
- class="mention-dropdown custom-scrollbar"
52
- max-h-240px
53
- overflow-y-auto
61
+ :style="panelStyle"
62
+ class="mention-dropdown"
63
+ w-max min-w-244px max-w-400px
64
+ max-h-260px overflow-y-auto no-scrollbar
65
+ rounded-2xl
66
+ b-0.5px border-strong
67
+ bg
68
+ p-4px
54
69
  >
55
- <div v-if="items.length === 0" px-12px py-10px>
56
- <span text-13px text-gray-400>{{ emptyMessage }}</span>
70
+ <div v-if="items.length === 0" px-12px py-6px>
71
+ <span body-14-regular text-tertiary>{{ emptyMessage }}</span>
57
72
  </div>
58
73
 
59
- <div ref="listRef" pb-8px flex="~ col" gap-4px>
74
+ <div v-else ref="listRef" flex="~ col">
60
75
  <template v-for="section in sections" :key="section.title">
61
- <div px-12px pt-10px pb-0>
62
- <span text-11px font-500 text-gray-400 uppercase tracking-wide>{{ section.title }}</span>
76
+ <div px-12px pb-2px>
77
+ <span body-12-medium text-tertiary>{{ section.title }}</span>
63
78
  </div>
64
79
  <button
65
80
  v-for="entry in section.items"
66
81
  :key="entry.item.id"
82
+ type="button"
67
83
  :data-highlighted="entry.globalIndex === highlightedIndex"
68
84
  flex items-center gap-8px
69
- w-full
70
- h-48px
71
- px-12px
85
+ w-full min-w-0
86
+ rounded-10px
87
+ px-12px py-4px
72
88
  text-left
73
- transition-colors
74
- :class="entry.globalIndex === highlightedIndex ? 'bg-gray-200' : 'hover:bg-gray-50'"
89
+ cursor-pointer select-none
90
+ outline-none
91
+ :class="entry.globalIndex === highlightedIndex ? 'bg-muted' : 'hover:bg-muted'"
75
92
  @mousedown.prevent="emit('select', entry.item)"
76
93
  >
77
- <div :class="entry.item.icon" text-16px text-gray-500 shrink-0 />
94
+ <img
95
+ v-if="entry.item.iconUrl"
96
+ :src="entry.item.iconUrl"
97
+ alt=""
98
+ aria-hidden="true"
99
+ w-18px h-18px shrink-0
100
+ >
101
+ <TelaIcon v-else :name="entry.item.icon" size="18px" color="icon-secondary" shrink-0 />
78
102
  <div flex="~ col" min-w-0>
79
- <span text-13px text-gray-900 truncate>{{ entry.item.label }}</span>
80
- <span v-if="entry.item.description" text-11px text-gray-400 truncate>{{ entry.item.description }}</span>
103
+ <span body-12-medium text-primary truncate>{{ entry.item.label }}</span>
104
+ <span v-if="entry.item.description" body-10-regular leading-12px text-tertiary truncate>{{ entry.item.description }}</span>
81
105
  </div>
82
106
  </button>
83
107
  </template>
@@ -86,5 +110,5 @@ watch(() => props.highlightedIndex, () => {
86
110
  </template>
87
111
 
88
112
  <style scoped>
89
- .mention-dropdown{animation:mention-dropdown-in .25s ease-out;border:.5px solid #e5e7eb;box-shadow:0 3px 10px 0 hsla(0,0%,64%,.08),0 12px 20px 0 hsla(0,0%,64%,.06)}.custom-scrollbar{scrollbar-color:#d1d5db transparent;scrollbar-width:thin}.custom-scrollbar::-webkit-scrollbar{width:4px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#d1d5db;border-radius:3px}@keyframes mention-dropdown-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
113
+ .mention-dropdown{bottom:100%;box-shadow:0 4px 16px 0 rgba(85,91,98,.14);margin-left:-4px;position:absolute;z-index:50}
90
114
  </style>
@@ -1,9 +1,14 @@
1
1
  import type { MentionItem } from '../../types/schemas/chat/ui.js';
2
+ type MentionDropdownItem = MentionItem & {
3
+ iconUrl?: string;
4
+ };
2
5
  type __VLS_Props = {
3
- items: MentionItem[];
6
+ items: MentionDropdownItem[];
4
7
  highlightedIndex: number;
5
8
  trigger: '@' | '/';
6
9
  query: string;
10
+ /** Viewport rect of the trigger character ('@' / '/') the panel anchors to. */
11
+ caretRect?: DOMRect | null;
7
12
  };
8
13
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
14
  select: (item: MentionItem) => any;
@@ -2,6 +2,7 @@
2
2
  import { useChatAuth } from "#chat-auth";
3
3
  import { useEmbedConfig } from "../../embed/composables/embed-config";
4
4
  import { useWorkspaceMembers } from "../../workspace/composables/workspace-members";
5
+ import { isPromptTooLongFailure } from "../../types/schemas/chat/messages.js";
5
6
  import { parseContentWithFiles } from "../../files/content-detection";
6
7
  import ChatVaultLink from "../../files/components/chat-vault-link.vue";
7
8
  const props = defineProps({
@@ -39,6 +40,7 @@ const isOwnMessage = computed(() => {
39
40
  });
40
41
  const isPending = computed(() => props.message.status === "pending");
41
42
  const isFailed = computed(() => props.message.status === "failed");
43
+ const isPromptTooLong = computed(() => isFailed.value && isPromptTooLongFailure(props.message.failReason));
42
44
  const hasReasoning = computed(() => {
43
45
  const data = props.message.reasoningData;
44
46
  return props.message.role === "assistant" && data && Array.isArray(data) && data.length > 0;
@@ -104,9 +106,9 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
104
106
  </script>
105
107
 
106
108
  <template>
107
- <div
108
- flex="~ col"
109
- :class="isOwnMessage ? 'items-end' : 'items-start'"
109
+ <TelaChatMessage
110
+ :from="message.role === 'user' ? 'user' : 'assistant'"
111
+ :align="isOwnMessage ? 'end' : 'start'"
110
112
  >
111
113
  <template v-if="message.role === 'user'">
112
114
  <div
@@ -121,46 +123,20 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
121
123
  items-center
122
124
  gap-4px
123
125
  >
124
- <div h-16px w-16px flex-shrink-0 overflow-hidden rounded-full>
125
- <img
126
- v-if="senderImage"
127
- :src="senderImage"
128
- :alt="senderName"
129
- h-full w-full object-cover
130
- >
131
- <div
132
- v-else
133
- h-full w-full
134
- flex
135
- items-center
136
- justify-center
137
- bg-neutral-200
138
- >
139
- <span text-8px text-neutral-500 font-medium>
140
- {{ senderName.charAt(0).toUpperCase() }}
141
- </span>
142
- </div>
143
- </div>
126
+ <TelaChatMessageAvatar
127
+ :src="senderImage ?? void 0"
128
+ :name="senderName"
129
+ size="2xs"
130
+ />
144
131
  <span text-12px leading-16px font-semibold text-primary>
145
132
  {{ senderName }}
146
133
  </span>
147
134
  </div>
148
135
 
149
- <div
150
- flex="~ col"
151
- gap-12px
152
- px-24px
153
- pt-16px
154
- pb-20px
155
- bg-subtle
156
- text-primary
157
- border-0.5px
158
- border-solid
159
- border-subtle
160
- :class="[
161
- isOwnMessage ? 'rounded-tl-[16px] rounded-tr-[16px] rounded-br-[4px] rounded-bl-[16px]' : 'rounded-tl-[4px] rounded-tr-[16px] rounded-br-[16px] rounded-bl-[16px]',
162
- isPending && 'op-80'
163
- ]"
136
+ <TelaChatMessageContent
137
+ variant="contained"
138
+ :align="isOwnMessage ? 'end' : 'start'"
139
+ :class="isPending && 'op-80'"
164
140
  >
165
141
  <div v-if="hasFiles" flex="~ col" gap-4px mb-8px>
166
142
  <a
@@ -186,7 +162,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
186
162
  <div v-else body-16-regular whitespace-pre-wrap>
187
163
  {{ message.content }}
188
164
  </div>
189
- </div>
165
+ </TelaChatMessageContent>
190
166
 
191
167
  <div
192
168
  mt-8px
@@ -213,35 +189,43 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
213
189
  v-if="!isPending && message.content"
214
190
  max-w-prose
215
191
  :class="[
216
- isFailed ? 'flex gap-12px items-start p-16px bg-subtle border border-solid rounded-12px w-full' : 'p-16px rounded-12px code-theme'
192
+ isFailed ? 'flex gap-12px items-start bg-subtle border border-solid rounded-12px w-full' : 'rounded-12px code-theme'
217
193
  ]"
218
194
  >
219
195
  <template v-if="isFailed">
220
196
  <div i-ph-warning-circle-fill text-16px text-red-600 shrink-0 mt-2px />
221
197
  <div flex flex-col gap-4px grow>
222
198
  <div flex items-center gap-4px>
223
- <span heading-h5-semibold text-primary>Erro</span>
199
+ <span heading-h5-semibold text-primary>{{ isPromptTooLong ? "Limite de contexto atingido" : "Erro" }}</span>
224
200
  <span body-10-regular text-secondary>{{ formatTime(message.createdAt) }}</span>
225
201
  </div>
226
- <div body-12-regular text-secondary whitespace-pre-wrap>
227
- {{ displayContent }}
202
+ <div v-if="isPromptTooLong" body-12-regular text-secondary whitespace-pre-wrap>
203
+ Esta conversa atingiu o limite de contexto do modelo e não pode continuar. Inicie uma nova conversa para seguir com o assunto.
228
204
  </div>
229
- <button
230
- mt-8px
231
- flex items-center gap-6px
232
- px-12px py-6px
233
- rounded-6px
234
- bg-gray-100
235
- hover:bg-gray-200
236
- text-13px text-gray-700
237
- transition-colors
238
- cursor-pointer
239
- self-start
240
- @click="emit('retry', message.id)"
241
- >
242
- <div i-ph-arrow-clockwise text-14px />
243
- <span>Tentar novamente</span>
244
- </button>
205
+ <template v-else>
206
+ <div body-12-regular text-secondary whitespace-pre-wrap>
207
+ {{ displayContent }}
208
+ </div>
209
+ <div v-if="message.failReason" body-10-regular text-tertiary whitespace-pre-wrap>
210
+ {{ message.failReason }}
211
+ </div>
212
+ <button
213
+ mt-8px
214
+ flex items-center gap-6px
215
+ px-12px py-6px
216
+ rounded-6px
217
+ bg-gray-100
218
+ hover:bg-gray-200
219
+ text-13px text-gray-700
220
+ transition-colors
221
+ cursor-pointer
222
+ self-start
223
+ @click="emit('retry', message.id)"
224
+ >
225
+ <div i-ph-arrow-clockwise text-14px />
226
+ <span>Tentar novamente</span>
227
+ </button>
228
+ </template>
245
229
  </div>
246
230
  </template>
247
231
  <template v-else>
@@ -285,23 +269,21 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
285
269
  mt-8px
286
270
  flex
287
271
  items-center
288
- gap-6px
272
+ gap-8px
289
273
  body-11-regular
290
- text-black-400
274
+ text-tertiary
291
275
  >
292
- <div w-full flex items-center justify-between gap-12px>
293
- <span>{{ formatTime(message.updatedAt || message.createdAt) }}</span>
294
- <div flex items-center gap-8px>
295
- <span v-if="formattedTtft" flex items-center gap-4px text-black-300>
296
- <span i-ph-timer text-12px />
297
- TTFT: {{ formattedTtft }}
298
- </span>
299
- <ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />
300
- </div>
276
+ <span>{{ formatTime(message.updatedAt || message.createdAt) }}</span>
277
+ <TelaDotSeparator v-if="formattedTtft || hasMessageFeedback" />
278
+ <div flex items-center gap-8px>
279
+ <span v-if="formattedTtft" flex items-center gap-4px>
280
+ {{ formattedTtft }}
281
+ </span>
282
+ <ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />
301
283
  </div>
302
284
  </div>
303
285
  </template>
304
286
  </div>
305
287
  </template>
306
- </div>
288
+ </TelaChatMessage>
307
289
  </template>
@@ -7,245 +7,41 @@ const props = defineProps({
7
7
  const embedConfig = useEmbedConfig();
8
8
  const { emit: emitFeedback } = useMessageFeedback();
9
9
  const config = computed(() => embedConfig?.feedbackConfig.value ?? null);
10
- const positiveReasons = computed(() => config.value?.positiveReasons ?? []);
11
- const negativeReasons = computed(() => config.value?.negativeReasons ?? []);
10
+ const positiveReasons = computed(() => [...config.value?.positiveReasons ?? []]);
11
+ const negativeReasons = computed(() => [...config.value?.negativeReasons ?? []]);
12
12
  const persistedMap = computed(() => embedConfig?.messageFeedback.value ?? null);
13
13
  const persistedRating = computed(() => persistedMap.value?.[props.message.id] ?? null);
14
14
  const localRating = ref(null);
15
15
  const rating = computed(() => persistedMap.value ? persistedRating.value : localRating.value);
16
- const openRating = ref(null);
17
- const positiveOpen = computed({
18
- get: () => openRating.value === "positive",
19
- set: (open) => {
20
- openRating.value = open ? "positive" : null;
21
- }
22
- });
23
- const negativeOpen = computed({
24
- get: () => openRating.value === "negative",
25
- set: (open) => {
26
- openRating.value = open ? "negative" : null;
27
- }
28
- });
29
- const selectedReasons = ref([]);
30
- const comment = ref("");
31
- watch(openRating, (opened) => {
32
- if (opened) {
33
- selectedReasons.value = [];
34
- comment.value = "";
35
- }
36
- });
37
- const canSubmitNegative = computed(() => {
38
- if (!config.value?.requireReason)
39
- return true;
40
- if (negativeReasons.value.length > 0)
41
- return selectedReasons.value.length > 0;
42
- return comment.value.trim().length > 0;
43
- });
44
- function submitRating(next) {
16
+ function handleSubmit(payload) {
45
17
  emitFeedback({
46
18
  conversationId: props.message.conversationId,
47
19
  messageId: props.message.id,
48
- rating: next,
49
- reasons: [...selectedReasons.value],
50
- comment: comment.value.trim()
20
+ rating: payload.rating,
21
+ reasons: payload.reasons,
22
+ comment: payload.comment
51
23
  });
52
- localRating.value = next;
53
- openRating.value = null;
54
- }
55
- function toggleReason(reason) {
56
- selectedReasons.value = selectedReasons.value.includes(reason) ? selectedReasons.value.filter((item) => item !== reason) : [...selectedReasons.value, reason];
24
+ localRating.value = payload.rating;
57
25
  }
58
26
  </script>
59
27
 
60
28
  <template>
61
- <div flex items-center gap-2px>
62
- <!-- Frictionless 👍: with no positive reasons configured there is nothing
63
- to ask, so the vote submits directly instead of opening an empty popover. -->
64
- <button
65
- v-if="positiveReasons.length === 0"
66
- type="button"
67
- h-28px w-28px
68
- flex items-center justify-center
69
- rounded-8px
70
- transition-colors
71
- cursor-pointer
72
- :class="rating === 'positive' ? 'bg-green-100 text-green-700' : 'text-black-400 hover:bg-neutral-100 hover:text-black-600'"
73
- aria-label="Resposta útil"
74
- title="Resposta útil"
75
- @click="submitRating('positive')"
76
- >
77
- <span :class="rating === 'positive' ? 'i-ph-thumbs-up-fill' : 'i-ph-thumbs-up'" text-15px />
78
- </button>
79
- <TelaPopover v-else v-model:open="positiveOpen">
80
- <TelaPopoverTrigger as-child>
81
- <button
82
- type="button"
83
- h-28px w-28px
84
- flex items-center justify-center
85
- rounded-8px
86
- transition-colors
87
- cursor-pointer
88
- :class="rating === 'positive' ? 'bg-green-100 text-green-700' : 'text-black-400 hover:bg-neutral-100 hover:text-black-600'"
89
- aria-label="Resposta útil"
90
- title="Resposta útil"
91
- >
92
- <span :class="rating === 'positive' ? 'i-ph-thumbs-up-fill' : 'i-ph-thumbs-up'" text-15px />
93
- </button>
94
- </TelaPopoverTrigger>
95
- <TelaPopoverContent
96
- align="end"
97
- :side-offset="4"
98
- class="!w-auto !rounded-12px !border-neutral-200 !bg-white !p-0 !text-neutral-900 !shadow-lg"
99
- >
100
- <div w-288px p-12px flex="~ col" gap-10px>
101
- <div flex="~ col" gap-2px>
102
- <span text-14px font-medium text-neutral-900 leading-18px>O que ajudou?</span>
103
- <span text-12px text-neutral-500 leading-16px>Marcar o que funcionou refina as próximas respostas.</span>
104
- </div>
105
- <div flex flex-wrap gap-6px>
106
- <button
107
- v-for="reason in positiveReasons"
108
- :key="reason"
109
- type="button"
110
- h-26px px-10px
111
- rounded-full
112
- b=".5px solid"
113
- text-12px leading-16px
114
- transition-colors
115
- cursor-pointer
116
- :class="selectedReasons.includes(reason) ? 'bg-green-100 text-green-700 border-transparent font-medium' : 'bg-white text-neutral-600 border-neutral-300 hover:bg-neutral-50'"
117
- @click="toggleReason(reason)"
118
- >
119
- {{ reason }}
120
- </button>
121
- </div>
122
- <textarea
123
- v-model="comment"
124
- rows="2"
125
- placeholder="Comentário (opcional)…"
126
- aria-label="Comentário"
127
- w-full
128
- px-10px py-8px
129
- rounded-8px
130
- b=".5px neutral-300"
131
- bg-white
132
- outline-none
133
- resize-none
134
- text-13px text-neutral-900 leading-18px
135
- focus:b-neutral-900
136
- @keydown.escape.prevent.stop="positiveOpen = false"
137
- />
138
- <div flex justify-end gap-6px>
139
- <button
140
- type="button"
141
- h-28px px-10px rounded-8px
142
- text-13px font-medium text-neutral-700
143
- hover:bg-neutral-100
144
- transition-colors
145
- @click="positiveOpen = false"
146
- >
147
- Cancelar
148
- </button>
149
- <button
150
- type="button"
151
- h-28px px-10px rounded-8px
152
- bg-neutral-900 text-white
153
- text-13px font-medium
154
- transition-opacity
155
- @click="submitRating('positive')"
156
- >
157
- Enviar
158
- </button>
159
- </div>
160
- </div>
161
- </TelaPopoverContent>
162
- </TelaPopover>
163
-
164
- <TelaPopover v-model:open="negativeOpen">
165
- <TelaPopoverTrigger as-child>
166
- <button
167
- type="button"
168
- h-28px w-28px
169
- flex items-center justify-center
170
- rounded-8px
171
- transition-colors
172
- cursor-pointer
173
- :class="rating === 'negative' ? 'bg-red-100 text-red-700' : 'text-black-400 hover:bg-neutral-100 hover:text-black-600'"
174
- aria-label="Resposta não útil"
175
- title="Resposta não útil"
176
- >
177
- <span :class="rating === 'negative' ? 'i-ph-thumbs-down-fill' : 'i-ph-thumbs-down'" text-15px />
178
- </button>
179
- </TelaPopoverTrigger>
180
- <TelaPopoverContent
181
- align="end"
182
- :side-offset="4"
183
- class="!w-auto !rounded-12px !border-neutral-200 !bg-white !p-0 !text-neutral-900 !shadow-lg"
184
- >
185
- <div w-288px p-12px flex="~ col" gap-10px>
186
- <div flex="~ col" gap-2px>
187
- <span text-14px font-medium text-neutral-900 leading-18px>O que não funcionou?</span>
188
- <span text-12px text-neutral-500 leading-16px>Seu retorno vai para a triagem de qualidade.</span>
189
- </div>
190
- <div v-if="negativeReasons.length > 0" flex flex-wrap gap-6px>
191
- <button
192
- v-for="reason in negativeReasons"
193
- :key="reason"
194
- type="button"
195
- h-26px px-10px
196
- rounded-full
197
- b=".5px solid"
198
- text-12px leading-16px
199
- transition-colors
200
- cursor-pointer
201
- :class="selectedReasons.includes(reason) ? 'bg-blue-100 text-blue-900 border-transparent font-medium' : 'bg-white text-neutral-600 border-neutral-300 hover:bg-neutral-50'"
202
- @click="toggleReason(reason)"
203
- >
204
- {{ reason }}
205
- </button>
206
- </div>
207
- <textarea
208
- v-model="comment"
209
- rows="2"
210
- placeholder="Conte o que faltou (opcional)…"
211
- aria-label="Comentário"
212
- w-full
213
- px-10px py-8px
214
- rounded-8px
215
- b=".5px neutral-300"
216
- bg-white
217
- outline-none
218
- resize-none
219
- text-13px text-neutral-900 leading-18px
220
- focus:b-neutral-900
221
- @keydown.escape.prevent.stop="negativeOpen = false"
222
- />
223
- <div flex justify-end gap-6px>
224
- <button
225
- type="button"
226
- h-28px px-10px rounded-8px
227
- text-13px font-medium text-neutral-700
228
- hover:bg-neutral-100
229
- transition-colors
230
- @click="negativeOpen = false"
231
- >
232
- Cancelar
233
- </button>
234
- <button
235
- type="button"
236
- h-28px px-10px rounded-8px
237
- bg-neutral-900 text-white
238
- text-13px font-medium
239
- transition-opacity
240
- :disabled="!canSubmitNegative"
241
- :class="!canSubmitNegative && 'opacity-40 cursor-not-allowed'"
242
- @click="submitRating('negative')"
243
- >
244
- Enviar
245
- </button>
246
- </div>
247
- </div>
248
- </TelaPopoverContent>
249
- </TelaPopover>
250
- </div>
29
+ <TelaChatMessageFeedback
30
+ :rating="rating"
31
+ :positive-reasons="positiveReasons"
32
+ :negative-reasons="negativeReasons"
33
+ :require-reason="config?.requireReason ?? false"
34
+ positive-title="O que ajudou?"
35
+ positive-description="Marcar o que funcionou refina as próximas respostas."
36
+ positive-comment-placeholder="Comentário (opcional)…"
37
+ negative-title="O que não funcionou?"
38
+ negative-description="Seu retorno vai para a triagem de qualidade."
39
+ negative-comment-placeholder="Conte o que faltou (opcional)…"
40
+ cancel-label="Cancelar"
41
+ submit-label="Enviar"
42
+ helpful-label="Resposta útil"
43
+ not-helpful-label="Resposta não útil"
44
+ comment-label="Comentário"
45
+ @submit="handleSubmit"
46
+ />
251
47
  </template>