diff --git a/app/service/adapter/textToShot.ts b/app/service/adapter/textToShot.ts index 3c6d5ca..de4da67 100644 --- a/app/service/adapter/textToShot.ts +++ b/app/service/adapter/textToShot.ts @@ -1,4 +1,4 @@ -import { ContentItem, LensType, SimpleCharacter } from '../domain/valueObject'; +import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject'; // 定义角色属性接口 interface CharacterAttributes { @@ -8,6 +8,12 @@ interface CharacterAttributes { avatar: string; } +// 定义高亮属性接口 +interface HighlightAttributes { + text: string; + color: string; +} + // 定义文本节点接口 interface TextNode { type: 'text'; @@ -20,8 +26,14 @@ interface CharacterTokenNode { attrs: CharacterAttributes; } +// 定义高亮节点接口 +interface HighlightNode { + type: 'highlightText'; + attrs: HighlightAttributes; +} + // 定义内容节点类型(文本或角色标记) -type ContentNode = TextNode | CharacterTokenNode; +type ContentNode = TextNode | CharacterTokenNode | HighlightNode; // 定义段落接口 interface Paragraph { @@ -100,6 +112,68 @@ export class TextToShotAdapter { return nodes; } + /** + * 解析高亮文本,识别tag并转换为节点数组 + * @param text 要解析的文本 + * @param tags 标签列表 + * @returns ContentNode[] 节点数组 + */ + public static parseHighlight(text: string, tags: TagValueObject[]): ContentNode[] { + const nodes: ContentNode[] = []; + let currentText = text; + // 按内容长度降序排序,避免短名称匹配到长名称的一部分 + const sortedTags = [...tags].sort((a, b) => String(b.content).length - String(a.content).length); + + while (currentText.length > 0) { + let matchFound = false; + + // 尝试匹配 + for (const tag of sortedTags) { + if (currentText.startsWith(String(tag.content))) { + // 如果当前文本以tag内容开头 + if (currentText.length > String(tag.content).length) { + // 添加标记节点 + nodes.push({ + type: 'highlightText', + attrs: { + text: String(tag.content), + color: tag?.color || 'yellow' + } + }); + // 移除已处理的tag内容 + currentText = currentText.slice(String(tag.content).length); + matchFound = true; + break; + } + } + } + + if (!matchFound) { + // 如果没有找到tag匹配,处理普通文本 + // 查找下一个可能的tag内容位置 + let nextTagIndex = currentText.length; + for (const tag of sortedTags) { + const index = currentText.indexOf(String(tag.content)); + if (index !== -1 && index < nextTagIndex) { + nextTagIndex = index; + } + } + + // 添加文本节点 + const textContent = currentText.slice(0, nextTagIndex); + if (textContent) { + nodes.push({ + type: 'text', + text: textContent + }); + } + // 移除已处理的文本 + currentText = currentText.slice(nextTagIndex); + } + } + + return nodes; + } private readonly ShotData: Shot; constructor(shotData: Shot) { this.ShotData = shotData; @@ -225,4 +299,29 @@ export class TextToShotAdapter { content ); } + + public static fromTextToRole(description: string, tags: TagValueObject[]): Paragraph[] { + const paragraph: Paragraph = { + type: 'paragraph', + content: [] + }; + const highlightNodes = TextToShotAdapter.parseHighlight(description, tags); + paragraph.content.push(...highlightNodes); + return [paragraph]; + } + public static fromRoleToText(paragraphs: Paragraph[]): string { + let text = ''; + paragraphs.forEach(paragraph => { + paragraph.content.forEach(node => { + if (node.type === 'highlightText') { + text += node.attrs.text; + } else if (node.type === 'text') { + text += node.text; + } else if (node.type === 'characterToken') { + text += node.attrs.name; + } + }); + }); + return text; + } } \ No newline at end of file diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts index 0d6e30a..25b5be2 100644 --- a/app/service/domain/valueObject.ts +++ b/app/service/domain/valueObject.ts @@ -93,6 +93,8 @@ export interface TagValueObject { loadingProgress: number; /** 禁止编辑 */ disableEdit: boolean; + /** 颜色 */ + color?: string; } diff --git a/components/pages/work-flow/use-edit-data.tsx b/components/pages/work-flow/use-edit-data.tsx index 64a7ce6..54495de 100644 --- a/components/pages/work-flow/use-edit-data.tsx +++ b/components/pages/work-flow/use-edit-data.tsx @@ -4,6 +4,20 @@ import { useShotService } from "@/app/service/Interaction/ShotService"; import { useSearchParams } from 'next/navigation'; import { useRoleServiceHook } from "@/app/service/Interaction/RoleService"; +const mockRoleData = [{ + id: '1', + name: 'KAPI', + imageUrl: 'https://c.huiying.video/images/420bfb4f-b5d4-475c-a2fb-5e40af770b29.jpg', + generateText: 'A 3 to 5-year-old boy with a light to medium olive skin tone, full cheeks, and warm brown eyes. He has short, straight, dark brown hair, neatly styled with a part on his left side. His facial structure includes a small, slightly upturned nose. His lips are typically held in a slight, gentle, closed-mouth smile, which can part to show his small, white teeth.', + tags: [ + { id: '1', content: 'boy', color: 'red' }, + { id: '2', content: '3 to 5-year-old', color: 'yellow' }, + { id: '3', content: 'light to medium olive skin tone', color: 'green' }, + { id: '4', content: 'full cheeks', color: 'blue' }, + { id: '5', content: 'warm brown eyes', color: 'purple' }, + ] +}] + export const useEditData = (tabType: string) => { const searchParams = useSearchParams(); const projectId = searchParams.get('episodeId') || ''; @@ -26,7 +40,10 @@ export const useEditData = (tabType: string) => { userRoleLibrary, fetchRoleList, selectRole, - fetchUserRoleLibrary + fetchUserRoleLibrary, + optimizeRoleText, + updateRoleText, + regenerateRole } = useRoleServiceHook(); useEffect(() => { @@ -39,6 +56,7 @@ export const useEditData = (tabType: string) => { setLoading(false); }); } else if (tabType === 'role') { + fetchUserRoleLibrary(); fetchRoleList(projectId).then(() => { setLoading(false); }).catch((err) => { @@ -55,7 +73,8 @@ export const useEditData = (tabType: string) => { }, [videoSegments]); useEffect(() => { - setRoleData(roleList); + // setRoleData(roleList); + setRoleData(mockRoleData); }, [roleList]); return { @@ -70,6 +89,8 @@ export const useEditData = (tabType: string) => { selectRole, selectedRole, userRoleLibrary, - fetchUserRoleLibrary + optimizeRoleText, + updateRoleText, + regenerateRole } } \ No newline at end of file diff --git a/components/ui/character-editor.tsx b/components/ui/character-editor.tsx index b0af4c8..1834e9d 100644 --- a/components/ui/character-editor.tsx +++ b/components/ui/character-editor.tsx @@ -1,11 +1,16 @@ -import { useState, useRef } from "react"; +import React, { useState, useRef, useEffect, forwardRef } from "react"; import { motion } from "framer-motion"; import { Sparkles, X, Plus, RefreshCw } from 'lucide-react'; import MainEditor from "./main-editor/MainEditor"; import { cn } from "@/public/lib/utils"; +import { TextToShotAdapter } from "@/app/service/adapter/textToShot"; +import { TagValueObject } from "@/app/service/domain/valueObject"; interface CharacterEditorProps { className?: string; + description: string; + highlight: TagValueObject[]; + onSmartPolish: (text: string) => void; } const mockContent = [ @@ -30,20 +35,50 @@ const mockContent = [ }, ]; - -export default function CharacterEditor({ +export const CharacterEditor = forwardRef(({ className, -}: CharacterEditorProps) { + description, + highlight, + onSmartPolish +}, ref) => { const [isOptimizing, setIsOptimizing] = useState(false); + const [content, setContent] = useState([]); + const [isInit, setIsInit] = useState(true); const handleSmartPolish = async () => { - + setIsOptimizing(true); + console.log('-==========handleSmartPolish===========-', content); + const text = TextToShotAdapter.fromRoleToText(content); + console.log('-==========getText===========-', text); + onSmartPolish(text); }; + useEffect(() => { + setIsInit(true); + console.log('-==========description===========-', description); + console.log('-==========highlight===========-', highlight); + const paragraphs = TextToShotAdapter.fromTextToRole(description, highlight); + console.log('-==========paragraphs===========-', paragraphs); + setContent(paragraphs); + setTimeout(() => { + setIsInit(false); + setIsOptimizing(false); + }, 100); + }, [description, highlight]); + + // 暴露方法给父组件 + React.useImperativeHandle(ref, () => ({ + getRoleText: () => { + return TextToShotAdapter.fromRoleToText(content); + } + })); + return (
{/* 自由输入区域 */} - + { + !isInit && + } {/* 智能润色按钮 */}
); -} +}); diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index 79b8710..bfc05c0 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ImageUp, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X, TriangleAlert } from 'lucide-react'; import { cn } from '@/public/lib/utils'; -import CharacterEditor from './character-editor'; +import { CharacterEditor } from './character-editor'; import ImageBlurTransition from './ImageBlurTransition'; import FloatingGlassPanel from './FloatingGlassPanel'; import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel'; @@ -72,6 +72,7 @@ export function CharacterTabContent({ const fileInputRef = useRef(null); const [enableAnimation, setEnableAnimation] = useState(true); const [showAddToLibrary, setShowAddToLibrary] = useState(true); + const characterEditorRef = useRef(null); const { loading, @@ -79,15 +80,25 @@ export function CharacterTabContent({ selectRole, selectedRole, userRoleLibrary, - fetchUserRoleLibrary + optimizeRoleText, + updateRoleText, + regenerateRole } = useEditData('role'); useEffect(() => { + console.log('-==========roleData===========-', roleData); if (roleData.length > 0) { selectRole(roleData[selectRoleIndex].id); } }, [selectRoleIndex, roleData]); + const handleSmartPolish = (text: string) => { + // 首先更新 + updateRoleText(text); + // 然后调用优化角色文本 + optimizeRoleText(text); + }; + const handleConfirmGotoReplace = () => { setIsRemindReplacePanelOpen(false); setIsReplacePanelOpen(true); @@ -148,12 +159,16 @@ export function CharacterTabContent({ const handleOpenReplaceLibrary = () => { setIsReplaceLibraryOpen(true); setShowAddToLibrary(true); - fetchUserRoleLibrary(); }; const handleRegenerate = () => { console.log('Regenerate'); - setShowAddToLibrary(true); + const text = characterEditorRef.current.getRoleText(); + console.log('-==========text===========-', text); + // 重生前 更新 当前项 generateText + updateRoleText(text); + // 然后调用重新生成角色 + regenerateRole(); }; const handleUploadClick = () => { @@ -260,8 +275,8 @@ export function CharacterTabContent({ {/* 角色预览图 */}
{/* 重新生成按钮、替换形象按钮 */}
diff --git a/components/ui/main-editor/MainEditor.tsx b/components/ui/main-editor/MainEditor.tsx index 35a96a2..605d1ac 100644 --- a/components/ui/main-editor/MainEditor.tsx +++ b/components/ui/main-editor/MainEditor.tsx @@ -1,19 +1,27 @@ import React, { useState, useCallback, useEffect } from 'react'; +import { flushSync } from 'react-dom'; import { EditorContent, useEditor } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { HighlightTextExtension } from './HighlightText'; interface MainEditorProps { content: any[]; + onChangeContent?: (content: any[]) => void; } -export default function MainEditor({ content }: MainEditorProps) { +export default function MainEditor({ content, onChangeContent }: MainEditorProps) { + const [renderContent, setRenderContent] = useState(content); + + useEffect(() => { + onChangeContent?.(renderContent);; + }, [renderContent]); + const editor = useEditor({ extensions: [ StarterKit, HighlightTextExtension, ], - content: { type: 'doc', content: content }, + content: { type: 'doc', content: renderContent }, editorProps: { attributes: { class: 'prose prose-invert max-w-none focus:outline-none' @@ -23,6 +31,12 @@ export default function MainEditor({ content }: MainEditorProps) { onCreate: ({ editor }) => { editor.setOptions({ editable: true }) }, + onUpdate: ({ editor }) => { + const json = editor.getJSON(); + flushSync(() => { + setRenderContent(json.content); + }); + }, }); if (!editor) { diff --git a/components/ui/shot-tab-content.tsx b/components/ui/shot-tab-content.tsx index 11cbf63..d2679b4 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -45,7 +45,7 @@ export function ShotTabContent({ if (shotData.length > 0) { setSelectedSegment(shotData[selectedIndex]); } - }, [selectedIndex]); + }, [selectedIndex, shotData]); // 处理扫描开始 const handleScan = () => {