Selaa lähdekoodia

feat: :sparkles: feat (chatbot implementado chatbot

foi implementado chatbot no perfil do cliente e do prestador

fase:dev | origin:escopo
Gustavo Zanatta 6 päivää sitten
vanhempi
commit
c30ad8292c

+ 6 - 0
src/api/chatbot.js

@@ -0,0 +1,6 @@
+import api from 'src/api';
+
+export const sendChatMessage = async ({ message, history = [] }) => {
+  const { data } = await api.post('/chatbot/message', { message, history });
+  return data.payload.reply;
+};

+ 163 - 51
src/components/profile/ProfileHelpDialog.vue

@@ -1,12 +1,23 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height column no-shadow">
+
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-subtitle1 text-weight-bold text-primary">{{ $t('profile.help.title') }}</span>
         <q-space />
-        <div style="width: 32px"></div>
+        <q-btn
+          v-if="store.messages.length > 1"
+          flat
+          round
+          dense
+          icon="mdi-delete-outline"
+          color="grey-5"
+          size="sm"
+          @click="clearChat"
+        />
+        <div v-else style="width: 32px"></div>
       </div>
 
       <div class="col overflow-auto">
@@ -39,28 +50,43 @@
           </div>
         </div>
 
-        <div class="q-px-md q-pt-lg q-pb-xl">
+        <div ref="messagesContainer" class="q-px-md q-pt-lg q-pb-xl messages-area">
 
-          <q-card class="bg-surface shadow-card q-mb-lg border-message-support" style="border-radius: 16px;">
-            <q-card-section class="q-pb-xs">
-              <div class="row items-center q-gutter-x-sm q-mb-sm">
-                <q-icon name="mdi-message-outline" color="primary" size="18px" />
-                <span class="text-caption text-weight-bold text-primary">{{ $t('profile.help.virtual_assistant') }}</span>
+          <div
+            v-for="(msg, idx) in store.messages"
+            :key="idx"
+            class="q-mb-md"
+            :class="msg.role === 'user' ? 'row justify-end' : 'row justify-start'"
+          >
+            <div
+              class="bubble q-pa-sm"
+              :class="msg.role === 'user' ? 'bubble-user' : 'bubble-bot'"
+            >
+              <p class="q-mb-xs bubble-text">{{ msg.text }}</p>
+              <span class="bubble-time">{{ msg.time }}</span>
+            </div>
+          </div>
+
+          <div v-if="loading" class="row justify-start q-mb-md">
+            <div class="bubble bubble-bot q-pa-sm">
+              <div class="row items-center q-gutter-x-xs typing-indicator">
+                <span /><span /><span />
               </div>
-              <p class="text-text q-mb-xs">{{ $t('profile.help.greeting_message') }}</p>
-              <span class="text-caption text-grey-5">{{ currentTime }}</span>
-            </q-card-section>
-          </q-card>
-          <div class="q-pt-sm">
+            </div>
+          </div>
+
+          <div v-if="store.messages.length === 1" class="q-pt-sm">
             <div class="col-12 text-caption text-grey-6 q-mb-sm">{{ $t('profile.help.quick_suggestions') }}</div>
-            <div 
+            <div
               v-for="suggestion in suggestions"
               :key="suggestion"
               class="row col-12 q-py-xs"
+              @click="sendMessage(suggestion)"
             >
-              <span class="text-text bg-surface suggestion-btn q-py-sm q-px-md">{{ suggestion }}</span>
+              <span class="text-text bg-surface suggestion-btn q-py-sm q-px-md cursor-pointer">{{ suggestion }}</span>
             </div>
           </div>
+
         </div>
       </div>
 
@@ -73,7 +99,8 @@
             borderless
             input-class="text-text"
             :placeholder="$t('profile.help.message_placeholder')"
-            @keyup.enter="sendMessage"
+            :disable="loading"
+            @keyup.enter="sendMessage()"
           />
           <q-btn
             round
@@ -81,8 +108,9 @@
             color="primary"
             icon="mdi-send"
             size="sm"
-            :disable="!messageInput.trim()"
-            @click="sendMessage"
+            :disable="!messageInput.trim() || loading"
+            :loading="loading"
+            @click="sendMessage()"
           />
         </div>
         <div class="footer-disclaimer text-text text-center q-my-md">
@@ -95,21 +123,22 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { ref, computed, nextTick, onMounted } from 'vue';
+import { useDialogPluginComponent } from 'quasar';
 import { useI18n } from 'vue-i18n';
+import { chatbotStore } from 'src/stores/chatbot';
+import { sendChatMessage } from 'src/api/chatbot';
 import diarinho_suporte from 'src/assets/diarinho_suporte.svg';
+
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef } = useDialogPluginComponent();
-const $q = useQuasar();
 const { t } = useI18n();
