Files
a-legacy-forged-obsid-sync/Templates/Generate Session Summary.md
T

121 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<%*
/* 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*SUMMARY-START\s*-->[\s\S]*?<!--\s*SUMMARY-END\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); // well 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="<!-- SUMMARY-START -->", END="<!-- SUMMARY-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.");
%>