5.4 KiB
<%* /* PF2e AI Session Summary — CHUNKED + ROBUST (AI for Templater)
- Summarizes each section separately to avoid long-context issues
- Preserves wikilinks; strips headings/labels/scaffolding
- Writes the final recap between SUMMARY markers */
const file = app.workspace.getActiveFile(); if (!file) { new Notice("Open a session note first."); return; }
// ---------- read & basic cleaning ----------
let raw = await app.vault.read(file);
raw = raw.replace(/^---\r?\n[\s\S]?\r?\n---\r?\n?/, ""); // strip YAML
raw = raw.replace(/[\s\S]?/gi, "");
raw = raw.replace(/[\s\S]*?/g, ""); // code fences
raw = raw.replace(/=\s*[^]+`/g, ""); // inline dataview
raw = raw.replace(/\r/g, "");
// ---------- helpers ----------
function sectionBetween(names){
for (const name of names){
const reHead = new RegExp(^##\\s*.*${name}.*$,"im");
const m = raw.match(reHead);
if (!m) continue;
const start = m.index + m[0].length;
const rest = raw.slice(start);
const next = rest.search(/^##\s+/m);
return raw.slice(start, next === -1 ? raw.length : start + next).trim();
}
return "";
}
function linesClean(block){
if (!block) return [];
const BOLD_LABEL=/^\s*(?:[-]\s+)?**[^]+:**\s*$/i; // Label:
const ROUND=/^\s*(?:[-]\s)?round\s+\d+\s*$/i;
const HR=/^\s*[-_]{3,}\s$/;
const QUOTE=/^\s*>/;
const H=/^\s*#{1,6}\s+/;
return block.split("\n")
.map(s=>s.trim())
.filter(s=>s && !BOLD_LABEL.test(s) && !ROUND.test(s) && !HR.test(s) && !QUOTE.test(s) && !H.test(s))
.map(s=>s.replace(/\s*/g,""))
.map(s=>s.replace(/\s+—\s*$/,""));
}
// ---------- pull sections (accept emoji or plain headings) ---------- const S = { whereWhen : sectionBetween(["Where / When"]), pcs : sectionBetween(["PCs Present / Absent","PCs Present","Present / Absent"]), highlights: sectionBetween(["⭐ Highlights","Highlights"]), quests : sectionBetween(["🎯 Quests & Progress","Quests & Progress","Quests"]), npcs : sectionBetween(["🗣️ NPCs Met / Factions","NPCs Met / Factions","NPCs / Factions","NPCs"]), encounters: sectionBetween(["⚔️ Encounters","Encounters"]), intel : sectionBetween(["🧠 Intel Captured","Intel Captured"]), treasure : sectionBetween(["📦 Treasure & Rewards","Treasure & Rewards","Treasure"]), nextHooks : sectionBetween(["🔜 Next Hooks / To-Dos","Next Hooks / To-Dos","Next Hooks","Hooks"]), };
// ---------- chunk prompts ---------- async function summarizeBullets(label, text, max=6){ text = linesClean(text).join("\n").trim(); if (!text) return []; const prompt = `You are a concise Pathfinder 2e GM assistant. Summarize the "${label}" section below as bullet points only (max ${max}). Keep existing wikilinks exactly as written. Do not echo headings, labels, or placeholders.
=== ${label.toUpperCase()} START === ${text} === ${label.toUpperCase()} END ===`; const out = await tp.ai.chat(prompt); return String(out) .split(/\n+/) .map(s=>s.replace(/^\s*[-•]\s/,"").trim()) .filter(Boolean) .slice(0, max); }
async function oneLineRecap(name, date, allBullets){
const prompt = Write a **single bold one-line recap** for a PF2e session called "${name}" on ${date}. Base it ONLY on these bullets:\n${allBullets.map(b=>- ${b}).join("\n")}\nReturn exactly one bold line.;
return String(await tp.ai.chat(prompt)).trim();
}
// ---------- run chunked summaries ---------- const cache = app.metadataCache.getFileCache(file) || {}; const fm = cache.frontmatter || {}; const seshName = fm.Session_Name || tp.file.title || file.basename; const seshDate = fm.Date || tp.date.now("YYYY-MM-DD");
const hi = await summarizeBullets("Highlights", S.highlights, 8); const enc = await summarizeBullets("Encounters", S.encounters, 6); const intel = await summarizeBullets("Intel", S.intel, 6); const quests= await summarizeBullets("Quests", S.quests, 6); const nexts = await summarizeBullets("Next Hooks", S.nextHooks, 6); const npcfx = await summarizeBullets("NPCs & Factions", S.npcs, 1); // we’ll show names inline instead of bullets const loot = await summarizeBullets("Treasure & Rewards", S.treasure, 3);
const allForRecap = [].concat(hi, enc, intel, quests, nexts).slice(0, 24);
let oneLine = await oneLineRecap(seshName, seshDate, allForRecap);
if (!oneLine.startsWith("**")) oneLine = **${seshName} — ${seshDate}:** ${allForRecap[0] || "Session recap."};
// ---------- compose final ----------
const out = [];
out.push(oneLine);
if (hi.length) out.push(...hi.map(b=>- ${b}));
if (enc.length) out.push(...enc.map(b=>- ${b}));
if (intel.length) out.push(...intel.map(b=>- ${b}));
if (quests.length)out.push(...quests.map(b=>- ${b}));
if (npcfx.length) out.push(- NPCs/Factions touched: ${npcfx[0].replace(/^NPCs?:\s*/i,"")});
if (loot.length) out.push(...loot.map(b=>- ${b}));
if (nexts.length) out.push(...nexts.map(b=>- ${b}));
// ---------- inject between markers ---------- const START="", END=""; let content = await app.vault.read(file); const sIdx = content.indexOf(START), eIdx = content.indexOf(END); if (sIdx===-1 || eIdx===-1 || eIdx<sIdx){ new Notice("Could not find SUMMARY markers."); return; } await app.vault.modify(file, content.slice(0, sIdx+START.length) + "\n" + out.join("\n") + "\n" + content.slice(eIdx)); new Notice("AI (chunked) session summary generated."); %>