| <template> |
| <div class="chat-view"> |
| |
| <div class="chat-header"> |
| <div class="chat-title"> |
| <n-icon size="24" class="chat-icon"><chatbubbles-outline /></n-icon> |
| <div> |
| <h2>Ask the Auditor</h2> |
| <p class="chat-subtitle">Ask questions about audit reports, findings, and public expenditure</p> |
| </div> |
| </div> |
| <n-button quaternary size="small" @click="chatStore.clearChat"> |
| <template #icon><n-icon><trash-outline /></n-icon></template> |
| New Chat |
| </n-button> |
| </div> |
| |
| |
| <div class="messages-container" ref="messagesContainer"> |
| <div class="messages-list"> |
| <chat-message |
| v-for="message in chatStore.allMessages" |
| :key="message.id" |
| :message="message" |
| @feedback="handleFeedback" |
| @citation-click="handleCitationClick" |
| /> |
| </div> |
| </div> |
| |
| |
| <div class="input-area"> |
| <div class="input-container"> |
| |
| <div v-if="showSuggestions" class="suggestions"> |
| <p class="suggestions-label">Try asking:</p> |
| <div class="suggestion-chips"> |
| <n-button |
| v-for="suggestion in suggestedQuestions" |
| :key="suggestion" |
| quaternary |
| size="small" |
| class="suggestion-chip" |
| @click="sendSuggestion(suggestion)" |
| > |
| {{ suggestion }} |
| </n-button> |
| </div> |
| </div> |
| |
| <div class="input-row"> |
| <n-input |
| v-model:value="inputMessage" |
| type="textarea" |
| placeholder="Ask about audit findings, ministries, counties, or specific reports..." |
| :autosize="{ minRows: 1, maxRows: 4 }" |
| class="chat-input" |
| @keydown.enter.prevent="handleSend" |
| /> |
| <n-button |
| type="primary" |
| class="send-btn" |
| :disabled="!inputMessage.trim() || chatStore.isLoading" |
| :loading="chatStore.isLoading" |
| @click="handleSend" |
| > |
| <template #icon> |
| <n-icon><send-outline /></n-icon> |
| </template> |
| </n-button> |
| </div> |
| |
| <div class="input-meta"> |
| <span class="disclaimer"> |
| Responses are generated from public OAG reports. Always verify with original documents. |
| </span> |
| <n-tag v-if="chatStore.isLoading" size="small" type="info" class="typing-tag"> |
| <template #icon> |
| <span class="typing-indicator"> |
| <span class="typing-dot"></span> |
| <span class="typing-dot"></span> |
| <span class="typing-dot"></span> |
| </span> |
| </template> |
| Searching reports... |
| </n-tag> |
| </div> |
| </div> |
| </div> |
| |
| |
| <n-modal v-model:show="showCitationModal" preset="card" style="max-width: 700px"> |
| <template #header> |
| <div class="citation-header"> |
| <n-icon size="20"><document-text-outline /></n-icon> |
| <span>Source Document</span> |
| </div> |
| </template> |
| <div v-if="selectedCitation" class="citation-content"> |
| <h3>{{ selectedCitation.title }}</h3> |
| <p class="citation-meta">Page {{ selectedCitation.page }}, Paragraph {{ selectedCitation.paragraph }}</p> |
| <n-blockquote>{{ selectedCitation.excerpt }}</n-blockquote> |
| <n-button type="primary" tag="a" :href="selectedCitation.url" target="_blank"> |
| <template #icon><n-icon><open-outline /></n-icon></template> |
| Open Full Report |
| </n-button> |
| </div> |
| </n-modal> |
| </div> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref, computed, nextTick, watch } from 'vue' |
| import { useChatStore } from '@/stores/chat' |
| import type { Citation } from '@/types' |
| import { |
| ChatbubblesOutline, |
| TrashOutline, |
| SendOutline, |
| DocumentTextOutline, |
| OpenOutline, |
| } from '@vicons/ionicons5' |
| import ChatMessage from '@/components/ChatMessage.vue' |
| |
| const chatStore = useChatStore() |
| const inputMessage = ref('') |
| const messagesContainer = ref<HTMLElement>() |
| const showCitationModal = ref(false) |
| const selectedCitation = ref<Citation | null>(null) |
| |
| const suggestedQuestions = [ |
| 'How much was lost in the Ministry of Health audit?', |
| 'Which counties received qualified opinions in 2023/24?', |
| 'What were the findings in the NYS special audit?', |
| 'Show me irregular expenditures in Nairobi County', |
| 'What does the Constitution say about public audit?', |
| ] |
| |
| const showSuggestions = computed(() => chatStore.messages.length <= 1 && !chatStore.isLoading) |
| |
| const scrollToBottom = () => { |
| nextTick(() => { |
| if (messagesContainer.value) { |
| messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight |
| } |
| }) |
| } |
| |
| watch(() => chatStore.allMessages.length, scrollToBottom) |
| |
| const handleSend = () => { |
| if (!inputMessage.value.trim() || chatStore.isLoading) return |
| chatStore.sendMessage(inputMessage.value) |
| inputMessage.value = '' |
| } |
| |
| const sendSuggestion = (suggestion: string) => { |
| chatStore.sendMessage(suggestion) |
| } |
| |
| const handleFeedback = (messageId: string, rating: number, correction?: string) => { |
| chatStore.submitFeedback(messageId, rating, correction) |
| } |
| |
| const handleCitationClick = (citation: Citation) => { |
| selectedCitation.value = citation |
| showCitationModal.value = true |
| } |
| </script> |
| |
| <style scoped lang="scss"> |
| .chat-view { |
| display: flex; |
| flex-direction: column; |
| height: calc(100vh - 64px - 48px); |
| max-height: 800px; |
| } |
| |
| .chat-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 16px 0; |
| border-bottom: 1px solid var(--border); |
| margin-bottom: 16px; |
| } |
| |
| .chat-title { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| |
| h2 { |
| font-size: 20px; |
| font-weight: 600; |
| margin: 0; |
| } |
| } |
| |
| .chat-icon { |
| color: var(--primary); |
| } |
| |
| .chat-subtitle { |
| font-size: 13px; |
| color: var(--text-muted); |
| margin: 4px 0 0; |
| } |
| |
| .messages-container { |
| flex: 1; |
| overflow-y: auto; |
| padding: 16px 0; |
| } |
| |
| .messages-list { |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| } |
| |
| .input-area { |
| border-top: 1px solid var(--border); |
| padding: 16px 0; |
| } |
| |
| .input-container { |
| max-width: 800px; |
| margin: 0 auto; |
| } |
| |
| .suggestions { |
| margin-bottom: 12px; |
| } |
| |
| .suggestions-label { |
| font-size: 12px; |
| color: var(--text-muted); |
| margin-bottom: 8px; |
| display: block; |
| } |
| |
| .suggestion-chips { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| } |
| |
| .suggestion-chip { |
| font-size: 13px; |
| } |
| |
| .input-row { |
| display: flex; |
| gap: 8px; |
| align-items: flex-end; |
| } |
| |
| .chat-input { |
| flex: 1; |
| |
| :deep(.n-input__textarea-el) { |
| font-size: 15px; |
| line-height: 1.5; |
| } |
| } |
| |
| .send-btn { |
| height: 42px; |
| width: 42px; |
| border-radius: 50%; |
| } |
| |
| .input-meta { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-top: 8px; |
| } |
| |
| .disclaimer { |
| font-size: 11px; |
| color: var(--text-muted); |
| } |
| |
| .typing-tag { |
| :deep(.n-tag__icon) { |
| margin-right: 4px; |
| } |
| } |
| |
| .typing-indicator { |
| display: inline-flex; |
| gap: 3px; |
| align-items: center; |
| } |
| |
| .typing-dot { |
| width: 4px; |
| height: 4px; |
| background: currentColor; |
| border-radius: 50%; |
| animation: typing 1.4s infinite ease-in-out both; |
| } |
| |
| .typing-dot:nth-child(1) { animation-delay: -0.32s; } |
| .typing-dot:nth-child(2) { animation-delay: -0.16s; } |
| |
| @keyframes typing { |
| 0%, 80%, 100% { transform: scale(0); } |
| 40% { transform: scale(1); } |
| } |
| |
| .citation-header { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .citation-content { |
| h3 { |
| font-size: 16px; |
| font-weight: 600; |
| margin-bottom: 8px; |
| } |
| } |
| |
| .citation-meta { |
| font-size: 13px; |
| color: var(--text-muted); |
| margin-bottom: 12px; |
| } |
| |
| @media (max-width: 768px) { |
| .chat-view { |
| height: calc(100vh - 64px); |
| } |
| |
| .input-row { |
| flex-direction: column; |
| } |
| |
| .send-btn { |
| width: 100%; |
| border-radius: var(--radius-md); |
| } |
| } |
| </style> |
| |