Commit 2a5514dd authored by nick zheng's avatar nick zheng

feat: 应用创建及发布

parent 51026394
node_modules node_modules
build build
dist dist
src/components/markdown-render/style
...@@ -24,6 +24,7 @@ export default [ ...@@ -24,6 +24,7 @@ export default [
AnyObject: 'readonly', AnyObject: 'readonly',
ConversationMessageItem: 'readonly', ConversationMessageItem: 'readonly',
ConversationMessageItemInfo: 'readonly', ConversationMessageItemInfo: 'readonly',
MittEvents: 'readonly',
}, },
parser: vueParser, parser: vueParser,
parserOptions: { parserOptions: {
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>POC-FE</title> <title>POC-FE</title>
</head> </head>
......
...@@ -17,9 +17,17 @@ ...@@ -17,9 +17,17 @@
}, },
"dependencies": { "dependencies": {
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@traptitech/markdown-it-katex": "^3.6.0",
"@unocss/reset": "^0.61.3", "@unocss/reset": "^0.61.3",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^10.11.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"clipboardy": "^4.0.0",
"dayjs": "^1.11.13",
"highlight.js": "^11.10.0",
"markdown-it": "^14.1.0",
"markdown-it-link-attributes": "^4.0.1",
"mitt": "^3.0.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.31", "vue": "^3.4.31",
...@@ -31,6 +39,8 @@ ...@@ -31,6 +39,8 @@
"@commitlint/config-conventional": "^19.2.2", "@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3", "@commitlint/types": "^19.0.3",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@typescript-eslint/parser": "^7.15.0", "@typescript-eslint/parser": "^7.15.0",
"@unocss/eslint-config": "^0.61.3", "@unocss/eslint-config": "^0.61.3",
......
This diff is collapsed.
import { request } from '@/utils/request'
/**
*
* @param payload agentApplicationInfo 应用参数
* @returns 新建或更新应用
*/
export function fetchSaveAgentApplication<T>(payload: object) {
return request.post<T>('/agentApplicationInfoRest/saveOrUpdate.json', payload)
}
/**
*
* @param payload 查询参数
* @returns 获取应用列表
*/
export function fetchGetApplicationList<T>(payload: object) {
return request.post<T>('/agentApplicationInfoRest/getListByMember.json', payload)
}
/**
*
* @param payload agentId 应用Id
* @returns 通过agentId删除应用
*/
export function fetchDeleteApplication<T>(agentId: string) {
return request.post<T>(`/agentApplicationInfoRest/delete.json?agentId=${agentId}`)
}
/**
*
* @param payload agentId 应用Id
* @returns 通过agentId获取调试应用详情
*/
export function fetchGetDebugApplicationInfo<T>(agentId: string) {
return request.post<T>(`/agentApplicationInfoRest/getInfo.json?agentId=${agentId}`)
}
/**
*
* @param payload agentId 应用Id
* @returns 通过agentId获取发布应用详情
*/
export function fetchGetApplicationInfo<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/getInfo.json?agentId=${agentId}`)
}
/**
*
* @param payload payload 应用参数
* @returns 发布应用
*/
export function fetchPublishApplication<T>(payload: object) {
return request.post<T>('/agentApplicationInfoRest/updateAndPublish.json', payload)
}
/**
* @returns 获取大模型列表
*/
export function fetchGetLargeModelList<T>() {
return request.post<T>('/agentApplicationInfoRest/getLargeModelList.json')
}
/**
* * @param agentId 应用Id
* @returns 创建会话Id
*/
export function fetchCreateDialogues<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/createDialogues.json?agentId=${agentId}`)
}
import { request } from '@/utils/request'
export function fetchUpload<T>(formdata: FormData) {
return request.post<T>('/bosRest/upload.json', formdata, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 120000,
})
}
<script setup lang="ts"> <script setup lang="ts">
import { zhCN, dateZhCN } from 'naive-ui' import { zhCN, dateZhCN } from 'naive-ui'
import { ref } from 'vue' import { provide, ref } from 'vue'
import mitt from 'mitt'
import { themeOverrides } from '@/config/theme-config' import { themeOverrides } from '@/config/theme-config'
import { useResizeObserver } from '@vueuse/core' import { useResizeObserver } from '@vueuse/core'
import { useDesignSettingStore } from '@/store/modules/design-setting' import { useDesignSettingStore } from '@/store/modules/design-setting'
// import { NThemeEditor } from 'naive-ui'
const designSettingStore = useDesignSettingStore() const designSettingStore = useDesignSettingStore()
const emitter = mitt<MittEvents>()
provide('emitter', emitter)
const currentLocale = ref(zhCN) const currentLocale = ref(zhCN)
const currentDateLocale = ref(dateZhCN) const currentDateLocale = ref(dateZhCN)
...@@ -48,6 +52,4 @@ useResizeObserver(rootContainer, (entries) => { ...@@ -48,6 +52,4 @@ useResizeObserver(rootContainer, (entries) => {
</RouterView> </RouterView>
</NConfigProvider> </NConfigProvider>
</div> </div>
<!-- <NThemeEditor /> -->
</template> </template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#2468f2;}.cls-2{fill:none;}</style></defs><g id="图层_2" data-name="图层 2"><g id="图层_4" data-name="图层 4"><path class="cls-1" d="M10.77,4.87l1.76,3.56a.85.85,0,0,0,.64.47l3.93.57a.86.86,0,0,1,.48,1.46L14.74,13.7a.84.84,0,0,0-.25.76l.67,3.91a.86.86,0,0,1-1.25.91L10.4,17.43a.87.87,0,0,0-.8,0L6.09,19.28a.86.86,0,0,1-1.25-.91l.67-3.91a.84.84,0,0,0-.25-.76L2.42,10.93A.86.86,0,0,1,2.9,9.47L6.83,8.9a.85.85,0,0,0,.64-.47L9.23,4.87A.86.86,0,0,1,10.77,4.87Z"/><rect class="cls-2" width="20" height="20"/></g></g></svg>
<script setup lang="ts">
import { computed } from 'vue'
import { modalHeaderStyle, modalContentStyle, modalFooterStyle } from './modal-style'
interface Props {
title: string // 弹窗标题
isShow: boolean // 是否显示
height?: number // 高度
width?: number // 宽度
borderRadius?: number // 圆角
btnLoading?: boolean // 按钮是否加载中
labelWidth?: number // 标签的宽度
labelPlacement?: 'left' | 'top' // 标签显示的位置
}
interface Emits {
(e: 'update:isShow', value: boolean): void
(e: 'close'): void
(e: 'confirm'): void
}
const props = withDefaults(defineProps<Props>(), {
height: 240,
width: 500,
borderRadius: 6,
labelWidth: 80,
labelPlacement: 'left',
})
const emit = defineEmits<Emits>()
const modalBasicStyle = {
width: props.width + 'px',
height: props.height + 'px',
borderRadius: props.borderRadius + 'px',
}
const showModal = computed({
get() {
return props.isShow
},
set(value: boolean) {
emit('update:isShow', value)
},
})
function handleCloseModal() {
showModal.value = false
emit('close')
}
function handleDetele() {
emit('confirm')
}
</script>
<template>
<NModal
preset="card"
transform-origin="center"
closable
:style="modalBasicStyle"
:show="showModal"
:bordered="false"
:auto-focus="false"
:header-style="modalHeaderStyle"
:content-style="modalContentStyle"
:footer-style="modalFooterStyle"
:on-mask-click="handleCloseModal"
@close="handleCloseModal"
>
<template #header>
<div class="text-xl">{{ title }}</div>
</template>
<div>
<slot name="content" />
</div>
<template #footer>
<div class="flex w-full items-center justify-end">
<NButton class="h-[32px]! w-[80px]! rounded-md!" @click="handleCloseModal"> 取 消 </NButton>
<NButton
:loading="btnLoading"
type="primary"
class="h-[32px]! w-[80px]! rounded-md! ml-4!"
@click="handleDetele"
>
确 定
</NButton>
</div>
</template>
</NModal>
</template>
export const modalHeaderStyle = {
padding: '24px 24px 16px',
fontSize: '20px',
}
export const modalContentStyle = {
padding: '0 24px 24px',
overflow: 'auto',
}
export const modalFooterStyle = {
padding: '0px 24px 24px 24px',
}
<script setup lang="ts">
export interface PaginationInfo {
pageNo: number
pageSize: number
totalPages: number
totalRows: number
}
interface Props {
pagingInfo: PaginationInfo
}
const pageSizes = [
{
label: '10 / 每页',
value: 10,
},
{
label: '20 / 每页',
value: 20,
},
{
label: '30 / 每页',
value: 30,
},
{
label: '40 / 每页',
value: 40,
},
]
interface Emits {
(e: 'update:pagingInfo', value: PaginationInfo): void
(e: 'updatePageNo', value: number): void
(e: 'updatePageSize', value: number): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
async function handleUpdatePageNo(pageNo: number) {
emit('updatePageNo', pageNo)
}
async function handleUpdatePageSize(pageSize: number) {
emit('updatePageSize', pageSize)
}
</script>
<template>
<div class="flex items-center">
<span class="text-[#999]">{{ pagingInfo.totalRows }}</span>
<NPagination
class="custom-pagination"
:page="pagingInfo.pageNo"
:page-count="pagingInfo.totalPages"
:page-sizes="pageSizes"
size="medium"
show-quick-jumper
show-size-picker
@update:page="handleUpdatePageNo"
@update:page-size="handleUpdatePageSize"
/>
<span class="ml-[10px] text-[#999]"></span>
</div>
</template>
<style lang="scss" scoped>
:deep(.custom-pagination .n-pagination-item) {
padding: 0;
margin-left: 2px;
border-radius: 5px;
}
:deep(.custom-pagination .n-pagination-item--button) {
background-color: white !important;
border: none !important;
}
:deep(.custom-pagination .n-base-selection) {
width: 100px;
border-radius: 6px;
--n-border: 1px solid #999 !important;
}
:deep(.custom-pagination .n-pagination-quick-jumper),
:deep(.custom-pagination .n-input__input-el),
:deep(.custom-pagination .n-base-selection-input__content) {
color: #999;
}
:deep(.custom-pagination .n-input) {
border-radius: 6px;
--n-border: 1px solid #999 !important;
}
</style>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import { useClipboard } from '@vueuse/core'
interface Props {
rawTextContent: string
fontSize?: number | string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
rawTextContent: '',
fontSize: '14px',
color: '#333',
})
const mkrContainer = ref()
const copyCodeText = ref('')
const { copy: copyTool } = useClipboard({ source: copyCodeText })
const mdi = new MarkdownIt({
html: true,
linkify: true,
highlight(code: any, language: any) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
}
return highlightBlock(hljs.highlightAuto(code).value, '')
},
})
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
const renderTextContent = computed(() => {
if (props.rawTextContent) {
const urlRegex = /((https?:\/\/[^\s]+))/g
const rawTextContent = props.rawTextContent.replace(urlRegex, '($1)')
return mdi.render(rawTextContent)
}
return ''
})
const wrapStyle = computed(() => {
return {
fontSize: props.fontSize,
color: props.color,
}
})
function highlightBlock(str: string, lang?: string) {
return (
'<pre class="code-block-wrapper">' +
'<div class="code-block-header">' +
`<span class="code-block-header__lang">${lang}</span>` +
'<span class="code-block-header__copy">复制代码</span>' +
'</div>' +
`<code class="hljs code-block-body ${lang}">${str}</code>` +
'</pre>'
)
}
function addCopyEvents() {
if (mkrContainer.value) {
const copyBtn = mkrContainer.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn: HTMLSpanElement) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) {
copyTool(code).then(() => {
btn.textContent = '复制成功'
setTimeout(() => (btn.textContent = '复制代码'), 1000)
})
}
})
})
}
}
function removeCopyEvents() {
if (mkrContainer.value) {
const copyBtn = mkrContainer.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn: HTMLSpanElement) => {
btn.removeEventListener('click', () => {})
})
}
}
onMounted(() => {
addCopyEvents()
})
onUpdated(() => {
addCopyEvents()
})
onUnmounted(() => {
removeCopyEvents()
})
</script>
<template>
<div ref="mkrContainer" class="markdown-render-container">
<article class="markdown-body" :style="wrapStyle" v-html="renderTextContent" />
</div>
</template>
<style lang="scss">
@import './style/highlight.scss';
@import './style/github-markdown.scss';
@import './style/custom-style.scss';
</style>
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #f4fafd;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
}
html.dark {
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
word-break: break-all;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
This diff is collapsed.
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.hljs {
color: #abb2bf;
background: #282c34;
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672;
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #a6e22e;
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9ccc65;
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag,
.hljs-formula {
color: #c678dd;
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379;
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66;
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4;
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801;
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2;
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import type { UploadFileInfo } from 'naive-ui'
import { fetchUpload } from '@/apis/upload'
interface Emit {
(e: 'afterUpload', data: UploadFileInfo[]): void
}
const props = defineProps({
saveFileArr: {
type: Array,
default: () => {
return []
},
},
fileType: {
type: String,
default: 'image-card',
},
maxNum: {
type: Number,
default: 1,
},
listType: {
type: String,
default: '.doc,.docx,.pdf,.xls,.xlsx,.zip,.rar,.jpg,.png,.jpeg,.svg,.gif',
},
})
const emit = defineEmits<Emit>()
const fileList = ref<UploadFileInfo[]>([])
const previewImageUrl = ref('')
const uploadPhotoRef = ref()
const imageRef = ref()
const showImage = false
watch(
() => props.saveFileArr,
(newValue) => {
fileList.value = []
if (newValue?.length) {
newValue?.forEach((item) => {
const params = {
id: new Date().getTime().toString(),
name: new Date().getTime().toString(),
status: 'finished' as any,
url: item as string,
}
fileList.value.push(params)
})
}
},
{ deep: true, immediate: true },
)
function handleUpload(file: any) {
if (file.event) {
const formdata = new FormData()
formdata.append('file', file.file.file)
fetchUpload(formdata).then((res) => {
if (res.code === 0) {
const fileData = {
id: file.file.id,
name: file.file.name,
status: 'finished' as any,
url: (res.data as string) || null,
}
fileList.value.push(fileData)
} else {
const fileData = {
id: file.file.id as string,
name: file.file.name,
status: 'error' as any,
}
fileList.value.push(fileData)
}
fileList.value = fileList.value.filter((item) => {
return item.status !== 'pending'
})
emit('afterUpload', fileList.value)
})
} else {
const fileId = file.file.id
fileList.value = fileList.value.filter((item) => {
return item.id !== fileId
})
emit('afterUpload', fileList.value)
}
}
function handlePreview(file: UploadFileInfo) {
const { url } = file
previewImageUrl.value = url as string
nextTick(() => {
imageRef.value.click()
})
}
</script>
<template>
<NUpload
ref="uploadPhotoRef"
v-model:file-list="fileList"
:max="props.maxNum"
:list-type="props.fileType"
@change="handleUpload"
@preview="handlePreview"
/>
<NImage
v-show="showImage"
ref="imageRef"
width="100"
:src="previewImageUrl"
:show-toolbar="false"
:preview-src="previewImageUrl"
/>
</template>
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { BASE_URLS } from '@/config/base-url'
import { useUserStore } from '@/store/modules/user'
const EVENT_SOURCE_BASE_URL = `${BASE_URLS[window.ENV || 'DEV']}`
export function fetchCustomEventSource(config: {
path: string
payload: any
controller: AbortController
onMessage: (data: string) => void
onRequestError: (err: any) => void
onError?: (err: any) => void
onFinally?: () => void
}) {
const userStore = useUserStore()
let responseError = false
fetchEventSource(`${EVENT_SOURCE_BASE_URL}${config.path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-Token': userStore.token || '',
},
body: JSON.stringify(config.payload || {}),
signal: config.controller?.signal,
openWhenHidden: true,
onmessage: (e) => {
if (e.data === '[DONE]' && !responseError) {
config.onMessage(e.data)
config.onFinally && config.onFinally()
return
}
try {
const data = JSON.parse(e.data)
if (data.code === -10) {
window.$message.info('身份已过期,请重新登陆')
config.onError && config.onError(data)
userStore.logout()
return
}
if (data.code === -1) {
responseError = true
window.$message.error(data.message)
config.controller?.abort()
config.onFinally && config.onFinally()
config.onError && config.onError(data)
return
}
config.onMessage(data.message)
} catch (err) {
config.onRequestError(err)
config.onFinally && config.onFinally()
}
},
onclose: () => {},
onerror: (err) => {
config.onRequestError(err)
window.$message.error(err.message || '操作失败请重试')
config.onFinally && config.onFinally()
throw err
},
})
}
import { computed } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
export function useLayoutConfig() {
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobileLayout = breakpoints.smaller('sm')
const isMobile = computed(() => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
})
return { isMobileLayout, isMobile }
}
import type { Ref } from 'vue'
import { nextTick, ref } from 'vue'
type ScrollElement = HTMLDivElement | null
interface ScrollReturn {
scrollRef: Ref<ScrollElement>
scrollToBottom: () => Promise<void>
scrollToTop: () => Promise<void>
scrollToBottomIfAtBottom: () => Promise<void>
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null)
const scrollToBottom = async () => {
await nextTick()
if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
const scrollToTop = async () => {
await nextTick()
if (scrollRef.value) scrollRef.value.scrollTop = 0
}
const scrollToBottomIfAtBottom = async () => {
await nextTick()
if (scrollRef.value) {
const threshold = 100 // 阈值,表示滚动条到底部的距离阈值
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
}
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
}
}
import { onMounted, nextTick, onUnmounted, ref, Ref, computed } from 'vue'
function debounce(handler: CallableFunction, delay: number) {
let timer: number | undefined = undefined
return () => {
clearTimeout(timer)
timer = setTimeout(handler, delay || 0)
}
}
export default function useTableScrollY(unavailableHeight = 0) {
const pageContentWrapRef = ref<HTMLElement | null>(null)
const pageContentWrapOffsetHeight = ref(0)
const tableContentY = computed(() => {
// 页面高度 - 不可用的高度
return pageContentWrapOffsetHeight.value - unavailableHeight
})
const updatePageContentWrapOffsetHeightWithDebounce = debounce(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
}, 100)
onMounted(() => {
nextTick(() => {
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
}, 200)
}, 200)
}, 200)
})
window.addEventListener('resize', updatePageContentWrapOffsetHeightWithDebounce)
})
onUnmounted(() => {
window.removeEventListener('resize', updatePageContentWrapOffsetHeightWithDebounce)
})
function updatePageContentWrapOffsetHeight(
pageContentWrapRef: Ref<HTMLElement | null>,
pageContentWrapOffsetHeight: Ref<number>,
) {
if (pageContentWrapRef.value) {
pageContentWrapOffsetHeight.value = pageContentWrapRef.value.offsetHeight || 0
}
}
return {
tableContentY,
pageContentWrapRef,
}
}
import { Directive } from 'vue'
// 创建遮罩层
function createMask(): HTMLElement {
const mask = document.createElement('div')
mask.className = 'loading-mask'
mask.style.position = 'absolute'
mask.style.top = '0px'
mask.style.left = '0px'
mask.style.width = '100%'
mask.style.height = '100%'
mask.style.backgroundColor = 'rgba(122, 122, 122, 0.8)'
mask.style.display = 'flex'
mask.style.justifyContent = 'center'
mask.style.alignItems = 'center'
const loadingIcon = document.createElement('div')
loadingIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z" opacity="0.5" />
<path fill="currentColor" d="M20 12h2A10 10 0 0 0 12 2V4A8 8 0 0 1 20 12Z">
<animateTransform attributeName="transform" dur="1s" from="0 12 12" repeatCount="indefinite" to="360 12 12" type="rotate" />
</path>
</svg>`
loadingIcon.style.color = '#2468f2'
loadingIcon.style.width = '44px'
loadingIcon.style.height = '44px'
mask.appendChild(loadingIcon)
return mask
}
// 自定义指令定义
const loading: Directive<HTMLElement, boolean> = {
mounted(el, binding) {
let isBody = false
if (binding.modifiers.body) {
isBody = true
}
const value = binding.value
if (value) {
if (isBody) {
document.body.appendChild(createMask())
} else {
el.appendChild(createMask())
}
}
},
updated(el, binding) {
let isBody = false
if (binding.modifiers.body) {
isBody = true
}
const value = binding.value
if (value) {
if (isBody) {
const mask = document.querySelector('.loading-mask')
if (!mask) {
document.body.appendChild(createMask())
}
} else {
const mask = el.querySelector('.loading-mask')
if (!mask) {
el.appendChild(createMask())
}
}
} else {
if (isBody) {
const mask = document.querySelector('.loading-mask')
if (mask) {
document.body.removeChild(mask)
}
} else {
const mask = el.querySelector('.loading-mask')
if (mask) {
el.removeChild(mask)
}
}
}
},
unmounted(el, binding) {
let isBody = false
if (binding.modifiers.body) {
isBody = true
}
if (isBody) {
const mask = document.querySelector('.loading-mask')
if (mask) {
document.body.removeChild(mask)
}
} else {
const mask = el.querySelector('.loading-mask')
if (mask) {
el.removeChild(mask)
}
}
},
}
export default loading
...@@ -39,9 +39,9 @@ function handleUpdateValue(_key: string, menuItemOption: MenuOption) { ...@@ -39,9 +39,9 @@ function handleUpdateValue(_key: string, menuItemOption: MenuOption) {
router.push({ name: menuItemOption.routeName }) router.push({ name: menuItemOption.routeName })
} }
// function handleToPersonAppSettingPage() { function handleToPersonAppSettingPage() {
// router.push({ name: 'PersonalAppSetting' }) router.push({ name: 'PersonalAppSetting' })
// } }
function handleDropdownSelect(key: string) { function handleDropdownSelect(key: string) {
if (key === 'logout') { if (key === 'logout') {
...@@ -69,7 +69,7 @@ function handleToLogin() { ...@@ -69,7 +69,7 @@ function handleToLogin() {
class="mx-auto my-[14px] flex h-[23px] w-[90px] bg-[url('@/assets/images/page-logo.png')] bg-contain bg-center bg-no-repeat" class="mx-auto my-[14px] flex h-[23px] w-[90px] bg-[url('@/assets/images/page-logo.png')] bg-contain bg-center bg-no-repeat"
/> />
<!-- <div class="py-5"> <div class="py-5">
<button <button
class="bg-theme-color flex h-[40px] w-[203px] items-center justify-center rounded-md text-white outline-none hover:opacity-80" class="bg-theme-color flex h-[40px] w-[203px] items-center justify-center rounded-md text-white outline-none hover:opacity-80"
@click="handleToPersonAppSettingPage" @click="handleToPersonAppSettingPage"
...@@ -77,7 +77,7 @@ function handleToLogin() { ...@@ -77,7 +77,7 @@ function handleToLogin() {
<CustomIcon icon="ic:outline-add" class="mr-1 h-[18px] w-[18px]" /> <CustomIcon icon="ic:outline-add" class="mr-1 h-[18px] w-[18px]" />
<span>创建应用</span> <span>创建应用</span>
</button> </button>
</div> --> </div>
<ul> <ul>
<li <li
......
...@@ -8,6 +8,7 @@ import '@/styles/reset.scss' ...@@ -8,6 +8,7 @@ import '@/styles/reset.scss'
import 'virtual:uno.css' import 'virtual:uno.css'
import '@unocss/reset/normalize.css' import '@unocss/reset/normalize.css'
import '@unocss/reset/tailwind.css' import '@unocss/reset/tailwind.css'
import LoadingDirective from './directives/loading'
async function bootstrap() { async function bootstrap() {
const app = createApp(App) const app = createApp(App)
...@@ -22,6 +23,8 @@ async function bootstrap() { ...@@ -22,6 +23,8 @@ async function bootstrap() {
meta.name = 'naive-ui-style' meta.name = 'naive-ui-style'
document.head.appendChild(meta) document.head.appendChild(meta)
app.directive('loading', LoadingDirective)
app.mount('#app') app.mount('#app')
} }
......
...@@ -2,7 +2,7 @@ import type { Router } from 'vue-router' ...@@ -2,7 +2,7 @@ import type { Router } from 'vue-router'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
/** 路由白名单 */ /** 路由白名单 */
const whitePathList = ['/login'] const whitePathList = ['/login', '/home']
export function createRouterGuards(router: Router) { export function createRouterGuards(router: Router) {
router.beforeEach((to) => { router.beforeEach((to) => {
...@@ -18,6 +18,10 @@ export function createRouterGuards(router: Router) { ...@@ -18,6 +18,10 @@ export function createRouterGuards(router: Router) {
if (whitePathList.includes(to.path)) { if (whitePathList.includes(to.path)) {
return true return true
} }
//忽略校验直接跳过
if (to.meta.ignoreAuth) {
return true
}
if (!userStore.isLogin && !whitePathList.includes(to.fullPath)) { if (!userStore.isLogin && !whitePathList.includes(to.fullPath)) {
return { path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } } return { path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } }
......
import { type RouteRecordRaw } from 'vue-router'
export default [
{
path: '/personal-space',
name: 'PersonalSpace',
meta: {
rank: 1001,
title: '个人空间',
icon: 'mingcute:user-2-line',
belong: 'personal-space',
},
component: () => import('@/layout/index.vue'),
redirect: '/personalSpaceLayout',
children: [
{
path: '/personalSpaceLayout',
name: 'PersonalSpaceLayout',
meta: {
rank: 1001,
title: '个人空间',
belong: 'PersonalSpace',
},
component: () => import('@/views/personal-space/personal-space.vue'),
redirect: '/personalSpace/app',
children: [
{
path: '/personalSpace/app',
name: 'PersonalSpaceApp',
meta: {
rank: 1001,
title: 'Agent应用',
belong: 'PersonalSpace',
},
component: () => import('@/views/personal-space/personal-app/personal-app.vue'),
},
],
},
],
},
{
path: '/personal-app-setting/:agentId?/:tabKey?',
name: 'PersonalAppSetting',
meta: {
rank: 1001,
title: '应用设置',
icon: 'mingcute:user-2-line',
belong: 'PersonalAppSetting',
hideSideMenItem: true,
},
component: () => import('@/views/personal-space/personal-app-setting/personal-app-setting.vue'),
},
] as RouteRecordRaw[]
import { type RouteRecordRaw } from 'vue-router'
export default [
{
path: '/share/web_source/:agentId?',
name: 'ShareWebApplication',
meta: {
rank: 1001,
title: '我的Agent应用',
hideSideMenItem: true,
ignoreAuth: true,
},
component: () => import('@/views/share/share-application-web.vue'),
},
{
path: '/share/mobile_source/:agentId?',
name: 'ShareMobileApplication',
meta: {
rank: 1001,
title: '我的Agent应用',
hideSideMenItem: true,
ignoreAuth: true,
},
component: () => import('@/views/share/share-application-mobile.vue'),
},
] as RouteRecordRaw[]
import { defineStore } from 'pinia'
import { PersonalAppConfigState } from '../types/personal-app-config'
export function defaultPersonalAppConfigState(): PersonalAppConfigState {
return {
baseInfo: {
agentId: '',
agentTitle: '我的Agent应用',
agentAvatar: 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp',
agentDesc: '',
agentSystem: '',
agentPublishStatus: 'draft',
},
commConfig: {
preamble: '',
featuredQuestions: [],
continuousQuestionStatus: 'default',
continuousQuestionSystem: '',
continuousQuestionTurn: 3,
},
knowledgeConfig: {
knowledgeIds: [],
},
commModelConfig: {
largeModel: 'ERNIE-4.0-8K',
topP: 0.0,
communicationTurn: 0,
},
modifiedTime: new Date(),
}
}
export const usePersonalAppConfigStore = defineStore('personal-app-config-store', {
state: (): PersonalAppConfigState => defaultPersonalAppConfigState(),
actions: {
updatePersonalAppConfigState(personalAppConfigState: Partial<PersonalAppConfigState>) {
this.$state = { ...this.$state, ...personalAppConfigState }
},
resetPersonalAppConfigState() {
this.$state = defaultPersonalAppConfigState()
},
},
})
export interface PersonalAppConfigState {
baseInfo: {
agentId: string //应用ID
agentTitle: string //应用标题
agentAvatar: string //应用头像
agentDesc: string //应用描述
agentSystem: string //角色指令
agentPublishStatus: 'draft' | 'publish' //发布状态 draft-草稿 publish-发布
}
commConfig: {
preamble: string //开场白
featuredQuestions: string[] //推荐问
continuousQuestionStatus: 'default' | 'customizable' | 'close' //追问状态
continuousQuestionSystem: string // 追问提示词 customizable时必填
continuousQuestionTurn: number // 追问轮次 1-5 customizable时必填
}
knowledgeConfig: {
knowledgeIds: string[] //知识库ID
}
commModelConfig: {
largeModel: string //大模型
topP: number //多样性 0-1.00
communicationTurn: number //参考对话轮次 0-100
}
modifiedTime: Date
}
...@@ -3,3 +3,24 @@ body { ...@@ -3,3 +3,24 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
} }
@mixin custom-scrollbar($scrollbarSize: 5px, $scrollbarColor: #ededed) {
::-webkit-scrollbar {
width: $scrollbarSize;
height: $scrollbarSize;
}
::-webkit-scrollbar-corner {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $scrollbarColor;
border-radius: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
}
import clipboard from 'clipboardy'
export function copyToClip(text: string) {
return clipboard.write(text)
}
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import 'dayjs/locale/zh-cn'
dayjs.extend(utc)
dayjs.extend(timezone)
export function formatDateTime(date: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
return dayjs(date).format(format)
}
import { h } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { formatDateTime } from '@/utils/date-formatter'
export function createChannelPublishColumn(
handleChannelPublishTableAction: (actionType: string, linkUrl: string) => void,
) {
return [
{
title: '发布渠道',
key: 'channel',
align: 'left',
width: 540,
render() {
return h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
},
},
{
default: () => [
h(CustomIcon, {
width: '24px',
icon: 'icon-park-solid:computer',
color: '#2468f2',
}),
h(
'div',
{
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
fontSize: '14px',
marginLeft: '12px',
},
},
{
default: () => [
h(
'span',
{},
{
default: () => '网页端',
},
),
h(
'span',
{
style: {
color: '#84868c',
},
},
{
default: () => '可通过PC或移动设备立即开始对话',
},
),
],
},
),
],
},
)
},
},
{
title: '状态',
key: 'agentPublishStatus',
align: 'left',
width: 220,
render() {
return h(
'div',
{
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
},
},
{
default: () => [
h(
'div',
{
style: {
background: '#34a853',
borderRadius: '4px',
padding: '2px 14px',
color: '#fff',
marginBottom: '4px',
},
},
{
default: () => '已发布',
},
),
h(
'span',
{
style: {
color: '#84868c',
},
},
{
default: () => formatDateTime(new Date()) + '发布',
},
),
],
},
)
},
},
{
title: '操作',
key: 'action',
align: 'left',
width: '460',
render(row: { linkUrl: string }) {
return h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
},
},
{
default: () => [
h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
fontSize: '14px',
padding: '4px 12px',
background: '#f7f7f9',
borderColor: '#2468f2',
color: '#2468f2',
},
className: 'cursor-pointer rounded-md border hover:opacity-80',
onClick: () => handleChannelPublishTableAction('accessPage', row.linkUrl),
},
{
default: () => [
h(CustomIcon, { icon: 'lets-icons:view', style: { marginRight: '6px', fontSize: '16px' } }),
h(
'span',
{},
{
default: () => '立即访问',
},
),
],
},
),
h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
fontSize: '14px',
marginLeft: '16px',
padding: '4px 12px',
background: '#f7f7f9',
},
className: 'cursor-pointer hover:text-theme-color rounded-md border hover:border-theme-color',
onClick: () => handleChannelPublishTableAction('copyLink', row.linkUrl),
},
{
default: () => [
h(CustomIcon, { icon: 'pepicons-pop:share-android-circle', style: { marginRight: '6px' } }),
h(
'span',
{},
{
default: () => '分享链接',
},
),
],
},
),
],
},
)
},
},
]
}
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
interface Props {
modalTitle: string
isShowModal: boolean
btnLoading: boolean
questionSystem: string
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'comfirm', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const continuousQuestionSystem = ref('')
watchEffect(() => {
continuousQuestionSystem.value = props.questionSystem
})
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
emit('update:isShowModal', value)
},
})
function handleAdditionalPrompt() {
emit('comfirm', continuousQuestionSystem.value)
}
</script>
<template>
<CustomModal
v-model:is-show="showModal"
:title="modalTitle"
:btn-loading="btnLoading"
:height="636"
:width="520"
@confirm="handleAdditionalPrompt"
>
<template #content>
<p class="mb-3 select-none text-[#84868c]">可在追问prompt中指引追问的字数、风格和内容范围。</p>
<NInput
v-model:value="continuousQuestionSystem"
type="textarea"
:rows="19"
maxlength="1000"
show-count
placeholder="请输入追问prompt"
class="rounded-lg!"
/>
</template>
</CustomModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Preamble from './preamble.vue'
import MessageList from './message-list.vue'
import FooterInput from './footer-input.vue'
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const messageList = ref<ConversationMessageItem[]>([])
function handleAddMessageItem(messageItem: ConversationMessageItem) {
messageList.value.push(messageItem)
}
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) {
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v
})
}
}
function handleDeleteLastMessageItem() {
messageList.value.pop()
}
function handleUpdatePageScroll() {
messageListRef.value?.scrollToBottom()
}
function handleClearAllMessage() {
window.$dialog.warning({
title: '确认要清空对话吗?',
content: '清空对话将清空调试区域所有历史对话内容,确定清空对话吗?',
negativeText: '取消',
positiveText: '确定',
onPositiveClick: () => {
messageList.value = []
window.$message.success('清空成功')
},
})
}
</script>
<template>
<div class="flex h-full min-w-[300px] flex-1 flex-col overflow-hidden bg-[#f2f5f9]">
<p class="mb-[18px] px-5 py-[18px] text-base">预览与调试</p>
<div class="flex w-full flex-1 overflow-hidden">
<div v-show="messageList.length === 0" class="w-full">
<Preamble />
</div>
<div v-show="messageList.length > 0" class="w-full">
<MessageList ref="messageListRef" :message-list="messageList" />
</div>
</div>
<FooterInput
:message-list="messageList"
@add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem"
@update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage"
/>
</div>
</template>
<style scoped lang="scss">
@include custom-scrollbar(6px);
</style>
<script lang="ts" setup>
import { readonly } from 'vue'
import { useRouter } from 'vue-router'
import { createChannelPublishColumn } from '../columns'
import { INDEX_URLS } from '@/config/base-url'
import useTableScrollY from '@/composables/useTableScrollY'
import { copyToClip } from '@/utils/copy'
const { pageContentWrapRef, tableContentY } = useTableScrollY()
const router = useRouter()
const channelPublishList = readonly([
{
linkUrl: `${INDEX_URLS[window.ENV || 'DEV']}share/web_source/${router.currentRoute.value.params.agentId}`,
},
])
const columns = createChannelPublishColumn(handleClickChannelPublishTableAction)
function handleClickChannelPublishTableAction(actionType: string, channleUrl: string) {
switch (actionType) {
case 'accessPage':
handleAccessPage(channleUrl)
break
case 'copyLink':
handleCopyShareLink(channleUrl)
break
}
}
function handleAccessPage(channleUrl: string) {
location.href = channleUrl
}
function handleCopyShareLink(channleUrl: string) {
copyToClip(channleUrl)
window.$message.success('链接复制成功,快分享给你的好友吧!')
}
</script>
<template>
<div ref="pageContentWrapRef" class="h-full overflow-hidden p-5">
<NDataTable
:bordered="true"
:bottom-bordered="true"
:single-line="false"
:data="channelPublishList"
:columns="columns"
:max-height="tableContentY"
:scroll-x="1220"
/>
</div>
</template>
<script setup lang="ts"></script>
<template>
<div class="loader" />
</template>
<style lang="scss" scoped>
.loader {
width: 6px;
aspect-ratio: 1;
border-radius: 50%;
animation: l5 1s infinite linear alternate;
}
@keyframes l5 {
0% {
background: #2468f2;
box-shadow:
13px 0 #2468f2,
-13px 0 #0002;
}
33% {
background: #0002;
box-shadow:
13px 0 #2468f2,
-13px 0 #0002;
}
66% {
background: #0002;
box-shadow:
13px 0 #0002,
-13px 0 #2468f2;
}
100% {
background: #2468f2;
box-shadow:
13px 0 #0002,
-13px 0 #2468f2;
}
}
</style>
<script setup lang="ts">
import { computed, inject, onUnmounted, ref } from 'vue'
import { Emitter } from 'mitt'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
interface Props {
messageList: ConversationMessageItem[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
addMessageItem: [value: ConversationMessageItem]
updateSpecifyMessageItem: [messageItemIndex: number, newObj: Partial<ConversationMessageItem>]
deleteLastMessageItem: []
updatePageScroll: []
clearAllMessage: []
}>()
const personalAppConfigStore = usePersonalAppConfigStore()
const emitter = inject<Emitter<MittEvents>>('emitter')
const inputeMessageContent = ref('')
const isAnswerResponseWait = ref(false)
let controller: AbortController | null = null
const agentId = computed(() => {
return personalAppConfigStore.baseInfo.agentId
})
const isAllowClearMessage = computed(() => {
return props.messageList.length > 0
})
const isSendBtnDisabled = computed(() => {
return !inputeMessageContent.value.trim()
})
onUnmounted(() => {
blockMessageResponse()
emitter?.off('selectFeaturedQuestion')
})
emitter?.on('selectFeaturedQuestion', (featuredQuestion) => {
inputeMessageContent.value = featuredQuestion
handleMessageSend()
})
function messageItemFactory() {
return {
timestamp: Date.now(),
role: 'user',
textContent: '',
isEmptyContent: false,
isTextContentLoading: false,
isAnswerResponseLoading: false,
} as const
}
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputeMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() {
if (!inputeMessageContent.value.trim() || isAnswerResponseWait.value) return ''
const messages: {
content: {
type: string
text: string
image_url: {
url: ''
}
}[]
role: string
}[] = []
emit('addMessageItem', { ...messageItemFactory(), textContent: inputeMessageContent.value })
emit('updatePageScroll')
props.messageList.forEach((messageItem) => {
messages.push({
content: [
{
type: 'text',
text: messageItem.textContent,
image_url: {
url: '',
},
},
],
role: messageItem.role,
})
})
inputeMessageContent.value = ''
isAnswerResponseWait.value = true
emit('addMessageItem', {
...messageItemFactory(),
role: 'assistant',
isTextContentLoading: true,
isAnswerResponseLoading: true,
})
emit('updatePageScroll')
const currentMessageIndex = props.messageList.length - 1
let replyTextContent = ''
controller = new AbortController()
fetchCustomEventSource({
path: '/api/rest/agentApplicationInfoRest/preview.json',
payload: {
agentId: agentId.value,
messages,
},
controller,
onMessage: (data: any) => {
if (data === '[DONE]') {
emit('updateSpecifyMessageItem', currentMessageIndex, {
isEmptyContent: !replyTextContent,
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
emit('updatePageScroll')
blockMessageResponse()
return
}
if (data) {
replyTextContent += data
emit('updateSpecifyMessageItem', currentMessageIndex, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
}
},
onRequestError: () => {
errorMessageResponse()
},
onError: () => {
errorMessageResponse()
},
onFinally: () => {
controller = null
},
})
}
function errorMessageResponse() {
emit('updateSpecifyMessageItem', props.messageList.length - 1, {
isTextContentLoading: false,
textContent: '',
})
emit('deleteLastMessageItem')
emit('deleteLastMessageItem')
blockMessageResponse()
}
function handleClearAllMessage() {
if (!isAllowClearMessage.value) return
blockMessageResponse()
emit('clearAllMessage')
}
function blockMessageResponse() {
controller?.abort()
isAnswerResponseWait.value = false
}
</script>
<template>
<div class="mb-3 mt-5 px-5">
<div class="flex">
<div class="mr-2 flex h-8 w-8 items-center justify-center">
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="fluent:delete-12-regular"
class="text-base outline-none"
:class="
isAllowClearMessage
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]'
: 'cursor-not-allowed text-[#b8babf]'
"
@click="handleClearAllMessage"
/>
</template>
<span class="text-xs">清空历史会话</span>
</NPopover>
</div>
<div class="relative flex-1">
<NInput
v-model:value="inputeMessageContent"
placeholder="请输入你的问题进行提问"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]"
@keypress="handleEnterKeypress"
/>
<div
class="absolute bottom-2 right-[20px] h-[24px] w-[24px] bg-[url('@/assets/images/send.png')] bg-contain"
:class="isSendBtnDisabled || isAnswerResponseWait ? 'opacity-60' : 'cursor-pointer'"
@click="handleMessageSend"
/>
</div>
</div>
<div class="mt-[9px] pl-10">
<span class="text-xs text-[#84868c]">以上内容均由AI生成,仅供参考</span>
</div>
</div>
</template>
<script setup lang="ts">
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
const personalAppConfigStore = usePersonalAppConfigStore()
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
}
defineProps<Props>()
const useAvatar = 'https://mkp-dev.oss-cn-shenzhen.aliyuncs.com/data/upload/20240827/1724728478476.png'
const assistantAvatar = personalAppConfigStore.baseInfo.agentAvatar
</script>
<template>
<div class="mb-5 flex last:mb-0">
<NImage
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
class="mr-2 mt-1.5 h-8 w-8 rounded-full"
/>
<div
class="min-w-[80px] max-w-[calc(100%-32px-12px)] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="messageItem.isEmptyContent ? '[空内容]' : messageItem.textContent"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import MessageItem from './message-item.vue'
import { useScroll } from '@/composables/useScroll'
interface Props {
messageList: ConversationMessageItem[]
}
defineProps<Props>()
const { scrollRef, scrollToBottom } = useScroll()
defineExpose({
scrollToBottom,
})
</script>
<template>
<main ref="scrollRef" class="h-full overflow-y-auto px-5">
<MessageItem
v-for="messageItem in messageList"
:key="messageItem.timestamp"
:role="messageItem.role"
:message-item="messageItem"
/>
</main>
</template>
<script setup lang="ts">
import { computed, h, onMounted, readonly, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { DropdownOption } from 'naive-ui'
import { sidebarMenus } from '@/router/index'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { useUserStore } from '@/store/modules/user'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { formatDateTime } from '@/utils/date-formatter'
import { fetchPublishApplication } from '@/apis/agent-application'
const defaultAvatar = 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
interface Emits {
(e: 'changeAgentAppTabKey', value: string): void
}
const emit = defineEmits<Emits>()
const router = useRouter()
const userStore = useUserStore()
const personalAppConfigStore = usePersonalAppConfigStore()
const avatarOptions = readonly([
{
label: '退出登录',
key: 'logout',
icon: () => h(CustomIcon, { icon: 'teenyicons:logout-solid' }),
},
])
const modifiedTime = ref(personalAppConfigStore.modifiedTime)
const isInitGetAgentAppDetail = ref(false)
const isUpdateAgentAppConfig = ref(false)
const currentAgentAppTabKey = ref('config')
const publishBtnloading = ref(false)
const agentAppOptionList = [
{
value: 'config',
label: '配置',
},
{
value: 'publish',
label: '发布',
},
]
const personalAppConfig = computed(() => personalAppConfigStore.$state)
const menuOptions = computed(() => {
return sidebarMenus.map((item) => {
return {
label: item.label,
key: item.key,
routeName: item.routeName,
icon: () => h(CustomIcon, { icon: item.icon || 'icon-home' }),
}
})
})
const isShowModifiedTime = computed(() => {
return isUpdateAgentAppConfig.value && personalAppConfigStore.baseInfo.agentId
})
const publishBtnText = computed(() => {
return personalAppConfigStore.baseInfo.agentPublishStatus === 'publish' ? '更新发布' : '发 布'
})
const isShowPublishBtn = computed(() => {
return currentAgentAppTabKey.value === 'config'
})
const isAllowClickPublish = computed(() => {
return personalAppConfigStore.baseInfo.agentId && personalAppConfigStore.baseInfo.agentPublishStatus === 'publish'
})
watch(
() => personalAppConfig.value,
() => {
if (isInitGetAgentAppDetail.value) {
isInitGetAgentAppDetail.value = false
return
}
modifiedTime.value = new Date()
!isUpdateAgentAppConfig.value && (isUpdateAgentAppConfig.value = true)
},
{ deep: true },
)
onMounted(() => {
if (router.currentRoute.value.params.agentId) {
isInitGetAgentAppDetail.value = true
}
if (router.currentRoute.value.query.tabKey) {
currentAgentAppTabKey.value = router.currentRoute.value.query.tabKey as string
}
})
function handleMenuSelect(_key: number, option: DropdownOption) {
router.replace({ name: option.routeName as string })
}
function handleBackPreviousPage() {
router.go(-1)
}
function handleDropdownSelect(key: number) {
if (key === 1) {
userStore.logout().then(() => {
router.push({ name: 'Login' })
})
}
}
function handleSwtichAgentAppOption(currentTabKey: string) {
if (!isAllowClickPublish.value) return
currentAgentAppTabKey.value = currentTabKey
router.replace({
name: router.currentRoute.value.name as string,
query: { tabKey: currentTabKey },
params: { ...router.currentRoute.value.params },
})
emit('changeAgentAppTabKey', currentTabKey)
}
async function handlePublishApplication() {
publishBtnloading.value = true
const res = await fetchPublishApplication(personalAppConfig.value).finally(() => (publishBtnloading.value = false))
if (res.code === 0) {
window.$message.success('发布成功')
currentAgentAppTabKey.value = 'publish'
router.replace({
name: router.currentRoute.value.name as string,
query: { tabKey: 'publish' },
params: { ...router.currentRoute.value.params },
})
emit('changeAgentAppTabKey', 'publish')
}
}
</script>
<template>
<header class="h-navbar flex w-full items-center justify-between bg-[#f2f5f9] px-5 shadow-[inset_0_-1px_#e8e9eb]">
<div class="flex flex-1 items-center">
<NDropdown trigger="hover" :options="menuOptions" @select="handleMenuSelect">
<CustomIcon
icon="weui:back-outlined"
class="hover:text-theme-color mr-5 outline-none"
@click="handleBackPreviousPage"
/>
</NDropdown>
<div class="flex flex-col items-start justify-center">
<NPopover trigger="hover">
<template #trigger>
<span class="font-500 line-clamp-1 max-w-[200px] break-words text-base text-[#000]">
{{ personalAppConfigStore.baseInfo.agentTitle }}
</span>
</template>
<span>{{ personalAppConfigStore.baseInfo.agentTitle }}</span>
</NPopover>
<div class="flex items-center text-xs">
<NPopover trigger="hover">
<template #trigger>
<span class="line-clamp-1 max-w-[200px] break-words text-[#84868c]">
{{ personalAppConfigStore.baseInfo.agentDesc || '暂无描述' }}
</span>
</template>
<span>{{ personalAppConfigStore.baseInfo.agentDesc || '暂无描述' }}</span>
</NPopover>
<div
v-show="personalAppConfigStore.baseInfo.agentId"
class="ml-3 h-5 rounded bg-white px-2 leading-5 text-[#5c5f66]"
>
已变更
</div>
<div v-show="isShowModifiedTime" class="ml-3 h-5 rounded-md bg-white px-2 leading-5 text-[#151b26]">
自动保存于 {{ formatDateTime(modifiedTime, 'HH:mm:ss') }}
</div>
</div>
</div>
</div>
<div class="flex h-[38px] rounded-lg bg-[#e3e8f0] p-0.5">
<div
v-for="optionItem in agentAppOptionList"
:key="optionItem.value"
:value="optionItem.value"
:label="optionItem.label"
class="flex w-20 items-center justify-center rounded-lg"
:class="[
currentAgentAppTabKey === optionItem.value ? 'text-theme-color bg-white' : 'text-[#84868c]',
isAllowClickPublish ? 'hover:text-theme-color cursor-pointer' : 'cursor-not-allowed',
]"
@click="handleSwtichAgentAppOption(optionItem.value)"
>
<NPopover v-if="!isAllowClickPublish" trigger="hover">
<template #trigger>
{{ optionItem.label }}
</template>
<span>请先完成发布</span>
</NPopover>
<div v-if="isAllowClickPublish">{{ optionItem.label }}</div>
</div>
</div>
<div class="flex flex-1 items-center justify-end">
<NButton
v-show="isShowPublishBtn"
type="primary"
class="h-[32px]! min-w-20! rounded-md!"
:loading="publishBtnloading"
@click="handlePublishApplication"
>
{{ publishBtnText }}
</NButton>
<NDropdown trigger="click" :options="avatarOptions" @select="handleDropdownSelect">
<div class="ml-10 flex h-full cursor-pointer items-center">
<NAvatar round :size="30" object-fit="cover" :src="userStore.userInfo.avatarUrl || defaultAvatar" />
</div>
</NDropdown>
</div>
</header>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { Emitter } from 'mitt'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
const personalAppConfigStore = usePersonalAppConfigStore()
const emitter = inject<Emitter<MittEvents>>('emitter')
const agentAvatar = computed(() => {
return (
personalAppConfigStore.baseInfo.agentAvatar || 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
)
})
function handleSelectFeaturedQuestion(featuredQuestion: string) {
emitter?.emit('selectFeaturedQuestion', featuredQuestion)
}
</script>
<template>
<div class="flex w-full flex-1 flex-col px-5">
<div class="mb-5 flex w-full justify-center pt-[50px]">
<img :src="agentAvatar" class="h-[72px] w-[72px] rounded-xl border" />
</div>
<div class="flex flex-col items-center justify-center">
<p class="font-500 mb-4 line-clamp-1 text-2xl text-[#151b26]">
{{ personalAppConfigStore.baseInfo.agentTitle }}
</p>
<div class="flex flex-col items-start justify-center">
<p
v-show="personalAppConfigStore.commConfig.preamble"
class="mb-6 select-none rounded-xl border border-[#e8e9eb] bg-white px-[16px] py-[12px] shadow-[0_2px_2px_#0000000a]"
>
{{ personalAppConfigStore.commConfig.preamble }}
</p>
<ul class="flex max-w-full flex-col items-start justify-center gap-3 overflow-hidden">
<li v-for="(featuredQuestionItem, index) in personalAppConfigStore.commConfig.featuredQuestions" :key="index">
<div
v-show="featuredQuestionItem"
class="w-full cursor-pointer rounded-xl border border-[#d4d6d9] bg-[#ffffff80] px-[14px] py-[11px] hover:opacity-80"
@click="handleSelectFeaturedQuestion(featuredQuestionItem)"
>
<NPopover trigger="hover">
<template #trigger>
<span class="break-all">{{ featuredQuestionItem }}</span>
</template>
<span>{{ featuredQuestionItem }}</span>
</NPopover>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import PageNarbar from './components/page-narbar.vue'
import AppSetting from './components/app-setting.vue'
import AppPreview from './components/app-preview.vue'
import AppPublish from './components/app-publish.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
const router = useRouter()
const personalAppConfigStore = usePersonalAppConfigStore()
const currentAgentAppTabKey = ref('config')
onMounted(() => {
if (router.currentRoute.value.query.tabKey) {
currentAgentAppTabKey.value = router.currentRoute.value.query.tabKey as string
}
})
onUnmounted(() => {
personalAppConfigStore.resetPersonalAppConfigState()
})
function handleChangeAgentAppTabKey(currentTabKey: string) {
currentAgentAppTabKey.value = currentTabKey
}
</script>
<template>
<main class="h-full min-w-[1000px]">
<PageNarbar @change-agent-app-tab-key="handleChangeAgentAppTabKey" />
<div class="h-content flex w-full flex-1">
<div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1">
<AppSetting />
<AppPreview />
</div>
<div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1">
<AppPublish />
</div>
</div>
</main>
</template>
This diff is collapsed.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import CustomPagination, { PaginationInfo } from '@/components/custom-pagination/custom-pagination.vue'
import { createPersonalAppColumn } from './columns.ts'
import useTableScrollY from '@/composables/useTableScrollY.ts'
import {
fetchDeleteApplication,
fetchGetDebugApplicationInfo,
fetchGetApplicationList,
fetchSaveAgentApplication,
} from '@/apis/agent-application.ts'
import { PersonalAppConfigState } from '@/store/types/personal-app-config.ts'
import { copyToClip } from '@/utils/copy.ts'
const router = useRouter()
const { pageContentWrapRef, tableContentY } = useTableScrollY(48 + 32 + 16 + 16 + 28)
const columns = createPersonalAppColumn(handleClickPersonalAppTableAction)
const pagingInfo = ref<PaginationInfo>({
pageNo: 1,
pageSize: 10,
totalPages: 0,
totalRows: 0,
})
const agentAppList = ref<PersonalAppConfigState[]>([])
const agentSearchInputValue = ref('')
const agentAppListTableLoading = ref(false)
onMounted(async () => {
await handleGetApplicationList()
})
async function handleGetApplicationList() {
agentAppListTableLoading.value = true
const res = await fetchGetApplicationList<PersonalAppConfigState[]>({
query: agentSearchInputValue.value,
pagingInfo: pagingInfo.value,
}).finally(() => (agentAppListTableLoading.value = false))
if (res.code === 0) {
agentAppList.value = res.data
pagingInfo.value = res.pagingInfo as PaginationInfo
}
}
function handleClickPersonalAppTableAction(actionType: string, agentId: string) {
switch (actionType) {
case 'copyAgentId':
handleCopyAgentId(agentId)
break
case 'openPublishDetail':
handleOpenPublishDetail(agentId)
break
case 'edit':
handleEditPersonalApp(agentId)
break
case 'copy':
handleCopyPersonalApp(agentId)
break
case 'delete':
handleDeletePersonalApp(agentId)
break
}
}
function handleCopyAgentId(agentId: string) {
copyToClip(agentId)
window.$message.success('复制成功')
}
function handleOpenPublishDetail(agentId: string) {
router.push({
name: 'PersonalAppSetting',
query: {
tabKey: 'publish',
},
params: {
agentId,
},
})
}
function handleEditPersonalApp(agentId: string) {
router.push({
name: 'PersonalAppSetting',
params: {
agentId,
},
})
}
async function handleCopyPersonalApp(agentId: string) {
const res = await fetchGetDebugApplicationInfo<PersonalAppConfigState>(agentId)
if (res.code === 0) {
const payload = res.data
payload.baseInfo.agentId = ''
payload.baseInfo.agentTitle += '的副本'
payload.baseInfo.agentPublishStatus = 'draft'
await fetchSaveAgentApplication(payload)
await handleGetApplicationList()
}
}
function handleDeletePersonalApp(agentId: string) {
window.$dialog.warning({
title: '确定要删除选中的应用吗?',
content: '删除后,如需再次使用,请重新创建',
negativeText: '取消',
positiveText: '确定',
onPositiveClick: async () => {
const res = await fetchDeleteApplication(agentId)
if (res.code === 0) {
window.$message.success('删除成功')
await handleGetApplicationList()
}
},
})
}
function handleToPersonAppSettingPage() {
router.push({ name: 'PersonalAppSetting' })
}
async function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
await handleGetApplicationList()
}
}
async function handleGetApplicationListUpdatePageNo(pageNo: number) {
pagingInfo.value.pageNo = pageNo
await handleGetApplicationList()
}
async function handleGetApplicationListUpdatePageSize(pageSize: number) {
pagingInfo.value.pageNo = 1
pagingInfo.value.pageSize = pageSize
await handleGetApplicationList()
}
</script>
<template>
<div ref="pageContentWrapRef" class="h-full">
<div class="mb-4 flex justify-between">
<NInput
v-model:value="agentSearchInputValue"
placeholder="请输入应用名称或描述"
class="w-[256px]! h-[32px]! rounded-md!"
@keypress="handleEnterKeypress"
>
<template #suffix>
<CustomIcon
icon="tdesign:search"
class="cursor-pointer text-base text-[#999]"
@click="handleGetApplicationList"
/>
</template>
</NInput>
<NButton type="primary" class="h-[32px]! w-[100px]! rounded-md!" @click="handleToPersonAppSettingPage">
创建应用
</NButton>
</div>
<div class="mb-4" :style="{ height: tableContentY + 48 + 'px' }">
<NDataTable
:loading="agentAppListTableLoading"
:bordered="true"
:bottom-bordered="true"
:single-line="false"
:data="agentAppList"
:columns="columns"
:max-height="tableContentY"
:scroll-x="1330"
/>
</div>
<footer class="flex justify-end">
<CustomPagination
:paging-info="pagingInfo"
@update-page-no="handleGetApplicationListUpdatePageNo"
@update-page-size="handleGetApplicationListUpdatePageSize"
/>
</footer>
</div>
</template>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -30,5 +30,6 @@ export default { ...@@ -30,5 +30,6 @@ export default {
'font-family-no-missing-generic-family-keyword': null, 'font-family-no-missing-generic-family-keyword': null,
'scss/at-import-partial-extension': 'always', 'scss/at-import-partial-extension': 'always',
'alpha-value-notation': 'number', 'alpha-value-notation': 'number',
'selector-class-pattern': null,
}, },
} }
declare interface ConversationMessageItem {
timestamp: number
role: 'user' | 'assistant'
textContent: string
isEmptyContent: boolean
isTextContentLoading: boolean
isAnswerResponseLoading: boolean
}
declare interface Window { declare interface Window {
ENV: 'DEV' | 'PROD'
$loadingBar: import('naive-ui').LoadingBarProviderInst $loadingBar: import('naive-ui').LoadingBarProviderInst
$dialog: import('naive-ui').DialogProviderInst $dialog: import('naive-ui').DialogProviderInst
$message: import('naive-ui').MessageProviderInst $message: import('naive-ui').MessageProviderInst
......
declare type MittEvents = {
selectFeaturedQuestion: string
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment