From a37f9fff1e6593c780c9999de3bd29282867bec0 Mon Sep 17 00:00:00 2001 From: pipipi-pikachu Date: Sat, 4 Jan 2025 10:24:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E7=A1=80AIPPT=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/mocks/AIPPT.json | 261 ++++++++++++++++++++++++++++++ src/hooks/useAIPPT.ts | 341 +++++++++++++++++++++++++++++++++++++++- src/types/AIPPT.ts | 39 +++++ 3 files changed, 637 insertions(+), 4 deletions(-) create mode 100644 public/mocks/AIPPT.json create mode 100644 src/types/AIPPT.ts diff --git a/public/mocks/AIPPT.json b/public/mocks/AIPPT.json new file mode 100644 index 00000000..f23852e8 --- /dev/null +++ b/public/mocks/AIPPT.json @@ -0,0 +1,261 @@ +[ + { + "type": "cover", + "data": { + "title": "犯罪心理学", + "text": "理解犯罪行为的心理机制" + } + }, + { + "type": "contents", + "data": { + "items": [ + "引言", + "犯罪心理学的定义与历史", + "犯罪心理学的主要理论", + "犯罪行为的心理因素", + "犯罪心理评估与诊断", + "犯罪心理干预与治疗", + "犯罪心理学在司法系统中的应用", + "犯罪心理学的未来发展方向", + "结论", + "参考文献" + ] + } + }, + { + "type": "transition", + "data": { + "title": "引言", + "text": "犯罪心理学的意义与研究犯罪心理学的重要性。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪心理学的定义与历史", + "items": [ + { + "title": "犯罪心理学的定义", + "text": "犯罪心理学是研究犯罪行为及其心理机制的学科,旨在理解犯罪者的心理过程和行为动机。" + }, + { + "title": "犯罪心理学的发展历史", + "text": "犯罪心理学起源于19世纪末,随着心理学和法学的发展,逐渐形成独立的学科体系。" + }, + { + "title": "重要人物与贡献", + "text": "如弗洛伊德、斯金纳等心理学家对犯罪心理学的理论发展做出了重要贡献。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "犯罪心理学的主要理论", + "text": "介绍犯罪心理学的主要理论框架和研究方法。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪心理学的主要理论", + "items": [ + { + "title": "生物学理论", + "text": "生物学理论强调遗传和生理因素对犯罪行为的影响,如基因、脑结构和神经递质等。" + }, + { + "title": "心理学理论", + "text": "心理学理论关注个体的心理过程,如人格特质、情绪和认知对犯罪行为的影响。" + }, + { + "title": "社会学理论", + "text": "社会学理论探讨社会环境、文化和社会结构对犯罪行为的塑造作用。" + }, + { + "title": "综合理论", + "text": "综合理论结合生物学、心理学和社会学因素,全面解释犯罪行为的成因。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "犯罪行为的心理因素", + "text": "探讨影响犯罪行为的心理因素及其作用机制。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪行为的心理因素", + "items": [ + { + "title": "人格特质与犯罪", + "text": "某些人格特质,如攻击性、冲动性和反社会性,与犯罪行为密切相关。" + }, + { + "title": "情绪与犯罪", + "text": "情绪状态,如愤怒、恐惧和抑郁,可能引发或加剧犯罪行为。" + }, + { + "title": "认知过程与犯罪", + "text": "认知偏差和决策错误可能导致个体选择犯罪行为作为解决问题的方式。" + }, + { + "title": "社会心理因素与犯罪", + "text": "社会心理因素,如群体压力、社会排斥和模仿行为,对犯罪行为有重要影响。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "犯罪心理评估与诊断", + "text": "介绍犯罪心理评估的方法和诊断流程。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪心理评估与诊断", + "items": [ + { + "title": "犯罪心理评估的方法", + "text": "犯罪心理评估包括临床访谈、心理测试和行为观察等多种方法。" + }, + { + "title": "常用的心理测试工具", + "text": "如MMPI、PCL-R等心理测试工具广泛应用于犯罪心理评估。" + }, + { + "title": "犯罪心理诊断的流程", + "text": "犯罪心理诊断通常包括初步评估、详细测试和综合诊断三个步骤。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "犯罪心理干预与治疗", + "text": "探讨犯罪心理干预的策略和治疗方法。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪心理干预与治疗", + "items": [ + { + "title": "犯罪心理干预的策略", + "text": "犯罪心理干预包括认知行为疗法、心理教育和家庭治疗等多种策略。" + }, + { + "title": "犯罪心理治疗的方法", + "text": "犯罪心理治疗方法包括个体治疗、团体治疗和社区康复等。" + }, + { + "title": "案例分析", + "text": "通过具体案例分析,展示犯罪心理干预与治疗的实际效果。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "犯罪心理学在司法系统中的应用", + "text": "介绍犯罪心理学在司法系统中的实际应用。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪心理学在司法系统中的应用", + "items": [ + { + "title": "犯罪心理学在侦查中的应用", + "text": "犯罪心理学在犯罪侦查中用于分析犯罪动机、行为模式和嫌疑人特征。" + }, + { + "title": "犯罪心理学在审判中的应用", + "text": "犯罪心理学在审判中用于评估被告人的心理状态和刑事责任能力。" + }, + { + "title": "犯罪心理学在矫正中的应用", + "text": "犯罪心理学在矫正中用于制定个性化的心理干预和康复计划。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "犯罪心理学的未来发展方向", + "text": "探讨犯罪心理学的未来研究方向和挑战。" + } + }, + { + "type": "content", + "data": { + "title": "犯罪心理学的未来发展方向", + "items": [ + { + "title": "新技术在犯罪心理学中的应用", + "text": "如人工智能、大数据和虚拟现实等新技术在犯罪心理学研究中的应用前景。" + }, + { + "title": "跨学科研究的前景", + "text": "犯罪心理学与神经科学、社会学和法学等学科的交叉研究将推动学科发展。" + }, + { + "title": "犯罪心理学的伦理问题", + "text": "犯罪心理学研究中的伦理问题,如隐私保护和研究对象的权益保障。" + } + ] + } + }, + { + "type": "transition", + "data": { + "title": "结论", + "text": "总结犯罪心理学的重要发现和未来研究建议。" + } + }, + { + "type": "content", + "data": { + "title": "结论", + "items": [ + { + "title": "犯罪心理学的重要发现", + "text": "犯罪心理学揭示了犯罪行为的复杂心理机制,为预防和干预提供了科学依据。" + }, + { + "title": "对未来研究的建议", + "text": "未来研究应加强跨学科合作,探索新技术在犯罪心理学中的应用,并关注伦理问题。" + } + ] + } + }, + { + "type": "content", + "data": { + "title": "参考文献", + "items": [ + { + "title": "主要参考文献列表", + "text": "列出犯罪心理学领域的主要参考文献,包括书籍、期刊文章和研究报告。" + } + ] + } + }, + { + "type": "end" + } +] \ No newline at end of file diff --git a/src/hooks/useAIPPT.ts b/src/hooks/useAIPPT.ts index d33efd96..a66e33a0 100644 --- a/src/hooks/useAIPPT.ts +++ b/src/hooks/useAIPPT.ts @@ -1,10 +1,343 @@ import api from '@/services' +import { nanoid } from 'nanoid' +import type { PPTTextElement, Slide, TextType } from '@/types/slides' +import type { AIPPTSlide } from '@/types/AIPPT' +import { useSlidesStore } from '@/store' +import useAddSlidesOrElements from './useAddSlidesOrElements' +import useSlideHandler from './useSlideHandler' + +const findClosestGreaterThanN = (templates: Slide[], n: number, type: TextType) => { + if (n === 1) { + const list = templates.filter(slide => { + const items = slide.elements.filter(el => el.type === 'text' && el.textType === type) + const titles = slide.elements.filter(el => el.type === 'text' && el.textType === 'title') + const texts = slide.elements.filter(el => el.type === 'text' && el.textType === 'content') + + return !items.length && titles.length === 1 && texts.length === 1 + }) + + if (list.length) return list + } + + let target: Slide | null = null + + const list = templates.filter(slide => { + const len = slide.elements.filter(el => el.type === 'text' && el.textType === type).length + return len >= n + }) + if (list.length === 0) { + const sorted = templates.sort((a, b) => { + const aLen = a.elements.filter(el => el.type === 'text' && el.textType === type).length + const bLen = b.elements.filter(el => el.type === 'text' && el.textType === type).length + return aLen - bLen + }) + target = sorted[sorted.length - 1] + } + else { + target = list.reduce((closest, current) => { + const currentLen = current.elements.filter(el => el.type === 'text' && el.textType === type).length + const closestLen = closest.elements.filter(el => el.type === 'text' && el.textType === type).length + return (currentLen - n) <= (closestLen - n) ? current : closest + }) + } + + return templates.filter(slide => { + const len = slide.elements.filter(el => el.type === 'text' && el.textType === type).length + const targetLen = target!.elements.filter(el => el.type === 'text' && el.textType === type).length + return len === targetLen + }) +} + +const getFontsizeInBox = ({ + text, + fontSize, + fontFamily, + width, + maxLine, +}: { + text: string + fontSize: number + fontFamily: string + width: number + maxLine: number +}) => { + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d')! + + let newFontSize = fontSize + const minFontSize = 10 + + while (newFontSize >= minFontSize) { + context.font = `${newFontSize}px ${fontFamily}` + const textWidth = context.measureText(text).width + const line = Math.ceil(textWidth / width) + + if (line <= maxLine) return newFontSize + + const step = newFontSize <= 22 ? 1 : 2 + newFontSize = newFontSize - step + } + + return minFontSize +} + +const getFontInfo = (htmlString: string) => { + const fontSizeRegex = /font-size:\s*(\d+)\s*px/i + const fontFamilyRegex = /font-family:\s*['"]?([^'";]+)['"]?\s*(?=;|>|$)/i + + const defaultInfo = { + fontSize: 16, + fontFamily: 'Microsoft Yahei', + } + + const fontSizeMatch = htmlString.match(fontSizeRegex) + const fontFamilyMatch = htmlString.match(fontFamilyRegex) + + return { + fontSize: fontSizeMatch ? (+fontSizeMatch[1].trim()) : defaultInfo.fontSize, + fontFamily: fontFamilyMatch ? fontFamilyMatch[1].trim() : defaultInfo.fontFamily, + } +} + +const getNewTextElData = ({ + el, + text, + maxLine, + longestText, +}: { + el: PPTTextElement + text: string + maxLine: number + longestText?: string +}) => { + const padding = 10 + const width = el.width - padding * 2 - 10 + + const fontInfo = getFontInfo(el.content) + const size = getFontsizeInBox({ + text: longestText || text, + fontSize: fontInfo.fontSize, + fontFamily: fontInfo.fontFamily, + width, + maxLine, + }) + + let content = el.content + + const parser = new DOMParser() + const doc = parser.parseFromString(content, 'text/html') + + const treeWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT) + + const firstTextNode = treeWalker.nextNode() + if (firstTextNode) firstTextNode.textContent = text + + if (doc.body.innerHTML.indexOf('font-size') === -1) { + const p = doc.querySelector('p') + if (p) p.style.fontSize = '16px' + } + + content = doc.body.innerHTML.replace(/font-size:(.+?)px/g, `font-size: ${size}px`) + + return { ...el, content } +} export default () => { - const AIPPT = () => { - api.getMockData('template').then(ret => { - console.log(ret) - }) + const slidesStore = useSlidesStore() + const { addSlidesFromData } = useAddSlidesOrElements() + const { isEmptySlide } = useSlideHandler() + + const AIPPT = async () => { + const templateSlides: Slide[] = await api.getMockData('template').then(ret => ret.slides) + const _AISlides: AIPPTSlide[] = await api.getMockData('AIPPT') + + const AISlides = [] + for (const template of _AISlides) { + if (template.type === 'content') { + const items = template.data.items + if (items.length === 5 || items.length === 6) { + const items1 = items.slice(0, 3) + const items2 = items.slice(3) + AISlides.push({ ...template, data: { ...template.data, items: items1 } }) + AISlides.push({ ...template, data: { ...template.data, items: items2 } }) + } + else if (items.length === 7 || items.length === 8) { + const items1 = items.slice(0, 4) + const items2 = items.slice(4) + AISlides.push({ ...template, data: { ...template.data, items: items1 } }) + AISlides.push({ ...template, data: { ...template.data, items: items2 } }) + } + else if (items.length === 9 || items.length === 10) { + const items1 = items.slice(0, 3) + const items2 = items.slice(3, 6) + const items3 = items.slice(6) + AISlides.push({ ...template, data: { ...template.data, items: items1 } }) + AISlides.push({ ...template, data: { ...template.data, items: items2 } }) + AISlides.push({ ...template, data: { ...template.data, items: items3 } }) + } + else if (items.length > 10) { + const items1 = items.slice(0, 4) + const items2 = items.slice(4, 8) + const items3 = items.slice(8) + AISlides.push({ ...template, data: { ...template.data, items: items1 } }) + AISlides.push({ ...template, data: { ...template.data, items: items2 } }) + AISlides.push({ ...template, data: { ...template.data, items: items3 } }) + } + else { + AISlides.push(template) + } + } + else AISlides.push(template) + } + + const coverTemplates = templateSlides.filter(slide => slide.type === 'cover') + const contentsTemplates = templateSlides.filter(slide => slide.type === 'contents') + const transitionTemplates = templateSlides.filter(slide => slide.type === 'transition') + const contentTemplates = templateSlides.filter(slide => slide.type === 'content') + const endTemplates = templateSlides.filter(slide => slide.type === 'end') + + const coverTemplate = coverTemplates[Math.floor(Math.random() * coverTemplates.length)] + const transitionTemplate = transitionTemplates[Math.floor(Math.random() * transitionTemplates.length)] + const endTemplate = endTemplates[Math.floor(Math.random() * endTemplates.length)] + + const slides = [] + + let transitionIndex = 0 + + for (const item of AISlides) { + if (item.type === 'cover') { + const elements = coverTemplate.elements.map(el => { + if (el.type === 'text' && el.textType === 'title' && item.data.title) { + return getNewTextElData({ el, text: item.data.title, maxLine: 1 }) + } + if (el.type === 'text' && el.textType === 'content' && item.data.text) { + return getNewTextElData({ el, text: item.data.text, maxLine: 3 }) + } + return el + }) + slides.push({ + ...coverTemplate, + id: nanoid(10), + elements, + }) + } + else if (item.type === 'contents') { + const _contentsTemplates = findClosestGreaterThanN(contentsTemplates, item.data.items.length, 'item') + const contentsTemplate = _contentsTemplates[Math.floor(Math.random() * _contentsTemplates.length)] + + const sortedItemIds = contentsTemplate.elements.filter(el => el.type === 'text' && el.textType === 'item').sort((a, b) => { + const aIndex = a.left + a.top * 2 + const bIndex = b.left + b.top * 2 + return aIndex - bIndex + }).map(el => el.id) + + const longestText = item.data.items.reduce((longest, current) => current.length > longest.length ? current : longest, '') + + const elements = contentsTemplate.elements.map(el => { + if (el.type === 'text' && el.textType === 'item') { + const index = sortedItemIds.findIndex(id => id === el.id) + const itemTitle = item.data.items[index] + if (itemTitle) return getNewTextElData({ el, text: itemTitle, maxLine: 1, longestText }) + } + return el + }) + slides.push({ + ...contentsTemplate, + id: nanoid(10), + elements, + }) + } + else if (item.type === 'transition') { + transitionIndex++ + const elements = transitionTemplate.elements.map(el => { + if (el.type === 'text' && el.textType === 'title' && item.data.title) { + return getNewTextElData({ el, text: item.data.title, maxLine: 1 }) + } + if (el.type === 'text' && el.textType === 'content' && item.data.text) { + return getNewTextElData({ el, text: item.data.text, maxLine: 3 }) + } + if (el.type === 'text' && el.textType === 'partNumber') { + return getNewTextElData({ el, text: transitionIndex + '', maxLine: 1 }) + } + return el + }) + slides.push({ + ...transitionTemplate, + id: nanoid(10), + elements, + }) + } + else if (item.type === 'content') { + const _contentTemplates = findClosestGreaterThanN(contentTemplates, item.data.items.length, 'item') + const contentTemplate = _contentTemplates[Math.floor(Math.random() * _contentTemplates.length)] + + const sortedTitleItemIds = contentTemplate.elements.filter(el => el.type === 'text' && el.textType === 'itemTitle').sort((a, b) => { + const aIndex = a.left + a.top * 2 + const bIndex = b.left + b.top * 2 + return aIndex - bIndex + }).map(el => el.id) + + const sortedTextItemIds = contentTemplate.elements.filter(el => el.type === 'text' && el.textType === 'item').sort((a, b) => { + const aIndex = a.left + a.top * 2 + const bIndex = b.left + b.top * 2 + return aIndex - bIndex + }).map(el => el.id) + + const itemTitles = [] + const itemTexts = [] + + for (const _item of item.data.items) { + if (_item.title) itemTitles.push(_item.title) + if (_item.text) itemTexts.push(_item.text) + } + const longestTitle = itemTitles.reduce((longest, current) => current.length > longest.length ? current : longest, '') + const longestText = itemTexts.reduce((longest, current) => current.length > longest.length ? current : longest, '') + + const elements = contentTemplate.elements.map(el => { + if (item.data.items.length === 1) { + const contentItem = item.data.items[0] + if (el.type === 'text' && el.textType === 'content' && contentItem.text) { + return getNewTextElData({ el, text: contentItem.text, maxLine: 6 }) + } + } + else { + if (el.type === 'text' && el.textType === 'itemTitle') { + const index = sortedTitleItemIds.findIndex(id => id === el.id) + const contentItem = item.data.items[index] + if (contentItem && contentItem.title) { + return getNewTextElData({ el, text: contentItem.title, longestText: longestTitle, maxLine: 1 }) + } + return { ...el, isInvalid: true } + } + if (el.type === 'text' && el.textType === 'item') { + const index = sortedTextItemIds.findIndex(id => id === el.id) + const contentItem = item.data.items[index] + if (contentItem && contentItem.text) { + return getNewTextElData({ el, text: contentItem.text, longestText, maxLine: 4 }) + } + return { ...el, isInvalid: true } + } + } + if (el.type === 'text' && el.textType === 'title' && item.data.title) { + return getNewTextElData({ el, text: item.data.title, maxLine: 1 }) + } + return el + }) + slides.push({ + ...contentTemplate, + id: nanoid(10), + elements, + }) + } + else if (item.type === 'end') { + slides.push({ + ...endTemplate, + id: nanoid(10), + }) + } + } + if (isEmptySlide.value) slidesStore.setSlides(slides) + else addSlidesFromData(slides) } return { diff --git a/src/types/AIPPT.ts b/src/types/AIPPT.ts new file mode 100644 index 00000000..092c66ab --- /dev/null +++ b/src/types/AIPPT.ts @@ -0,0 +1,39 @@ +export interface AIPPTCover { + type: 'cover' + data: { + title: string + text: string + } +} + +export interface AIPPTContents { + type: 'contents' + data: { + items: string[] + } +} + +export interface AIPPTTransition { + type: 'transition' + data: { + title: string + text: string + } +} + +export interface AIPPTContent { + type: 'content' + data: { + title: string + items: { + title: string + text: string + }[] + } +} + +export interface AIPPTEnd { + type: 'end' +} + +export type AIPPTSlide = AIPPTCover | AIPPTContents | AIPPTTransition | AIPPTContent | AIPPTEnd \ No newline at end of file