feat: Data Q&A page

This commit is contained in:
fit2cloud-chenyw
2025-05-09 11:41:39 +08:00
parent c3c850708e
commit c6210ee48f
2 changed files with 357 additions and 5 deletions
+3 -3
View File
@@ -24,7 +24,7 @@ const router = createRouter({
path: 'index',
name: 'chat',
component: chat,
meta: { title: 'ChatBI', icon: 'chat'}
meta: { title: 'Data Q&A', icon: 'chat'}
}
]
},
@@ -37,7 +37,7 @@ const router = createRouter({
path: 'index',
name: 'ds',
component: ds,
meta: { title: 'Datasource', icon: 'ds' }
meta: { title: 'Data Connections', icon: 'ds' }
}
]
},
@@ -63,7 +63,7 @@ const router = createRouter({
path: 'index',
name: 'setting',
component: setting,
meta: { title: 'Setting', icon: 'setting' }
meta: { title: 'Settings', icon: 'setting' }
}
]
},
+354 -2
View File
@@ -1,3 +1,355 @@
<template>
<h2>this is chatbi page</h2>
</template>
<div class="chat-container">
<!-- Sidebar for chat history -->
<div class="chat-sidebar">
<div class="sidebar-header">
<el-button type="primary" class="new-chat" @click="createNewChat">
<el-icon><Plus /></el-icon>
New Conversation
</el-button>
<div class="search-box">
<el-input
v-model="searchKeyword"
placeholder="Search conversation history"
:prefix-icon="Search"
/>
</div>
</div>
<div class="history-list">
<div
v-for="chat in filteredHistory"
:key="chat.id"
class="history-item"
:class="{ active: currentChatId === chat.id }"
@click="switchChat(chat.id)"
>
<span class="chat-title">{{ chat.title }}</span>
<el-dropdown trigger="hover" @command="handleCommand($event, chat.id)">
<span class="more-actions">
<el-icon><More /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="rename">Rename</el-dropdown-item>
<el-dropdown-item command="delete" divided>Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- Main chat area -->
<div class="chat-main">
<div class="chat-messages" ref="messagesRef">
<template v-if="currentChat?.messages?.length">
<div
v-for="(msg, index) in currentChat.messages"
:key="index"
class="message"
:class="msg.role"
>
<div class="message-content">{{ msg.content }}</div>
</div>
</template>
<div v-else class="empty-state">
<p>Welcome to SQLBot</p>
<p class="sub-text">You can ask questions in natural language, for example:</p>
<div class="examples">
<div class="example-item">View this month's sales growth compared to last month</div>
<div class="example-item">Analyze sales staff performance by region</div>
<div class="example-item">Calculate the relationship between customer purchase frequency and average order value</div>
</div>
</div>
</div>
<div class="chat-input">
<div class="input-wrapper">
<el-input
v-model="inputMessage"
type="textarea"
:rows="1"
:autosize="{ minRows: 1, maxRows: 8 }"
placeholder="Press Enter to send, Ctrl + Enter for new line"
@keydown.enter.exact.prevent="sendMessage"
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
/>
<div class="input-actions">
<el-button circle type="primary" class="send-btn" @click="sendMessage">
<el-icon><Position /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { Plus, Search, More, Position } from '@element-plus/icons-vue'
interface ChatMessage {
role: 'user' | 'assistant'
content: string
}
interface Chat {
id: string
title: string
messages: ChatMessage[]
}
const chatHistory = ref<Chat[]>([])
const currentChatId = ref('')
const searchKeyword = ref('')
const inputMessage = ref('')
const filteredHistory = computed(() => {
return chatHistory.value.filter(chat =>
chat.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
const currentChat = computed(() => {
return chatHistory.value.find(chat => chat.id === currentChatId.value)
})
const createNewChat = () => {
const newChat: Chat = {
id: Date.now().toString(),
title: `New Chat ${chatHistory.value.length + 1}`,
messages: []
}
chatHistory.value.unshift(newChat)
currentChatId.value = newChat.id
}
const switchChat = (chatId: string) => {
currentChatId.value = chatId
}
const sendMessage = () => {
if (!inputMessage.value.trim()) return
const chat = currentChat.value
if (!chat) return
chat.messages.push({
role: 'user',
content: inputMessage.value
})
inputMessage.value = ''
}
const handleCommand = (command: string, chatId: string) => {
switch (command) {
case 'rename':
break
case 'delete':
chatHistory.value = chatHistory.value.filter(chat => chat.id !== chatId)
if (currentChatId.value === chatId) {
currentChatId.value = chatHistory.value[0]?.id || ''
}
break
}
}
const handleCtrlEnter = (e: KeyboardEvent) => {
const textarea = e.target as HTMLTextAreaElement
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = textarea.value
inputMessage.value = value.substring(0, start) + '\n' + value.substring(end)
nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = start + 1
})
}
</script>
<style lang="less" scoped>
.chat-container {
height: 100%;
display: flex;
background-color: var(--white);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
.chat-sidebar {
width: 260px;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #e6e6e6;
.new-chat {
width: 100%;
margin-bottom: 12px;
}
.search-box {
:deep(.el-input__wrapper) {
background-color: #f5f5f5;
}
}
}
.history-list {
flex: 1;
overflow-y: auto;
padding: 8px;
.history-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 6px;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
&.active {
background-color: #e6f4ff;
}
.chat-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.more-actions {
opacity: 0;
padding: 4px;
}
&:hover .more-actions {
opacity: 1;
}
}
}
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 24px;
.message {
margin-bottom: 24px;
&.user {
text-align: right;
.message-content {
background-color: #e6f4ff;
}
}
&.assistant .message-content {
background-color: #f5f5f5;
}
.message-content {
display: inline-block;
padding: 12px 16px;
border-radius: 8px;
max-width: 80%;
}
}
.empty-state {
text-align: center;
color: #666;
padding: 40px 0;
.sub-text {
margin: 16px 0;
}
.examples {
.example-item {
background-color: #f5f5f5;
padding: 12px;
margin: 8px auto;
max-width: 400px;
border-radius: 8px;
cursor: pointer;
&:hover {
background-color: #e6f4ff;
}
}
}
}
}
.chat-input {
padding: 16px 24px;
border-top: 1px solid var(--el-border-color-lighter);
background: #fff;
.input-wrapper {
position: relative;
border-radius: 12px;
border: 1px solid var(--el-border-color);
background: #f5f5f5;
transition: all 0.3s;
&:hover, &:focus-within {
border-color: var(--el-border-color-darker);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
:deep(.el-textarea__inner) {
padding: 16px;
font-size: 14px;
// line-height: 1.6;
// min-height: 80px;
resize: none;
border: none;
background: transparent;
box-shadow: none !important;
&:focus {
box-shadow: none !important;
}
}
:deep(.el-textarea__wrapper) {
box-shadow: none !important;
padding: 0;
background: transparent;
}
.input-actions {
display: flex;
align-items: center;
justify-content: end;
height: 36px;
padding: 8px;
}
}
}
}
}
</style>