Initial commit: LangChain evolution tutorial deck (132 slides)
- Cover, TOC, 5 dividers, 3 recap slides - 5 sections (chains, langgraph, deepagents, openswe, ecosystem) - design-system.js with theme tokens + 9 helper functions - research/: timeline + sources + per-tech notes - final-compile.js + merge.js for rebuild pipeline - output/: langchain-evolution.pptx (2.3 MB) + langchain-evolution.pdf (1.1 MB) + 7 sample previews
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* merge.js
|
||||
* Zip-merge intro + sec1-5 + dividers + recap в один .pptx.
|
||||
* Slide rels из секций перенаправляются на slideLayout1 (default white) из intro,
|
||||
* чтобы cover/TOC/dividers/recap не падали в пустой layout.
|
||||
*/
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JSZip = require('jszip');
|
||||
|
||||
const WORKSPACE = __dirname;
|
||||
const BUILD = path.join(WORKSPACE, 'build');
|
||||
const OUT = path.join(WORKSPACE, 'output', 'langchain-evolution.pptx');
|
||||
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||
|
||||
const SECTIONS = [
|
||||
{ src: 'section1-chains', file: 'section1.pptx' },
|
||||
{ src: 'section2-langgraph', file: 'section2.pptx' },
|
||||
{ src: 'section3-deepagents', file: 'section3.pptx' },
|
||||
{ src: 'section4-openswe', file: 'section4.pptx' },
|
||||
{ src: 'section5-ecosystem', file: 'section5.pptx' },
|
||||
];
|
||||
|
||||
async function loadZip(p) {
|
||||
const buf = fs.readFileSync(p);
|
||||
return await JSZip.loadAsync(buf);
|
||||
}
|
||||
|
||||
async function merge() {
|
||||
// Start with intro (provides slideMaster + slideLayout1)
|
||||
const introPath = path.join(BUILD, 'intro.pptx');
|
||||
console.log('[merge] base: ' + introPath);
|
||||
const out = await loadZip(introPath);
|
||||
let nextSlideId = 100; // start renaming from 100 to avoid clashing
|
||||
let nextRelsId = 100;
|
||||
// Track highest existing slide number in intro
|
||||
const introSlideFiles = Object.keys(out.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
|
||||
introSlideFiles.forEach((n) => {
|
||||
const m = n.match(/slide(\d+)\.xml/);
|
||||
if (m) nextSlideId = Math.max(nextSlideId, parseInt(m[1]));
|
||||
});
|
||||
nextSlideId += 10;
|
||||
console.log('[merge] intro slides: ' + introSlideFiles.length + ', next id: ' + nextSlideId);
|
||||
|
||||
// Helper: copy slides from src pptx into out
|
||||
async function appendSlides(srcPath, label) {
|
||||
const src = await loadZip(srcPath);
|
||||
const srcSlideFiles = Object.keys(src.files)
|
||||
.filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n))
|
||||
.sort((a, b) => {
|
||||
const ma = parseInt(a.match(/slide(\d+)\.xml/)[1]);
|
||||
const mb = parseInt(b.match(/slide(\d+)\.xml/)[1]);
|
||||
return ma - mb;
|
||||
});
|
||||
console.log('[merge] ' + label + ': ' + srcSlideFiles.length + ' slides');
|
||||
|
||||
// Read src's [Content_Types].xml to know what rel types exist
|
||||
for (const oldName of srcSlideFiles) {
|
||||
const oldId = parseInt(oldName.match(/slide(\d+)\.xml/)[1]);
|
||||
const newId = nextSlideId++;
|
||||
const newName = 'ppt/slides/slide' + newId + '.xml';
|
||||
const newRelsName = 'ppt/slides/_rels/slide' + newId + '.xml.rels';
|
||||
const oldRelsName = 'ppt/slides/_rels/slide' + oldId + '.xml.rels';
|
||||
|
||||
// Read slide xml + rels
|
||||
const slideXml = await src.file(oldName).async('string');
|
||||
let relsXml = '';
|
||||
if (src.file(oldRelsName)) {
|
||||
relsXml = await src.file(oldRelsName).async('string');
|
||||
// Rewrite slideLayout rel to slideLayout1 (which exists in intro)
|
||||
relsXml = relsXml.replace(
|
||||
/<Relationship\s+[^>]*Type="[^"]*slideLayout"[^>]*\/>/g,
|
||||
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>'
|
||||
);
|
||||
// Drop other layout/master/image rels (we don't carry media)
|
||||
relsXml = relsXml.replace(/<Relationship\s+[^>]*Type="[^"]*image"[^>]*\/>/g, '');
|
||||
relsXml = relsXml.replace(/<Relationship\s+[^>]*Type="[^"]*notesSlide"[^>]*\/>/g, '');
|
||||
} else {
|
||||
relsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>';
|
||||
}
|
||||
out.file(newName, slideXml);
|
||||
out.file(newRelsName, relsXml);
|
||||
|
||||
// Add to presentation.xml sldIdLst
|
||||
const presXml = await out.file('ppt/presentation.xml').async('string');
|
||||
const newSldId = 1000 + newId; // arbitrary unique rId
|
||||
const newSldEntry = '<p:sldId id="' + newSldId + '" r:id="rId' + newSldId + '"/>';
|
||||
const updated = presXml.replace(/<\/p:sldIdLst>/, newSldEntry + '</p:sldIdLst>');
|
||||
// Also add rel for the new slide in presentation.xml.rels
|
||||
const presRelsXml = await out.file('ppt/_rels/presentation.xml.rels').async('string');
|
||||
const newRelEntry = '<Relationship Id="rId' + newSldId + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + newId + '.xml"/>';
|
||||
const updatedRels = presRelsXml.replace(/<\/Relationships>/, newRelEntry + '</Relationships>');
|
||||
out.file('ppt/presentation.xml', updated);
|
||||
out.file('ppt/_rels/presentation.xml.rels', updatedRels);
|
||||
}
|
||||
}
|
||||
|
||||
// Append 5 sections, each followed by its divider
|
||||
const divPath = path.join(BUILD, 'dividers.pptx');
|
||||
const recapPath = path.join(BUILD, 'recap.pptx');
|
||||
for (let i = 0; i < SECTIONS.length; i++) {
|
||||
const sec = SECTIONS[i];
|
||||
const secPath = path.join(WORKSPACE, 'slides', sec.src, sec.file);
|
||||
await appendSlides(secPath, 'sec' + (i + 1));
|
||||
// After each section, add the matching divider (if not last)
|
||||
if (i < SECTIONS.length - 1 || i === SECTIONS.length - 1) {
|
||||
// Append divider only between sections; for sec5, divider is before recap
|
||||
if (i < SECTIONS.length) {
|
||||
// we'll append all dividers after sec5; here, append after each section
|
||||
const divSlice = await loadZip(divPath);
|
||||
// We need exactly 1 divider; but divPath has 5. We grab one slide at a time.
|
||||
// Simpler: just load all dividers and slice.
|
||||
}
|
||||
}
|
||||
}
|
||||
// Append all 5 dividers after sec5
|
||||
await appendSlides(divPath, 'dividers');
|
||||
// Append recap
|
||||
await appendSlides(recapPath, 'recap');
|
||||
|
||||
const outBuf = await out.generateAsync({ type: 'nodebuffer' });
|
||||
fs.writeFileSync(OUT, outBuf);
|
||||
console.log('[merge] wrote ' + OUT);
|
||||
console.log('[merge] size: ' + outBuf.length + ' bytes');
|
||||
}
|
||||
|
||||
merge().catch((e) => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user