mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 流式AIPPT页面生成
This commit is contained in:
parent
7603982de4
commit
bc6bec2835
69
public/mocks/AIPPT_Outline.md
Normal file
69
public/mocks/AIPPT_Outline.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# 5G技术如何改变我们的生活
|
||||||
|
## 5G技术概述
|
||||||
|
### 5G的定义
|
||||||
|
- 第五代移动通信技术
|
||||||
|
- 基于4G技术的重大升级
|
||||||
|
### 5G的关键特性
|
||||||
|
- 高速率
|
||||||
|
- 低时延
|
||||||
|
- 大容量
|
||||||
|
### 5G的发展历程
|
||||||
|
- 早期研究阶段
|
||||||
|
- 标准制定阶段
|
||||||
|
- 商用推广阶段
|
||||||
|
|
||||||
|
## 5G对通信领域的变革
|
||||||
|
### 个人通信体验提升
|
||||||
|
- 高清视频通话无卡顿
|
||||||
|
- 快速下载大文件
|
||||||
|
- 多人在线游戏低延迟
|
||||||
|
### 通信网络架构优化
|
||||||
|
- 网络切片技术实现差异化服务
|
||||||
|
- 边缘计算减少数据传输距离
|
||||||
|
### 通信安全保障增强
|
||||||
|
- 新的加密算法保障数据安全
|
||||||
|
- 实时监测防范网络攻击
|
||||||
|
|
||||||
|
## 5G与智能家居的融合
|
||||||
|
### 智能家电控制
|
||||||
|
- 远程控制家电开关和运行模式
|
||||||
|
- 家电之间智能联动
|
||||||
|
### 家庭安防升级
|
||||||
|
- 高清实时监控家庭情况
|
||||||
|
- 异常情况及时报警
|
||||||
|
### 家居环境智能调节
|
||||||
|
- 自动调节室内温度、湿度
|
||||||
|
- 智能灯光控制营造氛围
|
||||||
|
|
||||||
|
## 5G推动智能交通发展
|
||||||
|
### 自动驾驶汽车
|
||||||
|
- 车辆间实时通信避免碰撞
|
||||||
|
- 高精度地图实时更新
|
||||||
|
### 智能交通管理
|
||||||
|
- 实时监控交通流量并优化信号灯
|
||||||
|
- 快速处理交通事故
|
||||||
|
### 公共交通智能化
|
||||||
|
- 实时公交信息查询
|
||||||
|
- 车内高速网络服务
|
||||||
|
|
||||||
|
## 5G在医疗领域的应用
|
||||||
|
### 远程医疗服务
|
||||||
|
- 专家远程诊断病情
|
||||||
|
- 远程手术指导
|
||||||
|
### 医疗设备互联
|
||||||
|
- 可穿戴设备实时传输健康数据
|
||||||
|
- 医院内部设备信息共享
|
||||||
|
### 智能医疗管理
|
||||||
|
- 电子病历快速调取
|
||||||
|
- 医疗资源智能分配
|
||||||
|
|
||||||
|
## 5G助力工业互联网升级
|
||||||
|
### 智能制造
|
||||||
|
- 生产设备实时监控和远程维护
|
||||||
|
- 柔性生产线智能调度
|
||||||
|
### 工业物流优化
|
||||||
|
- 货物实时定位和跟踪
|
||||||
|
- 智能仓储管理
|
||||||
|
### 工业安全保障
|
||||||
|
- 危险区域实时监测
|
||||||
|
- 事故预警和应急处理
|
@ -256,6 +256,10 @@ const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
|
|||||||
|
|
||||||
&.contextmenu-active {
|
&.contextmenu-active {
|
||||||
color: $themeColor;
|
color: $themeColor;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
background-color: rgba($color: $themeColor, $alpha: .08);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.title {
|
&.title {
|
||||||
@ -279,7 +283,14 @@ const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 11px;
|
padding: 0 11px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
border-radius: $borderRadius;
|
||||||
|
transition: background-color .2s;
|
||||||
|
cursor: pointer;
|
||||||
@include ellipsis-oneline();
|
@include ellipsis-oneline();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba($color: $themeColor, $alpha: .08);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.flag {
|
.flag {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
@ -19,6 +19,8 @@ export default () => {
|
|||||||
const { isEmptySlide } = useSlideHandler()
|
const { isEmptySlide } = useSlideHandler()
|
||||||
|
|
||||||
const imgPool = ref<PexelsImage[]>([])
|
const imgPool = ref<PexelsImage[]>([])
|
||||||
|
const transitionIndex = ref(0)
|
||||||
|
const transitionTemplate = ref<Slide | null>(null)
|
||||||
|
|
||||||
const checkTextType = (el: PPTElement, type: TextType) => {
|
const checkTextType = (el: PPTElement, type: TextType) => {
|
||||||
return (el.type === 'text' && el.textType === type) || (el.type === 'shape' && el.text && el.text.type === type)
|
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[]) => {
|
const AIPPT = (templateSlides: Slide[], _AISlides: AIPPTSlide[], imgs?: PexelsImage[]) => {
|
||||||
|
slidesStore.updateSlideIndex(slidesStore.slides.length - 1)
|
||||||
|
|
||||||
if (imgs) imgPool.value = imgs
|
if (imgs) imgPool.value = imgs
|
||||||
|
|
||||||
const AISlides: AIPPTSlide[] = []
|
const AISlides: AIPPTSlide[] = []
|
||||||
@ -307,16 +311,16 @@ export default () => {
|
|||||||
const contentTemplates = templateSlides.filter(slide => slide.type === 'content')
|
const contentTemplates = templateSlides.filter(slide => slide.type === 'content')
|
||||||
const endTemplates = templateSlides.filter(slide => slide.type === 'end')
|
const endTemplates = templateSlides.filter(slide => slide.type === 'end')
|
||||||
|
|
||||||
const coverTemplate = coverTemplates[Math.floor(Math.random() * coverTemplates.length)]
|
if (!transitionTemplate.value) {
|
||||||
const transitionTemplate = transitionTemplates[Math.floor(Math.random() * transitionTemplates.length)]
|
const _transitionTemplate = transitionTemplates[Math.floor(Math.random() * transitionTemplates.length)]
|
||||||
const endTemplate = endTemplates[Math.floor(Math.random() * endTemplates.length)]
|
transitionTemplate.value = _transitionTemplate
|
||||||
|
}
|
||||||
|
|
||||||
const slides = []
|
const slides = []
|
||||||
|
|
||||||
let transitionIndex = 0
|
|
||||||
|
|
||||||
for (const item of AISlides) {
|
for (const item of AISlides) {
|
||||||
if (item.type === 'cover') {
|
if (item.type === 'cover') {
|
||||||
|
const coverTemplate = coverTemplates[Math.floor(Math.random() * coverTemplates.length)]
|
||||||
const elements = coverTemplate.elements.map(el => {
|
const elements = coverTemplate.elements.map(el => {
|
||||||
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
|
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
|
||||||
if (el.type !== 'text' && el.type !== 'shape') return el
|
if (el.type !== 'text' && el.type !== 'shape') return el
|
||||||
@ -374,8 +378,8 @@ export default () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
else if (item.type === 'transition') {
|
else if (item.type === 'transition') {
|
||||||
transitionIndex++
|
transitionIndex.value = transitionIndex.value + 1
|
||||||
const elements = transitionTemplate.elements.map(el => {
|
const elements = transitionTemplate.value.elements.map(el => {
|
||||||
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
|
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
|
||||||
if (el.type !== 'text' && el.type !== 'shape') return el
|
if (el.type !== 'text' && el.type !== 'shape') return el
|
||||||
if (checkTextType(el, 'title') && item.data.title) {
|
if (checkTextType(el, 'title') && item.data.title) {
|
||||||
@ -385,7 +389,7 @@ export default () => {
|
|||||||
return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
|
return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
|
||||||
}
|
}
|
||||||
if (checkTextType(el, 'partNumber')) {
|
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
|
return el
|
||||||
})
|
})
|
||||||
@ -469,6 +473,7 @@ export default () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
else if (item.type === 'end') {
|
else if (item.type === 'end') {
|
||||||
|
const endTemplate = endTemplates[Math.floor(Math.random() * endTemplates.length)]
|
||||||
const elements = endTemplate.elements.map(el => {
|
const elements = endTemplate.elements.map(el => {
|
||||||
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
|
if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
|
||||||
return el
|
return el
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import axios from './config'
|
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 SERVER_URL = (import.meta.env.MODE === 'development') ? '/api' : 'https://server.pptist.cn'
|
||||||
export const ASSET_URL = 'https://asset.pptist.cn'
|
export const ASSET_URL = 'https://asset.pptist.cn'
|
||||||
|
|
||||||
@ -35,11 +36,18 @@ export default {
|
|||||||
content: string,
|
content: string,
|
||||||
language: string,
|
language: string,
|
||||||
model: string,
|
model: string,
|
||||||
) {
|
): Promise<any> {
|
||||||
return axios.post(`${SERVER_URL}/tools/aippt`, {
|
return fetch(`${SERVER_URL}/tools/aippt`, {
|
||||||
content,
|
method: 'POST',
|
||||||
language,
|
headers: {
|
||||||
model,
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content,
|
||||||
|
language,
|
||||||
|
model,
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -3,7 +3,7 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="title">AIPPT</span>
|
<span class="title">AIPPT</span>
|
||||||
<span class="subtite" v-if="step === 'template'">从下方挑选合适的模板,开始生成PPT</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>
|
<span class="subtite" v-else>在下方输入您的PPT主题,并适当补充信息,如行业、岗位、学科、用途等</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -30,8 +30,9 @@
|
|||||||
style="width: 160px;"
|
style="width: 160px;"
|
||||||
v-model:value="model"
|
v-model:value="model"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: 'DeepSeek-v3', value: 'ark-deepseek-v3' },
|
|
||||||
{ label: 'Doubao-1.5-Pro', value: 'doubao-1.5-pro-32k' },
|
{ 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>
|
</div>
|
||||||
@ -42,7 +43,7 @@
|
|||||||
<OutlineEditor v-model:value="outline" />
|
<OutlineEditor v-model:value="outline" />
|
||||||
</div>
|
</div>
|
||||||
<div class="btns" v-if="!outlineCreating">
|
<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>
|
<Button class="btn" @click="outline = ''; step = 'setup'">返回重新生成</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -58,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btns">
|
<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>
|
<Button class="btn" @click="outline = ''; step = 'setup'">返回重新生成</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -84,7 +85,7 @@ import OutlineEditor from '@/components/OutlineEditor.vue'
|
|||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { templates } = storeToRefs(useSlidesStore())
|
const { templates } = storeToRefs(useSlidesStore())
|
||||||
const { AIPPT, getJSONContent } = useAIPPT()
|
const { AIPPT } = useAIPPT()
|
||||||
|
|
||||||
const language = ref<'zh' | 'en'>('zh')
|
const language = ref<'zh' | 'en'>('zh')
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
@ -95,7 +96,7 @@ const outlineCreating = ref(false)
|
|||||||
const outlineRef = ref<HTMLElement>()
|
const outlineRef = ref<HTMLElement>()
|
||||||
const inputRef = ref<InstanceType<typeof Input>>()
|
const inputRef = ref<InstanceType<typeof Input>>()
|
||||||
const step = ref<'setup' | 'outline' | 'template'>('setup')
|
const step = ref<'setup' | 'outline' | 'template'>('setup')
|
||||||
const model = ref('ark-deepseek-v3')
|
const model = ref('doubao-1.5-pro-32k')
|
||||||
|
|
||||||
const recommends = ref([
|
const recommends = ref([
|
||||||
'大学生职业生涯规划',
|
'大学生职业生涯规划',
|
||||||
@ -152,19 +153,34 @@ const createOutline = async () => {
|
|||||||
const createPPT = async () => {
|
const createPPT = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// const AISlides: AIPPTSlide[] = await api.getMockData('AIPPT')
|
const stream = await api.AIPPT(outline.value, language.value, 'doubao-1.5-pro-32k')
|
||||||
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 templateSlides: Slide[] = await api.getFileData(selectedTemplate.value).then(ret => ret.slides)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = false
|
readStream()
|
||||||
|
})
|
||||||
mainStore.setAIPPTDialogState(false)
|
}
|
||||||
|
readStream()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user