enhance rag retrieval + summary
This commit is contained in:
+103
-123
@@ -3,27 +3,10 @@
|
||||
<h1 class="page-title">Knowledge Chat</h1>
|
||||
<p class="page-subtitle">Ask questions grounded in your uploaded medical textbooks.</p>
|
||||
|
||||
<!-- Step 1: Topic Selection -->
|
||||
<div v-if="!chatStore.session && !selectedTopic" class="topic-selection">
|
||||
<h2 class="section-title">Select a Topic</h2>
|
||||
<div class="topic-grid">
|
||||
<button
|
||||
v-for="topic in topicStore.topics"
|
||||
:key="topic.id"
|
||||
:class="['topic-tile', { 'topic-tile-freeform': topic.id === 'free-form' }]"
|
||||
@click="handleTopicSelect(topic)"
|
||||
>
|
||||
<span class="topic-tile-name">{{ topic.name }}</span>
|
||||
<span v-if="topic.id === 'free-form'" class="topic-tile-hint">Any neurosurgery question</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Topic selected — previous sessions + new chat -->
|
||||
<div v-else-if="!chatStore.session && selectedTopic" class="session-setup card">
|
||||
<!-- Session selection -->
|
||||
<div v-if="!chatStore.session" class="session-setup card">
|
||||
<div class="setup-header">
|
||||
<button class="btn-back" @click="handleBack">← Topics</button>
|
||||
<h2 class="section-title">{{ selectedTopic.name }}</h2>
|
||||
<h2 class="section-title">Free-form Chat</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="chatStore.error" class="error-banner">{{ chatStore.error }}</div>
|
||||
@@ -71,56 +54,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages Area -->
|
||||
<div class="messages-container" ref="messagesContainer">
|
||||
<div v-if="chatStore.loading && chatStore.messages.length === 0" class="empty-state">
|
||||
<div class="spinner spinner-dark" style="width:32px;height:32px;margin:0 auto 1rem;"></div>
|
||||
<p class="empty-state-text">Loading messages...</p>
|
||||
</div>
|
||||
<!-- Chat + Reader split -->
|
||||
<div class="chat-reader-split">
|
||||
<!-- Messages + Input -->
|
||||
<div class="chat-column">
|
||||
<!-- Messages Area -->
|
||||
<div class="messages-container" ref="messagesContainer">
|
||||
<div v-if="chatStore.loading && chatStore.messages.length === 0" class="empty-state">
|
||||
<div class="spinner spinner-dark" style="width:32px;height:32px;margin:0 auto 1rem;"></div>
|
||||
<p class="empty-state-text">Loading messages...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="chatStore.messages.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon">💬</div>
|
||||
<p class="empty-state-text">No messages yet</p>
|
||||
<p class="empty-state-hint">Ask a question about the uploaded books below.</p>
|
||||
</div>
|
||||
<div v-else-if="chatStore.messages.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon">💬</div>
|
||||
<p class="empty-state-text">No messages yet</p>
|
||||
<p class="empty-state-hint">Ask a question about the uploaded books below.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="messages-list">
|
||||
<ChatMessage
|
||||
v-for="message in chatStore.messages"
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
/>
|
||||
<div v-if="chatStore.sending" class="typing-indicator">
|
||||
<div class="typing-bubble">
|
||||
<span></span><span></span><span></span>
|
||||
<div v-else class="messages-list">
|
||||
<ChatMessage
|
||||
v-for="message in chatStore.messages"
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
@open-source="handleOpenSource"
|
||||
/>
|
||||
<div v-if="chatStore.sending" class="typing-indicator">
|
||||
<div class="typing-bubble">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="input-area card">
|
||||
<div v-if="chatStore.error" class="error-banner">{{ chatStore.error }}</div>
|
||||
<div class="input-row">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
class="message-input"
|
||||
placeholder="Ask a question about your uploaded books..."
|
||||
rows="2"
|
||||
:disabled="chatStore.sending"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
@keydown.enter.shift.exact="inputText += '\n'"
|
||||
></textarea>
|
||||
<button
|
||||
class="btn btn-primary send-btn"
|
||||
:disabled="!inputText.trim() || chatStore.sending"
|
||||
@click="handleSend"
|
||||
>
|
||||
<span v-if="chatStore.sending" class="spinner"></span>
|
||||
<span v-else>Send</span>
|
||||
</button>
|
||||
<!-- Input Area -->
|
||||
<div class="input-area card">
|
||||
<div v-if="chatStore.error" class="error-banner">{{ chatStore.error }}</div>
|
||||
<div class="input-row">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
class="message-input"
|
||||
placeholder="Ask a question about your uploaded books..."
|
||||
rows="2"
|
||||
:disabled="chatStore.sending"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
@keydown.enter.shift.exact="inputText += '\n'"
|
||||
></textarea>
|
||||
<button
|
||||
class="btn btn-primary send-btn"
|
||||
:disabled="!inputText.trim() || chatStore.sending"
|
||||
@click="handleSend"
|
||||
>
|
||||
<span v-if="chatStore.sending" class="spinner"></span>
|
||||
<span v-else>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="input-hint">Press Enter to send, Shift+Enter for new line.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="input-hint">Press Enter to send, Shift+Enter for new line.</p>
|
||||
|
||||
<!-- Inline book reader panel -->
|
||||
<BookPagePanel
|
||||
v-if="readerPanel"
|
||||
:book-id="readerPanel.bookId"
|
||||
:page="readerPanel.page"
|
||||
:book-title="readerPanel.bookTitle"
|
||||
class="reader-panel"
|
||||
@close="readerPanel = null"
|
||||
@navigate="(p) => readerPanel && (readerPanel.page = p)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,8 +131,10 @@
|
||||
import { ref, nextTick, onMounted, watch, inject } from 'vue'
|
||||
import { useChatStore } from '@/stores/chatStore'
|
||||
import { useTopicStore } from '@/stores/topicStore'
|
||||
import { useBookStore } from '@/stores/bookStore'
|
||||
import type { ChatSession } from '@/stores/chatStore'
|
||||
import ChatMessage from '@/components/ChatMessage.vue'
|
||||
import BookPagePanel from '@/components/BookPagePanel.vue'
|
||||
|
||||
interface Topic {
|
||||
id: string
|
||||
@@ -142,6 +145,7 @@ interface Topic {
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const topicStore = useTopicStore()
|
||||
const bookStore = useBookStore()
|
||||
const showToast = inject<(msg: string, type?: 'error' | 'success') => void>('showToast')
|
||||
|
||||
const selectedTopic = ref<Topic | null>(null)
|
||||
@@ -150,10 +154,22 @@ const loadingTopicSessions = ref(false)
|
||||
const inputText = ref('')
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
interface ReaderPanel { bookId: string; page: number; bookTitle?: string }
|
||||
const readerPanel = ref<ReaderPanel | null>(null)
|
||||
|
||||
function handleOpenSource(bookId: string, page: number) {
|
||||
const book = bookStore.books.find(b => b.id === bookId)
|
||||
readerPanel.value = { bookId, page, bookTitle: book?.title }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (topicStore.topics.length === 0) {
|
||||
await topicStore.fetchTopics()
|
||||
}
|
||||
const freeForm = topicStore.topics.find((t) => t.id === 'free-form')
|
||||
if (freeForm) {
|
||||
await handleTopicSelect(freeForm)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -189,11 +205,6 @@ async function handleTopicSelect(topic: Topic) {
|
||||
loadingTopicSessions.value = false
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
selectedTopic.value = null
|
||||
topicSessions.value = []
|
||||
}
|
||||
|
||||
async function handleNewChat() {
|
||||
const ok = await chatStore.createSession(selectedTopic.value!.id)
|
||||
if (!ok) {
|
||||
@@ -207,9 +218,7 @@ async function handleResumeSession(session: ChatSession) {
|
||||
}
|
||||
|
||||
function handleLeaveSession() {
|
||||
// Leave without deleting — session stays in DB and will appear in "Previous Chats"
|
||||
chatStore.leaveSession()
|
||||
// Refresh the sessions list for the current topic
|
||||
if (selectedTopic.value) {
|
||||
loadingTopicSessions.value = true
|
||||
chatStore.fetchSessionsByTopic(selectedTopic.value.id).then((sessions) => {
|
||||
@@ -231,12 +240,6 @@ async function handleSend() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.topic-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
@@ -244,52 +247,6 @@ async function handleSend() {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topic-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.topic-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
padding: 1rem 1.1rem;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.topic-tile:hover {
|
||||
border-color: #3182ce;
|
||||
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.15);
|
||||
}
|
||||
|
||||
.topic-tile-freeform {
|
||||
border-style: dashed;
|
||||
border-color: #a0aec0;
|
||||
}
|
||||
|
||||
.topic-tile-freeform:hover {
|
||||
border-color: #718096;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.topic-tile-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.topic-tile-hint {
|
||||
font-size: 0.78rem;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.session-setup {
|
||||
max-width: 540px;
|
||||
}
|
||||
@@ -381,6 +338,29 @@ async function handleSend() {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.chat-reader-split {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.chat-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.reader-panel {
|
||||
width: 420px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
margin-left: 1rem;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
.session-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user