feat: Rename & Delete Chat

This commit is contained in:
ulleo
2025-05-23 18:26:15 +08:00
parent 72d7104c34
commit 0ecca1e21f
9 changed files with 455 additions and 126 deletions
@@ -0,0 +1,52 @@
"""006_add_chat
Revision ID: ff653d5df198
Revises: 0a6f11be9be4
Create Date: 2025-05-21 15:44:29.763091
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'ff653d5df198'
down_revision = 'e6276ddab06e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chat',
sa.Column('id', sa.Integer(), sa.Identity(always=True), nullable=False),
sa.Column('create_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('create_by', sa.BigInteger(), nullable=True),
sa.Column('brief', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True),
sa.Column('chat_type', sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False),
sa.Column('datasource', sa.Integer(), nullable=False),
sa.Column('engine_type', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('chat_record',
sa.Column('id', sa.Integer(), sa.Identity(always=True), nullable=False),
sa.Column('chat_id', sa.Integer(), nullable=True),
sa.Column('create_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('create_by', sa.BigInteger(), nullable=True),
sa.Column('datasource', sa.Integer(), nullable=False),
sa.Column('engine_type', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False),
sa.Column('question', sa.Text(), nullable=True),
sa.Column('full_question', sa.Text(), nullable=True),
sa.Column('answer', sa.Text(), nullable=True),
sa.Column('run_time', sa.Float(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('chat_record')
op.drop_table('chat')
# ### end Alembic commands ###
+25 -2
View File
@@ -2,8 +2,9 @@ from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import select
from apps.chat.curd.chat import list_chats, get_chat_with_records, create_chat, save_question, save_answer
from apps.chat.models.chat_model import CreateChat, ChatRecord
from apps.chat.curd.chat import list_chats, get_chat_with_records, create_chat, save_question, save_answer, rename_chat, \
delete_chat
from apps.chat.models.chat_model import CreateChat, ChatRecord, RenameChat
from apps.chat.schemas.chat_base_schema import LLMConfig
from apps.chat.schemas.chat_schema import ChatQuestion
from apps.chat.schemas.llm import AgentService
@@ -32,6 +33,28 @@ async def list_chat(session: SessionDep, current_user: CurrentUser, chart_id: in
)
@router.post("/rename")
async def rename(session: SessionDep, chat: RenameChat):
try:
return rename_chat(session=session, rename_object=chat)
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
@router.get("/delete/{chart_id}")
async def delete(session: SessionDep, chart_id: int):
try:
return delete_chat(session=session, chart_id=chart_id)
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
@router.post("/start")
async def start_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat):
try:
+29 -1
View File
@@ -5,7 +5,7 @@ from typing import List
from sqlalchemy import text, and_
from sqlmodel import select
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo, RenameChat
from apps.chat.schemas.chat_schema import ChatQuestion
from apps.datasource.models.datasource import CoreDatasource
from common.core.deps import SessionDep, CurrentUser
@@ -17,6 +17,32 @@ def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]:
return chart_list
def rename_chat(session: SessionDep, rename_object: RenameChat) -> str:
chat = session.query(Chat).filter(Chat.id == rename_object.id).first()
if not chat:
raise Exception(f"Chat with id {rename_object.id} not found")
chat.brief = rename_object.brief.strip()[:20]
session.add(chat)
session.flush()
session.refresh(chat)
brief = chat.brief
session.commit()
return brief
def delete_chat(session, chart_id) -> str:
chat = session.query(Chat).filter(Chat.id == chart_id).first()
if not chat:
return f'Chat with id {chart_id} has been deleted'
session.delete(chat)
session.commit()
return f'Chat with id {chart_id} has been deleted'
def get_chat_with_records(session: SessionDep, chart_id: int, current_user: CurrentUser) -> ChatInfo:
chat = session.query(Chat).filter(Chat.id == chart_id).first()
if not chat:
@@ -98,6 +124,7 @@ def save_question(session: SessionDep, current_user: CurrentUser, question: Chat
return result
def save_full_question(session: SessionDep, id: int, full_question: str) -> ChatRecord:
if not id:
raise Exception("Record id cannot be None")
@@ -114,6 +141,7 @@ def save_full_question(session: SessionDep, id: int, full_question: str) -> Chat
return result
def save_answer(session: SessionDep, id: int, answer: str) -> ChatRecord:
if not id:
raise Exception("Record id cannot be None")
+5
View File
@@ -36,6 +36,11 @@ class CreateChat(BaseModel):
datasource: int = None
class RenameChat(BaseModel):
id: int = None
brief: str = ''
class ChatInfo(BaseModel):
id: Optional[int] = None
create_time: datetime = None
-56
View File
@@ -1,56 +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']
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']
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']
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']
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']
}
}
+11 -1
View File
@@ -147,6 +147,14 @@ function startChat(data: any): Promise<ChatInfo> {
return request.post('/chat/start', data)
}
function renameChat(chat_id: number, brief: string): Promise<string> {
return request.post('/chat/rename', {id: chat_id, brief: brief})
}
function deleteChat(id: number): Promise<string> {
return request.get(`/chat/delete/${id}`)
}
export const chatApi = {
toChatRecord,
toChatRecordList,
@@ -154,5 +162,7 @@ export const chatApi = {
toChatInfoList,
list,
get,
startChat
startChat,
renameChat,
deleteChat
}
+96 -44
View File
@@ -3,59 +3,67 @@
<div class="main-menu" :class="{ 'main-menu-sidebar': !topLayout, 'main-menu-topbar': topLayout }">
<div class="logo">SQLBot</div>
<div v-if="!topLayout || !showSubmenu" :class="{ 'workspace-area': !topLayout, 'topbar-workspace-area': topLayout }">
<div v-if="!topLayout || !showSubmenu"
:class="{ 'workspace-area': !topLayout, 'topbar-workspace-area': topLayout }">
<el-select
v-model="workspace"
placeholder="Select"
class="workspace-select"
size="large"
style="width: 240px"
v-model="workspace"
placeholder="Select"
class="workspace-select"
size="large"
style="width: 240px"
>
<template #label="{ label }">
<div class="workspace-label">
<el-icon><folder /></el-icon>
<el-icon>
<folder/>
</el-icon>
<span>{{ label }}</span>
</div>
</template>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<el-menu
v-if="!topLayout || !showSubmenu"
:default-active="activeMenu"
class="menu-container"
:mode="topLayout ? 'horizontal' : 'vertical'"
v-if="!topLayout || !showSubmenu"
:default-active="activeMenu"
class="menu-container"
:mode="topLayout ? 'horizontal' : 'vertical'"
>
<el-menu-item v-for="item in routerList" :key="item.path" :index="item.path" @click="menuSelect">
<el-icon v-if="item.meta.icon">
<component :is="resolveIcon(item.meta.icon)" />
<component :is="resolveIcon(item.meta.icon)"/>
</el-icon>
<span>{{ item.meta.title }}</span>
</el-menu-item>
</el-menu>
<div v-else class="top-bar-title">
<span class="split" />
<span class="split"/>
<span>System manage</span>
</div>
<div class="main-topbar-right" v-if="topLayout">
<div class="top-back-area" v-if="showSubmenu">
<el-button type="primary" text="primary" @click="backMain">
<el-icon class="el-icon--right"><ArrowLeftBold /></el-icon>Back
<el-icon class="el-icon--right">
<ArrowLeftBold/>
</el-icon>
Back
</el-button>
</div>
<el-tooltip content="System manage" placement="bottom" v-else>
<div class="header-icon-btn" @click="toSystem">
<el-icon><iconsystem /></el-icon>
<el-icon>
<iconsystem/>
</el-icon>
<!-- <span>System manage</span> -->
</div>
</el-tooltip>
@@ -82,11 +90,13 @@
<div class="header-actions">
<el-tooltip content="System manage" placement="bottom">
<div class="header-icon-btn" @click="toSystem">
<el-icon><iconsystem /></el-icon>
<el-icon>
<iconsystem/>
</el-icon>
<span>System manage</span>
</div>
</el-tooltip>
<el-dropdown trigger="click">
<div class="user-info">
<el-avatar size="small">{{ name?.charAt(0) }}</el-avatar>
@@ -102,38 +112,38 @@
</div>
</div>
</div>
<div v-if="sysRouterList.length && showSubmenu" class="sub-menu-container">
<el-menu
:default-active="activeMenu"
class="el-menu-demo"
:mode="!topLayout ? 'horizontal' : 'vertical'"
:default-active="activeMenu"
class="el-menu-demo"
:mode="!topLayout ? 'horizontal' : 'vertical'"
>
<el-menu-item v-for="item in sysRouterList" :key="item.path" :index="item.path" @click="menuSelect">
<el-icon v-if="item.meta.icon">
<component :is="resolveIcon(item.meta.icon)" />
<component :is="resolveIcon(item.meta.icon)"/>
</el-icon>
<span>{{ item.meta.title }}</span>
</el-menu-item>
</el-menu>
</div>
<div v-if="sysRouterList.length && showSubmenu" class="sys-page-content">
<div class="sys-inner-container">
<router-view />
<router-view/>
</div>
</div>
<div v-else class="page-content">
<router-view />
<router-view/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import {ref, computed, onMounted} from 'vue'
import {useRouter, useRoute} from 'vue-router'
import {useUserStore} from '@/stores/user'
import folder from '@/assets/svg/folder.svg'
import ds from '@/assets/svg/ds.svg'
import dashboard from '@/assets/svg/dashboard.svg'
@@ -142,9 +152,10 @@ import iconsetting from '@/assets/svg/setting.svg'
import iconsystem from '@/assets/svg/system.svg'
import icon_user from '@/assets/svg/icon_user.svg'
import icon_ai from '@/assets/svg/icon_ai.svg'
import { ArrowLeftBold } from '@element-plus/icons-vue'
import { useCache } from '@/utils/useCache'
const { wsCache } = useCache()
import {ArrowLeftBold} from '@element-plus/icons-vue'
import {useCache} from '@/utils/useCache'
const {wsCache} = useCache()
const topLayout = ref(false)
const router = useRouter()
const route = useRoute()
@@ -166,9 +177,9 @@ const showSubmenu = computed(() => {
})
const workspace = ref('1')
const options = [
{ value: '1', label: 'Default workspace' },
{ value: '2', label: 'Workspace 2' },
{ value: '3', label: 'Workspace 3' }
{value: '1', label: 'Default workspace'},
{value: '2', label: 'Workspace 2'},
{value: '3', label: 'Workspace 3'}
]
const currentPageTitle = computed(() => {
if (route.path.includes('/system')) {
@@ -214,26 +225,33 @@ onMounted(() => {
.app-topbar-container {
flex-direction: column;
}
.app-container {
display: flex;
height: 100vh;
.main-menu {
display: flex;
.workspace-area {
margin: 8px 16px;
width: 208px;
overflow: hidden;
.workspace-select {
width: 100% !important;
:deep(.ed-select__wrapper) {
border-radius: 10px;
box-shadow: none !important;
background-color: #f1f3f4;
line-height: 32px;
min-height: 48px;
.ed-select__selected-item {
height: 32px;
}
.workspace-label {
color: #2d2e31;
font-weight: 600;
@@ -245,6 +263,7 @@ onMounted(() => {
}
}
}
.logo {
height: 68px;
line-height: 68px;
@@ -261,6 +280,7 @@ onMounted(() => {
border-bottom: none;
}
}
.main-menu-sidebar {
width: 240px;
background: #fff;
@@ -268,6 +288,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
}
.main-menu-topbar {
height: 60px;
line-height: 60px;
@@ -278,6 +299,7 @@ onMounted(() => {
justify-content: space-between;
border-bottom: 1px solid var(--el-menu-border-color);
text-align: center;
.logo {
height: 60px;
line-height: 60px;
@@ -288,6 +310,7 @@ onMounted(() => {
height: 60px;
align-items: center;
padding-right: 24px;
.header-icon-btn {
display: flex;
column-gap: 12px;
@@ -300,42 +323,51 @@ onMounted(() => {
transition: all 0.3s;
font-size: 14px;
color: #5f6368;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
:deep(.user-info) {
display: flex;
column-gap: 4px;
align-items: center;
.ed-avatar {
background-color: var(--el-color-primary);
color: #fff;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #202124;
}
}
.top-back-area {
align-items: center;
display: flex;
}
}
.topbar-workspace-area {
margin: 0 32px;
height: auto;
width: 208px;
line-height: 54px;
.workspace-select {
width: 100% !important;
:deep(.ed-select__wrapper) {
border-radius: 10px;
box-shadow: none !important;
background-color: #f1f3f4;
line-height: 24px;
min-height: 32px;
min-height: 32px;
.workspace-label {
color: #2d2e31;
font-weight: 600;
@@ -347,6 +379,7 @@ onMounted(() => {
}
}
}
.top-bar-title {
font-size: 14px;
color: var(--el-color-info);
@@ -355,6 +388,7 @@ onMounted(() => {
left: 132px;
width: 200px;
position: fixed;
.split {
color: #bbbbbb;
border: 0.5px solid;
@@ -366,36 +400,42 @@ onMounted(() => {
.main-content {
width: calc(100% - 288px);
height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f7fa;
padding: 24px;
box-sizing: border-box;
.header-container {
box-sizing: border-box;
margin: 0;
padding: 0;
height: 60px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
.header {
height: 36px;
line-height: 36px;
color: #202124;
h1 {
font-size: 24px;
font-weight: 500;
height: 36px;
line-height: 36px;
}
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.header-actions {
display: flex;
height: 36px;
align-items: center;
.header-icon-btn {
display: flex;
column-gap: 12px;
@@ -408,18 +448,22 @@ onMounted(() => {
transition: all 0.3s;
font-size: 14px;
color: #5f6368;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
:deep(.user-info) {
display: flex;
column-gap: 4px;
align-items: center;
.ed-avatar {
background-color: var(--el-color-primary);
color: #fff;
}
.user-name {
font-size: 14px;
font-weight: 500;
@@ -429,12 +473,13 @@ onMounted(() => {
}
}
}
.page-content {
flex: 1;
overflow-y: auto;
}
.sys-page-content {
background-color: var(--white);
border-radius: var(--border-radius);
@@ -442,6 +487,7 @@ onMounted(() => {
box-shadow: var(--shadow);
margin-top: 24px;
flex: 1;
.sys-inner-container {
background: #fff;
border-radius: 8px;
@@ -449,16 +495,20 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.sub-menu-container {
overflow: hidden;
border-radius: 8px;
}
}
.main-conntent-withbar {
padding: 0;
height: 0;
flex: 1;
width: 100%;
display: flex;
flex-direction: row;
.sub-menu-container {
flex: 0 0 auto;
background-color: lightblue;
@@ -467,10 +517,12 @@ onMounted(() => {
border-right: 1px solid var(--el-menu-border-color);
border-radius: 0;
background-color: var(--white);
:deep(.ed-menu) {
border: none;
}
}
.sys-page-content {
margin: 0;
border-radius: 0;
+191
View File
@@ -0,0 +1,191 @@
<script setup lang="ts">
import {MoreFilled, EditPen, Delete} from '@element-plus/icons-vue'
import {type Chat, chatApi} from "@/api/chat.ts";
import {computed} from "vue";
const props = withDefaults(defineProps<{
currentChatId?: number
chatList: Array<Chat>
loading?: boolean
}>(), {
chatList: () => [],
loading: false
})
const emits = defineEmits(['chatSelected', 'chatRenamed', 'chatDeleted', "update:loading"])
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits("update:loading", v)
}
})
function onClickHistory(chat: Chat) {
emits("chatSelected", chat)
}
function handleCommand(command: string | number | object, chat: Chat) {
if (chat && chat.id !== undefined) {
switch (command) {
case "rename":
ElMessageBox.prompt('Please enter new brief', 'Rename', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
customStyle: {'padding-left': 'unset', padding: 'var(--ed-messagebox-padding-primary)'},
inputValue: chat.brief,
inputValidator: (value: string) => {
if (!value) return false
if (value.trim().length == 0) return false
return true
}
})
.then(({value}) => {
_loading.value = true
chatApi.renameChat(chat.id, value)
.then((res) => {
ElMessage({
type: 'success',
message: 'Successfully renamed chat',
})
emits('chatRenamed', {id: chat.id, brief: res})
})
.catch((err) => {
ElMessage({
type: 'error',
message: err.message,
})
console.error(err)
})
.finally(() => {
_loading.value = false
})
})
break
case "delete":
ElMessageBox.confirm(
'This action will permanently delete the chat. Continue?',
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
_loading.value = true
chatApi.deleteChat(chat.id)
.then((res) => {
ElMessage({
type: 'success',
message: res,
})
emits('chatDeleted', chat.id)
})
.catch((err) => {
ElMessage({
type: 'error',
message: err.message,
})
console.error(err)
})
.finally(() => {
_loading.value = false
})
})
break
}
}
}
</script>
<template>
<el-scrollbar>
<div class="chat-list-inner">
<template v-for="chat in chatList" :key="chat.id">
<div class="chat-list-item" :class="{'active': currentChatId === chat.id}" @click="onClickHistory(chat)">
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
<el-button class="icon-more" link>
<el-dropdown trigger="click" @command="(cmd: any)=> handleCommand(cmd, chat)">
<el-icon>
<MoreFilled/>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="EditPen" command="rename">Rename</el-dropdown-item>
<el-dropdown-item :icon="Delete" command="delete">Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-button>
</div>
</template>
</div>
</el-scrollbar>
</template>
<style scoped lang="less">
.chat-list-inner {
--hover-color: var(--ed-color-primary-light-9);
--active-color: var(--hover-color);
padding-left: 14px;
padding-right: 14px;
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
.chat-list-item {
width: 100%;
height: 42px;
cursor: pointer;
border-radius: 6px;
line-height: 1em;
display: flex;
align-items: center;
padding: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.title {
flex: 1;
}
.icon-more {
margin-left: auto;
display: none;
min-width: unset;
}
&:hover {
background-color: var(--hover-color);
.icon-more {
display: inline-flex;
}
}
&.active {
background-color: var(--active-color);
}
}
}
</style>
+46 -22
View File
@@ -11,29 +11,26 @@
</el-button>
</el-header>
<el-main class="chat-list">
<el-scrollbar>
<div v-for="chat in chatList" @click="onClickHistory(chat)"
:style="chat.id === currentChatId ? {border: 'solid 1px red'} : {}">
{{ chat.create_time }}<br/>
{{ chat.brief }}
</div>
</el-scrollbar>
<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>
<el-container :loading="loading">
<el-main>
<div v-for="message in computedMessages">
{{ message.role }}: {{ message.content }}
<div v-if="message.isWelcome">
<el-select v-model="currentChat.datasource" :disabled="currentChat.id!==undefined">
<el-option v-for="item in dsList"
:value="item.id"
:key="item.id"
:label="item.name"/>
</el-select>
<el-main class="chat-record-list">
<el-scrollbar>
<div v-for="message in computedMessages">
{{ message.role }}: {{ message.content }}
<div v-if="message.isWelcome">
<el-select v-model="currentChat.datasource" :disabled="currentChat.id!==undefined">
<el-option v-for="item in dsList"
:value="item.id"
:key="item.id"
:label="item.name"/>
</el-select>
</div>
</div>
</div>
</el-scrollbar>
</el-main>
<el-footer>
<div class="chat-input">
@@ -66,7 +63,8 @@
import {ref, computed, nextTick, watch, onMounted} from 'vue'
import {Plus, Position} from '@element-plus/icons-vue'
import {Chat, chatApi, ChatInfo, ChatRecord, questionApi} from '@/api/chat'
import {datasourceApi} from "@/api/datasource.ts";
import {datasourceApi} from "@/api/datasource.ts"
import ChatList from './ChatList.vue'
interface ChatMessage {
role: 'user' | 'assistant'
@@ -132,6 +130,7 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
const createNewChat = () => {
currentChat.value = new ChatInfo()
currentChatId.value = undefined
inputMessage.value = ''
listDs()
}
@@ -164,9 +163,28 @@ function onClickHistory(chat: Chat) {
}
}
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
}
}
}
function onChatRenamed(chat: Chat) {
chatList.value.forEach((c: Chat) => {
if (c.id === chat.id) {
c.brief = chat.brief
}
})
if (currentChat.value.id === chat.id) {
currentChat.value.brief = chat.brief
}
}
function listDs() {
datasourceApi.list().then((res) => {
console.log(res)
dsList.value = res
})
}
@@ -197,6 +215,7 @@ const sendMessage = async () => {
currentRecord.answer = ''
currentChat.value.records.push(currentRecord)
inputMessage.value = ''
let error = false
if (currentChatId.value === undefined) {
@@ -221,7 +240,6 @@ const sendMessage = async () => {
loading.value = false
})
}
if (error) return
try {
@@ -310,6 +328,8 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
border-right: solid 1px rgba(0, 0, 0, 0.3);
background: var(--ed-fill-color-blank);
.chat-container-right-container {
height: 100%;
@@ -320,12 +340,16 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
}
.chat-list {
padding-top: unset;
padding: 0 0 20px 0;
}
}
}
.chat-record-list {
padding: 20px 0 0 0;
}
}
</style>