feat(front): add TextInlineAIDrawer and BubbleMenuView components for enhanced AI interaction and text formatting options

This commit is contained in:
keven
2025-10-18 13:36:04 +08:00
parent a66c5a26b0
commit d294027463
3 changed files with 208 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import VeeForm from '../VeeForm.vue'
import FormButton from '@/components/Field/FormButton.vue'
import InputField from '@/components/Field/InputField.vue'
import { LucideCheckCheck, LucideLanguages, LucideSparkle, LucideWandSparkles } from 'lucide-vue-next'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import asyncWait from '~/lib/asyncWait'
const props = defineProps<{
hide: () => void
data: { html: string }
}>()
const formRef = ref<InstanceType<typeof VeeForm>>()
const actions = [
{
icon: LucideWandSparkles,
label: '总结',
onClick: () => {
formRef.value?.form?.setFieldValue('input', '总结这段话')
},
},
{
icon: LucideCheckCheck,
label: '修正拼写和语法错误',
onClick: () => {
formRef.value?.form?.setFieldValue('input', '修正这段话的拼写和语法错误')
},
},
{
icon: LucideLanguages,
label: '翻译成',
children: [
{
label: '英语',
onClick: () => {
formRef.value?.form?.setFieldValue('input', '把这段话翻译成英语')
},
},
{
label: '韩文',
onClick: () => {
formRef.value?.form?.setFieldValue('input', '把这段话翻译成韩文')
},
},
{
label: '中文',
onClick: () => {
formRef.value?.form?.setFieldValue('input', '把这段话翻译成中文')
},
},
],
},
]
</script>
<template>
<div class="flex flex-col gap-5">
<div class="text-xl font-bold">询问AI</div>
<div class="overflow-y-auto max-h-[160px] p-3 rounded-md bg-primary/5 border border-primary/10 text-black/80 text-sm">
<div v-html="data.html" />
</div>
<VeeForm ref="formRef">
<InputField name="input" placeholder="您想如何处理该文本" rules="required" />
<div class="flex flex-row gap-3">
<template v-for="action in actions">
<Button variant="outline" v-if="!action?.children" @click="action.onClick">
<component :is="action.icon" /> {{ action.label }}
</Button>
<DropdownMenu v-else>
<DropdownMenuTrigger>
<Button variant="outline"> <component :is="action.icon" /> {{ action.label }} </Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-for="item in action?.children" @click="item.onClick">{{ item?.label }}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
</div>
<FormButton
@click="
async () => {
await asyncWait(3000)
}
"
>
<LucideSparkle />
生成
</FormButton>
</VeeForm>
</div>
</template>

View File

@@ -0,0 +1,106 @@
<script lang="ts" setup>
import type { Editor } from '@tiptap/vue-3'
import { BubbleMenu } from '@tiptap/vue-3/menus'
import { LucideAArrowUp, LucideBold, LucideCode, LucideItalic, LucideSparkles, LucideStrikethrough } from 'lucide-vue-next'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import TextInlineAIDrawer from '@/components/Drawer/TextInlineAIDrawer.vue'
import { cx } from 'class-variance-authority'
import { getHTMLFromFragment } from '@tiptap/vue-3'
import showDrawer from '~/lib/showDrawer'
const props = defineProps<{
editor: Editor
}>()
const show = ref(false)
const menus = [
{
type: 'button',
label: '询问AI',
icon: LucideSparkles,
onClick: () => {
show.value = false
const { from, to } = props.editor.state.selection || {}
showDrawer({
render: ({ ...rest }) =>
h(TextInlineAIDrawer, {
...rest,
data: { html: getHTMLFromFragment(props.editor.state.doc.slice(from, to).content, props.editor.schema) },
}),
})
},
},
{
type: 'icon',
label: '加粗',
icon: LucideBold,
onClick: () => {
props.editor?.chain().focus().toggleBold().run()
},
},
{
type: 'icon',
label: '斜体',
icon: LucideItalic,
onClick: () => {
props.editor?.chain().focus().toggleItalic().run()
},
},
{
type: 'icon',
label: '删除线',
icon: LucideStrikethrough,
onClick: () => {
props.editor?.chain().focus().toggleStrike().run()
},
},
{
type: 'icon',
label: '代码',
icon: LucideCode,
onClick: () => {
props.editor?.chain().focus().toggleCode().run()
},
},
]
</script>
<template>
<bubble-menu
v-if="editor"
:editor="editor as any"
:options="{
placement: 'bottom',
offset: 8,
onShow: () => {
show = true
},
onHide: () => {
show = false
},
}"
>
<div :class="cx('bg-white rounded-md overflow-hidden', show ? 'block' : 'hidden')">
<div class="border border-black/10 bg-primary/30 text-primary p-1 flex flex-row gap-0.5 shadow-md">
<template v-for="menu in menus" :key="menu.label">
<Button v-if="menu.type === 'button'" variant="ghost" size="sm" @click="menu.onClick">
<component :is="menu.icon" /> {{ menu.label }}
</Button>
<TooltipProvider v-if="menu.type === 'icon'">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" class="!size-8" @click="menu.onClick">
<component :is="menu.icon" />
</Button>
</TooltipTrigger>
<TooltipContent>
{{ menu.label }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
</div>
</div>
</bubble-menu>
</template>

View File

@@ -3,6 +3,7 @@ import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from 'tiptap-markdown'
import Placeholder from '@tiptap/extension-placeholder'
import BubbleMenuView from './BubbleMenu/BubbleMenuView.vue'
import { cx } from 'class-variance-authority'
const props = defineProps<{
@@ -57,4 +58,5 @@ onUnmounted(() => {
"
>
</editor-content>
<BubbleMenuView :editor="editor as any" />
</template>