├── .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 |
459 |
460 | 468 |
469 |
470 | 471 | 472 | 473 |
474 |
475 | {presentationPreviews ? (
476 | {presentationPreviews.map((ppt: JSX.Element, index) => { 477 | return
478 |
479 | 484 | {ppt} 485 | 486 |
487 |
495 | {activeNoteIndex === index && ( 496 |
497 | {intermediate?.children[index].speakerNotes} 498 |
499 | )} 500 | 503 | 517 |
518 |
519 | })} 520 |
) : (
No slides to display
)} 521 |
522 |
523 |
524 | A project by Laurie Voss. It's open-source! 525 |
526 | 527 |

No slides to download

528 |
529 | 530 |
531 |
532 | 533 |

Set Anthropic API Key

534 |

This is stored only in your browser's local storage.

535 | setApiKey(e.target.value)} 539 | placeholder="Enter your API key" 540 | /> 541 | 542 |
543 | 544 |
545 |
546 |
547 | ); 548 | } 549 | 550 | interface IntermediateType { 551 | type: string; 552 | children: any[]; 553 | } 554 | 555 | interface Slide { 556 | type: string; 557 | children: Record[]; 558 | speakerNotes?: string[]; 559 | } 560 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------