├── .gitignore
├── LICENSE
├── README.md
├── next.config.mjs
├── package-lock.json
├── package.json
├── public
└── test.html
├── src
└── app
│ ├── api
│ ├── cleanup
│ │ └── route.js
│ └── format
│ │ └── route.js
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) Laurie Voss
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Llama-Slides, a presentation generator
2 |
3 | I give a lot of talks, and my method for preparing for talks (not shared by everybody!) is to write down pretty much exactly what I'm going to say. If I've practiced enough, then I don't need to read it, but if I haven't (and I give a LOT of talks, so I often don't have time to practice) I can fall back to reading the notes and not miss anything.
4 |
5 | But what about slides? I'm really lazy about slides. I usually know what the title of the slide is going to be, and maybe if there's an important link or something I'll put it on there, but usually I just don't put anything on my slides at all. That's partly because I like nice clean slides, but also partly because making slides is so much grunt work after I've already put the real work into writing the talk.
6 |
7 | ## Enter the slide generator
8 |
9 | A useful thing I noticed is that my talk notes have a pretty regular pattern:
10 |
11 | ```
12 | # A slide title I want to use
13 | > maybe some extra things
14 | > I'm sure will go on the slides
15 | And then the actual stuff
16 | that I intend to say out loud
17 | split up for pauses
18 | and stuff like that.
19 | ```
20 |
21 | And my talk notes will be dozens of those, separated by blank lines.
22 |
23 | So I figured it should be possible to make some software that generates a deck from my notes. And that's what this is!
24 |
25 | ## Features
26 |
27 | * Raw notes get converted into slides courtesy of [PptxGenJS](https://gitbrent.github.io/PptxGenJS/)
28 | * The slides are previewed on the client courest of [react-pptx](https://www.npmjs.com/package/react-pptx)
29 | * The actual content of the slides is generated by [Anthropic's Claude](https://claude.ai/) (so you need an Anthropic API key to make it work)
30 | * The raw speaking notes are included as actual speaker notes you can see in PowerPoint or by clicking `Show Notes`
31 | * If the initial generation creates a messy slide, the `Clean up` button will
32 | * Take a screenshot of the offending slide courtesy of [html2canvas](https://html2canvas.hertzen.com/)
33 | * Send it to the API which will get Claude to critique the slide and provide suggestions of how to make it less messy
34 | * Regenerate the slide with the suggestions taken into account
35 | * You can of course download an actual PowerPoint file directly to save it and send it to people
36 |
37 | # License
38 |
39 | It's MIT! Have your way with it. There's a lot of stuff that can be done to improve it.
40 |
41 | # TODO
42 |
43 | * Handle images (need to upload them, save them somewhere, etc. Complicated.)
44 | * Avoid overlapping text by converting single-line bullets into multi-line bullets as supported by PptxGenJS
45 | * Make links clickable
46 | * Handle bold and italicized text
47 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverComponentsExternalPackages: ['sharp', 'onnxruntime-node'],
5 | outputFileTracingIncludes: { "/api/*": ["./node_modules/**/*.wasm"], }
6 | }
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@anthropic-ai/sdk": "^0.27.1",
13 | "buble": "^0.20.0",
14 | "html2canvas": "^1.4.1",
15 | "llamaindex": "^0.5.20",
16 | "next": "14.2.7",
17 | "pptxgenjs": "^3.12.0",
18 | "react": "^18",
19 | "react-dom": "^18",
20 | "react-pptx": "^2.20.1"
21 | },
22 | "devDependencies": {
23 | "@types/node": "^20",
24 | "@types/react": "^18",
25 | "@types/react-dom": "^18",
26 | "typescript": "^5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/public/test.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/src/app/api/cleanup/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { Anthropic } from 'llamaindex';
3 | import { Anthropic as AnthropicApi } from '@anthropic-ai/sdk';
4 |
5 | export async function POST(request) {
6 | try {
7 | const formData = await request.formData();
8 | const screenshot = formData.get('screenshot');
9 | const slideIndex = formData.get('slideIndex');
10 | const rawText = formData.get('rawText');
11 | const apiKey = formData.get('apiKey')
12 |
13 | // Perform manipulation on the content here
14 | const recommendations = await manipulateContent(apiKey, screenshot, slideIndex, rawText);
15 |
16 | return NextResponse.json({ recommendations });
17 | } catch (error) {
18 | console.log(error);
19 | return NextResponse.json({ error: 'Error processing the request' }, { status: 500 });
20 | }
21 | }
22 |
23 | // Function to analyze the screenshot
24 | async function analyzeScreenshot(apiKey,screenshot,rawText) {
25 |
26 | // Initialize the Anthropic API client
27 | const anthropicApi = new AnthropicApi({
28 | apiKey: apiKey,
29 | });
30 |
31 | // Convert the screenshot to base64
32 | const buffer = await screenshot.arrayBuffer();
33 | const base64Image = Buffer.from(buffer).toString('base64');
34 |
35 | // Prepare the message for Claude
36 | const messages = [
37 | {
38 | role: 'user',
39 | content: [
40 | {
41 | type: 'image',
42 | source: {
43 | type: 'base64',
44 | media_type: 'image/png',
45 | data: base64Image,
46 | },
47 | },
48 | {
49 | type: 'text',
50 | text: `This is a screenshot of a presentation slide generated from the raw text in tags below. Please analyze the image and suggest improvements to make the slide cleaner, in particular to prevent text overflowing along the bottom. Make sure to say what text is clearly visible on the slide so that a later cleanup operation can include only that text.
51 |
52 | Keep in mind a few things:
53 | 1. The first line of rawtext is going to be included no matter what.
54 | 2. Any lines beginning with ">" will be included no matter what.
55 | So if there's no room for additional content after that, recommend that no content be generated.
56 |
57 |
58 | ${rawText}
59 | `,
60 | },
61 | ],
62 | },
63 | ];
64 |
65 | // Send the request to Claude
66 | const response = await anthropicApi.messages.create({
67 | model: 'claude-3-5-sonnet-20240620',
68 | max_tokens: 1000,
69 | messages: messages,
70 | });
71 |
72 | let recommendations = response.content[0].text;
73 |
74 | console.log("Recommendations are", recommendations)
75 |
76 | return recommendations
77 | }
78 |
79 |
80 | async function manipulateContent(apiKey,screenshot, slideIndex, rawText) {
81 |
82 | // get recommendations about what to do with the screenshot
83 | let recommendations = analyzeScreenshot(apiKey,screenshot,rawText)
84 | return recommendations;
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/api/format/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { Anthropic } from 'llamaindex';
3 |
4 | export async function POST(request) {
5 | try {
6 | const { content, formattingInstructions, apiKey } = await request.json();
7 |
8 | // Perform manipulation on the content here
9 | const formattedContent = await manipulateContent(apiKey, content,formattingInstructions);
10 |
11 | return NextResponse.json({ formattedContent });
12 | } catch (error) {
13 | console.log(error);
14 | return NextResponse.json({ error: 'Error processing the request' }, { status: 500 });
15 | }
16 | }
17 |
18 | async function manipulateContent(apiKey,content,formattingInstructions) {
19 | const llm = new Anthropic({
20 | model: 'claude-3-5-sonnet',
21 | apiKey: apiKey,
22 | });
23 |
24 | console.log("Generating formatted slide from ", content)
25 | console.log("with additional instructions: ", formattingInstructions)
26 |
27 | let prompt = `
28 | You will be given raw text inside of tags. These are the speaking notes for a slide in a presentation. Your job is to extract important points from these notes and format them into markdown that can be used in a slide. You should prefer brief bullet points, or if there is only one key point, you can just write it as a sentence.
29 |
30 | The first line of each slide is preceded by a "-" and represents the title of the slide. You do not need to include this title in your response.
31 |
32 | Any lines that begin with ">" will be included automatically so you don't need to include them or repeat the content in them.
33 |
34 | You do not need to generate heading tags (no # or ##), but you can use bold text for emphasis.
35 |
36 | Things that look like code should be formatted with backticks.
37 |
38 | Things that look like links should be made into markdown links.
39 |
40 | You should respond with only the formatted content, no preamble or explanations are necessary.
41 |
42 | If there are tags below the rawtext, pay attention to what they suggest.
43 |
44 | If you don't want to render anything beyond what will get automatically included, respond with just the string "NO_EXTRA_CONTENT".
45 |
46 |
47 | ${content}
48 |
49 |
50 | `;
51 |
52 | if (formattingInstructions) {
53 | prompt += `${formattingInstructions}`
54 | }
55 |
56 | const response = await llm.complete({prompt: prompt});
57 |
58 | console.log("Formatted content: ", response.text)
59 |
60 | return response.text;
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/llama-slides/b066f8fd1216cee0fdfc0efb9acccec18d759408/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | #three-column {
2 | display: flex;
3 | justify-content: space-between;
4 | height: 100vh;
5 | }
6 |
7 | #source, #slides {
8 | flex: 2;
9 | padding: 20px;
10 | }
11 |
12 | #convertButton {
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | gap: 10px;
18 | height: 100%;
19 | }
20 |
21 | #convertButton button {
22 | width: 100%;
23 | }
24 |
25 | #rawTextArea {
26 | width: 100%;
27 | height: 100%;
28 | resize: none;
29 | }
30 |
31 | #slides {
32 | overflow-y: scroll;
33 | }
34 |
35 | .speakerNotesPopup {
36 | border: 1px solid #ccc;
37 | padding: 10px;
38 | margin-top: 5px;
39 | background-color: #f9f9f9;
40 | }
41 |
42 | #apiKeyDialog {
43 | padding: 20px;
44 | border-radius: 8px;
45 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
46 | background-color: #ffffff;
47 | }
48 |
49 | #apiKeyDialog h2 {
50 | margin-top: 0;
51 | margin-bottom: 15px;
52 | font-size: 1.5em;
53 | color: #333;
54 | }
55 |
56 | #apiKeyDialog input[type="text"] {
57 | width: 100%;
58 | padding: 8px;
59 | margin-bottom: 15px;
60 | border: 1px solid #ccc;
61 | border-radius: 4px;
62 | }
63 |
64 | #apiKeyDialog button {
65 | padding: 8px 16px;
66 | margin-right: 10px;
67 | border: none;
68 | border-radius: 4px;
69 | background-color: #007bff;
70 | color: white;
71 | cursor: pointer;
72 | transition: background-color 0.3s ease;
73 | }
74 |
75 | #apiKeyDialog button:hover {
76 | background-color: #0056b3;
77 | }
78 |
79 | #apiKeyDialog button[type="submit"] {
80 | background-color: #28a745;
81 | }
82 |
83 | #apiKeyDialog button[type="submit"]:hover {
84 | background-color: #218838;
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "llama-slides",
9 | description: "A web app to generate PowerPoint decks from speaker notes",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useCallback, useRef, useEffect } from "react";
4 | import crypto from 'crypto';
5 | import {
6 | Presentation, Slide, Text,
7 | Shape, Image, render as renderPptx
8 | } from "react-pptx"
9 | import Preview from "react-pptx/preview";
10 | import pptxgen from "pptxgenjs";
11 | import html2canvas from "html2canvas";
12 |
13 | export default function Home() {
14 | const [rawText, setRawText] = useState(`RAG and Agents in 2024
15 | ======================
16 | Only the title of the first slide is used
17 | So anything after that is ignored.
18 | So I've put some instructions in here that only humans will see.
19 | Slides are separated by 2 breaklines.
20 | A slide has a title denoted by #
21 | Lines beginning with > are included after the title, verbatim.
22 |
23 | # Who is this guy?
24 | Hi everybody!
25 | I'm Laurie Voss
26 | VP of developer relations at LlamaIndex
27 | In a former life I co-founded npm Inc
28 | So some of you may know me from then.
29 | These days instead of JavaScript
30 | I'm talking about AI
31 |
32 | # What are we talking about
33 | > What is LlamaIndex
34 | > What is RAG
35 | > Building RAG in LlamaIndex
36 | > Building Agentic RAG
37 | > Building agentic workflows
38 | Specifically today I'm going to be talking about RAG and agents
39 | RAG stands for Retrieval-Augmented Generation
40 | First I'll introduce you to LlamaIndex
41 | Then we'll cover the basics of RAG
42 | And how to build RAG in llamaindex
43 | Then we'll talk about why we need agents
44 | And how to build those in Llamaindex, too.
45 | And finally we'll talk about workflows
46 | the latest feature of llamaindex
47 | released just 4 days ago.
48 |
49 | # What is LlamaIndex
50 | > docs.llamaindex.ai
51 | > ts.llamaindex.ai
52 | So what is LlamaIndex?
53 | It's a bunch of things.
54 | Start with the most obvious: we are a framework
55 | in both python and typescript
56 | that helps you build generative AI applications.
57 | The Python framework is older and bigger
58 | the typescript framework is growing fast.
59 | Both are obviously open source and free to use.
60 | But that's not all we do.
61 |
62 | # LlamaParse
63 | > cloud.llamaindex.ai
64 | LlamaParse is a service from LlamaIndex
65 | that will parse complicated documents in any format
66 | into a form that can be understood by an LLM.
67 | This is critical for lots of gen AI applications
68 | because if your LLM can't understand what it's reading
69 | you'll get nonsense results.
70 | LlamaParse is also free to use for 1000 pages/day
71 | So it's easy to try out.
72 |
73 | # LlamaCloud
74 | Then there's LlamaCloud, our enterprise service.
75 | If what you want to do is stuff documents in one end
76 | and run retrieval-augmented generation on the other end
77 | without having to think about the stuff in the middle
78 | this is the service for you.
79 | Think of it as LlamaIndex building a LlamaIndex app for you.
80 | We're currently in early previews of the service
81 | but you can sign up for our waitlist.
82 |
83 | # LlamaHub
84 | Then there's LlamaHub, our registry of helper software.
85 | Need to connect to any database in the world? We gotchu.
86 | Want to get data out of notion, or slack, or salesforce? No problem.
87 | Need to store your data in a vector store? We support them all.
88 | Want to use OpenAI, Mistral, Anthropic, some other LLM?
89 | We support over 30 different LLMs including local ones like Llama 3.
90 | Want to build an agent and want some pre-built tools to do that?
91 | We have dozens of agent tools already built for you.
92 |
93 | # Why use llamaindex?
94 | Why should you use LlamaIndex?
95 | Because we will help you go faster.
96 | You're a developer, you have limited time
97 | you have actual business and technology problems to solve.
98 | Don't get stuck figuring out the basics.
99 | We've solved a bunch of the foundational problems for you
100 | so you can focus on your actual business problems.`);
101 | const [rawTextSlides, setRawTextSlides] = useState([]);
102 | const [instructions, setInstructions] = useState([]);
103 | const [intermediate, setIntermediate] = useState(undefined);
104 | const [presentationPreviews, setPresentationPreviews] = useState(null);
105 | const [formatCache] = useState(new Map());
106 | const dialogRef = useRef(null);
107 | const [activeNoteIndex, setActiveNoteIndex] = useState(null);
108 | const apiKeyDialogRef = useRef(null);
109 | const [apiKey, setApiKey] = useState("");
110 |
111 | useEffect(() => {
112 | const storedApiKey = localStorage.getItem("apiKey");
113 | if (storedApiKey) setApiKey(storedApiKey);
114 | }, []);
115 |
116 | const handleSetApiKey = () => {
117 | apiKeyDialogRef.current?.showModal();
118 | };
119 |
120 | const saveApiKey = () => {
121 | localStorage.setItem("apiKey", apiKey);
122 | apiKeyDialogRef.current?.close();
123 | };
124 |
125 | const hashContent = (content: string): string => {
126 | return crypto.createHash('md5').update(content).digest('hex');
127 | };
128 |
129 | // converts the intermediate representation to pptx
130 | const convertToPptx = async (intermediate: { children: any[] }) => {
131 | let pres = new pptxgen();
132 | for (let child of intermediate.children) {
133 | let slide = pres.addSlide()
134 | let content = child.children
135 | for (let el of content) {
136 | switch(el.type) {
137 | case "text.bullet":
138 | slide.addText(
139 | el.children[0].content,
140 | {
141 | ...el.style,
142 | bullet: true
143 | }
144 | )
145 | break;
146 | case "text":
147 | slide.addText(
148 | el.children[0].content,
149 | el.style
150 | )
151 | }
152 | }
153 | slide.addNotes(child.speakerNotes)
154 | }
155 | return pres
156 | }
157 |
158 | // this creates the actual file you download
159 | const generatePptx = async () => {
160 | if (!intermediate) {
161 | dialogRef.current?.showModal();
162 | return;
163 | }
164 | let pres = await convertToPptx(intermediate)
165 | let presBlob = await pres.write({outputType: "blob"})
166 |
167 | const a = document.createElement("a");
168 | const url = URL.createObjectURL(presBlob as Blob | MediaSource);
169 | a.href = url;
170 | a.download = "presentation.pptx";
171 | a.click();
172 | }
173 |
174 | const formatWithCache = useCallback(async (content: string, index: number) => {
175 |
176 | let formattingInstructions = null
177 | if (instructions[index]) {
178 | formattingInstructions = instructions[index]
179 | }
180 |
181 | const contentHash = hashContent(content+formattingInstructions);
182 |
183 | if (formatCache.has(contentHash)) {
184 | return formatCache.get(contentHash);
185 | }
186 |
187 | const response = await fetch("/api/format", {
188 | method: "POST",
189 | headers: {
190 | "Content-Type": "application/json",
191 | },
192 | body: JSON.stringify({ content, formattingInstructions, apiKey }),
193 | });
194 |
195 | if (!response.ok) {
196 | throw new Error('Failed to format content');
197 | }
198 |
199 | const formattedContent = (await response.json()).formattedContent;
200 | formatCache.set(contentHash, formattedContent);
201 |
202 | return formattedContent;
203 | }, [formatCache]);
204 |
205 | const generateIntermediateRepresentation = async (overrideRawTextSlides: string[] | null = null) => {
206 |
207 | let sourceTextSlides = rawTextSlides
208 | if (overrideRawTextSlides) {
209 | sourceTextSlides = overrideRawTextSlides
210 | }
211 |
212 | let slides = await Promise.all(sourceTextSlides.map(async (slideText, index) => {
213 | /* the whole slide is 10 inches wide by 5.6 inches high */
214 | let firstline = slideText.split("\n")[0];
215 |
216 | // first slide is a title slide
217 | if (index === 0) {
218 | return {
219 | type: "slide",
220 | children: [
221 | {
222 | type: "text",
223 | style: {
224 | x: 1,
225 | y: 2,
226 | w: 8,
227 | h: 1,
228 | fontSize: 80
229 | },
230 | children: [
231 | {
232 | type: "string",
233 | content: firstline
234 | }
235 | ]
236 | }
237 | ]
238 | }
239 | }
240 |
241 | // all other slides follow the same format
242 | let formattedContent = await formatWithCache(slideText,index);
243 | console.log(`Formatted content: for slide ${index}:`, formattedContent)
244 |
245 | let slide: Slide = {
246 | type: "slide",
247 | children: [] as Record[]
248 | }
249 |
250 | // first line is big
251 | if (firstline.startsWith("- ") || firstline.startsWith("# ")) {
252 | firstline = firstline.slice(2);
253 | }
254 | let yPosition = 0
255 | slide.children.push({
256 | type: "text",
257 | style: {
258 | x: 1,
259 | y: yPosition += 0.7,
260 | w: 8,
261 | h: 1,
262 | fontSize: 40
263 | },
264 | children: [
265 | {
266 | type: "string",
267 | content: firstline
268 | }
269 | ]
270 | })
271 |
272 | // lines with > are meant to be included verbatim
273 | let verbatim = slideText.split("\n").filter( (line) => line.startsWith("> "))
274 | if (verbatim.length > 0) yPosition += 0.5
275 | for (let line of verbatim) {
276 | slide.children.push({
277 | type: "text",
278 | style: {
279 | x: 1,
280 | y: yPosition += 0.5,
281 | w: 8,
282 | h: 1,
283 | fontSize: 25
284 | },
285 | children: [
286 | {
287 | type: "string",
288 | content: line.slice(2)
289 | }
290 | ]
291 | })
292 | }
293 |
294 | let speakerNotes = slideText.split("\n").filter( (line, index) => {
295 | if (index == 0) return
296 | if (line.startsWith("> ")) return
297 | return line
298 | })
299 | slide.speakerNotes = speakerNotes
300 |
301 | if (formattedContent != "NO_EXTRA_CONTENT") {
302 | // subsequent lines are mostly bullet points
303 | slide.children = slide.children.concat(formattedContent.split("\n").map((line: string, index: number) => {
304 | //console.log("Line: ", line)
305 | if (line.startsWith("- ")) {
306 | return {
307 | type: "text.bullet",
308 | style: {
309 | x: 1,
310 | y: yPosition += 0.5,
311 | w: 8,
312 | h: 1,
313 | fontSize: 20
314 | },
315 | children: [
316 | {
317 | type: "string",
318 | content: line.slice(2)
319 | }
320 | ]
321 | }
322 | } else {
323 | return {
324 | type: "text",
325 | style: {
326 | x: 1,
327 | y: yPosition += 0.5,
328 | w: 8,
329 | h: 1,
330 | fontSize: 20
331 | },
332 | children: [
333 | {
334 | type: "string",
335 | content: line
336 | }
337 | ]
338 | }
339 | }
340 | }))
341 | }
342 | return slide
343 | }));
344 |
345 | let presentation = {
346 | type: "presentation",
347 | children: slides
348 | }
349 |
350 | return presentation
351 | }
352 |
353 | const convertPreviewChildren = (children: any[]) => {
354 | return children.map((child) => {
355 | switch (child.type) {
356 | // in the previews, each slide is in its own presentation
357 | case "slide":
358 | return {convertPreviewChildren(child.children)}
359 | case "text":
360 | return {convertPreviewChildren(child.children)}
361 | case "text.bullet":
362 | return {convertPreviewChildren(child.children)}
363 | case "string":
364 | return child.content
365 | }
366 | })
367 | }
368 |
369 | const convertToPreviews = async (tree: { children: any[] }) => {
370 | return convertPreviewChildren(tree.children)
371 | }
372 |
373 | const generatePreviews = async () => {
374 |
375 | let waitPresentations: JSX.Element[] = [
376 |
377 |
378 | Generating...
382 |
383 |
384 | ];
385 | setPresentationPreviews(waitPresentations); // waiting state
386 |
387 | let sourceTextSlides = rawTextSlides
388 | if (sourceTextSlides.length === 0) {
389 | sourceTextSlides = rawText.split("\n\n");
390 | console.log("Got raw text slides",sourceTextSlides)
391 | setRawTextSlides(sourceTextSlides)
392 | }
393 |
394 | // get intermediate state
395 | let newIntermediate = await generateIntermediateRepresentation(sourceTextSlides)
396 | setIntermediate(newIntermediate)
397 | console.log("Intermediate form ", newIntermediate)
398 | // convert it into an array of single-slide presentations plus notes etc.
399 | let presentationPreviews = await convertToPreviews(newIntermediate)
400 | console.log("Presentation previews", presentationPreviews)
401 |
402 | setPresentationPreviews(presentationPreviews)
403 | };
404 |
405 | const cleanUpSlide = async (slideIndex: number) => {
406 | if (!intermediate) return;
407 |
408 | let canvas = await html2canvas(document.querySelector(`[data-slide-number="${slideIndex}"]`) as HTMLElement)
409 |
410 | // Convert canvas to PNG
411 | const pngDataUrl = canvas.toDataURL('image/png');
412 |
413 | // Create a Blob from the data URL
414 | const blobBin = atob(pngDataUrl.split(',')[1]);
415 | const array = [];
416 | for (let i = 0; i < blobBin.length; i++) {
417 | array.push(blobBin.charCodeAt(i));
418 | }
419 | const pngBlob = new Blob([new Uint8Array(array)], {type: 'image/png'});
420 |
421 | // Create a File object from the Blob
422 | const pngFile = new File([pngBlob], `slide_${slideIndex}.png`, { type: 'image/png' });
423 |
424 | const formData = new FormData();
425 | formData.append('screenshot', pngFile);
426 | formData.append('slideIndex', slideIndex.toString());
427 | formData.append('rawText', rawTextSlides[slideIndex])
428 | formData.append('apiKey',apiKey)
429 |
430 | const response = await fetch("/api/cleanup", {
431 | method: "POST",
432 | body: formData,
433 | });
434 |
435 | if (!response.ok) {
436 | console.error('Failed to clean up slide');
437 | return;
438 | }
439 |
440 | const recommendations = (await response.json()).recommendations;
441 |
442 | console.log("Cleanup recommendations are ", recommendations)
443 |
444 | // set the recommendation
445 | let newInstructions = instructions
446 | newInstructions[slideIndex] = recommendations
447 | setInstructions(newInstructions)
448 |
449 | // Regenerate previews
450 | let newIntermediate = await generateIntermediateRepresentation()
451 | setIntermediate(newIntermediate)
452 | const updatedPreviews = await convertToPreviews( newIntermediate );
453 | setPresentationPreviews(updatedPreviews);
454 | };
455 |
456 | return (
457 |
458 |