+const store = chatbotStore();
 
 const messageInput = ref('');
-
-const currentTime = computed(() => {
-  return new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
-});
+const loading = ref(false);
+const messagesContainer = ref(null);
 
 const suggestions = computed(() => [
   t('profile.help.suggestion_cancel'),
@@ -118,17 +147,56 @@ const suggestions = computed(() => [
   t('profile.help.suggestion_human'),
 ]);
 
-const sendMessage = () => {
-  if (!messageInput.value.trim()) return;
-  $q.notify({ type: 'info', message: t('profile.help.coming_soon') });
+const scrollToBottom = async () => {
+  await nextTick();
+  if (messagesContainer.value) {
+    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
+  }
+};
+
+const sendMessage = async (text) => {
+  const content = (text ?? messageInput.value).trim();
+  if (!content || loading.value) return;
+
   messageInput.value = '';
+  store.addMessage('user', content);
+  await scrollToBottom();
+
+  loading.value = true;
+
+  try {
+    const history = store.messages
+      .slice(-6, -1)
+      .map(({ role, text: msgText }) => ({ role, text: msgText }));
+
+    const reply = await sendChatMessage({ message: content, history });
+    store.addMessage('model', reply);
+  } catch {
+    store.addMessage('model', t('profile.help.error_message'));
+  } finally {
+    loading.value = false;
+    await scrollToBottom();
+  }
 };
+
+const clearChat = () => {
+  store.clear();
+  store.addMessage('model', t('profile.help.greeting_message'));
+};
+
+onMounted(() => {
+  if (store.messages.length === 0) {
+    store.addMessage('model', t('profile.help.greeting_message'));
+  }
+  scrollToBottom();
+});
 </script>
 
 <style scoped lang="scss">
 .support-banner {
   background: linear-gradient(180deg, #6C54C1 0%, #9A7FF6 100%);
   min-height: 120px;
+  flex-shrink: 0;
 }
 
 .support-avatar-placeholder {
@@ -138,30 +206,88 @@ const sendMessage = () => {
   background: rgba(255, 255, 255, 0.2);
 }
 
+.messages-area {
+  overflow-y: auto;
+}
+
+.bubble {
+  max-width: 78%;
+  border-radius: 16px;
+  word-break: break-word;
+}
+
+.bubble-bot {
+  background: white;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  border-bottom-left-radius: 4px;
+}
+
+.bubble-user {
+  background: #6C54C1;
+  border-bottom-right-radius: 4px;
+
+  .bubble-text { color: white; }
+  .bubble-time { color: rgba(255, 255, 255, 0.7); }
+}
+
+.bubble-text {
+  font-size: 14px;
+  line-height: 1.45;
+  color: #2c2c2c;
+  margin: 0 0 4px 0;
+  white-space: pre-wrap;
+}
+
+.bubble-time {
+  font-size: 10px;
+  color: #aaa;
+  display: block;
+  text-align: right;
+}
+
+.typing-indicator {
+  span {
+    width: 7px;
+    height: 7px;
+    border-radius: 50%;
+    background: #aaa;
+    display: inline-block;
+    animation: typing-bounce 1.2s infinite ease-in-out;
+
+    &:nth-child(1) { animation-delay: 0s; }
+    &:nth-child(2) { animation-delay: 0.2s; }
+    &:nth-child(3) { animation-delay: 0.4s; }
+  }
+}
+
+@keyframes typing-bounce {
+  0%, 60%, 100% { transform: translateY(0); }
+  30%           { transform: translateY(-6px); }
+}
+
 .suggestion-btn {
   border-radius: 32px;
   font-size: 12px;
-  height: auto;
-  min-height: unset;
   border: 1.15px solid #d8d7d7ce;
   font-family: Inter;
   font-weight: 400;
-  font-style: Regular;
-  font-size: 12px;
   line-height: 16px;
-  letter-spacing: 0px;
+  transition: background 0.2s;
+
+  &:active { background: #ede8fc !important; }
 }
 
 .chat-footer {
   border-top: 1px solid rgba(0, 0, 0, 0.06);
+  flex-shrink: 0;
 }
 
 .shadow-up {
   box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
 }
 
-.border-message-support {
-  border: 1px solid rgba(0, 0, 0, 0.151);
+.shadow-profile {
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
 }
 
 .input-suporte {
@@ -176,27 +302,16 @@ const sendMessage = () => {
     background: var(--q-page, #f5f5f5);
     border: 1px solid rgba(0, 0, 0, 0.12);
     border-radius: 32px;
-
     padding: 10px 14px;
   }
 
-  :deep(.q-field__native) {
-    padding: 0 !important;
-  }
+  :deep(.q-field__native) { padding: 0 !important; }
 
   :deep(.q-field__control::before),
-  :deep(.q-field__control::after) {
-    display: none !important;
-  }
-
-  :deep(.q-field--focused .q-field__control-container) {
-    box-shadow: none;
-  }
+  :deep(.q-field__control::after) { display: none !important; }
 
   :deep(.q-field__bottom),
-  :deep(.q-field__marginal) {
-    display: none;
-  }
+  :deep(.q-field__marginal) { display: none; }
 
   :deep(.q-field__control),
   :deep(.q-field__control:before),
@@ -206,7 +321,6 @@ const sendMessage = () => {
     outline: none !important;
   }
 
-  :deep(.q-field--focused .q-field__control),
   :deep(.q-field--focused .q-field__control-container) {
     box-shadow: none !important;
     border-color: rgba(0, 0, 0, 0.12) !important;
@@ -216,10 +330,8 @@ const sendMessage = () => {
 .footer-disclaimer {
   font-family: Inter;
   font-weight: 400;
-  font-style: Regular;
   font-size: 12px;
   line-height: 15px;
-  letter-spacing: 0px;
   text-align: center;
 }
 </style>

+ 1 - 0
src/i18n/locales/en.json

@@ -647,6 +647,7 @@
       "message_placeholder": "Type your message",
       "footer_disclaimer": "AI Assistant powered by Diarinho",
       "coming_soon": "Coming soon",
+      "error_message": "Could not process your message. Please try again.",
       "suggestion_cancel": "How to cancel a daily service?",
       "suggestion_data": "How to update my data?",
       "suggestion_payment": "How does payment work?",

+ 1 - 0
src/i18n/locales/es.json

@@ -643,6 +643,7 @@
       "message_placeholder": "Escribe tu mensaje",
       "footer_disclaimer": "Asistente de IA impulsado por Diarinho",
       "coming_soon": "Próximamente",
+      "error_message": "No se pudo procesar tu mensaje. Inténtalo de nuevo.",
       "suggestion_cancel": "¿Cómo cancelar un servicio diario?",
       "suggestion_data": "¿Cómo actualizar mis datos?",
       "suggestion_payment": "¿Cómo funciona el pago?",

+ 1 - 0
src/i18n/locales/pt.json

@@ -647,6 +647,7 @@
       "message_placeholder": "Digite sua mensagem",
       "footer_disclaimer": "Assistente de IA alimentado por Diarinho",
       "coming_soon": "Em breve",
+      "error_message": "Não foi possível processar sua mensagem. Tente novamente.",
       "suggestion_cancel": "Como cancelar uma diária?",
       "suggestion_data": "Como atualizar meus dados?",
       "suggestion_payment": "Como funciona o pagamento?",

+ 2 - 2
src/pages/profile/ProfilePage.vue

@@ -130,8 +130,8 @@ import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue';
 import ProfilePrivacyDialog from 'src/components/profile/ProfilePrivacyDialog.vue';
 import { useRouter } from 'vue-router';
 
-const PRIVACY_POLICY_URL = 'https://https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html';
-const SUPPORT_PAGE_URL   = 'https://https://politicas.softpar.inf.br/suportes/suporteDiaristaCliente.html';
+const PRIVACY_POLICY_URL = 'https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html';
+const SUPPORT_PAGE_URL   = 'https://politicas.softpar.inf.br/suportes/suporteDiaristaCliente.html';
 
 const $q = useQuasar();
 const store = userStore();

+ 20 - 0
src/stores/chatbot.js

@@ -0,0 +1,20 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+export const chatbotStore = defineStore('chatbot', () => {
+  const messages = ref([]);
+
+  const addMessage = (role, text) => {
+    messages.value.push({
+      role,
+      text,
+      time: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
+    });
+  };
+
+  const clear = () => {
+    messages.value = [];
+  };
+
+  return { messages, addMessage, clear };
+});