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'
# Conflicts: # frontend/components.d.ts
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,11 +2,13 @@ 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, Chat
|
||||
from apps.chat.schemas.chat_base_schema import LLMConfig
|
||||
from apps.chat.schemas.chat_schema import ChatQuestion
|
||||
from apps.chat.schemas.llm import AgentService
|
||||
from apps.datasource.crud.datasource import get_table_obj_by_ds
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
from apps.system.models.system_model import AiModelDetail
|
||||
from common.core.deps import SessionDep, CurrentUser
|
||||
@@ -32,6 +34,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:
|
||||
@@ -57,26 +81,18 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
"""
|
||||
question = request_question.question
|
||||
|
||||
# Get available AI model
|
||||
aimodel = session.exec(select(AiModelDetail).where(
|
||||
AiModelDetail.status == True,
|
||||
AiModelDetail.api_key.is_not(None)
|
||||
)).first()
|
||||
|
||||
# Get available datasource
|
||||
ds = session.exec(select(CoreDatasource).where(
|
||||
CoreDatasource.status == 'Success'
|
||||
)).first()
|
||||
|
||||
if not aimodel:
|
||||
chat = session.query(Chat).filter(Chat.id == request_question.chat_id).first()
|
||||
if not chat:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No available AI model configuration found"
|
||||
detail=f"Chat with id {request_question.chart_id} not found"
|
||||
)
|
||||
|
||||
# Get available datasource
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == chat.datasource).first()
|
||||
if not ds:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
status_code=500,
|
||||
detail="No available datasource configuration found"
|
||||
)
|
||||
|
||||
@@ -89,6 +105,17 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
detail=str(e1)
|
||||
)
|
||||
|
||||
# Get available AI model
|
||||
aimodel = session.exec(select(AiModelDetail).where(
|
||||
AiModelDetail.status == True,
|
||||
AiModelDetail.api_key.is_not(None)
|
||||
)).first()
|
||||
if not aimodel:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No available AI model configuration found"
|
||||
)
|
||||
|
||||
# Use Tongyi Qianwen
|
||||
tongyi_config = LLMConfig(
|
||||
model_type="openai",
|
||||
@@ -113,10 +140,39 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
""" result = llm_service.generate_sql(question)
|
||||
return result """
|
||||
|
||||
# get schema
|
||||
schema_str = ""
|
||||
table_objs = get_table_obj_by_ds(session=session, ds=ds)
|
||||
db_name = table_objs[0].schema
|
||||
schema_str += f"【DB_ID】 {db_name}\n【Schema】\n"
|
||||
for obj in table_objs:
|
||||
schema_str += f"# Table: {db_name}.{obj.table.table_name}"
|
||||
table_comment = ''
|
||||
if obj.table.custom_comment:
|
||||
table_comment = obj.table.custom_comment.strip()
|
||||
if table_comment == '':
|
||||
schema_str += '\n[\n'
|
||||
else:
|
||||
schema_str += f", {table_comment}\n[\n"
|
||||
|
||||
field_list = []
|
||||
for field in obj.fields:
|
||||
field_comment = ''
|
||||
if field.custom_comment:
|
||||
field_comment = field.custom_comment.strip()
|
||||
if field_comment == '':
|
||||
field_list.append(f"({field.field_name}:{field.field_type})")
|
||||
else:
|
||||
field_list.append(f"({field.field_name}:{field.field_type}, {field_comment})")
|
||||
schema_str += ",\n".join(field_list)
|
||||
schema_str += '\n]\n'
|
||||
|
||||
print(schema_str)
|
||||
|
||||
async def event_generator():
|
||||
all_text = ''
|
||||
try:
|
||||
async for chunk in llm_service.async_generate(question):
|
||||
async for chunk in llm_service.async_generate(question, schema_str):
|
||||
data = json.loads(chunk.replace('data: ', ''))
|
||||
|
||||
if data['type'] in ['final', 'tool_result']:
|
||||
|
||||
@@ -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:
|
||||
@@ -33,7 +59,7 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
|
||||
chat_info.datasource_name = ds.name
|
||||
|
||||
record_list = session.query(ChatRecord).filter(
|
||||
and_(Chat.create_by == current_user.id, ChatRecord.id == chart_id)).order_by(ChatRecord.create_time).all()
|
||||
and_(Chat.create_by == current_user.id, ChatRecord.chat_id == chart_id)).order_by(ChatRecord.create_time).all()
|
||||
|
||||
chat_info.records = record_list
|
||||
|
||||
@@ -98,10 +124,11 @@ 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")
|
||||
record = session.query(ChatRecord).filter(Chat.id == id).first()
|
||||
record = session.query(ChatRecord).filter(ChatRecord.id == id).first()
|
||||
record.full_question = full_question
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
@@ -114,11 +141,12 @@ 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")
|
||||
|
||||
record = session.query(ChatRecord).filter(Chat.id == id).first()
|
||||
record = session.query(ChatRecord).filter(ChatRecord.id == id).first()
|
||||
record.answer = answer
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -101,16 +101,17 @@ class AgentService:
|
||||
schema = self.db.get_table_info()
|
||||
return chain.invoke({"schema": schema, "question": question})
|
||||
|
||||
async def async_generate(self, question: str) -> AsyncGenerator[str, None]:
|
||||
async def async_generate(self, question: str, schema: str) -> AsyncGenerator[str, None]:
|
||||
|
||||
chain = self.prompt | self.agent_executor
|
||||
# schema = self.db.get_table_info()
|
||||
|
||||
schema_engine = SchemaEngine(engine=self.db._engine)
|
||||
mschema = schema_engine.mschema
|
||||
mschema_str = mschema.to_mschema()
|
||||
# schema_engine = SchemaEngine(engine=self.db._engine)
|
||||
# mschema = schema_engine.mschema
|
||||
# mschema_str = mschema.to_mschema()
|
||||
|
||||
async for chunk in chain.astream({"schema": mschema_str, "question": question}):
|
||||
# async for chunk in chain.astream({"schema": mschema_str, "question": question}):
|
||||
async for chunk in chain.astream({"schema": schema, "question": question}):
|
||||
if not isinstance(chunk, dict):
|
||||
continue
|
||||
|
||||
|
||||
@@ -211,10 +211,12 @@ def preview(session: SessionDep, id: int, data: TableObj):
|
||||
return exec_sql(ds, sql)
|
||||
|
||||
|
||||
def get_tableobj_by_ds(session: SessionDep, id: int) -> List[TableAndFields]:
|
||||
list: List = []
|
||||
tables = session.query(CoreTable).filter(CoreTable.ds_id == id).all()
|
||||
def get_table_obj_by_ds(session: SessionDep, ds: CoreDatasource) -> List[TableAndFields]:
|
||||
_list: List = []
|
||||
tables = session.query(CoreTable).filter(CoreTable.ds_id == ds.id).all()
|
||||
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config()
|
||||
schema = conf.dbSchema if conf.dbSchema is not None and conf.dbSchema != "" else conf.database
|
||||
for table in tables:
|
||||
fields = session.query(CoreField).filter(and_(CoreField.table_id == table.id, CoreField.checked == True)).all()
|
||||
list.append(TableAndFields(table=table, fields=fields))
|
||||
return list
|
||||
_list.append(TableAndFields(schema=schema, table=table, fields=fields))
|
||||
return _list
|
||||
|
||||
@@ -111,9 +111,11 @@ class ColumnSchema:
|
||||
|
||||
|
||||
class TableAndFields:
|
||||
def __init__(self, table, fields):
|
||||
def __init__(self, schema, table, fields):
|
||||
self.schema = schema
|
||||
self.table = table
|
||||
self.fields = fields
|
||||
|
||||
schema: str
|
||||
table: CoreTable
|
||||
fields: List[CoreField]
|
||||
|
||||
@@ -32,7 +32,7 @@ dependencies = [
|
||||
"pymssql (>=2.3.4,<3.0.0)",
|
||||
"pandas (>=2.2.3,<3.0.0)",
|
||||
"openpyxl (>=3.1.5,<4.0.0)",
|
||||
"psycopg2 (>=2.9.10,<3.0.0)",
|
||||
"psycopg2-binary (>=2.9.10,<3.0.0)",
|
||||
"oracledb (>=3.1.1,<4.0.0)"
|
||||
]
|
||||
[[tool.uv.index]]
|
||||
|
||||
@@ -16,6 +16,14 @@ export const questionApi = {
|
||||
query: (id: number) => request.get(`/chat/question/${id}`)
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
create_time?: Date | string
|
||||
content?: string | number
|
||||
isTyping?: boolean
|
||||
isWelcome?: boolean
|
||||
}
|
||||
|
||||
export class ChatRecord {
|
||||
id?: number
|
||||
chat_id?: number
|
||||
@@ -147,6 +155,14 @@ function startChat(data: any): Promise<ChatInfo> {
|
||||
return request.post('/chat/start', data)
|
||||
}
|
||||
|
||||
function renameChat(chat_id: number | undefined, brief: string): Promise<string> {
|
||||
return request.post('/chat/rename', {id: chat_id, brief: brief})
|
||||
}
|
||||
|
||||
function deleteChat(id: number | undefined): Promise<string> {
|
||||
return request.get(`/chat/delete/${id}`)
|
||||
}
|
||||
|
||||
export const chatApi = {
|
||||
toChatRecord,
|
||||
toChatRecordList,
|
||||
@@ -154,5 +170,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,27 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import type {ChatMessage} from "@/api/chat.ts";
|
||||
|
||||
defineProps<{
|
||||
msg?: ChatMessage
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-block">
|
||||
<slot>
|
||||
<div v-html="msg?.content"></div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chat-block {
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import ChatBlock from './ChatBlock.vue'
|
||||
import WelcomeBlock from './WelcomeBlock.vue'
|
||||
import {ChatInfo, type ChatMessage} from "@/api/chat.ts";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
msg: ChatMessage
|
||||
currentChat: ChatInfo
|
||||
datasource?: number
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
const emits = defineEmits(["update:datasource"])
|
||||
|
||||
const _datasource = computed({
|
||||
get() {
|
||||
return props.datasource
|
||||
},
|
||||
set(v) {
|
||||
emits("update:datasource", v)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-row" :class="{'right-to-left': msg.role === 'user'}">
|
||||
<el-avatar shape="square" v-if="msg.role === 'assistant'">SQBot</el-avatar>
|
||||
<el-avatar shape="square" v-if="msg.role === 'user'"/>
|
||||
<ChatBlock v-if="!msg.isWelcome" :msg="msg" :class="{'row-full': msg.role === 'assistant'}">
|
||||
<slot></slot>
|
||||
</ChatBlock>
|
||||
<WelcomeBlock v-else v-model="_datasource" :current-chat="currentChat" class="row-full"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chat-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
padding: 20px 20px 0;
|
||||
|
||||
&.right-to-left {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.row-full {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import ChatBlock from './ChatBlock.vue'
|
||||
import {ChatInfo} from "@/api/chat.ts";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {datasourceApi} from "@/api/datasource.ts";
|
||||
import DatasourceItemCard from '../ds/DatasourceItemCard.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: number
|
||||
currentChat: ChatInfo
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
const dsList = ref<Array<any>>([])
|
||||
|
||||
const editable = computed(() => {
|
||||
return props.currentChat?.id === undefined
|
||||
})
|
||||
|
||||
const ds = computed(() => {
|
||||
for (let i = 0; i < dsList.value.length; i++) {
|
||||
const item = dsList.value[i]
|
||||
if (props.modelValue === item.id) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
if (props.currentChat && props.currentChat.datasource !== undefined) {
|
||||
return {
|
||||
id: props.currentChat.datasource,
|
||||
name: props.currentChat.datasource_name ?? 'Datasource does not exist',
|
||||
type: props.currentChat.engine_type
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const emits = defineEmits(["update:modelValue"])
|
||||
|
||||
function selectDs(ds: any) {
|
||||
if (editable.value) {
|
||||
emits("update:modelValue", ds.id)
|
||||
}
|
||||
}
|
||||
|
||||
function listDs() {
|
||||
datasourceApi.list().then((res) => {
|
||||
dsList.value = res
|
||||
})
|
||||
}
|
||||
|
||||
function showDs() {
|
||||
listDs()
|
||||
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
listDs()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ChatBlock>
|
||||
<div>你好,我是SQLBot,很高兴为你服务</div>
|
||||
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
|
||||
<template v-if="editable">
|
||||
<template v-if="dsList.length>0">
|
||||
<div class="ds-select-row">
|
||||
<div>选择数据源</div>
|
||||
<el-button @click="showDs" link type="primary">查看更多</el-button>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<template v-for="(item, _index) in dsList" :key="_index">
|
||||
<DatasourceItemCard :ds="item" @click="selectDs(item)" v-if="_index<3 || item?.id===modelValue"
|
||||
class="ds-card" :class="[item?.id===modelValue? 'ds-card-selected': '']"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
数据源为空,请新建后再开启智能问数!
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ds-select-row">
|
||||
<div>已选择数据源</div>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<DatasourceItemCard :ds="ds"/>
|
||||
</div>
|
||||
</template>
|
||||
</ChatBlock>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.sub {
|
||||
color: grey;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.ds-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ds-row-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
|
||||
}
|
||||
|
||||
.ds-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ds-card-selected {
|
||||
border-color: var(--ed-color-primary-light-5);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -11,50 +11,42 @@
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<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"/>
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div class="chat-input">
|
||||
<div class="input-wrapper">
|
||||
<el-input
|
||||
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"
|
||||
/>
|
||||
<div class="input-actions">
|
||||
<el-button circle type="primary" class="send-btn" @click="sendMessage">
|
||||
<el-icon>
|
||||
<Position/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-footer class="chat-footer">
|
||||
<div style="height: 24px;">
|
||||
<template v-if="currentChat.datasource">
|
||||
使用数据源:{{ 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"
|
||||
/>
|
||||
<el-button link type="primary" class="input-icon" @click="sendMessage" :disabled="isTyping">
|
||||
<el-icon size="20">
|
||||
<Position/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
@@ -65,22 +57,14 @@
|
||||
<script setup lang="ts">
|
||||
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";
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
create_time?: Date | string
|
||||
content?: string | number
|
||||
isTyping?: boolean
|
||||
isWelcome?: boolean
|
||||
}
|
||||
import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi} from '@/api/chat'
|
||||
import ChatList from './ChatList.vue'
|
||||
import ChatRow from './ChatRow.vue'
|
||||
|
||||
const inputMessage = ref('')
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const chatList = ref<Array<ChatInfo>>([])
|
||||
const dsList = ref<any>([])
|
||||
|
||||
const currentChatId = ref<number | undefined>()
|
||||
const currentChat = ref<ChatInfo>(new ChatInfo())
|
||||
@@ -132,7 +116,7 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
|
||||
const createNewChat = () => {
|
||||
currentChat.value = new ChatInfo()
|
||||
currentChatId.value = undefined
|
||||
listDs()
|
||||
inputMessage.value = ''
|
||||
}
|
||||
|
||||
function getChatList() {
|
||||
@@ -164,16 +148,29 @@ function onClickHistory(chat: Chat) {
|
||||
}
|
||||
}
|
||||
|
||||
function listDs() {
|
||||
datasourceApi.list().then((res) => {
|
||||
console.log(res)
|
||||
dsList.value = res
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getChatList()
|
||||
listDs()
|
||||
})
|
||||
|
||||
|
||||
@@ -197,6 +194,7 @@ const sendMessage = async () => {
|
||||
currentRecord.answer = ''
|
||||
|
||||
currentChat.value.records.push(currentRecord)
|
||||
inputMessage.value = ''
|
||||
|
||||
let error = false
|
||||
if (currentChatId.value === undefined) {
|
||||
@@ -221,7 +219,6 @@ const sendMessage = async () => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
if (error) return
|
||||
|
||||
try {
|
||||
@@ -310,6 +307,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 +319,48 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
padding-top: unset;
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-list {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
--ed-footer-height: 120px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
.input-area {
|
||||
height: 100%;
|
||||
padding-bottom: 8px;
|
||||
|
||||
:deep(.ed-textarea__inner) {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
min-width: unset;
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {datetimeFormat} from "@/utils/utils.ts";
|
||||
import SQLServerDs from "@/assets/svg/ds/sqlServer-ds.svg";
|
||||
import ExcelDs from "@/assets/svg/ds/Excel-ds.svg";
|
||||
import PgDs from "@/assets/svg/ds/pg-ds.svg";
|
||||
import MysqlDs from "@/assets/svg/ds/mysql-ds.svg";
|
||||
import OracleDs from "@/assets/svg/ds/oracle-ds.svg";
|
||||
|
||||
defineProps<{
|
||||
ds: any
|
||||
}>()
|
||||
|
||||
const getStatus = (status: string) => {
|
||||
if (status === 'Success') {
|
||||
return 'connected'
|
||||
}
|
||||
if (status === 'Fail') {
|
||||
return 'failed'
|
||||
}
|
||||
if (status === 'Checking') {
|
||||
return 'needs-verification'
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="connection-card"
|
||||
>
|
||||
<div class="connection-icon">
|
||||
<Icon>
|
||||
<MysqlDs v-if="ds.type === 'mysql'"/>
|
||||
<SQLServerDs v-else-if="ds.type === 'sqlServer'"/>
|
||||
<PgDs v-else-if="ds.type === 'pg'"/>
|
||||
<ExcelDs v-else-if="ds.type === 'excel'"/>
|
||||
<OracleDs v-else-if="ds.type === 'oracle'"/>
|
||||
</Icon>
|
||||
</div>
|
||||
<div class="connection-details">
|
||||
<div class="connection-name">{{ ds.name }}</div>
|
||||
<div class="connection-type">{{ ds.type_name }}</div>
|
||||
<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>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.connection-card {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
position: relative;
|
||||
border: 1px solid #dadce0;
|
||||
transition: all 0.2s ease;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connection-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background-color: #e8f0fe;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.connection-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.connection-name {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #202124;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.3;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.connection-type {
|
||||
color: #5f6368;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.connection-host {
|
||||
color: #5f6368;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connection-last {
|
||||
color: #5f6368;
|
||||
font-size: 14px;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 50px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
background-color: #e6f4ea;
|
||||
color: #34a853;
|
||||
}
|
||||
|
||||
.connection-status.failed {
|
||||
background-color: #fce8e6;
|
||||
color: #ea4335;
|
||||
}
|
||||
|
||||
.connection-status.needs-verification {
|
||||
background-color: #fef7e0;
|
||||
color: #fbbc05;
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="Add Datasource"
|
||||
:title="title"
|
||||
width="600"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@closed="close"
|
||||
modal-class="add-datasource_dialog"
|
||||
>
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<template #header="{ title }">
|
||||
<div style="display: flex">
|
||||
<div style="margin-right: 24px">Add Datasource</div>
|
||||
<div style="margin-right: 24px">{{ title }}</div>
|
||||
<el-steps
|
||||
v-show="isCreate"
|
||||
:active="active"
|
||||
@@ -180,6 +180,7 @@ const excelUploadSuccess = ref(false);
|
||||
const tableListLoading = ref(true);
|
||||
const token = wsCache.get("user.token");
|
||||
const headers = ref<any>({ "X-SQLBOT-TOKEN": `Bearer ${token}` });
|
||||
const title = ref("");
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
@@ -222,6 +223,7 @@ const close = () => {
|
||||
const open = (item: any, editTable: boolean = false) => {
|
||||
isEditTable.value = false;
|
||||
if (item) {
|
||||
title.value = "Edit Datasource";
|
||||
isCreate.value = false;
|
||||
form.value.id = item.id;
|
||||
form.value.name = item.name;
|
||||
@@ -243,6 +245,7 @@ const open = (item: any, editTable: boolean = false) => {
|
||||
}
|
||||
|
||||
if (editTable) {
|
||||
title.value = "Choose Tables";
|
||||
active.value = 1;
|
||||
isEditTable.value = true;
|
||||
isCreate.value = false;
|
||||
@@ -275,6 +278,7 @@ const open = (item: any, editTable: boolean = false) => {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
title.value = "Add Datasource";
|
||||
isCreate.value = true;
|
||||
isEditTable.value = false;
|
||||
checkList.value = [];
|
||||
|
||||
+50
-160
@@ -3,15 +3,17 @@
|
||||
<div class="header">
|
||||
<div class="mt-4">
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
style="max-width: 300px"
|
||||
placeholder="Search Datasource..."
|
||||
class="input-with-select"
|
||||
clearable
|
||||
@change="searchHandle"
|
||||
v-model="searchValue"
|
||||
style="max-width: 300px"
|
||||
placeholder="Search Datasource..."
|
||||
class="input-with-select"
|
||||
clearable
|
||||
@change="searchHandle"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><Search /></el-icon>
|
||||
<el-icon>
|
||||
<Search/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
@@ -20,33 +22,17 @@
|
||||
</div>
|
||||
|
||||
<div class="connections-container">
|
||||
<div
|
||||
class="connection-card"
|
||||
v-for="ds in dsList"
|
||||
>
|
||||
<div class="connection-icon">
|
||||
<Icon>
|
||||
<MysqlDs v-if="ds.type === 'mysql'"/>
|
||||
<SQLServerDs v-else-if="ds.type === 'sqlServer'"/>
|
||||
<PgDs v-else-if="ds.type === 'pg'"/>
|
||||
<ExcelDs v-else-if="ds.type === 'excel'"/>
|
||||
<OracleDs v-else-if="ds.type === 'oracle'"/>
|
||||
</Icon>
|
||||
</div>
|
||||
<div class="connection-details">
|
||||
<div class="connection-name">{{ ds.name }}</div>
|
||||
<div class="connection-type">{{ ds.type_name }}</div>
|
||||
<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-actions">
|
||||
<el-button class="action-btn" circle @click="getTables(ds.id, ds.name)" :icon="List" />
|
||||
<el-button class="action-btn" circle @click="editTables(ds)" :icon="CreditCard" />
|
||||
<el-button v-if="ds.type !== 'excel'" type="primary" class="action-btn" circle @click="editDs(ds)" :icon="IconOpeEdit"/>
|
||||
<el-button type="danger" class="action-btn" circle @click="deleteDs(ds)" :icon="IconOpeDelete"/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="ds in dsList">
|
||||
<DatasourceItemCard :ds="ds">
|
||||
<div class="connection-actions">
|
||||
<el-button class="action-btn" circle @click="getTables(ds.id, ds.name)" :icon="List"/>
|
||||
<el-button class="action-btn" circle @click="editTables(ds)" :icon="CreditCard"/>
|
||||
<el-button v-if="ds.type !== 'excel'" type="primary" class="action-btn" circle @click="editDs(ds)"
|
||||
:icon="IconOpeEdit"/>
|
||||
<el-button type="danger" class="action-btn" circle @click="deleteDs(ds)" :icon="IconOpeDelete"/>
|
||||
</div>
|
||||
</DatasourceItemCard>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<DsForm ref="dsForm" @refresh="refresh"/>
|
||||
@@ -55,18 +41,13 @@
|
||||
import IconOpeAdd from '@/assets/svg/operate/ope-add.svg'
|
||||
import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
|
||||
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg'
|
||||
import MysqlDs from '@/assets/svg/ds/mysql-ds.svg'
|
||||
import SQLServerDs from '@/assets/svg/ds/sqlServer-ds.svg'
|
||||
import PgDs from '@/assets/svg/ds/pg-ds.svg'
|
||||
import ExcelDs from '@/assets/svg/ds/Excel-ds.svg'
|
||||
import OracleDs from '@/assets/svg/ds/oracle-ds.svg'
|
||||
import { Search, List, CreditCard } from '@element-plus/icons-vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {Search, List, CreditCard} from '@element-plus/icons-vue'
|
||||
import {ref, onMounted} from 'vue'
|
||||
import DsForm from './form.vue'
|
||||
import { datasourceApi } from '@/api/datasource'
|
||||
import { datetimeFormat } from '@/utils/utils'
|
||||
import { ElMessageBox } from 'element-plus-secondary'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {datasourceApi} from '@/api/datasource'
|
||||
import {ElMessageBox} from 'element-plus-secondary'
|
||||
import {useRouter} from 'vue-router'
|
||||
import DatasourceItemCard from "@/views/ds/DatasourceItemCard.vue";
|
||||
|
||||
const searchValue = ref<string>('')
|
||||
const dsForm = ref()
|
||||
@@ -75,21 +56,12 @@ const allDsList = ref<any>([])// all ds list
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
|
||||
const getStatus = (status: string) => {
|
||||
if (status === 'Success') {
|
||||
return 'connected'
|
||||
}
|
||||
if (status === 'Fail') {
|
||||
return 'failed'
|
||||
}
|
||||
if (status === 'Checking') {
|
||||
return 'needs-verification'
|
||||
}
|
||||
}
|
||||
|
||||
function searchHandle() {
|
||||
if(searchValue.value) {
|
||||
dsList.value = JSON.parse(JSON.stringify(allDsList.value)).filter((item: any) => {return item.name.toLowerCase().includes(searchValue.value.toLowerCase())})
|
||||
if (searchValue.value) {
|
||||
dsList.value = JSON.parse(JSON.stringify(allDsList.value)).filter((item: any) => {
|
||||
return item.name.toLowerCase().includes(searchValue.value.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
dsList.value = JSON.parse(JSON.stringify(allDsList.value))
|
||||
}
|
||||
@@ -118,22 +90,22 @@ const editTables = (item: any) => {
|
||||
|
||||
const deleteDs = (item: any) => {
|
||||
ElMessageBox.confirm(
|
||||
'Delete this datasource?',
|
||||
'Delete',
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
'Delete this datasource?',
|
||||
'Delete',
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
datasourceApi.delete(item.id).then(() => {
|
||||
console.log('success')
|
||||
list()
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
.then(() => {
|
||||
datasourceApi.delete(item.id).then(() => {
|
||||
console.log('success')
|
||||
list()
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const getTables = (id: number, name: string) => {
|
||||
@@ -157,7 +129,7 @@ onMounted(() => {
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.header{
|
||||
.header {
|
||||
background-color: white;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
@@ -171,102 +143,20 @@ onMounted(() => {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 24px;
|
||||
.connection-card {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
position: relative;
|
||||
border: 1px solid #dadce0;
|
||||
transition: all 0.2s ease;
|
||||
align-items: center;
|
||||
}
|
||||
.connection-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background-color: #e8f0fe;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.connection-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.connection-name {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #202124;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.3;
|
||||
display: flex;
|
||||
}
|
||||
.connection-type {
|
||||
color: #5f6368;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
.connection-host {
|
||||
color: #5f6368;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.connection-last {
|
||||
color: #5f6368;
|
||||
font-size: 14px;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.connection-status {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 50px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.connection-status.connected {
|
||||
background-color: #e6f4ea;
|
||||
color: #34a853;
|
||||
}
|
||||
.connection-status.failed {
|
||||
background-color: #fce8e6;
|
||||
color: #ea4335;
|
||||
}
|
||||
.connection-status.needs-verification {
|
||||
background-color: #fef7e0;
|
||||
color: #fbbc05;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
|
||||
.action-btn {
|
||||
display: none;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
:hover .action-btn{
|
||||
|
||||
:hover .action-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts" setup>
|
||||
import { Document } from "@element-plus/icons-vue";
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
name: string;
|
||||
description: string;
|
||||
creator: string;
|
||||
}>(),
|
||||
{
|
||||
name: "",
|
||||
description: "",
|
||||
creator: "",
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="datasource-card">
|
||||
<div class="description">
|
||||
<el-icon size="20" style="margin-right: 12px">
|
||||
<Document></Document>
|
||||
</el-icon>
|
||||
<span class="value">{{ name }}</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="key">describe:</span>
|
||||
<span class="value">{{ description }}</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="key">Founder:</span>
|
||||
<span class="value">{{ creator }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.datasource-card {
|
||||
height: 120px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
|
||||
.description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
.key {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue";
|
||||
import DatasourceCard from "./DatasourceCard.vue";
|
||||
interface Datasource {
|
||||
id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
creator: string;
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
datasourceList: Array<Datasource>;
|
||||
}>(),
|
||||
{
|
||||
datasourceList: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
const datasourceName = ref("");
|
||||
const datasourceListComputed = computed(() =>
|
||||
props.datasourceList.filter((val) =>
|
||||
val.name.toLowerCase().includes(datasourceName.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-init_tip">
|
||||
<div class="hello-sqlbot">Hello, I'm SQLBot, happy to serve you!</div>
|
||||
<div class="function-sqlbot">
|
||||
I can help you query data, generate charts, detect data anomalies, predict
|
||||
data, etc. Please select a data source and start intelligent data query~
|
||||
</div>
|
||||
<div class="select-datasource">
|
||||
<span class="title">Select data source</span>
|
||||
<el-button @click="dialogVisible = true" text>View more</el-button>
|
||||
</div>
|
||||
<div class="datasource-content">
|
||||
<DatasourceCard
|
||||
:name="ele.name"
|
||||
:description="ele.description"
|
||||
:creator="ele.creator"
|
||||
v-for="ele in datasourceList"
|
||||
:key="ele.id"
|
||||
></DatasourceCard>
|
||||
</div>
|
||||
<el-button type="primary">Create a new data source</el-button>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="Select data source"
|
||||
width="800"
|
||||
modal-class="select-datasource_dialog"
|
||||
>
|
||||
<div class="search-datasource">
|
||||
<el-input
|
||||
v-model="datasourceName"
|
||||
style="width: 240px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</div>
|
||||
<div class="datasource-content">
|
||||
<DatasourceCard
|
||||
:name="ele.name"
|
||||
:description="ele.description"
|
||||
:creator="ele.creator"
|
||||
v-for="ele in datasourceListComputed"
|
||||
:key="ele.id"
|
||||
></DatasourceCard>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="dialogVisible = false">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chat-init_tip {
|
||||
width: 780px;
|
||||
padding: 16px;
|
||||
.hello-sqlbot {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.function-sqlbot {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.select-datasource {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.datasource-content {
|
||||
margin: 12px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.select-datasource_dialog {
|
||||
.search-datasource {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.datasource-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user