ProfileHelpDialog.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <template>
  2. <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
  3. <div class="bg-page full-height column no-shadow">
  4. <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
  5. <q-btn v-close-popup class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" />
  6. <q-space />
  7. <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.help.title') }}</span>
  8. <q-space />
  9. <q-btn
  10. flat
  11. round
  12. dense
  13. icon="mdi-face-agent"
  14. color="primary"
  15. size="sm"
  16. @click="openHumanSupport"
  17. >
  18. <q-tooltip>{{ $t('profile.help.suggestion_human') }}</q-tooltip>
  19. </q-btn>
  20. <q-btn
  21. v-if="store.messages.length > 1"
  22. flat
  23. round
  24. dense
  25. icon="mdi-delete-outline"
  26. color="grey-5"
  27. size="sm"
  28. @click="clearChat"
  29. />
  30. </div>
  31. <div class="col overflow-auto">
  32. <div class="support-banner row no-wrap">
  33. <div class="col-8 q-pa-md">
  34. <div class="row items-center q-mb-xs q-gutter-x-sm">
  35. <span class="text-white font16 fontbold">{{ $t('profile.help.support_title') }}</span>
  36. </div>
  37. <div class="row items-center q-gutter-x-xs q-mb-md">
  38. <q-icon name="mdi-circle" color="green-4" size="10px" />
  39. <span class="text-white font14 fontmedium">{{ $t('profile.help.online_status') }}</span>
  40. </div>
  41. <div class="row items-center q-gutter-x-sm">
  42. <q-icon name="mdi-robot-outline" color="white" size="18px" />
  43. <span class="text-white font14 fontmedium">{{ $t('profile.help.ai_assistant_label') }}</span>
  44. </div>
  45. </div>
  46. <div class="col-4 flex items-end">
  47. <q-img
  48. :src="diarinho_suporte"
  49. style="width: 110px; height: 110px; object-fit: cover;"
  50. >
  51. <template #error>
  52. <div class="support-avatar-placeholder column flex-center">
  53. <q-icon name="mdi-face-agent" size="48px" color="white" />
  54. </div>
  55. </template>
  56. </q-img>
  57. </div>
  58. </div>
  59. <div ref="messagesContainer" class="q-px-md q-pt-lg q-pb-xl messages-area">
  60. <div
  61. v-for="(msg, idx) in store.messages"
  62. :key="idx"
  63. class="q-mb-md"
  64. :class="msg.role === 'user' ? 'row justify-end' : 'row justify-start'"
  65. >
  66. <div
  67. class="bubble q-pa-sm"
  68. :class="msg.role === 'user' ? 'bubble-user' : 'bubble-bot'"
  69. >
  70. <p class="q-mb-xs bubble-text">{{ msg.text }}</p>
  71. <span class="bubble-time">{{ msg.time }}</span>
  72. </div>
  73. </div>
  74. <div v-if="loading" class="row justify-start q-mb-md">
  75. <div class="bubble bubble-bot q-pa-sm">
  76. <div class="row items-center q-gutter-x-xs typing-indicator">
  77. <span /><span /><span />
  78. </div>
  79. </div>
  80. </div>
  81. <div v-if="store.messages.length === 1" class="q-pt-sm">
  82. <div class="col-12 text-grey-6 q-mb-sm">{{ $t('profile.help.quick_suggestions') }}</div>
  83. <div
  84. v-for="suggestion in suggestions"
  85. :key="suggestion.value"
  86. class="row col-12 q-py-xs"
  87. @click="onSuggestionClick(suggestion)"
  88. >
  89. <span class="text-text bg-surface suggestion-btn q-py-sm q-px-md cursor-pointer">{{ suggestion.label }}</span>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. <div class="bg-surface chat-footer q-px-md q-py-sm shadow-up">
  95. <div class="row items-center q-gutter-x-sm">
  96. <q-input
  97. v-model="messageInput"
  98. dense
  99. class="col input-suporte"
  100. borderless
  101. input-class="text-text"
  102. :placeholder="$t('profile.help.message_placeholder')"
  103. :disable="loading"
  104. :maxlength="MESSAGE_MAX_LENGTH"
  105. @keyup.enter="sendMessage()"
  106. />
  107. <q-btn
  108. round
  109. unelevated
  110. color="primary"
  111. icon="mdi-send"
  112. size="sm"
  113. :disable="!messageInput.trim() || loading"
  114. :loading="loading"
  115. @click="sendMessage()"
  116. />
  117. </div>
  118. <div class="footer-disclaimer text-text text-center q-my-md font9 fontregular">
  119. {{ $t('profile.help.footer_disclaimer') }}
  120. </div>
  121. </div>
  122. </div>
  123. </q-dialog>
  124. </template>
  125. <script setup>
  126. import { ref, computed, nextTick, onMounted } from 'vue';
  127. import { useDialogPluginComponent, useQuasar } from 'quasar';
  128. import { useI18n } from 'vue-i18n';
  129. import { chatbotStore } from 'src/stores/chatbot';
  130. import { sendChatMessage } from 'src/api/chatbot';
  131. import SupportRequestDialog from 'src/components/profile/SupportRequestDialog.vue';
  132. import diarinho_suporte from 'src/assets/diarinho_suporte.svg';
  133. defineEmits([...useDialogPluginComponent.emits]);
  134. const { dialogRef } = useDialogPluginComponent();
  135. const $q = useQuasar();
  136. const { t } = useI18n();
  137. const store = chatbotStore();
  138. const MESSAGE_MAX_LENGTH = 4000;
  139. const messageInput = ref('');
  140. const loading = ref(false);
  141. const messagesContainer = ref(null);
  142. const suggestions = computed(() => [
  143. { label: t('profile.help.suggestion_cancel'), value: 'cancel' },
  144. { label: t('profile.help.suggestion_data'), value: 'data' },
  145. { label: t('profile.help.suggestion_payment'), value: 'payment' },
  146. { label: t('profile.help.suggestion_human'), value: 'human' },
  147. ]);
  148. const onSuggestionClick = (suggestion) => {
  149. if (suggestion.value === 'human') {
  150. openHumanSupport();
  151. return;
  152. }
  153. sendMessage(suggestion.label);
  154. };
  155. const openHumanSupport = () => {
  156. $q.dialog({
  157. component: SupportRequestDialog,
  158. });
  159. };
  160. const scrollToBottom = async () => {
  161. await nextTick();
  162. if (messagesContainer.value) {
  163. messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
  164. }
  165. };
  166. const sendMessage = async (text) => {
  167. const content = (text ?? messageInput.value).trim();
  168. if (!content || loading.value) return;
  169. messageInput.value = '';
  170. store.addMessage('user', content);
  171. await scrollToBottom();
  172. loading.value = true;
  173. try {
  174. const history = store.messages
  175. .slice(0, -1)
  176. .map(({ role, text: msgText }) => ({ role, text: msgText }));
  177. const reply = await sendChatMessage({ message: content, history });
  178. store.addMessage('model', reply);
  179. } catch {
  180. store.addMessage('model', t('profile.help.error_message'));
  181. } finally {
  182. loading.value = false;
  183. await scrollToBottom();
  184. }
  185. };
  186. const clearChat = () => {
  187. store.clear();
  188. store.addMessage('model', t('profile.help.greeting_message'));
  189. };
  190. onMounted(() => {
  191. if (store.messages.length === 0) {
  192. store.addMessage('model', t('profile.help.greeting_message'));
  193. }
  194. scrollToBottom();
  195. });
  196. </script>
  197. <style scoped lang="scss">
  198. .support-banner {
  199. background: linear-gradient(180deg, #6C54C1 0%, #9A7FF6 100%);
  200. min-height: 120px;
  201. flex-shrink: 0;
  202. }
  203. .support-avatar-placeholder {
  204. width: 90px;
  205. height: 90px;
  206. border-radius: 50%;
  207. background: rgba(255, 255, 255, 0.2);
  208. }
  209. .messages-area {
  210. overflow-y: auto;
  211. }
  212. .bubble {
  213. max-width: 78%;
  214. border-radius: 16px;
  215. word-break: break-word;
  216. }
  217. .bubble-bot {
  218. background: white;
  219. border: 1px solid rgba(0, 0, 0, 0.08);
  220. border-bottom-left-radius: 4px;
  221. }
  222. .bubble-user {
  223. background: #6C54C1;
  224. border-bottom-right-radius: 4px;
  225. .bubble-text { color: white; }
  226. .bubble-time { color: rgba(255, 255, 255, 0.7); }
  227. }
  228. .bubble-text {
  229. line-height: 1.45;
  230. color: #2c2c2c;
  231. margin: 0 0 4px 0;
  232. white-space: pre-wrap;
  233. }
  234. .bubble-time {
  235. color: #aaa;
  236. display: block;
  237. text-align: right;
  238. }
  239. .typing-indicator {
  240. span {
  241. width: 7px;
  242. height: 7px;
  243. border-radius: 50%;
  244. background: #aaa;
  245. display: inline-block;
  246. animation: typing-bounce 1.2s infinite ease-in-out;
  247. &:nth-child(1) { animation-delay: 0s; }
  248. &:nth-child(2) { animation-delay: 0.2s; }
  249. &:nth-child(3) { animation-delay: 0.4s; }
  250. }
  251. }
  252. @keyframes typing-bounce {
  253. 0%, 60%, 100% { transform: translateY(0); }
  254. 30% { transform: translateY(-6px); }
  255. }
  256. .suggestion-btn {
  257. border-radius: 32px;
  258. border: 1.15px solid #d8d7d7ce;
  259. line-height: 16px;
  260. transition: background 0.2s;
  261. &:active { background: #ede8fc !important; }
  262. }
  263. .chat-footer {
  264. border-top: 1px solid rgba(0, 0, 0, 0.06);
  265. flex-shrink: 0;
  266. }
  267. .shadow-up {
  268. box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
  269. }
  270. .shadow-profile {
  271. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
  272. }
  273. .input-suporte {
  274. :deep(.q-field__control) {
  275. background: transparent;
  276. border: none;
  277. box-shadow: none;
  278. padding: 0;
  279. }
  280. :deep(.q-field__control-container) {
  281. background: var(--q-page, #f5f5f5);
  282. border: 1px solid rgba(0, 0, 0, 0.12);
  283. border-radius: 32px;
  284. padding: 10px 14px;
  285. }
  286. :deep(.q-field__native) { padding: 0 !important; }
  287. :deep(.q-field__control::before),
  288. :deep(.q-field__control::after) { display: none !important; }
  289. :deep(.q-field__bottom),
  290. :deep(.q-field__marginal) { display: none; }
  291. :deep(.q-field__control),
  292. :deep(.q-field__control:before),
  293. :deep(.q-field__control:after) {
  294. border: none !important;
  295. box-shadow: none !important;
  296. outline: none !important;
  297. }
  298. :deep(.q-field--focused .q-field__control-container) {
  299. box-shadow: none !important;
  300. border-color: rgba(0, 0, 0, 0.12) !important;
  301. }
  302. }
  303. .footer-disclaimer {
  304. line-height: 15px;
  305. text-align: center;
  306. }
  307. </style>