mirror of
https://github.com/keven1024/015.git
synced 2026-05-26 07:08:02 +00:00
feat(front): add TextInlineAIDrawer and BubbleMenuView components for enhanced AI interaction and text formatting options
This commit is contained in:
100
front/components/Drawer/TextInlineAIDrawer.vue
Normal file
100
front/components/Drawer/TextInlineAIDrawer.vue
Normal 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>
|
||||
106
front/components/Tiptap/BubbleMenu/BubbleMenuView.vue
Normal file
106
front/components/Tiptap/BubbleMenu/BubbleMenuView.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user