mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: Rename & Delete Chat
This commit is contained in:
@@ -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 ###
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
-56
@@ -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']
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user