feat: chat

This commit is contained in:
wangdan-fit2cloud 2025-06-13 21:56:01 +08:00
parent 5acf2f60e5
commit 16b1a79c0b
8 changed files with 190 additions and 222 deletions

2
ui/env/.env.chat vendored
View File

@ -1,5 +1,5 @@
VITE_APP_NAME=chat
VITE_BASE_PATH=/chat/
VITE_APP_PORT=3000
VITE_APP_PORT=3001
VITE_APP_TITLE = 'MaxKB'
VITE_ENTRY="entry/chat/index.html"

View File

@ -3,8 +3,14 @@ import type { RouteRecordRaw } from 'vue-router'
export const routes: Array<RouteRecordRaw> = [
// 对话
{
path: '/chat/:accessToken',
path: '/:accessToken',
name: 'Chat',
component: () => import('@/views/chat/index.vue'),
},
// 对话用户登录
{
path: '/user-login/:accessToken',
name: 'UserLogin',
component: () => import('@/views/chat/user-login/index.vue'),
},
]

View File

@ -24,7 +24,7 @@ export const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/chat/index.vue'),
},
// 对话用户登录
// 对话用户登录
{
path: '/user-login/:accessToken',
name: 'UserLogin',

View File

@ -62,15 +62,16 @@ const useApplicationStore = defineStore('application', {
async asyncGetAppProfile(loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
// applicationApi
// .getAppProfile(loading)
// .then((res) => {
// sessionStorage.setItem('language', res.data?.language || getBrowserLang())
// resolve(res)
// })
// .catch((error) => {
// reject(error)
// })
console.log('xxxx')
applicationApi
.getAppProfile(loading)
.then((res) => {
sessionStorage.setItem('language', res.data?.language || getBrowserLang())
resolve(res)
})
.catch((error) => {
reject(error)
})
})
},
@ -80,16 +81,16 @@ const useApplicationStore = defineStore('application', {
authentication_value?: any,
) {
return new Promise((resolve, reject) => {
// applicationApi
// .postAppAuthentication(token, loading, authentication_value)
// .then((res) => {
// localStorage.setItem(`${token}-accessToken`, res.data)
// sessionStorage.setItem(`${token}-accessToken`, res.data)
// resolve(res)
// })
// .catch((error) => {
// reject(error)
// })
applicationApi
.postAppAuthentication(token, loading, authentication_value)
.then((res) => {
localStorage.setItem(`${token}-accessToken`, res.data)
sessionStorage.setItem(`${token}-accessToken`, res.data)
resolve(res)
})
.catch((error) => {
reject(error)
})
})
},
async refreshAccessToken(token: string) {

View File

@ -41,12 +41,10 @@
>
<img :src="detail?.icon" alt="" />
</el-avatar>
<el-avatar
v-else-if="detail?.name"
:name="detail?.name"
pinyinColor
shape="square"
:size="32"
<LogoIcon
v-else
height="28px"
style="width: 28px; height: 28px; display: block"
/>
</div>

View File

@ -92,13 +92,7 @@
>
<img :src="detail?.icon" alt="" />
</el-avatar>
<el-avatar
v-else-if="detail?.name"
:name="detail?.name"
pinyinColor
shape="square"
:size="32"
/>
<LogoIcon v-else height="28px" style="width: 28px; height: 28px; display: block" />
</div>
<h4>
@ -157,7 +151,7 @@ const isDefaultTheme = computed(() => {
return user.isDefaultTheme()
})
const {
params: { id }
params: { id },
} = route as any
let interval: any
@ -182,7 +176,7 @@ function back() {
confirmButtonText: t('views.applicationWorkflow.setting.exitSave'),
cancelButtonText: t('views.applicationWorkflow.setting.exit'),
type: 'warning',
distinguishCancelAndClose: true
distinguishCancelAndClose: true,
})
.then(() => {
saveApplication(true, true)
@ -268,7 +262,7 @@ async function publicHandle() {
?.validate()
.then(async () => {
const obj = {
work_flow: getGraphData()
work_flow: getGraphData(),
}
await application.asyncPutApplication(id, obj, loading)
const workflow = new WorkFlowInstance(obj.work_flow)
@ -293,14 +287,14 @@ async function publicHandle() {
MsgError(
res.node.properties?.stepName +
` ${t('views.applicationWorkflow.node').toLowerCase()} ` +
err_message.toLowerCase()
err_message.toLowerCase(),
)
} else {
const keys = Object.keys(err_message)
MsgError(
node.properties?.stepName +
` ${t('views.applicationWorkflow.node').toLowerCase()} ` +
err_message[keys[0]]?.[0]?.message.toLowerCase()
err_message[keys[0]]?.[0]?.message.toLowerCase(),
)
}
})
@ -318,7 +312,7 @@ const clickShowDebug = () => {
...detail.value,
type: 'WORK_FLOW',
...workflow.get_base_node()?.properties.node_data,
work_flow: getGraphData()
work_flow: getGraphData(),
}
showDebug.value = true
@ -331,14 +325,14 @@ const clickShowDebug = () => {
const err_message = res.errMessage
if (typeof err_message == 'string') {
MsgError(
res.node.properties?.stepName + ` ${t('views.applicationWorkflow.node')}` + err_message
res.node.properties?.stepName + ` ${t('views.applicationWorkflow.node')}` + err_message,
)
} else {
const keys = Object.keys(err_message)
MsgError(
node.properties?.stepName +
` ${t('views.applicationWorkflow.node')}` +
err_message[keys[0]]?.[0]?.message
err_message[keys[0]]?.[0]?.message,
)
}
})
@ -376,7 +370,7 @@ function getDetail() {
function saveApplication(bool?: boolean, back?: boolean) {
const obj = {
work_flow: getGraphData()
work_flow: getGraphData(),
}
loading.value = back || false
application

View File

@ -1,27 +1,4 @@
<template>
<div class="chat-pc__header" :style="customStyle">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<el-avatar
v-if="isAppIcon(application_profile?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="application_profile?.icon" alt="" />
</el-avatar>
<el-avatar
v-else-if="application_profile?.name"
:name="application_profile?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ application_profile?.name }}</h4>
</div>
</div>
<component
:is="auth_components[`/src/views/chat/auth/component/${auth_type}.vue`].default"
v-model="is_auth"
@ -33,7 +10,7 @@ import { computed } from 'vue'
import { isAppIcon } from '@/utils/common'
const auth_components: any = import.meta.glob('@/views/chat/auth/component/*.vue', {
eager: true
eager: true,
})
const emit = defineEmits(['update:modelValue'])
@ -42,8 +19,8 @@ const props = withDefaults(
defineProps<{ modelValue: boolean; application_profile: any; auth_type?: string; style?: any }>(),
{
auth_type: 'password',
style: {}
}
style: {},
},
)
const is_auth = computed({
get: () => {
@ -51,7 +28,7 @@ const is_auth = computed({
},
set: (v) => {
emit('update:modelValue', v)
}
},
})
const customStyle = computed(() => {
@ -59,7 +36,7 @@ const customStyle = computed(() => {
background: props.application_profile?.custom_theme?.theme_color,
color: props.application_profile?.custom_theme?.header_font_color,
border: 'none',
...props.style
...props.style,
}
})
</script>

View File

@ -5,157 +5,149 @@
v-loading="loading"
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1),
}"
>
<div class="chat-pc__header" :style="customStyle">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<el-avatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</el-avatar>
<el-avatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
<div>
<div class="flex">
<div class="chat-pc__left border-r">
<div class="p-24 pb-0">
<el-button class="add-button w-full primary" @click="newChat">
<el-icon>
<Plus />
</el-icon>
<span class="ml-4">{{ $t('chat.createChat') }}</span>
</el-button>
<p class="mt-20 mb-8">{{ $t('chat.history') }}</p>
</div>
<div class="left-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(
applicationDetail?.custom_theme?.theme_color,
0.1
)
}"
:data="chatLogData"
class="mt-8"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-show="mouseId === row.id && row.id !== 'new'">
<el-dropdown trigger="click" :teleported="false">
<el-icon class="rotate-90 mt-4"><MoreFilled /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.stop="editLogTitle(row)">
<el-icon><EditPen /></el-icon>
{{ $t('common.edit') }}
</el-dropdown-item>
<el-dropdown-item @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogData?.length" class="gradient-divider lighter mt-8">
<span>{{ $t('chat.only20history') }}</span>
</div>
</el-scrollbar>
11111111111
<div class="flex">
<div class="chat-pc__left border-r">
<div class="p-24 pb-0">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<el-avatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</el-avatar>
<LogoIcon v-else height="28px" style="width: 28px; height: 28px; display: block" />
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
<el-button class="add-button w-full primary" @click="newChat">
<el-icon>
<Plus />
</el-icon>
<span class="ml-4">{{ $t('chat.createChat') }}</span>
</el-button>
<p class="mt-20 mb-8">{{ $t('chat.history') }}</p>
</div>
<div class="chat-pc__right">
<div class="right-header border-b mb-24 p-16-24 flex-between">
<h4 class="ellipsis-1" style="width: 66%">
{{ currentChatName }}
</h4>
<span class="flex align-center" v-if="currentRecordList.length">
<AppIcon
v-if="paginationConfig.total"
iconName="app-chat-record"
class="info mr-8"
style="font-size: 16px"
></AppIcon>
<span v-if="paginationConfig.total" class="lighter">
{{ paginationConfig.total }} {{ $t('chat.question_count') }}
</span>
<el-dropdown class="ml-8">
<AppIcon
iconName="app-export"
class="cursor"
:title="$t('chat.exportRecords')"
></AppIcon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="exportMarkdown"
>{{ $t('common.export') }} Markdown</el-dropdown-item
>
<el-dropdown-item @click="exportHTML"
>{{ $t('common.export') }} HTML</el-dropdown-item
>
</el-dropdown-menu>
<div class="left-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(
applicationDetail?.custom_theme?.theme_color,
0.1,
),
}"
:data="chatLogData"
class="mt-8"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-show="mouseId === row.id && row.id !== 'new'">
<el-dropdown trigger="click" :teleported="false">
<el-icon class="rotate-90 mt-4"><MoreFilled /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.stop="editLogTitle(row)">
<el-icon><EditPen /></el-icon>
{{ $t('common.edit') }}
</el-dropdown-item>
<el-dropdown-item @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
</el-dropdown>
</span>
</div>
<div class="right-height chat-width">
<AiChat
ref="AiChatRef"
v-model:applicationDetails="applicationDetail"
:available="applicationAvailable"
type="ai-chat"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
>
</AiChat>
</div>
<template #empty>
<div class="text-center">
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogData?.length" class="gradient-divider lighter mt-8">
<span>{{ $t('chat.only20history') }}</span>
</div>
</el-scrollbar>
</div>
</div>
<div class="collapse">
<el-button @click="isCollapse = !isCollapse">
<el-icon> <component :is="isCollapse ? 'Fold' : 'Expand'" /></el-icon>
</el-button>
<div class="chat-pc__right">
<div class="right-header border-b mb-24 p-16-24 flex-between">
<h4 class="ellipsis-1" style="width: 66%">
{{ currentChatName }}
</h4>
<span class="flex align-center" v-if="currentRecordList.length">
<AppIcon
v-if="paginationConfig.total"
iconName="app-chat-record"
class="info mr-8"
style="font-size: 16px"
></AppIcon>
<span v-if="paginationConfig.total" class="lighter">
{{ paginationConfig.total }} {{ $t('chat.question_count') }}
</span>
<el-dropdown class="ml-8">
<AppIcon
iconName="app-export"
class="cursor"
:title="$t('chat.exportRecords')"
></AppIcon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="exportMarkdown"
>{{ $t('common.export') }} Markdown</el-dropdown-item
>
<el-dropdown-item @click="exportHTML"
>{{ $t('common.export') }} HTML</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div class="right-height chat-width">
<AiChat
ref="AiChatRef"
v-model:applicationDetails="applicationDetail"
:available="applicationAvailable"
type="ai-chat"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
>
</AiChat>
</div>
</div>
</div>
<div class="collapse">
<el-button @click="isCollapse = !isCollapse">
<el-icon> <component :is="isCollapse ? 'Fold' : 'Expand'" /></el-icon>
</el-button>
</div>
<EditTitleDialog ref="EditTitleDialogRef" @refresh="refreshFieldTitle" />
</div>
</template>
@ -181,7 +173,7 @@ const isCollapse = ref(false)
const customStyle = computed(() => {
return {
background: applicationDetail.value?.custom_theme?.theme_color,
color: applicationDetail.value?.custom_theme?.header_font_color
color: applicationDetail.value?.custom_theme?.header_font_color,
}
})
@ -189,13 +181,13 @@ const classObj = computed(() => {
return {
mobile: common.isMobile(),
hideLeft: !isCollapse.value,
openLeft: isCollapse.value
openLeft: isCollapse.value,
}
})
const newObj = {
id: 'new',
abstract: t('chat.createChat')
abstract: t('chat.createChat'),
}
const props = defineProps<{
application_profile: any
@ -209,7 +201,7 @@ const applicationDetail = computed({
get: () => {
return props.application_profile
},
set: (v) => {}
set: (v) => {},
})
const chatLogData = ref<any[]>([])
@ -217,7 +209,7 @@ const chatLogData = ref<any[]>([])
const paginationConfig = ref({
current_page: 1,
page_size: 20,
total: 0
total: 0,
})
const currentRecordList = ref<any>([])
@ -286,7 +278,7 @@ function newChat() {
function getChatLog(id: string, refresh?: boolean) {
const page = {
current_page: 1,
page_size: 20
page_size: 20,
}
chatLog.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
@ -313,7 +305,7 @@ function getChatRecord() {
currentChatId.value,
paginationConfig.value,
loading,
false
false,
)
.then((res: any) => {
paginationConfig.value.total = res.data.total
@ -323,7 +315,7 @@ function getChatRecord() {
v['record_id'] = v.id
})
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
a.create_time.localeCompare(b.create_time),
)
if (paginationConfig.value.current_page === 1) {
nextTick(() => {