|
|
@@ -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>
|