feat: 流式AIPPT页面生成

This commit is contained in:
pipipi-pikachu 2025-02-23 15:36:09 +08:00
parent 7603982de4
commit bc6bec2835
5 changed files with 138 additions and 29 deletions

View File

@ -0,0 +1,69 @@
# 5G技术如何改变我们的生活
## 5G技术概述
### 5G的定义
- 第五代移动通信技术
- 基于4G技术的重大升级
### 5G的关键特性
- 高速率
- 低时延
- 大容量
### 5G的发展历程
- 早期研究阶段
- 标准制定阶段
- 商用推广阶段
## 5G对通信领域的变革
### 个人通信体验提升
- 高清视频通话无卡顿
- 快速下载大文件
- 多人在线游戏低延迟
### 通信网络架构优化
- 网络切片技术实现差异化服务
- 边缘计算减少数据传输距离
### 通信安全保障增强
- 新的加密算法保障数据安全
- 实时监测防范网络攻击
## 5G与智能家居的融合
### 智能家电控制
- 远程控制家电开关和运行模式
- 家电之间智能联动
### 家庭安防升级
- 高清实时监控家庭情况
- 异常情况及时报警
### 家居环境智能调节
- 自动调节室内温度、湿度
- 智能灯光控制营造氛围
## 5G推动智能交通发展
### 自动驾驶汽车
- 车辆间实时通信避免碰撞
- 高精度地图实时更新
### 智能交通管理
- 实时监控交通流量并优化信号灯
- 快速处理交通事故
### 公共交通智能化
- 实时公交信息查询
- 车内高速网络服务
## 5G在医疗领域的应用
### 远程医疗服务
- 专家远程诊断病情
- 远程手术指导
### 医疗设备互联
- 可穿戴设备实时传输健康数据
- 医院内部设备信息共享
### 智能医疗管理
- 电子病历快速调取
- 医疗资源智能分配
## 5G助力工业互联网升级
### 智能制造
- 生产设备实时监控和远程维护
- 柔性生产线智能调度
### 工业物流优化
- 货物实时定位和跟踪
- 智能仓储管理
### 工业安全保障
- 危险区域实时监测
- 事故预警和应急处理

View File

