Skip to content

Commit 9fceb1f

Browse files
committed
feat: Chat UI
1 parent 4f830d9 commit 9fceb1f

File tree

7 files changed

+477
-212
lines changed

7 files changed

+477
-212
lines changed

frontend/src/api/chat.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export const questionApi = {
1616
query: (id: number) => request.get(`/chat/question/${id}`)
1717
}
1818

19+
export interface ChatMessage {
20+
role: 'user' | 'assistant'
21+
create_time?: Date | string
22+
content?: string | number
23+
isTyping?: boolean
24+
isWelcome?: boolean
25+
}
26+
1927
export class ChatRecord {
2028
id?: number
2129
chat_id?: number
@@ -147,11 +155,11 @@ function startChat(data: any): Promise<ChatInfo> {
147155
return request.post('/chat/start', data)
148156
}
149157

150-
function renameChat(chat_id: number, brief: string): Promise<string> {
158+
function renameChat(chat_id: number | undefined, brief: string): Promise<string> {
151159
return request.post('/chat/rename', {id: chat_id, brief: brief})
152160
}
153161

154-
function deleteChat(id: number): Promise<string> {
162+
function deleteChat(id: number | undefined): Promise<string> {
155163
return request.get(`/chat/delete/${id}`)
156164
}
157165

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
3+
import type {ChatMessage} from "@/api/chat.ts";
4+
5+
defineProps<{
6+
msg?: ChatMessage
7+
}>()
8+
9+
</script>
10+
11+
<template>
12+
<div class="chat-block">
13+
<slot>
14+
{{ msg?.content }}
15+
</slot>
16+
</div>
17+
</template>
18+
19+
<style scoped lang="less">
20+
.chat-block {
21+
border-radius: 2px;
22+
background-color: white;
23+
padding: 12px;
24+
word-wrap: break-word;
25+
}
26+
</style>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<script setup lang="ts">
2+
import ChatBlock from './ChatBlock.vue'
3+
import WelcomeBlock from './WelcomeBlock.vue'
4+
import {ChatInfo, type ChatMessage} from "@/api/chat.ts";
5+
import {computed} from "vue";
6+
7+
const props = withDefaults(defineProps<{
8+
msg: ChatMessage
9+
currentChat: ChatInfo
10+
datasource?: number
11+
}>(),
12+
{}
13+
)
14+
15+
const emits = defineEmits(["update:datasource"])
16+
17+
const _datasource = computed({
18+
get() {
19+
return props.datasource
20+
},
21+
set(v) {
22+
emits("update:datasource", v)
23+
}
24+
})
25+
26+
</script>
27+
28+
<template>
29+
<div class="chat-row" :class="{'right-to-left': msg.role === 'user'}">
30+
<el-avatar shape="square" v-if="msg.role === 'assistant'">SQBot</el-avatar>
31+
<el-avatar shape="square" v-if="msg.role === 'user'"/>
32+
<ChatBlock v-if="!msg.isWelcome" :msg="msg" :class="{'row-full': msg.role === 'assistant'}">
33+
<slot></slot>
34+
</ChatBlock>
35+
<WelcomeBlock v-else v-model="_datasource" :current-chat="currentChat" class="row-full"/>
36+
</div>
37+
</template>
38+
39+
<style scoped lang="less">
40+
.chat-row {
41+
display: flex;
42+
flex-direction: row;
43+
align-items: flex-start;
44+
gap: 8px;
45+
46+
padding: 20px 20px 0;
47+
48+
&.right-to-left {
49+
flex-direction: row-reverse;
50+
}
51+
52+
.row-full {
53+
flex: 1;
54+
}
55+
}
56+
57+
58+
</style>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script setup lang="ts">
2+
import ChatBlock from './ChatBlock.vue'
3+
import {ChatInfo} from "@/api/chat.ts";
4+
import {computed, onMounted, ref} from "vue";
5+
import {datasourceApi} from "@/api/datasource.ts";
6+
import DatasourceItemCard from '../ds/DatasourceItemCard.vue'
7+
8+
const props = withDefaults(defineProps<{
9+
modelValue?: number
10+
currentChat: ChatInfo
11+
}>(),
12+
{}
13+
)
14+
15+
const dsList = ref<Array<any>>([])
16+
17+
const editable = computed(() => {
18+
return props.currentChat?.id === undefined
19+
})
20+
21+
const ds = computed(() => {
22+
for (let i = 0; i < dsList.value.length; i++) {
23+
const item = dsList.value[i]
24+
if (props.modelValue === item.id) {
25+
return item
26+
}
27+
}
28+
if (props.currentChat && props.currentChat.datasource !== undefined) {
29+
return {
30+
id: props.currentChat.datasource,
31+
name: props.currentChat.datasource_name ?? 'Datasource does not exist',
32+
type: props.currentChat.engine_type
33+
}
34+
}
35+
return undefined
36+
})
37+
38+
const emits = defineEmits(["update:modelValue"])
39+
40+
function selectDs(ds: any) {
41+
if (editable.value) {
42+
emits("update:modelValue", ds.id)
43+
}
44+
}
45+
46+
function listDs() {
47+
datasourceApi.list().then((res) => {
48+
dsList.value = res
49+
})
50+
}
51+
52+
function showDs() {
53+
listDs()
54+
55+
}
56+
57+
58+
onMounted(() => {
59+
listDs()
60+
})
61+
62+
</script>
63+
64+
<template>
65+
<ChatBlock>
66+
<div>你好,我是SQLBot,很高兴为你服务</div>
67+
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
68+
<template v-if="editable">
69+
<template v-if="dsList.length>0">
70+
<div class="ds-select-row">
71+
<div>选择数据源</div>
72+
<el-button @click="showDs" link type="primary">查看更多</el-button>
73+
</div>
74+
<div class="ds-row-container">
75+
<template v-for="(item, _index) in dsList" :key="_index">
76+
<DatasourceItemCard :ds="item" @click="selectDs(item)" v-if="_index<3 || item?.id===modelValue"
77+
class="ds-card" :class="[item?.id===modelValue? 'ds-card-selected': '']"/>
78+
</template>
79+
</div>
80+
</template>
81+
<div v-else>
82+
数据源为空,请新建后再开启智能问数!
83+
</div>
84+
</template>
85+
<template v-else>
86+
<div class="ds-select-row">
87+
<div>已选择数据源</div>
88+
</div>
89+
<div class="ds-row-container">
90+
<DatasourceItemCard :ds="ds"/>
91+
</div>
92+
</template>
93+
</ChatBlock>
94+
</template>
95+
96+
<style scoped lang="less">
97+
.sub {
98+
color: grey;
99+
font-size: 0.8em;
100+
}
101+
102+
.ds-select-row {
103+
display: flex;
104+
align-items: center;
105+
flex-direction: row;
106+
justify-content: space-between;
107+
}
108+
109+
.ds-row-container {
110+
display: grid;
111+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
112+
gap: 24px;
113+
114+
}
115+
116+
.ds-card {
117+
cursor: pointer;
118+
}
119+
120+
.ds-card-selected {
121+
border-color: var(--ed-color-primary-light-5);
122+
}
123+
124+
125+
</style>

frontend/src/views/chat/index.vue

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,33 @@
1919
<el-container :loading="loading">
2020
<el-main class="chat-record-list">
2121
<el-scrollbar>
22-
<div v-for="message in computedMessages">
23-
{{ message.role }}: {{ message.content }}
24-
<div v-if="message.isWelcome">
25-
<el-select v-model="currentChat.datasource" :disabled="currentChat.id!==undefined">
26-
<el-option v-for="item in dsList"
27-
:value="item.id"
28-
:key="item.id"
29-
:label="item.name"/>
30-
</el-select>
31-
</div>
32-
</div>
22+
<template v-for="(message, _index) in computedMessages" :key="_index">
23+
<ChatRow :current-chat="currentChat" v-model:datasource="currentChat.datasource" :msg="message"/>
24+
</template>
3325
</el-scrollbar>
3426
</el-main>
35-
<el-footer>
36-
<div class="chat-input">
37-
<div class="input-wrapper">
38-
<el-input
39-
v-model="inputMessage"
40-
type="textarea"
41-
:rows="1"
42-
:autosize="{ minRows: 1, maxRows: 8 }"
43-
placeholder="Press Enter to send, Ctrl + Enter for new line"
44-
@keydown.enter.exact.prevent="sendMessage"
45-
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
46-
/>
47-
<div class="input-actions">
48-
<el-button circle type="primary" class="send-btn" @click="sendMessage">
49-
<el-icon>
50-
<Position/>
51-
</el-icon>
52-
</el-button>
53-
</div>
54-
</div>
27+
<el-footer class="chat-footer">
28+
<div style="height: 24px;">
29+
<template v-if="currentChat.datasource">
30+
使用数据源:{{ currentChat.datasource_name }}
31+
</template>
32+
</div>
33+
<div class="input-wrapper">
34+
<el-input
35+
class="input-area"
36+
v-model="inputMessage"
37+
type="textarea"
38+
:rows="1"
39+
:autosize="{ minRows: 1, maxRows: 8 }"
40+
placeholder="Press Enter to send, Ctrl + Enter for new line"
41+
@keydown.enter.exact.prevent="sendMessage"
42+
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
43+
/>
44+
<el-button link type="primary" class="input-icon" @click="sendMessage">
45+
<el-icon size="20">
46+
<Position/>
47+
</el-icon>
48+
</el-button>
5549
</div>
5650
</el-footer>
5751
</el-container>
@@ -62,23 +56,14 @@
6256
<script setup lang="ts">
6357
import {ref, computed, nextTick, watch, onMounted} from 'vue'
6458
import {Plus, Position} from '@element-plus/icons-vue'
65-
import {Chat, chatApi, ChatInfo, ChatRecord, questionApi} from '@/api/chat'
66-
import {datasourceApi} from "@/api/datasource.ts"
59+
import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi} from '@/api/chat'
6760
import ChatList from './ChatList.vue'
68-
69-
interface ChatMessage {
70-
role: 'user' | 'assistant'
71-
create_time?: Date | string
72-
content?: string | number
73-
isTyping?: boolean
74-
isWelcome?: boolean
75-
}
61+
import ChatRow from './ChatRow.vue'
7662
7763
const inputMessage = ref('')
7864
7965
const loading = ref<boolean>(false);
8066
const chatList = ref<Array<ChatInfo>>([])
81-
const dsList = ref<any>([])
8267
8368
const currentChatId = ref<number | undefined>()
8469
const currentChat = ref<ChatInfo>(new ChatInfo())
@@ -131,7 +116,6 @@ const createNewChat = () => {
131116
currentChat.value = new ChatInfo()
132117
currentChatId.value = undefined
133118
inputMessage.value = ''
134-
listDs()
135119
}
136120
137121
function getChatList() {
@@ -183,15 +167,9 @@ function onChatRenamed(chat: Chat) {
183167
}
184168
}
185169
186-
function listDs() {
187-
datasourceApi.list().then((res) => {
188-
dsList.value = res
189-
})
190-
}
191170
192171
onMounted(() => {
193172
getChatList()
194-
listDs()
195173
})
196174
197175
@@ -347,7 +325,39 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
347325
}
348326
349327
.chat-record-list {
350-
padding: 20px 0 0 0;
328+
padding: 0 0 20px 0;
329+
}
330+
331+
.chat-footer {
332+
--ed-footer-height: 120px;
333+
334+
display: flex;
335+
flex-direction: column;
336+
337+
.input-wrapper {
338+
flex: 1;
339+
340+
position: relative;
341+
342+
.input-area {
343+
height: 100%;
344+
padding-bottom: 8px;
345+
346+
:deep(.ed-textarea__inner) {
347+
height: 100% !important;
348+
}
349+
}
350+
351+
.input-icon {
352+
min-width: unset;
353+
position: absolute;
354+
bottom: 14px;
355+
right: 8px;
356+
}
357+
358+
}
359+
360+
351361
}
352362
353363

0 commit comments

Comments
 (0)