Ukweli / web /src /views /ChatView.vue
Benard John
Initial commit
42a0d15
Raw
History Blame Contribute Delete
8.05 kB
<template>
<div class="chat-view">
<!-- Chat Header -->
<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>
<!-- Messages Area -->
<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>
<!-- Input Area -->
<div class="input-area">
<div class="input-container">
<!-- Suggested Questions -->
<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>
<!-- Citation Modal -->
<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>