vault commit: 2025-10-09 00:46, 16 files changed
This commit is contained in:
@@ -1,101 +1,120 @@
|
||||
<%*
|
||||
/* PF2e AI Session Summary — FULL NOTE MODE (single-string call)
|
||||
- Uses the whole note body (not just bullets)
|
||||
- Strips frontmatter, code fences, and prior summaries/history
|
||||
- Lets you pick Short / Standard / Detailed length
|
||||
/* 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 & sanitize whole note ---------- */
|
||||
let body = await app.vault.read(file);
|
||||
// ---------- 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, "");
|
||||
|
||||
// strip YAML frontmatter
|
||||
body = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
||||
// ---------- 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*$/,""));
|
||||
}
|
||||
|
||||
// strip prior summaries & history blocks if present
|
||||
body = body.replace(/<!--\s*SUMMARY-START\s*-->[\s\S]*?<!--\s*SUMMARY-END\s*-->/gi, "");
|
||||
body = body.replace(/<!--\s*SUMMARY-HISTORY-START\s*-->[\s\S]*?<!--\s*SUMMARY-HISTORY-END\s*-->/gi, "");
|
||||
// ---------- 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"]),
|
||||
};
|
||||
|
||||
// strip code fences & inline dataview
|
||||
body = body.replace(/```[\s\S]*?```/g, "");
|
||||
body = body.replace(/`=\s*[^`]+`/g, "");
|
||||
// ---------- 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.
|
||||
|
||||
// normalize whitespace
|
||||
body = body.replace(/\r/g, "").trim();
|
||||
=== ${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);
|
||||
}
|
||||
|
||||
// If the note is very long, cap input (you can raise this safely)
|
||||
const MAX_INPUT_CHARS = 40000;
|
||||
if (body.length > MAX_INPUT_CHARS) body = body.slice(0, MAX_INPUT_CHARS);
|
||||
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();
|
||||
}
|
||||
|
||||
/* ---------- Metadata ---------- */
|
||||
// ---------- 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");
|
||||
|
||||
/* ---------- Length selector ---------- */
|
||||
const lenPick = await tp.system.suggester(
|
||||
["Short (~6 bullets)","Standard (~12 bullets)","Detailed (~20 bullets)"],
|
||||
["short","standard","detailed"]
|
||||
) ?? "standard";
|
||||
const bulletTarget = (lenPick === "short") ? 6 : (lenPick === "detailed") ? 20 : 12;
|
||||
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);
|
||||
|
||||
/* ---------- Build single-string prompt ---------- */
|
||||
const prompt = `You are a concise Pathfinder 2e GM assistant. Summarize the FULL session note below.
|
||||
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."}`;
|
||||
|
||||
Return ONLY:
|
||||
**One-line recap (bold)**
|
||||
- Key beats (bulleted, chronological, ${bulletTarget} bullets max)
|
||||
- NPCs & factions touched (preserve existing [[wikilinks]] verbatim if present)
|
||||
- Encounters/checks/outcomes (brief)
|
||||
- Loot/rewards/XP (brief)
|
||||
- Next hooks/to-dos (bullets)
|
||||
// ---------- 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}`));
|
||||
|
||||
DO NOT echo raw headings, “Highlight::”, or placeholder text. Cover ALL major content, not just bullet lists.
|
||||
|
||||
Session: "${seshName}" on ${seshDate}
|
||||
|
||||
=== NOTE START ===
|
||||
${body}
|
||||
=== NOTE END ===`;
|
||||
|
||||
/* ---------- Call AI (string-only) ---------- */
|
||||
let summary = "";
|
||||
try {
|
||||
const resp = await tp.ai.chat(prompt);
|
||||
summary = (typeof resp === "string") ? resp : (resp?.content ?? resp?.text ?? "");
|
||||
} catch (e) {
|
||||
console.error("AI error:", e);
|
||||
}
|
||||
|
||||
/* ---------- Fallback if model returns empty ---------- */
|
||||
if (!summary || !summary.trim()) {
|
||||
// Build a tidy, non-noisy fallback from the whole note (no headings/labels)
|
||||
const lines = body
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
.filter(s => s && !/^#{1,6}\s/.test(s)) // no markdown headings
|
||||
.filter(s => !/^\*\*[^*]+:\*\*$/.test(s)) // no bold-only section labels
|
||||
.filter(s => !/^Highlight::/i.test(s)) // no "Highlight::"
|
||||
.filter(s => !/^Round\s+\d+/i.test(s)) // no "Round X"
|
||||
.filter(s => s !== "[[ ]]"); // no empty wikilinks
|
||||
|
||||
const oneLine = `**${seshName} — ${seshDate}:** ${lines[0] || "Session recap unavailable."}`;
|
||||
const bullets = lines.slice(1, 1 + Math.min(20, bulletTarget + 4)).map(s => `- ${s}`);
|
||||
summary = [oneLine, ...bullets].join("\n");
|
||||
}
|
||||
|
||||
/* ---------- Inject between SUMMARY markers ---------- */
|
||||
const START = "<!-- SUMMARY-START -->";
|
||||
const END = "<!-- SUMMARY-END -->";
|
||||
// ---------- 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; }
|
||||
|
||||
const before = content.slice(0, sIdx + START.length);
|
||||
const after = content.slice(eIdx);
|
||||
await app.vault.modify(file, before + "\n" + summary.trim() + "\n" + after);
|
||||
new Notice("AI session summary generated.");
|
||||
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.");
|
||||
%>
|
||||
|
||||
Reference in New Issue
Block a user