Commit ecbc18b2 authored by tyyin lan's avatar tyyin lan

feat(首页): 输入框编辑器prompt制订

parent 4ed028c3
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_f6b3gmjqw8q.css" /> <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_4rkp9lj7gda.css" />
<link <link
rel="preload" rel="preload"
href="https://gsst-poe-sit.gz.bcebos.com/front/SourceHanSansCN-Medium.otf" href="https://gsst-poe-sit.gz.bcebos.com/front/SourceHanSansCN-Medium.otf"
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
"@icon-park/vue-next": "^1.4.2", "@icon-park/vue-next": "^1.4.2",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@unocss/reset": "^0.61.9", "@unocss/reset": "^66.1.1",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^10.11.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bowser": "^2.11.0", "bowser": "^2.11.0",
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"qs": "^6.14.0", "qs": "^6.14.0",
"quill": "^2.0.3",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"type-fest": "^4.26.1", "type-fest": "^4.26.1",
"validator": "^13.12.0", "validator": "^13.12.0",
...@@ -63,9 +64,9 @@ ...@@ -63,9 +64,9 @@
"@types/spark-md5": "^3.0.4", "@types/spark-md5": "^3.0.4",
"@types/validator": "^13.12.2", "@types/validator": "^13.12.2",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"@unocss/eslint-config": "^0.61.9", "@unocss/eslint-config": "^66.1.1",
"@unocss/postcss": "66.1.0-beta.10", "@unocss/postcss": "66.1.1",
"@unocss/transformer-directives": "66.1.0-beta.10", "@unocss/transformer-directives": "66.1.1",
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^4.6.2",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
...@@ -95,7 +96,7 @@ ...@@ -95,7 +96,7 @@
"tinymce": "^7.7.1", "tinymce": "^7.7.1",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"typescript-eslint": "^7.18.0", "typescript-eslint": "^7.18.0",
"unocss": "^0.61.9", "unocss": "^66.1.1",
"unplugin-auto-import": "^0.17.8", "unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.26.0", "unplugin-vue-components": "^0.26.0",
"vite": "^5.4.6", "vite": "^5.4.6",
......
This diff is collapsed.
...@@ -43,3 +43,7 @@ export function fetchFileUpload<T>( ...@@ -43,3 +43,7 @@ export function fetchFileUpload<T>(
signal: config.signal, signal: config.signal,
}) })
} }
export function fetchSceneList<T>() {
return request.post<T>('/agentApplicationRest/getHomePlugins.json?search=')
}
...@@ -15,6 +15,8 @@ import { useSystemLanguageStore } from '@/store/modules/system-language' ...@@ -15,6 +15,8 @@ import { useSystemLanguageStore } from '@/store/modules/system-language'
import { PluginType } from '@/enums/plugin' import { PluginType } from '@/enums/plugin'
import { NAvatar, NEllipsis } from 'naive-ui' import { NAvatar, NEllipsis } from 'naive-ui'
import type { SelectRenderLabel, SelectRenderTag } from 'naive-ui' import type { SelectRenderLabel, SelectRenderTag } from 'naive-ui'
import RichTextInputBox from './rich-text-input-box/index.vue'
import type { WorkModeType } from '@/views/home/components/rich-text-input-box/types/index'
interface Props { interface Props {
currentSessionId: string currentSessionId: string
...@@ -38,6 +40,7 @@ const isAgentResponding = defineModel<boolean>('isAgentResponding', { required: ...@@ -38,6 +40,7 @@ const isAgentResponding = defineModel<boolean>('isAgentResponding', { required:
const currentFetchEventSourceController = defineModel<AbortController | null>('currentFetchEventSourceController', { const currentFetchEventSourceController = defineModel<AbortController | null>('currentFetchEventSourceController', {
required: true, required: true,
}) })
const editorDrawerConfig = defineModel<{ isShow: boolean; content: string }>('editorDrawerConfig', { required: true })
const { t } = useI18n() const { t } = useI18n()
...@@ -61,6 +64,12 @@ const currentInputFileInfo = ref({ ...@@ -61,6 +64,12 @@ const currentInputFileInfo = ref({
uploading: false, uploading: false,
}) })
const currentSceneConfig = ref<{ agentId: string; agentTitle: string; agentDesc: string; agentAvatar: string } | null>(
null,
)
const workMode = ref<WorkModeType>('ApplicationMode')
const editorModeEnable = ref(false)
const isQuestionSubmitBtnDisabled = computed(() => { const isQuestionSubmitBtnDisabled = computed(() => {
return ( return (
questionContent.value.trim().length === 0 || questionContent.value.trim().length === 0 ||
...@@ -138,7 +147,7 @@ function handleCreateNewSession() { ...@@ -138,7 +147,7 @@ function handleCreateNewSession() {
} }
} }
function questionSubmit() { function questionSubmit(question: string) {
const latestUserMessageKey = nanoid() const latestUserMessageKey = nanoid()
const latestAssistantMessageKey = nanoid() const latestAssistantMessageKey = nanoid()
...@@ -148,7 +157,7 @@ function questionSubmit() { ...@@ -148,7 +157,7 @@ function questionSubmit() {
emit('addMessageItem', latestUserMessageKey, { emit('addMessageItem', latestUserMessageKey, {
role: 'user', role: 'user',
agentId: '', agentId: '',
content: questionContent.value.trim(), content: question.trim(),
timestamp: Date.now(), timestamp: Date.now(),
isAnswerLoading: false, isAnswerLoading: false,
avatar: '', avatar: '',
...@@ -184,12 +193,15 @@ function questionSubmit() { ...@@ -184,12 +193,15 @@ function questionSubmit() {
let messageContent = '' let messageContent = ''
let reasoningContent = '' let reasoningContent = ''
const agentId =
workMode.value === 'ApplicationMode' ? currentAgentApplication.value.agentId : currentSceneConfig.value?.agentId
currentFetchEventSourceController.value = fetchEventStreamSource( currentFetchEventSourceController.value = fetchEventStreamSource(
'/agentApplicationRest/callAgentApplication.json', '/agentApplicationRest/callAgentApplication.json',
{ {
dialogsId: props.currentSessionId, //会话ID dialogsId: props.currentSessionId, //会话ID
agentId: currentAgentApplication.value.agentId, //应用ID agentId: agentId, //应用ID
input: questionContent.value.trim(), //提问文本 input: question.trim(), //提问文本
fileUrls: currentInputFileInfo.value.url ? [currentInputFileInfo.value.url] : [], fileUrls: currentInputFileInfo.value.url ? [currentInputFileInfo.value.url] : [],
channel: ChannelType.index, channel: ChannelType.index,
imageUrl: uploadImageList.value?.[0]?.url || '', // 图片链接 imageUrl: uploadImageList.value?.[0]?.url || '', // 图片链接
...@@ -234,6 +246,13 @@ function questionSubmit() { ...@@ -234,6 +246,13 @@ function questionSubmit() {
}) })
props.isNotShowBackBottomBtn && messageListScrollToBottomThrottle() props.isNotShowBackBottomBtn && messageListScrollToBottomThrottle()
if (editorModeEnable.value) {
editorDrawerConfig.value = {
isShow: true,
content: messageContent,
}
}
}, 100) }, 100)
}, },
onclose: () => { onclose: () => {
...@@ -257,15 +276,15 @@ function questionSubmit() { ...@@ -257,15 +276,15 @@ function questionSubmit() {
uploadImageList.value = [] uploadImageList.value = []
} }
function handleQuestionSubmitEnter(event: KeyboardEvent) { // function handleQuestionSubmitEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) { // if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault() // event.preventDefault()
if (!isQuestionSubmitBtnDisabled.value) { // if (!isQuestionSubmitBtnDisabled.value) {
questionSubmit() // questionSubmit()
} // }
} // }
} // }
function handleFileUploadPopup() { function handleFileUploadPopup() {
inputFileRef.value && inputFileRef.value.click() inputFileRef.value && inputFileRef.value.click()
...@@ -429,6 +448,10 @@ function onAgentApplicationInfoUpdate(_key: string, option: AgentApplicationReco ...@@ -429,6 +448,10 @@ function onAgentApplicationInfoUpdate(_key: string, option: AgentApplicationReco
} }
} }
function onWorkModeChange(mode: WorkModeType) {
workMode.value = mode
}
defineExpose({ defineExpose({
clearSessionReferenceFile: handleFileUploadCancel, clearSessionReferenceFile: handleFileUploadCancel,
}) })
...@@ -437,7 +460,9 @@ defineExpose({ ...@@ -437,7 +460,9 @@ defineExpose({
<template> <template>
<div class="mx-auto mt-auto w-[750px] pr-[10px]"> <div class="mx-auto mt-auto w-[750px] pr-[10px]">
<div class="mb-[10px] flex select-none justify-between"> <div class="mb-[10px] flex select-none justify-between">
<div class="w-[190px]"> <div>
<Transition mode="out-in">
<div v-if="workMode === 'ApplicationMode'" class="w-[190px]">
<n-select <n-select
:value="currentAgentApplication.agentId" :value="currentAgentApplication.agentId"
class="application-menu" class="application-menu"
...@@ -453,8 +478,32 @@ defineExpose({ ...@@ -453,8 +478,32 @@ defineExpose({
/> />
</div> </div>
<div
v-else-if="workMode === 'SceneMode'"
class="flex h-[34px] w-[190px] items-center rounded-[10px] border border-[#9ea3ff] px-[12px]"
>
<!-- <i class="iconfont icon-xiezuo mr-[6px] text-[18px]" :class="{ [`${currentSceneConfig?.icon}`]: true }"></i> -->
<NAvatar
:src="currentSceneConfig?.agentAvatar"
:size="22"
object-fit="contain"
class="mr-[6px] shrink-0"
></NAvatar>
<span class="flex-1 overflow-hidden text-[14px]">
<NEllipsis>{{ currentSceneConfig?.agentTitle }}</NEllipsis>
</span>
<i
class="iconfont icon-close hover:text-theme-color ml-[4px] cursor-pointer text-[14px] transition"
@click="onWorkModeChange('ApplicationMode')"
></i>
</div>
</Transition>
</div>
<div class="flex items-center"> <div class="flex items-center">
<Transition name="file-upload" mode="out-in"> <Transition mode="out-in">
<div v-if="!currentInputFileInfo.fileName"> <div v-if="!currentInputFileInfo.fileName">
<n-popover trigger="hover"> <n-popover trigger="hover">
<template #trigger> <template #trigger>
...@@ -562,7 +611,16 @@ defineExpose({ ...@@ -562,7 +611,16 @@ defineExpose({
</div> </div>
<div class="relative"> <div class="relative">
<n-input <RichTextInputBox
v-model:question-content="questionContent"
v-model:current-scene-config="currentSceneConfig"
v-model:editor-mode-enable="editorModeEnable"
:is-question-submit-btn-disabled="isQuestionSubmitBtnDisabled"
:work-mode="workMode"
@submit-question="questionSubmit"
@work-mode-switch="onWorkModeChange"
/>
<!-- <n-input
v-model:value.trim="questionContent" v-model:value.trim="questionContent"
class="content-input" class="content-input"
type="textarea" type="textarea"
...@@ -578,7 +636,7 @@ defineExpose({ ...@@ -578,7 +636,7 @@ defineExpose({
</n-button> </n-button>
</div> </div>
</template> </template>
</n-input> </n-input> -->
<div v-show="isHasUploadImage" class="absolute bottom-[9px] left-[11px] flex gap-[11px]"> <div v-show="isHasUploadImage" class="absolute bottom-[9px] left-[11px] flex gap-[11px]">
<div <div
...@@ -634,15 +692,15 @@ defineExpose({ ...@@ -634,15 +692,15 @@ defineExpose({
} }
} }
.file-upload-enter-active, .v-enter-active,
.file-upload-leave-active { .v-leave-active {
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
transition-duration: 0.2s; transition-duration: 0.2s;
transition-property: opacity; transition-property: opacity;
} }
.file-upload-enter-from, .v-enter-from,
.file-upload-leave-to { .v-leave-to {
opacity: 0; opacity: 0;
} }
......
<script setup lang="ts"> <script setup lang="ts">
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import type { MessageItemInterface } from '../types' import type { MessageItemInterface } from '../types'
import { provide, ref, useTemplateRef } from 'vue' import { computed, provide, useTemplateRef } from 'vue'
import { ScrollbarInst } from 'naive-ui' import { ScrollbarInst } from 'naive-ui'
import EditorDrawer from '@/components/editor-drawer/editor-drawer.vue' import EditorDrawer from '@/components/editor-drawer/editor-drawer.vue'
import { useElementVisibility } from '@vueuse/core' import { useElementVisibility } from '@vueuse/core'
...@@ -16,6 +16,8 @@ const emit = defineEmits<{ ...@@ -16,6 +16,8 @@ const emit = defineEmits<{
updateSpecifyMessageItem: [messageId: string, newMessageItem: Partial<MessageItemInterface>] updateSpecifyMessageItem: [messageId: string, newMessageItem: Partial<MessageItemInterface>]
}>() }>()
const editorDrawerConfig = defineModel<{ isShow: boolean; content: string }>('editorDrawerConfig', { required: true })
const scrollbarRef = useTemplateRef<ScrollbarInst | null>('scrollbarRef') const scrollbarRef = useTemplateRef<ScrollbarInst | null>('scrollbarRef')
const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef') const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef')
...@@ -27,8 +29,22 @@ provide('updateSpecifyMessageItem', { ...@@ -27,8 +29,22 @@ provide('updateSpecifyMessageItem', {
}, },
}) })
const isShowEditorDrawer = ref(false) const isShowEditorDrawer = computed({
const contentEdit = ref('') get: () => {
return editorDrawerConfig.value.isShow
},
set: (value) => {
editorDrawerConfig.value.isShow = value
},
})
const contentEdit = computed({
get: () => {
return editorDrawerConfig.value.content
},
set: (value) => {
editorDrawerConfig.value.content = value
},
})
function scrollToBottom() { function scrollToBottom() {
if (scrollbarRef.value) { if (scrollbarRef.value) {
......
<script setup lang="ts">
import { onMounted, ref, useTemplateRef, watch, watchEffect } from 'vue'
import { useFocus } from '@vueuse/core'
import { fetchSceneList } from '@/apis/home-agent'
interface SceneConfigItemInterface {
agentId: string
agentTitle: string
agentDesc: string
agentAvatar: string
}
const showSceneList = defineModel<boolean>('showSceneList', { default: false })
const currentSceneConfig = defineModel<SceneConfigItemInterface | null>('currentSceneConfig', { default: null })
const sceneListContainerRef = useTemplateRef<HTMLParagraphElement>('sceneListContainerRef')
const { focused: sceneListContainerFocus } = useFocus(sceneListContainerRef)
const sceneListConfig = ref<SceneConfigItemInterface[]>([])
watch(
() => showSceneList.value,
(isShow) => {
if (isShow) {
sceneListContainerFocus.value = true
}
},
)
watchEffect(() => {
if (!sceneListContainerFocus.value && showSceneList.value) {
showSceneList.value = false
}
})
onMounted(() => {
getSceneListConfig()
})
function getSceneListConfig() {
fetchSceneList<SceneConfigItemInterface[]>().then((res) => {
if (res.code !== 0) return null
sceneListConfig.value = res.data
})
}
function handleSceneSelection(sceneConfig: SceneConfigItemInterface) {
currentSceneConfig.value = {
...sceneConfig,
}
showSceneList.value = false
}
</script>
<template>
<div ref="sceneListContainerRef" class="absolute" tabindex="0">
<Transition name="scene-menu">
<ul
v-show="showSceneList"
class="z-99 absolute bottom-0 left-0 h-fit w-[192px] select-none rounded-[10px] border border-[#9EA3FF] bg-[#e1e1fc] p-[6px]"
>
<template v-if="sceneListConfig.length !== 0">
<li
v-for="(sceneItem, index) in sceneListConfig"
:key="index"
class="flex cursor-pointer rounded-[10px] px-[9px] py-[7px] text-[14px]"
:class="{
'hover:bg-[#D7DAF8]': currentSceneConfig?.agentId !== sceneItem.agentId,
'bg-[#D2D4FB]': currentSceneConfig?.agentId === sceneItem.agentId,
}"
@click="handleSceneSelection(sceneItem)"
>
<n-avatar :src="sceneItem.agentAvatar" :size="22" object-fit="contain" class="mr-[6px] shrink-0"></n-avatar>
<span class="flex-1 overflow-hidden">
<n-ellipsis>{{ sceneItem.agentTitle }}</n-ellipsis>
</span>
</li>
</template>
<li v-else class="rounded-[10px] px-[9px] py-[7px] text-[14px]">空数据</li>
</ul>
</Transition>
</div>
</template>
<style lang="scss" scoped>
.scene-menu-enter-active,
.scene-menu-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.3s;
transition-property: opacity;
}
.scene-menu-enter-from,
.scene-menu-leave-to {
overflow: 1;
opacity: 0;
}
</style>
<script setup lang="ts">
import { readonly, ref, useTemplateRef, watch } from 'vue'
import { templateMenuConfigFactory } from '../data/template-menu'
import { useFocus } from '@vueuse/core'
import { debounce } from 'lodash-es'
import type { TemplateMenuConfig } from '@/views/home/components/rich-text-input-box/data/template-menu'
const showTemplateMenu = defineModel<boolean>('showTemplateMenu', { default: false })
const emit = defineEmits<{
templateConfirm: [templateConfig: TemplateMenuConfig['templateList'][0]]
}>()
const templateMenuContainerRef = useTemplateRef<HTMLParagraphElement>('templateMenuContainerRef')
const { focused: templateMenuContainerFocus } = useFocus(templateMenuContainerRef)
const templateMenuConfig = readonly<TemplateMenuConfig[]>(templateMenuConfigFactory())
const currentTemplateCategory = ref<string>('Work')
const templateMenuClose = debounce(
() => {
showTemplateMenu.value && (showTemplateMenu.value = false)
},
100,
{ leading: true, trailing: false },
)
watch(
() => showTemplateMenu.value,
(isShow) => {
if (isShow) {
currentTemplateCategory.value = 'Work'
templateMenuContainerFocus.value = true
}
},
)
watch(
() => templateMenuContainerFocus.value,
(isFocus) => {
if (!isFocus) {
templateMenuClose()
}
},
)
function handleTemplateSelect(templateItem: Readonly<TemplateMenuConfig['templateList'][0]>) {
emit('templateConfirm', templateItem)
showTemplateMenu.value = false
}
</script>
<template>
<div ref="templateMenuContainerRef" class="absolute" tabindex="0">
<Transition name="template-menu">
<div
v-show="showTemplateMenu"
class="z-99 absolute bottom-0 left-0 h-fit w-[514px] select-none rounded-[10px] border border-[#9EA3FF] bg-white px-[12px]"
>
<n-tabs
:value="currentTemplateCategory"
size="medium"
animated
:on-update:value="(value: string) => (currentTemplateCategory = value)"
>
<n-tab-pane
v-for="templateCategoryItem in templateMenuConfig"
:key="templateCategoryItem.category"
:name="templateCategoryItem.category"
:tab="templateCategoryItem.title"
>
<n-scrollbar x-scrollable>
<ul class="flex select-none pb-[12px]">
<li
v-for="(templateItem, index) in templateCategoryItem.templateList"
:key="index"
class="mr-[15px] w-[120px] cursor-pointer rounded-[10px] border border-[#9EA3FF] bg-[#E0E1FF] p-[7px] last:mr-0"
@click="handleTemplateSelect(templateItem)"
>
<div class="flex items-center">
<span class="flex-center inline-flex rounded-[4px] bg-[#8e83f1] px-[4px]">
<i class="iconfont text-[12px] text-white" :class="{ [`${templateItem.icon}`]: true }"></i>
</span>
<span class="ml-[5px] flex-1 overflow-hidden text-[12px]">
<n-ellipsis>{{ templateItem.title }}</n-ellipsis>
</span>
</div>
<div class="mt-[6px] w-full overflow-hidden text-[10px] text-[#999]">
<n-ellipsis>{{ templateItem.doc }}</n-ellipsis>
</div>
</li>
</ul>
</n-scrollbar>
</n-tab-pane>
</n-tabs>
</div>
</Transition>
</div>
</template>
<style lang="scss" scoped>
.template-menu-enter-active,
.template-menu-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.3s;
transition-property: opacity;
}
.template-menu-enter-from,
.template-menu-leave-to {
overflow: 1;
opacity: 0;
}
</style>
export interface TemplateMenuConfig {
category: string
title: string
templateList: {
title: string
icon: string
doc: string
template: string
}[]
}
export function templateMenuConfigFactory(): TemplateMenuConfig[] {
return [
{
category: 'Work',
title: '工作',
templateList: [
{
title: '周报',
icon: 'icon-yingpingmoban',
doc: '总结一周的工作成果',
template:
'我的职业是【输入职业】,帮我写一份本周的工作周报,内容包含【输入本周工作内容】,下周计划【输入下周工作计划】。',
},
{
title: '日报',
icon: 'icon-yingpingmoban',
doc: '每日工作的总结回顾',
template:
'我的职业是【输入职业】,帮我写一份今天的工作日报,内容包含【输入今日工作内容】,明日计划【输入明日工作计划】。',
},
{
title: '月报',
icon: 'icon-yingpingmoban',
doc: '整月工作的清晰回顾',
template:
'我的职业是【输入职业】,帮我写一份本月的工作月报,内容包含【输入本月工作内容】,下月计划【输入下月工作计划】。',
},
{
title: '邮件',
icon: 'icon-yingpingmoban',
doc: '高效专业撰写邮件',
template: '帮我写一封发给【输入发送对象】的邮件,内容是【输入主题】。',
},
{
title: '会议总结',
icon: 'icon-yingpingmoban',
doc: '会议内容清晰明了',
template:
'帮我写一个讨论【输入会议内容】的会议总结,语气需要正式,内容要包含会议的主要讨论内容、结论,使用Markdown格式。',
},
],
},
{
category: 'BusinessMarketing',
title: '商务营销',
templateList: [
{
title: '活动策划',
icon: 'icon-yingpingmoban',
doc: '高效定制各类策划方案',
template:
'我是一名【活动策划师】,帮我写一个【音乐交流活动】的方案,内容包含但不限于策划主题、策划目的、详细计划、所需资源、策划预算、风险应对、效果评估。',
},
{
title: '市场调研',
icon: 'icon-yingpingmoban',
doc: '精准洞察市场',
template:
'我是一名市场调研专家 ,帮我写一个关于【主题】的市场调研报告,需要包含调研背景与目的、研究方法、市场分析、消费者分析、数据分析与发现、结论与建议等。',
},
{
title: '营销slogan',
icon: 'icon-yingpingmoban',
doc: '吸睛创意广告语',
template: '帮我写5个面向【输入目标人群】宣传【输入品牌商品】的广告营销slogan,简洁明了,富有创意。',
},
],
},
{
category: 'SocialMediaCopywriting',
title: '社媒文案',
templateList: [
{
title: '小红书',
icon: 'icon-yingpingmoban',
doc: '打造小红书吸睛内容',
template:
'生成一篇关于【主题】的小红书文案。内容要有标题和正文。标题需为小红书风格,正文适当使用 emoji丰富文案。',
},
{
title: '微博',
icon: 'icon-yingpingmoban',
doc: '快速创作微博文案',
template: '帮我生成一个关于【主题】的微博文案',
},
{
title: '评价',
icon: 'icon-yingpingmoban',
doc: '创造个性化评语',
template: '我想写一段关于【输入评价主题:如书籍评价】的好评,发布在【输入平台】上,字数不少于【100】字。',
},
],
},
{
category: 'LiteratureAndArt',
title: '文学艺术',
templateList: [
{
title: '诗歌',
icon: 'icon-yingpingmoban',
doc: '创作优美动人的诗篇',
template: '你是一个诗人,帮我创作一首关于【输入主题】的诗歌,使用【输入格式:如四言律诗】的格式。',
},
{
title: '故事',
icon: 'icon-yingpingmoban',
doc: '编写有趣吸睛的故事',
template: '帮我创作一篇关于【输入主题】的故事,要有标题和正文,情节要跌宕起伏,引人入胜,字数在800字左右。',
},
// {
// title: '',
// icon: 'icon-yingpingmoban',
// doc: '',
// template: '',
// },
],
},
]
}
import Quill from 'quill/core'
import Embed from 'quill/blots/embed'
class TemplateInputPromptBlot extends Embed {
static blotName = 'template-input-prompt'
static tagName = 'span'
static className = 'template-input-prompt-container'
static create(config: { placeholder: string; index: string }) {
const root = super.create() as HTMLElement
root.classList.add(`prompt-index-${config.index}`)
// root.classList.add('prompt-blank', 'relative')
const outerEl = document.createElement('span')
outerEl.dataset.placeholder = `[${config.placeholder}]`
// outerEl.classList.add('prompt-blank', 'relative', 'inline-block', 'min-w-fit')
outerEl.classList.add(
'relative',
'inline-block',
// 'bg-[rgb(0,87,255)]',
'px-[6px]',
// 'py-[2px]',
'mx-[2px]',
'rounded-[4px]',
)
outerEl.style.backgroundColor = 'rgba(0,87,255,0.06)'
// outerEl.style.color = '#b5caff'
outerEl.style.color = '#9ea3ff'
outerEl.style.lineHeight = '24px'
const placeholderEl = document.createElement('span')
placeholderEl.textContent = `[${config.placeholder}]`
outerEl.appendChild(placeholderEl)
const inputContainerEl = document.createElement('span')
inputContainerEl.setAttribute('contenteditable', 'true')
inputContainerEl.classList.add(
'prompt-content',
// 'inline-block',
'absolute',
'z-1',
// 'min-w-full',
// 'left-0',
// 'right-0',
'focus:outline-none',
'px-[6px]',
// 'text-[#0057ff]',
'whitespace-pre-wrap',
'break-all',
)
inputContainerEl.style.left = '0'
inputContainerEl.style.right = '0'
inputContainerEl.style.color = '#000dff'
inputContainerEl.textContent = ''
// 事件绑定
inputContainerEl.addEventListener('keydown', (e) => {
if (e.key === 'Backspace') {
e.stopPropagation()
}
})
inputContainerEl.addEventListener('input', (e) => {
const target = e.target as HTMLSpanElement
if (target.textContent?.length !== 0) {
placeholderEl.classList.remove('inline-block')
placeholderEl.classList.add('hidden')
inputContainerEl.classList.remove('absolute', 'px-[6px]')
} else {
placeholderEl.classList.remove('hidden')
placeholderEl.classList.add('inline-block')
inputContainerEl.classList.add('absolute', 'px-[6px]')
}
})
outerEl.appendChild(inputContainerEl)
root.appendChild(outerEl)
return root
}
}
Quill.register(TemplateInputPromptBlot)
<script setup lang="ts">
import { nextTick, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
import SceneList from './components/scene-list.vue'
import TemplateMenu from './components/template-menu.vue'
import Quill from 'quill/core'
import 'quill/dist/quill.core.css'
import './formats/index'
import { getQuillOptions } from './quill-opstions'
import textStructureExtractor from './utils/text-structure-extractor'
import { Delta } from 'quill/core'
import type { TemplateMenuConfig } from '@/views/home/components/rich-text-input-box/data/template-menu'
import { debounce } from 'lodash-es'
import type { WorkModeType } from '@/views/home/components/rich-text-input-box/types/index'
interface Props {
isQuestionSubmitBtnDisabled: boolean
workMode: WorkModeType
}
const props = defineProps<Props>()
const emit = defineEmits<{
submitQuestion: [question: string]
workModeSwitch: [workMode: 'ApplicationMode' | 'SceneMode']
}>()
const questionContent = defineModel<string>('questionContent', { default: '' })
const currentSceneConfig = defineModel<{
agentId: string
agentTitle: string
agentDesc: string
agentAvatar: string
} | null>('currentSceneConfig', {
default: null,
})
const editorModeEnable = defineModel<boolean>('editorModeEnable', { default: false })
const quillInst = shallowRef<Quill | null>(null)
const questionContentDraft = ref('')
const isInputFocus = ref(false)
const showSceneList = ref(false)
const showTemplateMenu = ref(false)
// const editorModeEnable = ref(false)
const onQuillTextChange = debounce((delta: Delta) => {
const question = quillContentExtractor(delta)
questionContentDraft.value = question.trim()
questionContent.value = questionContentDraft.value
}, 100)
watch(
() => currentSceneConfig.value,
(newConfig) => {
if (newConfig) {
emit('workModeSwitch', 'SceneMode')
nextTick(() => {
quillInst.value?.focus()
})
}
},
)
watch(
() => questionContent.value,
(newQuestion) => {
if (newQuestion && newQuestion !== questionContentDraft.value) {
if (quillInst.value) {
quillInst.value.setContents([
{
insert: questionContent.value.trim() || '',
},
])
}
}
},
{ immediate: true },
)
watchEffect(() => {
if (quillInst.value) {
quillInst.value.on('text-change', onQuillTextChange)
}
})
onMounted(() => {
initQuill()
})
function initQuill() {
quillInst.value = new Quill(
'#rich-text-input-box-editor',
getQuillOptions({
keyboardCallback: {
enter: () => {
if (!props.isQuestionSubmitBtnDisabled) {
handleQuestionSubmit()
}
},
at: () => {
showSceneList.value = true
quillInst.value?.blur()
},
},
}),
)
}
function handleShowTemplateMenuSwitch() {
nextTick(() => {
if (!showTemplateMenu.value) {
showTemplateMenu.value = true
}
})
}
function quillContentExtractor(delta: Delta) {
const questionFragmentMap = new Map<number, string>()
let templateInputPromptIndex = 0
delta.forEach((op, opIndex) => {
if (typeof op.insert === 'object' && op.insert['template-input-prompt']) {
const promptEl = document.querySelector(
`#rich-text-input-box-editor .template-input-prompt-container.prompt-index-${templateInputPromptIndex} .prompt-content`,
)
let promptContent = ''
if (promptEl) {
promptContent = promptEl.textContent || ''
}
questionFragmentMap.set(opIndex, promptContent)
templateInputPromptIndex++
} else {
questionFragmentMap.set(opIndex, (op.insert as string) || '')
}
})
let question = ''
questionFragmentMap.forEach((fragment) => (question += fragment))
return question
}
function handleQuestionSubmit() {
if (quillInst.value) {
const delta = quillInst.value.getContents()
const question = quillContentExtractor(delta)
emit('submitQuestion', question)
questionContentDraft.value = ''
questionContent.value = ''
quillInst.value.setContents([])
}
}
function onTemplateConfirm(templateConfig: TemplateMenuConfig['templateList'][0]) {
const promptArr = textStructureExtractor(templateConfig.template || '')
const delta = new Delta()
let templateInputPromptIndex = 0
promptArr.forEach((promptItem) => {
if (promptItem.type === 'text') {
delta.insert(promptItem.content)
} else if (promptItem.type === 'placeholder') {
delta.insert({
'template-input-prompt': {
placeholder: promptItem.content,
index: templateInputPromptIndex,
},
})
templateInputPromptIndex++
}
})
quillInst.value?.setContents(delta)
}
</script>
<template>
<SceneList v-model:show-scene-list="showSceneList" v-model:current-scene-config="currentSceneConfig" />
<TemplateMenu v-model:show-template-menu="showTemplateMenu" @template-confirm="onTemplateConfirm" />
<div
class="hover:border-theme-color rounded-[5px] border border-[#9ea3ff] p-[12px] pr-0 transition duration-300"
:class="{ 'shadow-[0_0_0_2px_rgba(0,13,255,0.2)]': isInputFocus }"
>
<n-scrollbar class="max-h-[140px]">
<div class="pr-[12px]">
<div id="rich-text-input-box-editor" class="font-['SourceHanSansCN-Regular'] text-[14px] text-[#333]"></div>
</div>
</n-scrollbar>
<div class="mt-[8px] flex items-center justify-end px-[12px]">
<Transition>
<div v-show="workMode === 'SceneMode'" class="mr-[15px]">
<n-tooltip trigger="hover">
<template #trigger>
<button
class="mr-[5px] h-[24px] w-[32px] rounded-[5px] border border-[#333]/60 transition"
:class="[showTemplateMenu ? 'border-[#9EA3FF] text-[#9EA3FF]' : '']"
@click="handleShowTemplateMenuSwitch"
>
<i class="iconfont icon-moban1 text-[14px]"></i>
</button>
</template>
模板
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<button
class="h-[24px] w-[32px] rounded-[5px] border border-[#333]/60 transition"
:class="[editorModeEnable ? 'border-[#9EA3FF] text-[#9EA3FF]' : '']"
@click="editorModeEnable = !editorModeEnable"
>
<i class="iconfont icon-wendangbianjiqi1 text-[14px]"></i>
</button>
</template>
消息编辑器
</n-tooltip>
</div>
</Transition>
<div>
<n-button type="primary" :disabled="isQuestionSubmitBtnDisabled" @click="handleQuestionSubmit">
<i class="iconfont icon-send-icon"></i>
</n-button>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
#rich-text-input-box-editor {
:deep(.ql-editor) {
padding: 0;
overflow: hidden;
line-height: 28px;
overflow-wrap: break-word;
&::before {
left: 0;
}
}
}
.v-enter-active,
.v-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
transition-property: opacity;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
import type { QuillOptions } from 'quill'
import type Quill from 'quill'
interface ConfigInterface {
keyboardCallback: {
enter?: (quillInst?: Quill, range?: any, context?: any) => void
at?: (quillInst?: Quill, range?: any, context?: any) => void
}
}
export function getQuillOptions(config: ConfigInterface): QuillOptions {
return {
placeholder: '发送信息、输入@选择工具',
modules: {
keyboard: {
bindings: {
enter: {
key: 'Enter',
handler: function (range: any, context: any) {
if (config.keyboardCallback.enter) {
config.keyboardCallback.enter((this as any).quill as Quill, range, context)
}
},
},
process: {
key: 'Process',
shiftKey: true,
handler: function (range: any, context: any) {
if (config.keyboardCallback.at) {
config.keyboardCallback.at((this as any).quill as Quill, range, context)
}
},
},
'@': {
key: '@',
shiftKey: true,
handler: function (range: any, context: any) {
if (config.keyboardCallback.at) {
config.keyboardCallback.at((this as any).quill as Quill, range, context)
}
},
},
// tab: {
// key: 'Tab',
// handler: function () {
// },
// },
},
},
},
}
}
export type WorkModeType = 'ApplicationMode' | 'SceneMode'
type SegmentType = 'text' | 'placeholder'
interface Segment {
type: SegmentType
content: string
}
export default function textStructureExtractor(str: string): Segment[] {
const segments = str.split(/【(.*?)】/g)
const result: Segment[] = []
segments.forEach((text, index) => {
if (index % 2 === 0) {
// 偶数索引:普通文本
if (text) {
result.push({ type: 'text', content: text })
}
} else {
// 奇数索引:占位符
result.push({ type: 'placeholder', content: text })
}
})
return result
}
...@@ -66,6 +66,11 @@ const currentFetchEventSourceController = ref<AbortController | null>(null) ...@@ -66,6 +66,11 @@ const currentFetchEventSourceController = ref<AbortController | null>(null)
}) })
}, 60) */ }, 60) */
const editorDrawerConfig = ref({
isShow: false,
content: '',
})
const homeContainerWidthWatchDebounce = debounce((newWidth) => { const homeContainerWidthWatchDebounce = debounce((newWidth) => {
if (newWidth <= 1120) { if (newWidth <= 1120) {
isShowHistoryMenu.value = false isShowHistoryMenu.value = false
...@@ -230,6 +235,7 @@ function onSmartFormsStatusFreezeCheck(messageItem: MessageItemInterface) { ...@@ -230,6 +235,7 @@ function onSmartFormsStatusFreezeCheck(messageItem: MessageItemInterface) {
<MessageList <MessageList
v-show="isShowMessageList" v-show="isShowMessageList"
ref="messageListRef" ref="messageListRef"
v-model:editor-drawer-config="editorDrawerConfig"
:message-list="messageList" :message-list="messageList"
@update-specify-message-item="onUpdateSpecifyMessageItem" @update-specify-message-item="onUpdateSpecifyMessageItem"
/> />
...@@ -240,6 +246,7 @@ function onSmartFormsStatusFreezeCheck(messageItem: MessageItemInterface) { ...@@ -240,6 +246,7 @@ function onSmartFormsStatusFreezeCheck(messageItem: MessageItemInterface) {
v-model:is-agent-responding="isAgentResponding" v-model:is-agent-responding="isAgentResponding"
v-model:question-content="questionContent" v-model:question-content="questionContent"
v-model:current-fetch-event-source-controller="currentFetchEventSourceController" v-model:current-fetch-event-source-controller="currentFetchEventSourceController"
v-model:editor-drawer-config="editorDrawerConfig"
:current-session-id="currentSessionId" :current-session-id="currentSessionId"
:message-list-length="messageList.size" :message-list-length="messageList.size"
:is-not-show-back-bottom-btn="messageListRef?.isNotShowBackBottomBtn" :is-not-show-back-bottom-btn="messageListRef?.isNotShowBackBottomBtn"
......
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