mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-09 17:57:06 +08:00
feat: chat view
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import type {ChatMessage} from "@/api/chat.ts";
|
||||
import {computed, ref} from "vue";
|
||||
import type {TabsPaneContext} from 'element-plus'
|
||||
import {Loading} from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
message?: ChatMessage
|
||||
}>()
|
||||
|
||||
const settings = ref<{
|
||||
type: "chart" | "sql" | "data"
|
||||
showAnswer: boolean
|
||||
}>({
|
||||
type: "chart",
|
||||
showAnswer: false,
|
||||
})
|
||||
|
||||
const renderSqlThinking = computed(() => {
|
||||
//todo md render?
|
||||
return props.message?.record?.sql_answer
|
||||
})
|
||||
|
||||
const renderChartThinking = computed(() => {
|
||||
//todo md render?
|
||||
return props.message?.record?.chart_answer
|
||||
})
|
||||
|
||||
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
||||
console.log(tab, event)
|
||||
}
|
||||
|
||||
const dataObject = computed<{
|
||||
fields: Array<string>,
|
||||
data: Array<{ [key: string]: any }>,
|
||||
}>(() => {
|
||||
if (props.message?.record?.data) {
|
||||
return JSON.parse(props.message.record.data)
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const chartObject = computed<{
|
||||
type: "table" | "bar" | "line" | "pie"
|
||||
title: string
|
||||
axis: { x: { name: string, value: string }, y: { name: string, value: string } }
|
||||
columns: Array<{ name: string, value: string }>
|
||||
}>(() => {
|
||||
if (props.message?.record?.chart) {
|
||||
return JSON.parse(props.message.record.chart)
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const currentChartType = ref<"table" | "bar" | "line" | "pie" | undefined>(undefined)
|
||||
|
||||
const chartType = computed<"table" | "bar" | "line" | "pie">({
|
||||
get() {
|
||||
if (currentChartType.value) {
|
||||
return currentChartType.value
|
||||
}
|
||||
return chartObject.value.type ?? "table"
|
||||
},
|
||||
set(v) {
|
||||
currentChartType.value = v
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="message">
|
||||
<div>
|
||||
<div v-if="message.isTyping">Thinking ...</div>
|
||||
<div v-if="chartObject.title">{{ chartObject.title }}</div>
|
||||
<el-tabs v-model="settings.type" class="demo-tabs" @tab-click="handleClick" tab-position="top">
|
||||
<el-tab-pane label="Chart" name="chart">
|
||||
<template #label>
|
||||
<el-select v-model="chartType" style="width: 80px">
|
||||
<el-option value="table">table</el-option>
|
||||
<el-option value="bar">bar</el-option>
|
||||
<el-option value="line">line</el-option>
|
||||
<el-option value="pie">pie</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="SQL" name="sql"></el-tab-pane>
|
||||
<el-tab-pane label="Data" name="data"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<template v-if="message.record">
|
||||
<el-collapse expand-icon-position="left">
|
||||
<el-collapse-item name="1">
|
||||
<template #title>
|
||||
Inference process
|
||||
<el-icon v-if="props.message.isTyping">
|
||||
<Loading/>
|
||||
</el-icon>
|
||||
</template>
|
||||
<div>
|
||||
<template v-if="message.record.sql_answer">
|
||||
<div>SQL Generation:</div>
|
||||
<div v-if="message.record.sql_answer" v-html="renderSqlThinking"></div>
|
||||
</template>
|
||||
<template v-if="message.record.chart_answer">
|
||||
<el-divider></el-divider>
|
||||
<div>Chart Generation:</div>
|
||||
<div v-html="renderChartThinking"></div>
|
||||
</template>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<div>
|
||||
<template v-if="settings.type === 'sql'">
|
||||
<div>
|
||||
<div v-if="message.record.sql">
|
||||
{{ message.record.sql }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="settings.type === 'chart'">
|
||||
<div>
|
||||
|
||||
<div v-if="message.record.chart">
|
||||
{{ chartObject }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="message.record.error" style="color: red">
|
||||
{{ message.record.error }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<div v-if="dataObject.fields">
|
||||
{{ dataObject.fields }}
|
||||
{{ dataObject.data }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.ed-tabs__nav-scroll) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -9,19 +9,30 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-block">
|
||||
<slot>
|
||||
<div v-html="msg?.content"></div>
|
||||
</slot>
|
||||
<div class="chat-block-outer">
|
||||
<div class="chat-block">
|
||||
<slot>
|
||||
<div v-html="msg?.content"></div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="chat-block-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chat-block {
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
.chat-block-outer {
|
||||
.chat-block {
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.chat-block-footer {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,6 +31,9 @@ const _datasource = computed({
|
||||
<el-avatar shape="square" v-if="msg.role === 'user'"/>
|
||||
<ChatBlock v-if="!msg.isWelcome" :msg="msg" :class="{'row-full': msg.role === 'assistant'}">
|
||||
<slot></slot>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</ChatBlock>
|
||||
<WelcomeBlock v-else v-model="_datasource" :current-chat="currentChat" class="row-full"/>
|
||||
</div>
|
||||
@@ -51,6 +54,7 @@ const _datasource = computed({
|
||||
|
||||
.row-full {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+111
-145
@@ -1,24 +1,23 @@
|
||||
<template>
|
||||
<el-container class="chat-container">
|
||||
{{ typedContent }} {{ isTypings }}
|
||||
<el-aside class="chat-container-left">
|
||||
<el-container class="chat-container-right-container">
|
||||
<el-header class="chat-list-header">
|
||||
<el-button type="primary" @click="createNewChat">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
New Conversation
|
||||
</el-button>
|
||||
</el-header>
|
||||
<el-main class="chat-list">
|
||||
<ChatList
|
||||
:current-chat-id="currentChatId"
|
||||
v-model:loading="loading"
|
||||
:chat-list="chatList"
|
||||
@chat-selected="onClickHistory"
|
||||
@chat-deleted="onChatDeleted"
|
||||
@chat-renamed="onChatRenamed"
|
||||
:current-chat-id="currentChatId"
|
||||
v-model:loading="loading"
|
||||
:chat-list="chatList"
|
||||
@chat-selected="onClickHistory"
|
||||
@chat-deleted="onChatDeleted"
|
||||
@chat-renamed="onChatRenamed"
|
||||
/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
@@ -28,38 +27,15 @@
|
||||
<el-scrollbar>
|
||||
<template v-for="(message, _index) in computedMessages" :key="_index">
|
||||
<ChatRow
|
||||
:current-chat="currentChat"
|
||||
v-model:datasource="currentChat.datasource"
|
||||
:msg="message"
|
||||
:current-chat="currentChat"
|
||||
v-model:datasource="currentChat.datasource"
|
||||
:msg="message"
|
||||
>
|
||||
<template v-if="message.role === 'assistant'">
|
||||
<div v-if="message.isTyping">Thinking ...</div>
|
||||
<template v-if="message.record">
|
||||
<div>
|
||||
<div v-if="message.record.sql_answer">
|
||||
{{ message.record.sql_answer }}
|
||||
</div>
|
||||
<div v-if="message.record.sql">
|
||||
{{ message.record.sql }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="message.record.data">
|
||||
{{ message.record.data }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="message.record.chart_answer">
|
||||
{{ message.record.chart_answer }}
|
||||
</div>
|
||||
<div v-if="message.record.chart">
|
||||
{{ message.record.chart }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="message.record.error" style="color: red">
|
||||
{{ message.record.error }}
|
||||
</div>
|
||||
</template>
|
||||
<ChatAnswer :message="message"/>
|
||||
</template>
|
||||
<template #footer v-if="message.role === 'assistant'">
|
||||
<!--<div>Suggestion</div>-->
|
||||
</template>
|
||||
</ChatRow>
|
||||
</template>
|
||||
@@ -68,32 +44,32 @@
|
||||
<el-footer class="chat-footer">
|
||||
<div style="height: 24px">
|
||||
<template
|
||||
v-if="currentChat.datasource && currentChat.datasource_name"
|
||||
v-if="currentChat.datasource && currentChat.datasource_name"
|
||||
>
|
||||
使用数据源:{{ currentChat.datasource_name }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<el-input
|
||||
:disabled="isTyping"
|
||||
class="input-area"
|
||||
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"
|
||||
:disabled="isTyping"
|
||||
class="input-area"
|
||||
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"
|
||||
/>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="input-icon"
|
||||
@click="sendMessage"
|
||||
:disabled="isTyping"
|
||||
link
|
||||
type="primary"
|
||||
class="input-icon"
|
||||
@click="sendMessage"
|
||||
:disabled="isTyping"
|
||||
>
|
||||
<el-icon size="20">
|
||||
<Position />
|
||||
<Position/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -103,8 +79,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { Plus, Position } from "@element-plus/icons-vue";
|
||||
import {computed, nextTick, onMounted, ref} from "vue";
|
||||
import {Plus, Position} from "@element-plus/icons-vue";
|
||||
import {
|
||||
Chat,
|
||||
chatApi,
|
||||
@@ -115,7 +91,7 @@ import {
|
||||
} from "@/api/chat";
|
||||
import ChatList from "./ChatList.vue";
|
||||
import ChatRow from "./ChatRow.vue";
|
||||
import useTypedEffect from "./typed";
|
||||
import ChatAnswer from "./ChatAnswer.vue";
|
||||
|
||||
const inputMessage = ref("");
|
||||
|
||||
@@ -155,8 +131,6 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(messages);
|
||||
|
||||
return messages;
|
||||
});
|
||||
|
||||
@@ -169,13 +143,13 @@ const createNewChat = () => {
|
||||
function getChatList() {
|
||||
loading.value = true;
|
||||
chatApi
|
||||
.list()
|
||||
.then((res) => {
|
||||
chatList.value = chatApi.toChatInfoList(res);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
.list()
|
||||
.then((res) => {
|
||||
chatList.value = chatApi.toChatInfoList(res);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onClickHistory(chat: Chat) {
|
||||
@@ -184,17 +158,16 @@ function onClickHistory(chat: Chat) {
|
||||
currentChatId.value = chat.id;
|
||||
loading.value = true;
|
||||
chatApi
|
||||
.get(chat.id)
|
||||
.then((res) => {
|
||||
const info = chatApi.toChatInfo(res);
|
||||
console.log(info);
|
||||
if (info) {
|
||||
currentChat.value = info;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
.get(chat.id)
|
||||
.then((res) => {
|
||||
const info = chatApi.toChatInfo(res);
|
||||
if (info) {
|
||||
currentChat.value = info;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,16 +195,6 @@ onMounted(() => {
|
||||
getChatList();
|
||||
});
|
||||
|
||||
const content = ref("");
|
||||
const typingEnabled = ref(true);
|
||||
const typingStep = ref(1);
|
||||
const typingInterval = ref(50);
|
||||
const [typedContent, isTypings] = useTypedEffect(
|
||||
content,
|
||||
typingEnabled,
|
||||
typingStep,
|
||||
typingInterval
|
||||
);
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim()) return;
|
||||
@@ -255,29 +218,29 @@ const sendMessage = async () => {
|
||||
let error = false;
|
||||
if (currentChatId.value === undefined) {
|
||||
await chatApi
|
||||
.startChat({
|
||||
question: currentRecord.question.trim(),
|
||||
datasource: currentChat.value.datasource,
|
||||
})
|
||||
.then((res) => {
|
||||
const chat = chatApi.toChatInfo(res);
|
||||
if (chat !== undefined) {
|
||||
chatList.value.unshift(chat);
|
||||
currentChatId.value = chat.id;
|
||||
chat.records.push(currentRecord);
|
||||
currentChat.value = chat;
|
||||
} else {
|
||||
.startChat({
|
||||
question: currentRecord.question.trim(),
|
||||
datasource: currentChat.value.datasource,
|
||||
})
|
||||
.then((res) => {
|
||||
const chat = chatApi.toChatInfo(res);
|
||||
if (chat !== undefined) {
|
||||
chatList.value.unshift(chat);
|
||||
currentChatId.value = chat.id;
|
||||
chat.records.push(currentRecord);
|
||||
currentChat.value = chat;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
isTyping.value = false;
|
||||
error = true;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
isTyping.value = false;
|
||||
error = true;
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
if (error) return;
|
||||
|
||||
@@ -289,8 +252,11 @@ const sendMessage = async () => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let sql_answer = ''
|
||||
let chart_answer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
const {done, value} = await reader.read();
|
||||
console.log(done);
|
||||
if (done) {
|
||||
isTyping.value = false;
|
||||
@@ -301,36 +267,36 @@ const sendMessage = async () => {
|
||||
console.log(chunk);
|
||||
const data = JSON.parse(chunk);
|
||||
|
||||
await nextTick(() => {
|
||||
switch (data.type) {
|
||||
case "info":
|
||||
console.log(data.msg);
|
||||
break;
|
||||
case "error":
|
||||
currentRecord.error = data.content;
|
||||
isTyping.value = false;
|
||||
break;
|
||||
case "sql-result":
|
||||
currentRecord.sql_answer = currentRecord.sql_answer + data.content;
|
||||
break;
|
||||
case "sql":
|
||||
currentRecord.sql = data.content;
|
||||
break;
|
||||
case "sql-data":
|
||||
currentRecord.data = data.content;
|
||||
break;
|
||||
case "chart-result":
|
||||
currentRecord.chart_answer =
|
||||
currentRecord.chart_answer + data.content;
|
||||
break;
|
||||
case "chart":
|
||||
currentRecord.chart = data.content;
|
||||
break;
|
||||
case "finish":
|
||||
isTyping.value = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
switch (data.type) {
|
||||
case "info":
|
||||
console.log(data.msg);
|
||||
break;
|
||||
case "error":
|
||||
currentRecord.error = data.content;
|
||||
isTyping.value = false;
|
||||
break;
|
||||
case "sql-result":
|
||||
sql_answer += data.content;
|
||||
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer;
|
||||
break;
|
||||
case "sql":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content;
|
||||
break;
|
||||
case "sql-data":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].data = data.content;
|
||||
break;
|
||||
case "chart-result":
|
||||
chart_answer += data.content;
|
||||
currentChat.value.records[currentChat.value.records.length - 1].chart_answer = chart_answer;
|
||||
break;
|
||||
case "chart":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content;
|
||||
break;
|
||||
case "finish":
|
||||
isTyping.value = false;
|
||||
break;
|
||||
}
|
||||
await nextTick()
|
||||
}
|
||||
} catch (error) {
|
||||
if (!currentRecord.error) {
|
||||
@@ -349,11 +315,11 @@ const sendMessage = async () => {
|
||||
const formatMessage = (content: string) => {
|
||||
if (!content) return "";
|
||||
return content
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\n/g, "<br>")
|
||||
.replace(/ {2}/g, " ");
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\n/g, "<br>")
|
||||
.replace(/ {2}/g, " ");
|
||||
};
|
||||
|
||||
const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user