mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -24,7 +24,7 @@ async def chats(session: SessionDep, current_user: CurrentUser):
|
||||
|
||||
|
||||
@router.get("/get/{chart_id}")
|
||||
async def list_chat(session: SessionDep, current_user: CurrentUser, chart_id: int):
|
||||
async def get_chat(session: SessionDep, current_user: CurrentUser, chart_id: int):
|
||||
try:
|
||||
return get_chat_with_records(chart_id=chart_id, session=session, current_user=current_user)
|
||||
except Exception as e:
|
||||
|
||||
@@ -2,6 +2,7 @@ import datetime
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import load_only
|
||||
|
||||
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo, RenameChat, ChatQuestion
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
@@ -55,7 +56,11 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
|
||||
chat_info.datasource_exists = True
|
||||
chat_info.datasource_name = ds.name
|
||||
|
||||
record_list = session.query(ChatRecord).filter(
|
||||
record_list = session.query(ChatRecord).options(
|
||||
load_only(ChatRecord.id, ChatRecord.chat_id, ChatRecord.create_time, ChatRecord.finish_time,
|
||||
ChatRecord.question, ChatRecord.sql_answer, ChatRecord.sql, ChatRecord.data,
|
||||
ChatRecord.chart_answer, ChatRecord.chart, ChatRecord.finish, ChatRecord.error,
|
||||
ChatRecord.run_time)).filter(
|
||||
and_(Chat.create_by == current_user.id, ChatRecord.chat_id == chart_id)).order_by(ChatRecord.create_time).all()
|
||||
|
||||
chat_info.records = record_list
|
||||
@@ -96,6 +101,9 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
|
||||
chat_info.id = chat.id
|
||||
session.commit()
|
||||
|
||||
chat_info.datasource_exists = True
|
||||
chat_info.datasource_name = ds.name
|
||||
|
||||
return chat_info
|
||||
|
||||
|
||||
@@ -214,6 +222,7 @@ def save_error_message(session: SessionDep, record_id: int, message: str) -> Cha
|
||||
record = session.query(ChatRecord).filter(ChatRecord.id == record_id).first()
|
||||
record.error = message
|
||||
record.finish = True
|
||||
record.finish_time = datetime.datetime.now()
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
@@ -242,11 +251,13 @@ def save_sql_exec_data(session: SessionDep, record_id: int, data: str) -> ChatRe
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def finish_record(session: SessionDep, record_id: int) -> ChatRecord:
|
||||
if not record_id:
|
||||
raise Exception("Record id cannot be None")
|
||||
record = session.query(ChatRecord).filter(ChatRecord.id == record_id).first()
|
||||
record.finish = True
|
||||
record.finish_time = datetime.datetime.now()
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
@@ -257,4 +268,3 @@ def finish_record(session: SessionDep, record_id: int) -> ChatRecord:
|
||||
session.commit()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from sqlmodel import SQLModel, Field
|
||||
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity, Boolean
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Any
|
||||
|
||||
from apps.template.generate_chart.generator import get_chart_template
|
||||
from apps.template.generate_sql.generator import get_sql_template
|
||||
|
||||
@@ -9,10 +9,10 @@ from fastapi import APIRouter, File, UploadFile, HTTPException
|
||||
from apps.db.engine import create_table, get_data_engine, insert_data
|
||||
from common.core.deps import SessionDep, CurrentUser
|
||||
from ..crud.datasource import get_datasource_list, check_status, create_ds, update_ds, delete_ds, getTables, getFields, \
|
||||
execSql, update_table_and_fields, getTablesByDs, chooseTables, preview
|
||||
execSql, update_table_and_fields, getTablesByDs, chooseTables, preview, updateTable, updateField
|
||||
from ..crud.field import get_fields_by_table_id
|
||||
from ..crud.table import get_tables_by_ds_id
|
||||
from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable
|
||||
from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable, CoreField
|
||||
|
||||
router = APIRouter(tags=["datasource"], prefix="/datasource")
|
||||
path = "/opt/sqlbot/data/excel"
|
||||
@@ -83,6 +83,16 @@ async def edit_local(session: SessionDep, data: TableObj):
|
||||
update_table_and_fields(session, data)
|
||||
|
||||
|
||||
@router.post("/editTable")
|
||||
async def edit_table(session: SessionDep, table: CoreTable):
|
||||
updateTable(session, table)
|
||||
|
||||
|
||||
@router.post("/editField")
|
||||
async def edit_field(session: SessionDep, field: CoreField):
|
||||
updateField(session, field)
|
||||
|
||||
|
||||
@router.post("/previewData/{id}")
|
||||
async def edit_local(session: SessionDep, id: int, data: TableObj):
|
||||
return preview(session, id, data)
|
||||
|
||||
@@ -192,6 +192,14 @@ def update_table_and_fields(session: SessionDep, data: TableObj):
|
||||
update_field(session, field)
|
||||
|
||||
|
||||
def updateTable(session: SessionDep, table: CoreTable):
|
||||
update_table(session, table)
|
||||
|
||||
|
||||
def updateField(session: SessionDep, field: CoreField):
|
||||
update_field(session, field)
|
||||
|
||||
|
||||
def preview(session: SessionDep, id: int, data: TableObj):
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
|
||||
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config()
|
||||
@@ -225,6 +233,8 @@ def get_table_obj_by_ds(session: SessionDep, ds: CoreDatasource) -> List[TableAn
|
||||
def get_table_schema(session: SessionDep, ds: CoreDatasource) -> str:
|
||||
schema_str = ""
|
||||
table_objs = get_table_obj_by_ds(session=session, ds=ds)
|
||||
if len(table_objs) == 0:
|
||||
return schema_str
|
||||
db_name = table_objs[0].schema
|
||||
schema_str += f"【DB_ID】 {db_name}\n【Schema】\n"
|
||||
for obj in table_objs:
|
||||
|
||||
@@ -12,7 +12,7 @@ from common.core.config import settings
|
||||
|
||||
def get_engine_config():
|
||||
return DatasourceConf(username=settings.POSTGRES_USER, password=settings.POSTGRES_PASSWORD,
|
||||
host="127.0.0.1", port=settings.POSTGRES_PORT, database=settings.POSTGRES_DB,
|
||||
host=settings.POSTGRES_SERVER, port=settings.POSTGRES_PORT, database=settings.POSTGRES_DB,
|
||||
dbSchema="public")
|
||||
|
||||
|
||||
|
||||
+20
-11
@@ -20,6 +20,7 @@ export interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
create_time?: Date | string
|
||||
content?: string | number
|
||||
record?: ChatRecord
|
||||
isTyping?: boolean
|
||||
isWelcome?: boolean
|
||||
}
|
||||
@@ -28,24 +29,32 @@ export class ChatRecord {
|
||||
id?: number
|
||||
chat_id?: number
|
||||
create_time?: Date | string
|
||||
create_by?: number
|
||||
datasource?: number
|
||||
engine_type?: string
|
||||
finish_time?: Date | string
|
||||
question?: string
|
||||
answer?: string
|
||||
sql_answer?: string
|
||||
sql?: string
|
||||
data?: string
|
||||
chart_answer?: string
|
||||
chart?: string
|
||||
finish?: boolean = false
|
||||
error?: string
|
||||
run_time: number = 0
|
||||
|
||||
constructor()
|
||||
constructor(id: number, chat_id: number, create_time: Date | string, create_by: number, datasource: number, engine_type: string, question: string, answer: string, run_time: number)
|
||||
constructor(id?: number, chat_id?: number, create_time?: Date | string, create_by?: number, datasource?: number, engine_type?: string, question?: string, answer?: string, run_time?: number) {
|
||||
constructor(id: number, chat_id: number, create_time: Date | string, finish_time: Date | string | undefined, question: string, sql_answer: string | undefined, sql: string | undefined, data: string | undefined, chart_answer: string | undefined, chart: string | undefined, finish: boolean, error: string | undefined, run_time: number)
|
||||
constructor(id?: number, chat_id?: number, create_time?: Date | string, finish_time?: Date | string, question?: string, sql_answer?: string, sql?: string, data?: string, chart_answer?: string, chart?: string, finish?: boolean, error?: string, run_time?: number) {
|
||||
this.id = id
|
||||
this.chat_id = chat_id
|
||||
this.create_time = getDate(create_time)
|
||||
this.create_by = create_by
|
||||
this.datasource = datasource
|
||||
this.engine_type = engine_type
|
||||
this.finish_time = getDate(finish_time)
|
||||
this.question = question
|
||||
this.answer = answer
|
||||
this.sql_answer = sql_answer
|
||||
this.sql = sql
|
||||
this.data = data
|
||||
this.chart_answer = chart_answer
|
||||
this.chart = chart
|
||||
this.finish = finish
|
||||
this.error = error
|
||||
this.run_time = run_time ?? 0
|
||||
}
|
||||
}
|
||||
@@ -111,7 +120,7 @@ function toChatRecord(data?: any): ChatRecord | undefined {
|
||||
if (!data) {
|
||||
return undefined
|
||||
}
|
||||
return new ChatRecord(data.id, data.number, data.create_time, data.create_by, data.datasource, data.engine_type, data.question, data.answer, data.run_time)
|
||||
return new ChatRecord(data.id, data.chat_id, data.create_time, data.finish_time, data.question, data.sql_answer, data.sql, data.data, data.chart_answer, data.chart, data.finish, data.error, data.run_time)
|
||||
}
|
||||
|
||||
function toChatRecordList(list: any = []): ChatRecord[] {
|
||||
|
||||
@@ -15,4 +15,6 @@ export const datasourceApi = {
|
||||
fieldList: (id: Number) => request.post(`/datasource/fieldList/${id}`),
|
||||
edit: (data: any) => request.post('/datasource/editLocalComment', data),
|
||||
previewData: (id: Number, data: any) => request.post(`/datasource/previewData/${id}`, data),
|
||||
saveTable: (data: any) => request.post('/datasource/editTable', data),
|
||||
saveField: (data: any) => request.post('/datasource/editField', data)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import chat from "@/views/chat/index.vue";
|
||||
import ds from "@/views/ds/index.vue";
|
||||
import DashboardEditor from "@/views/dashboard/editor/index.vue";
|
||||
import Dashboard from "@/views/dashboard/index.vue";
|
||||
import setting from "@/views/setting/index.vue";
|
||||
// import setting from "@/views/setting/index.vue";
|
||||
import { watchRouter } from "./watch";
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
@@ -81,7 +81,7 @@ const router = createRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
/* {
|
||||
path: "/setting",
|
||||
component: Layout,
|
||||
redirect: "/setting/index",
|
||||
@@ -93,7 +93,7 @@ const router = createRouter({
|
||||
meta: { title: "Settings", icon: "setting" },
|
||||
},
|
||||
],
|
||||
},
|
||||
}, */
|
||||
{
|
||||
path: "/system",
|
||||
component: Layout,
|
||||
|
||||
+257
-187
@@ -1,18 +1,25 @@
|
||||
<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"/>
|
||||
<ChatList
|
||||
:current-chat-id="currentChatId"
|
||||
v-model:loading="loading"
|
||||
:chat-list="chatList"
|
||||
@chat-selected="onClickHistory"
|
||||
@chat-deleted="onChatDeleted"
|
||||
@chat-renamed="onChatRenamed"
|
||||
/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-aside>
|
||||
@@ -20,139 +27,182 @@
|
||||
<el-main class="chat-record-list">
|
||||
<el-scrollbar>
|
||||
<template v-for="(message, _index) in computedMessages" :key="_index">
|
||||
<ChatRow :current-chat="currentChat" v-model:datasource="currentChat.datasource" :msg="message"/>
|
||||
<ChatRow
|
||||
: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>
|
||||
</template>
|
||||
</ChatRow>
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<el-footer class="chat-footer">
|
||||
<div style="height: 24px;">
|
||||
<template v-if="currentChat.datasource">
|
||||
<div style="height: 24px">
|
||||
<template
|
||||
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">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="input-icon"
|
||||
@click="sendMessage"
|
||||
:disabled="isTyping"
|
||||
>
|
||||
<el-icon size="20">
|
||||
<Position/>
|
||||
<Position />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, nextTick, watch, onMounted} from 'vue'
|
||||
import {Plus, Position} from '@element-plus/icons-vue'
|
||||
import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi} from '@/api/chat'
|
||||
import ChatList from './ChatList.vue'
|
||||
import ChatRow from './ChatRow.vue'
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { Plus, Position } from "@element-plus/icons-vue";
|
||||
import {
|
||||
Chat,
|
||||
chatApi,
|
||||
ChatInfo,
|
||||
type ChatMessage,
|
||||
ChatRecord,
|
||||
questionApi,
|
||||
} from "@/api/chat";
|
||||
import ChatList from "./ChatList.vue";
|
||||
import ChatRow from "./ChatRow.vue";
|
||||
import useTypedEffect from "./typed";
|
||||
|
||||
const inputMessage = ref('')
|
||||
const inputMessage = ref("");
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const chatList = ref<Array<ChatInfo>>([])
|
||||
const chatList = ref<Array<ChatInfo>>([]);
|
||||
|
||||
const currentChatId = ref<number | undefined>()
|
||||
const currentChat = ref<ChatInfo>(new ChatInfo())
|
||||
const isTyping = ref<boolean>(false)
|
||||
const currentChatId = ref<number | undefined>();
|
||||
const currentChat = ref<ChatInfo>(new ChatInfo());
|
||||
const isTyping = ref<boolean>(false);
|
||||
|
||||
const computedMessages = computed<Array<ChatMessage>>(() => {
|
||||
const welcome: ChatMessage = {
|
||||
role: 'assistant',
|
||||
role: "assistant",
|
||||
create_time: currentChat.value?.create_time,
|
||||
content: currentChat.value?.datasource,
|
||||
isTyping: false,
|
||||
isWelcome: true,
|
||||
}
|
||||
const messages: Array<ChatMessage> = [welcome]
|
||||
};
|
||||
const messages: Array<ChatMessage> = [welcome];
|
||||
if (currentChatId.value === undefined) {
|
||||
return messages
|
||||
return messages;
|
||||
}
|
||||
let appendThinking = false
|
||||
for (let i = 0; i < currentChat.value.records.length; i++) {
|
||||
const record = currentChat.value.records[i]
|
||||
const record = currentChat.value.records[i];
|
||||
if (record.question !== undefined) {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
create_time: record.create_time,
|
||||
content: record.question,
|
||||
})
|
||||
});
|
||||
}
|
||||
if (record.answer !== undefined && record.answer !== '') {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
create_time: record.create_time,
|
||||
content: record.answer,
|
||||
isTyping: i === currentChat.value.records.length - 1 && isTyping.value
|
||||
})
|
||||
} else {
|
||||
appendThinking = true
|
||||
}
|
||||
}
|
||||
if (isTyping.value && appendThinking) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: 'Thinking...',
|
||||
isTyping: true
|
||||
})
|
||||
role: "assistant",
|
||||
create_time: record.create_time,
|
||||
record: record,
|
||||
isTyping: i === currentChat.value.records.length - 1 && isTyping.value,
|
||||
});
|
||||
}
|
||||
return messages
|
||||
})
|
||||
|
||||
console.log(messages);
|
||||
|
||||
return messages;
|
||||
});
|
||||
|
||||
const createNewChat = () => {
|
||||
currentChat.value = new ChatInfo()
|
||||
currentChatId.value = undefined
|
||||
inputMessage.value = ''
|
||||
}
|
||||
currentChat.value = new ChatInfo();
|
||||
currentChatId.value = undefined;
|
||||
inputMessage.value = "";
|
||||
};
|
||||
|
||||
function getChatList() {
|
||||
loading.value = true
|
||||
chatApi.list()
|
||||
.then((res) => {
|
||||
chatList.value = chatApi.toChatInfoList(res)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
loading.value = true;
|
||||
chatApi
|
||||
.list()
|
||||
.then((res) => {
|
||||
chatList.value = chatApi.toChatInfoList(res);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onClickHistory(chat: Chat) {
|
||||
currentChat.value = new ChatInfo(chat)
|
||||
currentChat.value = new ChatInfo(chat);
|
||||
if (chat !== undefined && chat.id !== undefined && !loading.value) {
|
||||
currentChatId.value = chat.id
|
||||
loading.value = true
|
||||
chatApi.get(chat.id)
|
||||
.then((res) => {
|
||||
const info = chatApi.toChatInfo(res)
|
||||
if (info) {
|
||||
currentChat.value = info
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onChatDeleted(id: number) {
|
||||
for (let i = 0; i < chatList.value.length; i++) {
|
||||
if (chatList.value[i].id === id) {
|
||||
chatList.value.splice(i, 1)
|
||||
return
|
||||
chatList.value.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,141 +210,164 @@ function onChatDeleted(id: number) {
|
||||
function onChatRenamed(chat: Chat) {
|
||||
chatList.value.forEach((c: Chat) => {
|
||||
if (c.id === chat.id) {
|
||||
c.brief = chat.brief
|
||||
c.brief = chat.brief;
|
||||
}
|
||||
})
|
||||
});
|
||||
if (currentChat.value.id === chat.id) {
|
||||
currentChat.value.brief = chat.brief
|
||||
currentChat.value.brief = chat.brief;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getChatList()
|
||||
})
|
||||
getChatList();
|
||||
});
|
||||
|
||||
|
||||
const updateMessageContent = (content: string) => {
|
||||
if (currentChat.value) {
|
||||
currentChat.value.records[currentChat.value.records.length - 1].answer = content
|
||||
}
|
||||
}
|
||||
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
|
||||
if (computedMessages.value[0].content === undefined) return
|
||||
if (!inputMessage.value.trim()) return;
|
||||
if (computedMessages.value[0].content === undefined) return;
|
||||
|
||||
loading.value = true
|
||||
isTyping.value = true
|
||||
loading.value = true;
|
||||
isTyping.value = true;
|
||||
|
||||
const currentRecord = new ChatRecord()
|
||||
currentRecord.create_time = new Date()
|
||||
currentRecord.chat_id = currentChatId.value
|
||||
currentRecord.question = inputMessage.value
|
||||
currentRecord.answer = ''
|
||||
const currentRecord = new ChatRecord();
|
||||
currentRecord.create_time = new Date();
|
||||
currentRecord.chat_id = currentChatId.value;
|
||||
currentRecord.question = inputMessage.value;
|
||||
currentRecord.sql_answer = "";
|
||||
currentRecord.sql = "";
|
||||
currentRecord.chart_answer = "";
|
||||
currentRecord.chart = "";
|
||||
|
||||
currentChat.value.records.push(currentRecord)
|
||||
inputMessage.value = ''
|
||||
currentChat.value.records.push(currentRecord);
|
||||
inputMessage.value = "";
|
||||
|
||||
let error = false
|
||||
let error = false;
|
||||
if (currentChatId.value === undefined) {
|
||||
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 {
|
||||
error = true
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
isTyping.value = false
|
||||
error = true
|
||||
console.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
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 {
|
||||
error = true;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
isTyping.value = false;
|
||||
error = true;
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
if (error) return
|
||||
if (error) return;
|
||||
|
||||
try {
|
||||
const response = await questionApi.add({question: currentRecord.question, chat_id: currentChatId.value})
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const response = await questionApi.add({
|
||||
question: currentRecord.question,
|
||||
chat_id: currentChatId.value,
|
||||
});
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read()
|
||||
const { done, value } = await reader.read();
|
||||
console.log(done);
|
||||
if (done) {
|
||||
isTyping.value = false
|
||||
break
|
||||
isTyping.value = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value)
|
||||
const lines = chunk.split('\n\n')
|
||||
const chunk = decoder.decode(value);
|
||||
console.log(chunk);
|
||||
const data = JSON.parse(chunk);
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = JSON.parse(line.replace('data:', '').trim())
|
||||
let realContent = data.content
|
||||
|
||||
switch (data.type) {
|
||||
case 'html':
|
||||
realContent = '\n' + data.content
|
||||
break
|
||||
|
||||
case 'error':
|
||||
realContent = '\nError: ' + data.content
|
||||
break
|
||||
|
||||
default:
|
||||
if (data.content) {
|
||||
const newContent = currentRecord.answer + realContent
|
||||
|
||||
updateMessageContent(newContent)
|
||||
await nextTick()
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
updateMessageContent(currentRecord.answer + '\nError: Failed to get response')
|
||||
console.error('Error:', error)
|
||||
isTyping.value = false
|
||||
if (!currentRecord.error) {
|
||||
currentRecord.error = "";
|
||||
}
|
||||
if (currentRecord.error.trim().length !== 0) {
|
||||
currentRecord.error = currentRecord.error + "\n";
|
||||
}
|
||||
currentRecord.error = currentRecord.error + "Error:" + error;
|
||||
console.error("Error:", error);
|
||||
isTyping.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => currentChat.value?.records[currentChat.value.records.length - 1]?.answer, () => {
|
||||
//scrollToBottom()
|
||||
}, {deep: true})
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const formatMessage = (content: string) => {
|
||||
if (!content) return ''
|
||||
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) => {
|
||||
const textarea = e.target as HTMLTextAreaElement
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const value = textarea.value
|
||||
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)
|
||||
inputMessage.value = value.substring(0, start) + "\n" + value.substring(end);
|
||||
|
||||
nextTick(() => {
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 1
|
||||
})
|
||||
}
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -302,7 +375,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
height: 100%;
|
||||
|
||||
.chat-container-left {
|
||||
|
||||
--el-aside-width: 260px;
|
||||
|
||||
border-right: solid 1px rgba(0, 0, 0, 0.3);
|
||||
@@ -321,7 +393,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
.chat-list {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,12 +426,11 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
bottom: 14px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.send-btn {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { computed, onWatcherCleanup, unref, watch, ref } from 'vue';
|
||||
import type { Ref, VNode } from 'vue'
|
||||
|
||||
function isString(str: any): str is string {
|
||||
return typeof str === 'string';
|
||||
}
|
||||
|
||||
|
||||
function useState<T, R = Ref<T>>(
|
||||
defaultStateValue?: T | (() => T),
|
||||
): [R, (val: T) => void] {
|
||||
const initValue: T =
|
||||
typeof defaultStateValue === 'function' ? (defaultStateValue as any)() : defaultStateValue;
|
||||
|
||||
const innerValue = ref(initValue) as Ref<T>;
|
||||
|
||||
function triggerChange(newValue: T) {
|
||||
innerValue.value = newValue;
|
||||
}
|
||||
|
||||
return [innerValue as unknown as R, triggerChange];
|
||||
}
|
||||
/**
|
||||
* Return typed content and typing status when typing is enabled.
|
||||
* Or return content directly.
|
||||
*/
|
||||
const useTypedEffect = (
|
||||
content: Ref<VNode | object | string>,
|
||||
typingEnabled: Ref<boolean>,
|
||||
typingStep: Ref<number>,
|
||||
typingInterval: Ref<number>,
|
||||
): [typedContent: Ref<VNode | object | string>, isTyping: Ref<boolean>] => {
|
||||
const [prevContent, setPrevContent] = useState<VNode | object | string>('');
|
||||
const [typingIndex, setTypingIndex] = useState<number>(1);
|
||||
|
||||
const mergedTypingEnabled = computed(() => typingEnabled.value && isString(content.value));
|
||||
|
||||
// Reset typing index when content changed
|
||||
watch(content, () => {
|
||||
const prevContentValue = unref(prevContent);
|
||||
setPrevContent(content.value);
|
||||
if (!mergedTypingEnabled.value && isString(content.value)) {
|
||||
setTypingIndex(content.value.length);
|
||||
} else if (isString(content.value) && isString(prevContentValue) && content.value.indexOf(prevContentValue) !== 0) {
|
||||
setTypingIndex(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Start typing
|
||||
watch([typingIndex, typingEnabled, content], () => {
|
||||
if (mergedTypingEnabled.value && isString(content.value) && unref(typingIndex) < content.value.length) {
|
||||
const id = setTimeout(() => {
|
||||
setTypingIndex(unref(typingIndex) + typingStep.value);
|
||||
}, typingInterval.value);
|
||||
|
||||
onWatcherCleanup(() => {
|
||||
clearTimeout(id);
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const mergedTypingContent = computed(() => mergedTypingEnabled.value && isString(content.value) ? content.value.slice(0, unref(typingIndex)) : content.value);
|
||||
|
||||
return [mergedTypingContent, computed(() => mergedTypingEnabled.value && isString(content.value) && unref(typingIndex) < content.value.length)];
|
||||
};
|
||||
|
||||
export default useTypedEffect;
|
||||
@@ -11,17 +11,17 @@ defineProps<{
|
||||
ds: any
|
||||
}>()
|
||||
|
||||
const getStatus = (status: string) => {
|
||||
if (status === 'Success') {
|
||||
return 'connected'
|
||||
}
|
||||
if (status === 'Fail') {
|
||||
return 'failed'
|
||||
}
|
||||
if (status === 'Checking') {
|
||||
return 'needs-verification'
|
||||
}
|
||||
}
|
||||
// const getStatus = (status: string) => {
|
||||
// if (status === 'Success') {
|
||||
// return 'connected'
|
||||
// }
|
||||
// if (status === 'Fail') {
|
||||
// return 'failed'
|
||||
// }
|
||||
// if (status === 'Checking') {
|
||||
// return 'needs-verification'
|
||||
// }
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
@@ -44,7 +44,7 @@ const getStatus = (status: string) => {
|
||||
<div class="connection-host">{{ ds.description }}</div>
|
||||
<div class="connection-last">{{ datetimeFormat(ds.create_time) }}</div>
|
||||
</div>
|
||||
<div class="connection-status" :class="`${getStatus(ds.status)}`">{{ ds.status }}</div>
|
||||
<!-- <div class="connection-status" :class="`${getStatus(ds.status)}`">{{ ds.status }}</div> -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-button text style="color: #fff" :icon="ArrowLeft" @click="back()" />
|
||||
{{ props.dsName }}
|
||||
</div>
|
||||
<el-button type="primary" @click="save()"> Save </el-button>
|
||||
<!-- <el-button type="primary" @click="save()"> Save </el-button> -->
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="left-side">
|
||||
@@ -43,7 +43,8 @@
|
||||
<span>{{ currentTable.table_name }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span>Comment:</span>
|
||||
<el-input style="margin-left: 12px;" v-model="currentTable.custom_comment" />
|
||||
<span>{{ currentTable.custom_comment }}</span>
|
||||
<el-button style="margin-left: 10px;" text class="action-btn" :icon="IconOpeEdit" @click="editTable"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs
|
||||
@@ -58,15 +59,16 @@
|
||||
<el-table-column prop="field_comment" label="Comment" />
|
||||
<el-table-column label="Custom Comment">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.custom_comment" />
|
||||
<div class="field-comment">
|
||||
<span>{{ scope.row.custom_comment }}</span>
|
||||
<el-button text class="action-btn" :icon="IconOpeEdit" @click="editField(scope.row)"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Status" width="180">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-switch v-model="scope.row.checked" size="small" />
|
||||
<el-switch v-model="scope.row.checked" size="small" @change="changeStatus(scope.row)"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -89,6 +91,48 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="tableDialog"
|
||||
title="Edit Table Comment"
|
||||
width="600"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@closed="closeTable"
|
||||
>
|
||||
<div>Table Comment</div>
|
||||
<el-input v-model="currentTable.custom_comment" />
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
|
||||
<el-button @click="closeTable">Cancel</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="saveTable"
|
||||
>Confirm</el-button
|
||||
>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="fieldDialog"
|
||||
title="Edit Field Comment"
|
||||
width="600"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@closed="closeField"
|
||||
>
|
||||
<div>Field Comment</div>
|
||||
<el-input v-model="currentField.custom_comment" />
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
|
||||
<el-button @click="closeField">Cancel</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="saveField"
|
||||
>Confirm</el-button
|
||||
>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -98,6 +142,7 @@ import { datasourceApi } from "@/api/datasource";
|
||||
import { onMounted } from "vue";
|
||||
import { ArrowLeft } from "@element-plus/icons-vue";
|
||||
import type { TabsPaneContext } from "element-plus-secondary";
|
||||
import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
|
||||
|
||||
const props = defineProps({
|
||||
dsId: { type: [Number], required: true },
|
||||
@@ -108,10 +153,13 @@ const dsId = ref<Number>(0);
|
||||
const searchValue = ref("");
|
||||
const tableList = ref<any>([]);
|
||||
const currentTable = ref<any>({});
|
||||
const currentField = ref<any>({});
|
||||
const fieldList = ref<any>([]);
|
||||
const previewData = ref<any>({});
|
||||
|
||||
const activeName = ref("schema");
|
||||
const tableDialog = ref<boolean>(false)
|
||||
const fieldDialog = ref<boolean>(false)
|
||||
|
||||
const buildData = () => {
|
||||
return { table: currentTable.value, fields: fieldList.value };
|
||||
@@ -121,15 +169,59 @@ const back = () => {
|
||||
history.back();
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
datasourceApi.edit(buildData()).then(() => {
|
||||
// const save = () => {
|
||||
// datasourceApi.edit(buildData()).then(() => {
|
||||
// ElMessage({
|
||||
// message: "Save success",
|
||||
// type: "success",
|
||||
// showClose: true,
|
||||
// });
|
||||
// });
|
||||
// };
|
||||
|
||||
const editTable = () => {
|
||||
tableDialog.value = true
|
||||
}
|
||||
|
||||
const closeTable = () => {
|
||||
tableDialog.value = false
|
||||
}
|
||||
|
||||
const saveTable = () => {
|
||||
datasourceApi.saveTable(currentTable.value).then(() => {
|
||||
closeTable()
|
||||
ElMessage({
|
||||
message: "Save success",
|
||||
message: "Edit Success",
|
||||
type: "success",
|
||||
showClose: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const editField = (row: any) => {
|
||||
currentField.value = row
|
||||
fieldDialog.value = true
|
||||
}
|
||||
|
||||
const changeStatus = (row: any) => {
|
||||
currentField.value = row
|
||||
saveField()
|
||||
}
|
||||
|
||||
const closeField = () => {
|
||||
fieldDialog.value = false
|
||||
}
|
||||
|
||||
const saveField = () => {
|
||||
datasourceApi.saveField(currentField.value).then(() => {
|
||||
closeField()
|
||||
ElMessage({
|
||||
message: "Edit Success",
|
||||
type: "success",
|
||||
showClose: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clickTable = (table: any) => {
|
||||
currentTable.value = table;
|
||||
@@ -200,4 +292,13 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-comment{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-btn{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user