ChatWindow.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. <template>
  2. <div class="flex h-full min-h-[520px] w-full flex-col gap-4">
  3. <div
  4. ref="chatEl"
  5. class="min-h-0 flex-1 overflow-auto rounded-2xl border border-gray-200 bg-background/60 p-4 dark:border-gray-700 dark:bg-black/20"
  6. >
  7. <div v-if="!messages.length" class="grid h-full place-items-center">
  8. <div class="text-center">
  9. <div class="text-xl font-semibold tracking-tight">Nova conversa</div>
  10. <div class="mt-2 text-sm text-gray-500 dark:text-gray-400">Digite sua pergunta abaixo para começar.</div>
  11. </div>
  12. </div>
  13. <div v-else class="grid gap-4">
  14. <div v-for="(m, idx) in messages" :key="idx" class="flex flex-col" :class="m.role === 'user' ? 'items-end' : 'items-start'">
  15. <div class="mb-0.5 flex items-center gap-1.5" :class="m.role === 'user' ? 'flex-row-reverse' : ''">
  16. <div
  17. class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-bold text-white"
  18. :class="m.role === 'user' ? 'bg-primary' : 'bg-gray-400 dark:bg-gray-600'"
  19. >
  20. {{ m.role === 'user' ? 'V' : 'O' }}
  21. </div>
  22. <span class="text-xs font-medium text-gray-400 dark:text-gray-500">
  23. {{ m.role === 'user' ? 'Você' : 'Oráculo' }}
  24. </span>
  25. <span v-if="m.sentAt" class="text-[10px] text-gray-300 dark:text-gray-600">
  26. {{ formatarHora(m.sentAt) }}
  27. </span>
  28. </div>
  29. <div
  30. class="max-w-[78%] whitespace-pre-wrap rounded-2xl border px-3 py-2 leading-snug text-sm"
  31. :class="
  32. m.role === 'user'
  33. ? 'border-primary/25 bg-primary/10 text-foreground'
  34. : 'border-gray-200 bg-white/50 text-foreground dark:border-gray-700 dark:bg-white/[0.07]'
  35. "
  36. >
  37. {{ m.content }}
  38. </div>
  39. </div>
  40. <div v-if="loading" class="flex flex-col items-start gap-0.5">
  41. <div class="mb-0.5 flex items-center gap-1.5">
  42. <div class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-gray-400 text-[9px] font-bold text-white dark:bg-gray-600">O</div>
  43. <span class="text-xs font-medium text-gray-400 dark:text-gray-500">Oráculo</span>
  44. </div>
  45. <div class="rounded-2xl border border-gray-200 bg-white/50 px-4 py-3 dark:border-gray-700 dark:bg-white/[0.07]">
  46. <span class="inline-flex items-center gap-1">
  47. <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-gray-400 dark:bg-gray-500" style="animation-delay: 0ms" />
  48. <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-gray-400 dark:bg-gray-500" style="animation-delay: 150ms" />
  49. <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-gray-400 dark:bg-gray-500" style="animation-delay: 300ms" />
  50. </span>
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. <div class="grid gap-3 md:grid-cols-[1fr_auto] md:items-end">
  56. <textarea
  57. v-model="draft"
  58. rows="2"
  59. placeholder="Digite sua pergunta..."
  60. class="resize-none"
  61. style="resize: none"
  62. @keydown.enter.exact.prevent="onSend"
  63. />
  64. <button
  65. class="inline-flex items-center justify-center gap-2 rounded-xl border border-primary bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground transition-colors hover:border-[var(--primary-600)] hover:bg-[var(--primary-600)] disabled:opacity-50 disabled:cursor-not-allowed"
  66. :disabled="loading || !draft.trim()"
  67. @click="onSend"
  68. >
  69. <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  70. <path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
  71. </svg>
  72. Enviar
  73. </button>
  74. </div>
  75. <div v-if="error" class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-300">
  76. Erro: {{ error }}
  77. </div>
  78. </div>
  79. </template>
  80. <script setup>
  81. import { ref, nextTick, watch } from "vue";
  82. const props = defineProps({
  83. messages: { type: Array, required: true },
  84. loading: { type: Boolean, required: true },
  85. error: { type: String, required: true }
  86. });
  87. const emit = defineEmits(["send"]);
  88. const draft = ref("");
  89. const chatEl = ref(null);
  90. function formatarHora(ts) {
  91. if (!ts) return "";
  92. try {
  93. return new Date(ts).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
  94. } catch {
  95. return "";
  96. }
  97. }
  98. watch(
  99. () => [props.messages.length, props.loading],
  100. async () => {
  101. await nextTick();
  102. const el = chatEl.value;
  103. if (!el) return;
  104. el.scrollTop = el.scrollHeight;
  105. },
  106. { immediate: true }
  107. );
  108. async function onSend() {
  109. const text = draft.value;
  110. draft.value = "";
  111. emit("send", text);
  112. await nextTick();
  113. }
  114. </script>