@ -256,6 +256,10 @@ const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
&.contextmenu-active {
color: $themeColor;
.text {
background-color: rgba($color: $themeColor, $alpha: .08);
}
}
&.title {
@ -279,7 +283,14 @@ const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
height: 100%;
padding: 0 11px;
line-height: 32px;
border-radius: $borderRadius;
transition: background-color .2s;
cursor: pointer;
@include ellipsis-oneline();
&:hover {
background-color: rgba($color: $themeColor, $alpha: .08);
}
}
.flag {
width: 32px;

View File

@ -19,6 +19,8 @@ export default () => {
const { isEmptySlide } = useSlideHandler()
const imgPool = ref<PexelsImage[]>([])
const transitionIndex = ref(0)
const transitionTemplate = ref<Slide | null>(null)
const checkTextType = (el: PPTElement, type: TextType) => {
return (el.type === 'text' && el.textType === type) || (el.type === 'shape' && el.text && el.text.type === type)
@ -226,6 +228,8 @@ export default () => {
}
const AIPPT = (templateSlides: Slide[], _AISlides: AIPPTSlide[], imgs?: PexelsImage[]) => {
slidesStore.updateSlideIndex(slidesStore.slides.length - 1)
if (imgs) imgPool.value = imgs
const AISlides: AIPPTSlide[] = []
@ -307,16 +311,16 @@ export default () => {
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)]
if (!transitionTemplate.value) {
const _transitionTemplate = transitionTemplates[Math.floor(Math.random() * transitionTemplates.length)]
transitionTemplate.value = _transitionTemplate
}
const slides = []
let transitionIndex = 0
for (const item of AISlides) {
if (item.type === 'cover') {
const coverTemplate = coverTemplates[Math.floor(Math.random() * coverTemplates.length)]
const elements = coverTemplate.elements.map(el => {
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
if (el.type !== 'text' && el.type !== 'shape') return el
@ -374,8 +378,8 @@ export default () => {
})
}
else if (item.type === 'transition') {
transitionIndex++
const elements = transitionTemplate.elements.map(el => {
transitionIndex.value = transitionIndex.value + 1
const elements = transitionTemplate.value.elements.map(el => {
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
if (el.type !== 'text' && el.type !== 'shape') return el
if (checkTextType(el, 'title') && item.data.title) {
@ -385,7 +389,7 @@ export default () => {
return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
}
if (checkTextType(el, 'partNumber')) {
return getNewTextElement({ el, text: transitionIndex + '', maxLine: 1, digitPadding: true })
return getNewTextElement({ el, text: transitionIndex.value + '', maxLine: 1, digitPadding: true })
}
return el
})
@ -469,6 +473,7 @@ export default () => {
})
}
else if (item.type === 'end') {
const endTemplate = endTemplates[Math.floor(Math.random() * endTemplates.length)]
const elements = endTemplate.elements.map(el => {
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
return el

View File

@ -1,5 +1,6 @@
import axios from './config'
// export const SERVER_URL = 'http://localhost:5000'
export const SERVER_URL = (import.meta.env.MODE === 'development') ? '/api' : 'https://server.pptist.cn'
export const ASSET_URL = 'https://asset.pptist.cn'
@ -35,11 +36,18 @@ export default {
content: string,
language: string,
model: string,
) {
return axios.post(`${SERVER_URL}/tools/aippt`, {
): Promise<any> {
return fetch(`${SERVER_URL}/tools/aippt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content,
language,
model,
stream: true,
}),
})
},
}

View File

@ -3,7 +3,7 @@
<div class="header">
<span class="title">AIPPT</span>
<span class="subtite" v-if="step === 'template'">从下方挑选合适的模板开始生成PPT</span>
<span class="subtite" v-else-if="step === 'outline'">确认下方内容大纲点击编辑内容右键添加/删除大纲项开始选模板</span>
<span class="subtite" v-else-if="step === 'outline'">确认下方内容大纲点击编辑内容右键添加/删除大纲项开始模板</span>
<span class="subtite" v-else>在下方输入您的PPT主题并适当补充信息如行业岗位学科用途等</span>
</div>
@ -30,8 +30,9 @@
style="width: 160px;"
v-model:value="model"
:options="[
{ label: 'DeepSeek-v3', value: 'ark-deepseek-v3' },
{ label: 'Doubao-1.5-Pro', value: 'doubao-1.5-pro-32k' },
{ label: 'DeepSeek-v3', value: 'ark-deepseek-v3' },
{ label: 'GLM-4-Flash', value: 'GLM-4-flash' },
]"
/>
</div>
@ -42,7 +43,7 @@
<OutlineEditor v-model:value="outline" />
</div>
<div class="btns" v-if="!outlineCreating">
<Button class="btn" type="primary" @click="step = 'template'">选模板</Button>
<Button class="btn" type="primary" @click="step = 'template'">模板</Button>
<Button class="btn" @click="outline = ''; step = 'setup'">返回重新生成</Button>
</div>
</div>
@ -58,7 +59,7 @@
</div>
</div>
<div class="btns">
<Button class="btn" type="primary" @click="createPPT()">继续</Button>
<Button class="btn" type="primary" @click="createPPT()">生成</Button>
<Button class="btn" @click="outline = ''; step = 'setup'">返回重新生成</Button>
</div>
</div>
@ -84,7 +85,7 @@ import OutlineEditor from '@/components/OutlineEditor.vue'
const mainStore = useMainStore()
const { templates } = storeToRefs(useSlidesStore())
const { AIPPT, getJSONContent } = useAIPPT()
const { AIPPT } = useAIPPT()
const language = ref<'zh' | 'en'>('zh')
const keyword = ref('')
@ -95,7 +96,7 @@ const outlineCreating = ref(false)
const outlineRef = ref<HTMLElement>()
const inputRef = ref<InstanceType<typeof Input>>()
const step = ref<'setup' | 'outline' | 'template'>('setup')
const model = ref('ark-deepseek-v3')
const model = ref('doubao-1.5-pro-32k')
const recommends = ref([
'大学生职业生涯规划',
@ -152,19 +153,34 @@ const createOutline = async () => {
const createPPT = async () => {
loading.value = true
// const AISlides: AIPPTSlide[] = await api.getMockData('AIPPT')
const AISlides: AIPPTSlide[] = await api.AIPPT(outline.value, language.value, 'doubao-1.5-pro-32k').then(ret => {
const obj = JSON.parse(getJSONContent(ret.data[0].content))
return obj.data
})
const stream = await api.AIPPT(outline.value, language.value, 'doubao-1.5-pro-32k')
const templateSlides: Slide[] = await api.getFileData(selectedTemplate.value).then(ret => ret.slides)
// const templateSlides: Slide[] = await api.getMockData(selectedTemplate.value).then(ret => ret.slides)
AIPPT(templateSlides, AISlides)
const reader: ReadableStreamDefaultReader = stream.body.getReader()
const decoder = new TextDecoder('utf-8')
const readStream = () => {
reader.read().then(({ done, value }) => {
if (done) {
loading.value = false
mainStore.setAIPPTDialogState(false)
return
}
const chunk = decoder.decode(value, { stream: true })
try {
const slide: AIPPTSlide = JSON.parse(chunk)
AIPPT(templateSlides, [slide])
}
catch (err) {
// eslint-disable-next-line
console.error(err)
}
readStream()
})
}
readStream()
}
</script>