feat: add spark table completion and rolling support
Implement "spark tables" functionality, which allows users to roll dice against markdown tables to retrieve specific values. - Add `sparkTablesSource` to scan markdown files for tables starting with a dice notation (e.g., d6, d20). - Implement `/spark` command in the journal to resolve and roll spark tables. - Add a new message type `spark` with a dedicated UI component to render the results. - Update completions API to include spark table metadata.
This commit is contained in:
@@ -24,6 +24,7 @@ import { sendMessage, useJournalStream } from "../stores/journalStream";
|
||||
import { linkPrefill, setLinkPrefill } from "../stores/reveal";
|
||||
import { useJournalCompletions, ensureCompletions } from "./completions";
|
||||
import { resolveRollPayload } from "./types/roll";
|
||||
import { resolveSparkPayload } from "./types/spark";
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
@@ -34,7 +35,7 @@ interface CompletionItem {
|
||||
}
|
||||
|
||||
interface ParsedInput {
|
||||
type: "chat" | "roll" | "link";
|
||||
type: "chat" | "roll" | "spark" | "link";
|
||||
payload: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
@@ -47,6 +48,13 @@ function parseInput(raw: string): ParsedInput {
|
||||
return { type: "roll", payload: { notation, label: notation } };
|
||||
}
|
||||
|
||||
if (raw.startsWith("/spark ")) {
|
||||
const key = raw.slice("/spark ".length).trim();
|
||||
if (!key)
|
||||
return { type: "spark", payload: {}, error: "Spark table key required" };
|
||||
return { type: "spark", payload: { key } };
|
||||
}
|
||||
|
||||
if (raw.startsWith("/link ")) {
|
||||
const arg = raw.slice("/link ".length).trim();
|
||||
if (!arg) return { type: "link", payload: {}, error: "Path required" };
|
||||
@@ -57,8 +65,8 @@ function parseInput(raw: string): ParsedInput {
|
||||
return { type: "link", payload: { path, section } };
|
||||
}
|
||||
|
||||
// /roll or /link with no space — need to complete, don't send
|
||||
if (raw === "/roll" || raw === "/link") {
|
||||
// /roll, /spark, or /link with no space — need to complete, don't send
|
||||
if (raw === "/roll" || raw === "/spark" || raw === "/link") {
|
||||
return { type: "chat", payload: {}, error: "Complete the command" };
|
||||
}
|
||||
|
||||
@@ -101,7 +109,7 @@ export const JournalInput: Component = () => {
|
||||
|
||||
// ---- Send ----
|
||||
|
||||
function handleSend() {
|
||||
async function handleSend() {
|
||||
const raw = text().trim();
|
||||
if (!raw) return;
|
||||
|
||||
@@ -142,6 +150,30 @@ export const JournalInput: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// GM spark: resolve the spark table roll locally
|
||||
if (parsed.type === "spark") {
|
||||
try {
|
||||
const key = (parsed.payload as { key: string }).key;
|
||||
// Look up filePath from completions data
|
||||
const match = comp.data.sparkTables.find((s) => s.slug === key);
|
||||
const filePath = match?.filePath ?? "";
|
||||
const p = await resolveSparkPayload({ key, filePath });
|
||||
const result = sendMessage("spark", p);
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
} else {
|
||||
setText("");
|
||||
}
|
||||
setSending(false);
|
||||
textareaRef?.focus();
|
||||
return;
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to roll spark table");
|
||||
setSending(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = sendMessage(parsed.type, parsed.payload);
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
@@ -172,6 +204,7 @@ export const JournalInput: Component = () => {
|
||||
const data = comp.data;
|
||||
const commands = [
|
||||
{ label: "/roll", kind: "command" as const, insertText: "/roll " },
|
||||
{ label: "/spark", kind: "command" as const, insertText: "/spark " },
|
||||
{ label: "/link", kind: "command" as const, insertText: "/link " },
|
||||
];
|
||||
|
||||
@@ -206,6 +239,32 @@ export const JournalInput: Component = () => {
|
||||
}));
|
||||
}
|
||||
|
||||
// After /spark — show spark table suggestions
|
||||
if (raw.startsWith("/spark ")) {
|
||||
const prefix = raw.slice("/spark ".length).toLowerCase();
|
||||
const matches = data.sparkTables
|
||||
.filter(
|
||||
(s) =>
|
||||
s.slug.toLowerCase().includes(prefix) ||
|
||||
s.label.toLowerCase().includes(prefix),
|
||||
)
|
||||
.slice(0, 8);
|
||||
if (matches.length === 0) {
|
||||
return [
|
||||
{
|
||||
label: "No spark tables found",
|
||||
kind: "no-results",
|
||||
insertText: "",
|
||||
},
|
||||
];
|
||||
}
|
||||
return matches.map((s) => ({
|
||||
label: `${s.filePath} § ${s.slug} (${s.notation})`,
|
||||
kind: "value" as const,
|
||||
insertText: `/spark ${s.slug}`,
|
||||
}));
|
||||
}
|
||||
|
||||
// After /link — show article and heading suggestions
|
||||
if (raw.startsWith("/link ")) {
|
||||
const prefix = raw.slice("/link ".length).toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user