feat: chat view

This commit is contained in:
ulleo
2025-06-05 19:12:02 +08:00
parent 20ec595120
commit a4e456ce2e
9 changed files with 349 additions and 243 deletions
+9 -2
View File
@@ -70,18 +70,25 @@ class AiModelQuestion(BaseModel):
engine: str = ""
db_schema: str = ""
sql: str = ""
rule: str = """
请逐步推理后给出答案:
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
步骤1: [思考内容]
步骤2: [思考内容]
最终答案: [结果]
"""
def sql_sys_question(self):
return get_sql_template()['system'].format(engine=self.engine, schema=self.db_schema, question=self.question)
def sql_user_question(self):
return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question)
return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question, rule=self.rule)
def chart_sys_question(self):
return get_chart_template()['system'].format(sql=self.sql, question=self.question)
def chart_user_question(self):
return get_chart_template()['user'].format(sql=self.sql, question=self.question)
return get_chart_template()['user'].format(sql=self.sql, question=self.question, rule=self.rule)
class ChatQuestion(AiModelQuestion):
+31 -12
View File
@@ -1,9 +1,7 @@
import asyncio
import json
import logging
import re
import warnings
from typing import Any, List, Union, AsyncGenerator, Dict
from typing import Any, List, Union, Dict
from langchain_community.utilities import SQLDatabase
from langchain_core.language_models import BaseLLM
@@ -21,6 +19,8 @@ from common.core.deps import SessionDep, CurrentUser
warnings.filterwarnings("ignore")
base_message_count_limit = 5
class LLMService:
ds: CoreDatasource
@@ -55,34 +55,51 @@ class LLMService:
last_chart_messages[-1].full_chart_message
last_sql_messages: List[dict[str, Any]] = json.loads(last_sql_message_str)
# todo maybe can configure
count_limit = 0 - base_message_count_limit
self.sql_message = []
if last_sql_messages is None or len(last_sql_messages) == 0:
# add sys prompt
self.sql_message.append(SystemMessage(content=self.chat_question.sql_sys_question()))
else:
# limit count
for last_sql_message in last_sql_messages:
if last_sql_message['type'] == 'system':
_msg = SystemMessage(content=last_sql_message['content'])
self.sql_message.append(_msg)
break
for last_sql_message in last_sql_messages[count_limit:]:
_msg: BaseMessage
if last_sql_message['type'] == 'human':
_msg = HumanMessage(content=last_sql_message['content'])
self.sql_message.append(_msg)
elif last_sql_message['type'] == 'ai':
_msg = AIMessage(content=last_sql_message['content'])
else:
_msg = SystemMessage(content=last_sql_message['content'])
self.sql_message.append(_msg)
self.sql_message.append(_msg)
last_chart_messages: List[dict[str, Any]] = json.loads(last_chart_message_str)
self.chart_message = []
if last_chart_messages is None or len(last_chart_messages) == 0:
# add sys prompt
self.chart_message.append(SystemMessage(content=self.chat_question.chart_sys_question()))
else:
# limit count
for last_chart_message in last_chart_messages:
if last_chart_message['type'] == 'system':
_msg = SystemMessage(content=last_chart_message['content'])
self.chart_message.append(_msg)
break
for last_chart_message in last_chart_messages:
_msg: BaseMessage
if last_chart_message['type'] == 'human':
_msg = HumanMessage(content=last_chart_message['content'])
self.chart_message.append(_msg)
elif last_chart_message['type'] == 'ai':
_msg = AIMessage(content=last_chart_message['content'])
else:
_msg = SystemMessage(content=last_chart_message['content'])
self.chart_message.append(_msg)
self.chart_message.append(_msg)
def init_record(self, session: SessionDep, current_user: CurrentUser) -> ChatRecord:
self.record = save_question(session=session, current_user=current_user, question=self.chat_question)
@@ -98,6 +115,7 @@ class LLMService:
full_sql_text = ''
res = self.llm.stream(self.sql_message)
for chunk in res:
print(chunk)
if isinstance(chunk, dict):
full_sql_text += chunk['content']
yield chunk['content']
@@ -196,9 +214,10 @@ class LLMService:
return save_error_message(session=session, record_id=self.record.id, message=message)
def save_sql_data(self, session: SessionDep, data_obj: Dict[str, Any]):
return save_sql_exec_data(session=session, record_id=self.record.id, data=json.dumps(data_obj, ensure_ascii=False))
return save_sql_exec_data(session=session, record_id=self.record.id,
data=json.dumps(data_obj, ensure_ascii=False))
def finish(self,session: SessionDep):
def finish(self, session: SessionDep):
return finish_record(session=session, record_id=self.record.id)
def execute_sql(self, sql: str):
@@ -237,7 +256,7 @@ def extract_nested_json(text):
pass
else:
stack = [] # 括号不匹配则重置
if results[0]:
if len(results) > 0 and results[0]:
return results[0]
return None
+18 -9
View File
@@ -15,17 +15,23 @@ template:
{{"success":false,"message":"抱歉,我无法回答您的问题。"}}
- 如果根据提供的表结构不能生成符合问题与条件的SQL,回答:
{{"success":false,"message":"无法生成SQL的原因"}}
user: |
表结构:
- 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/bar/line/pie)"}}
user: |
### 表结构:
{schema}
问题:
### 问题:
{question}
### 其他规则:
{rule}
chart:
system: |
### 说明:
您的任务是通过给定的问题和SQL生成 JSON 以进行数据可视化。
请遵守以下规则:
请遵守以下规则:
- 如果需要表格,则生成的 JSON 格式应为:
{{"type":"table", "title": "标题", "columns": [{{"name":"中文字段名1", "value": "SQL 查询列 1(有别名用别名)"}}, {{"name": "中文字段名 2", "value": "SQL 查询列 2(有别名用别名)"}}]}}
必须从 SQL 查询列中提取“columns”。
@@ -36,22 +42,25 @@ template:
{{"type":"line", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称","value": "x轴的SQL查询列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "y轴的SQL查询列(有别名用别名)"}}}}
其中“x”和“y”必须从SQL查询列中提取。
- 如果需要饼图,则生成的 JSON 格式应为:
{{"type":"pie", "title": "标题", "column": {{"name":"中文字段名","value":"SQL查询列1(有别名用别名)"}}}}
{{"type":"pie", "title": "标题", "column": [{{"name":"中文字段名1","value":"SQL查询列1(有别名用别名)"}},{{"name":"中文字段名2","value":"SQL查询列2(有别名用别名)"}}]}}
其中“column”必须从SQL查询列中提取。
- 如果答案未知或者与生成JSON无关,则生成的 JSON 格式应为:
{{"type":"error", "reason": "抱歉,我无法回答您的问题。"}}
### 示例
### 示例:
如果 SQL 为: SELECT products_sales_data.category, AVG(products_sales_data.price) AS average_price FROM products_sales_data GROUP BY products_sales_data.category;
问题是:每个商品分类的平均价格
则生成的 JSON 可以是: {{"type":"table", "title": "每个商品分类的平均价格", "columns": [{{"name":"商品分类","value":"category"}}, {{"name":"平均价格","value":"average_price"}}]}}
### 响应:
### 响应:
根据您的指示,这是我生成的与 问题 和 sql 匹配的 JSON:
```json
user: |
### SQL:
{sql}
### 问题:
{question}
### 问题:
{question}
### 其他规则:
{rule}
-62
View File
@@ -1,62 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ElAside: typeof import('element-plus-secondary/es')['ElAside']
ElAvatar: typeof import('element-plus-secondary/es')['ElAvatar']
ElButton: typeof import('element-plus-secondary/es')['ElButton']
ElCheckbox: typeof import('element-plus-secondary/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus-secondary/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus-secondary/es')['ElCol']
ElContainer: typeof import('element-plus-secondary/es')['ElContainer']
ElDialog: typeof import('element-plus-secondary/es')['ElDialog']
ElDivider: typeof import('element-plus-secondary/es')['ElDivider']
ElDropdown: typeof import('element-plus-secondary/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus-secondary/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus-secondary/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus-secondary/es')['ElEmpty']
ElFooter: typeof import('element-plus-secondary/es')['ElFooter']
ElForm: typeof import('element-plus-secondary/es')['ElForm']
ElFormItem: typeof import('element-plus-secondary/es')['ElFormItem']
ElHeader: typeof import('element-plus-secondary/es')['ElHeader']
ElIcon: typeof import('element-plus-secondary/es')['ElIcon']
ElInput: typeof import('element-plus-secondary/es')['ElInput']
ElMain: typeof import('element-plus-secondary/es')['ElMain']
ElMenu: typeof import('element-plus-secondary/es')['ElMenu']
ElMenuItem: typeof import('element-plus-secondary/es')['ElMenuItem']
ElOption: typeof import('element-plus-secondary/es')['ElOption']
ElPagination: typeof import('element-plus-secondary/es')['ElPagination']
ElPopover: typeof import('element-plus-secondary/es')['ElPopover']
ElRadio: typeof import('element-plus-secondary/es')['ElRadio']
ElRadioGroup: typeof import('element-plus-secondary/es')['ElRadioGroup']
ElRow: typeof import('element-plus-secondary/es')['ElRow']
ElScrollbar: typeof import('element-plus-secondary/es')['ElScrollbar']
ElSelect: typeof import('element-plus-secondary/es')['ElSelect']
ElStep: typeof import('element-plus-secondary/es')['ElStep']
ElSteps: typeof import('element-plus-secondary/es')['ElSteps']
ElSwitch: typeof import('element-plus-secondary/es')['ElSwitch']
ElTable: typeof import('element-plus-secondary/es')['ElTable']
ElTableColumn: typeof import('element-plus-secondary/es')['ElTableColumn']
ElTabPane: typeof import('element-plus-secondary/es')['ElTabPane']
ElTabs: typeof import('element-plus-secondary/es')['ElTabs']
ElTooltip: typeof import('element-plus-secondary/es')['ElTooltip']
ElTree: typeof import('element-plus-secondary/es')['ElTree']
ElTreeSelect: typeof import('element-plus-secondary/es')['ElTreeSelect']
ElUpload: typeof import('element-plus-secondary/es')['ElUpload']
Icon: typeof import('./src/components/icon-custom/src/Icon.vue')['default']
Layout: typeof import('./src/components/layout/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SinglePage: typeof import('./src/components/layout/SinglePage.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus-secondary/es')['ElLoadingDirective']
}
}
+3 -3
View File
@@ -11,13 +11,13 @@
"dependencies": {
"@npkg/tinymce-plugins": "^0.0.7",
"@tinymce/tinymce-vue": "^5.1.0",
"tinymce": "^5.8.2",
"dayjs": "^1.11.13",
"lodash": "^4.17.21",
"snowflake-id": "^1.1.0",
"tinymce": "^5.8.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"web-storage-cache": "^1.1.1",
"dayjs": "^1.11.13"
"web-storage-cache": "^1.1.1"
},
"devDependencies": {
"@element-plus/icons-vue": "^2.3.1",
+152
View File
@@ -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>
+21 -10
View File
@@ -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>
+4
View File
@@ -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
View File
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "<br>")
.replace(/ {2}/g, "&nbsp;&nbsp;");
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "<br>")
.replace(/ {2}/g, "&nbsp;&nbsp;");
};
const handleCtrlEnter = (e: KeyboardEvent) => {