obsidian-hermes-plugin/src/settings.ts

210 lines
8.4 KiB
TypeScript

import { App, PluginSettingTab, Setting } from "obsidian";
import type HermesChatPlugin from "./main";
import type { Message } from "./types";
export interface HermesChatSettings {
gatewayUrl: string;
apiKey: string;
showModelInfo: boolean;
enableContextInjection: boolean;
chatFontSize: number;
customCommands: { trigger: string; description: string; prompt: string }[];
chatMessages: Message[];
chatSessionId: string;
}
/** Built-in slash commands shipped with the plugin. */
export const BUILTIN_COMMANDS: { trigger: string; description: string; prompt: string }[] = [
{
trigger: "/threads",
description: "Crawl the campaign wiki for unresolved plot threads",
prompt: "Crawl the campaign wiki for unresolved plot threads relevant to the next session. Cross-reference current party location, active PCs, and open story hooks. Rank by narrative urgency and suggest specific integration points.",
},
{
trigger: "/npc",
description: "Generate a detailed NPC with full backstory, beliefs, and ties",
prompt: "Generate a detailed D&D NPC with full backstory, core beliefs, motivations, ties, a secret, and a voice note. Use the npc-generator skill in detailed mode. Context:",
},
{
trigger: "/npc-fast",
description: "Quick NPC — name, race, age, description only (mid-session)",
prompt: "Generate a quick D&D NPC — just name, race, age range, and 3 bullet descriptions. Use the npc-generator skill in fast mode. Context:",
},
{
trigger: "/location",
description: "Generate a detailed location with atmosphere, features, and wiki ties",
prompt: "Generate a detailed D&D location with atmosphere, notable features, potential encounters, and wiki connections. Use the location-generator skill in detailed mode. Context:",
},
{
trigger: "/location-fast",
description: "Quick location — name, vibe, one interesting detail (mid-session)",
prompt: "Generate a quick D&D location — just name, type, 3-sentence atmosphere, and one notable detail. Use the location-generator skill in fast mode. Context:",
},
{
trigger: "/scene",
description: "Generate atmospheric read-aloud text for a D&D scene",
prompt: "Generate atmospheric read-aloud text for this D&D scene. Pull details from the campaign wiki if the location exists there. Use the scene-setter skill. Context:",
},
{
trigger: "/relations",
description: "Map all wiki-documented relationships for an NPC",
prompt: "Map all wiki-documented relationships, connections, session appearances, and faction ties for this campaign NPC. Use the relation-mapper skill. NPC:",
},
{
trigger: "/hook",
description: "Generate quest hooks tied to existing campaign threads",
prompt: "Generate 2-3 quest hooks tied to existing campaign wiki threads. Every hook must have a documented source. Use the hook-generator skill. Context:",
},
{
trigger: "/encounter",
description: "Build a themed encounter with creatures, hazards, and wiki ties",
prompt: "Build a thematically appropriate D&D encounter with creatures, environmental features, and a wiki connection. Include multiple resolution paths. Use the encounter-builder skill. Context:",
},
{
trigger: "/loot",
description: "Generate themed treasure — coins, items, and a signature piece with a story hook",
prompt: "Generate D&D treasure scaled to party level and themed to the context. Include coin, 1-2 items, and one signature item with a story hook. Use the loot-generator skill. Context:",
},
{
trigger: "/lint",
description: "Scan the campaign wiki for lore inconsistencies and stale data",
prompt: "Scan the campaign wiki for lore inconsistencies — temporal contradictions, spatial impossibilities, one-way relationships, factual conflicts, orphaned entities, stale records, and dead plot threads. Flag issues for GM resolution. Use the lore-lint skill. Scope:",
},
{
trigger: "/prep-session",
description: "Prep the next D&D session",
prompt: "Run the full session-prep pipeline for the next SKT session: scan recent session notes, identify loose threads, generate a session guide with modular flow-chart sections.",
},
{
trigger: "/sync-content",
description: "Sync Content Sink notes to vault",
prompt: "/run content-sink-sync",
},
{
trigger: "/lint-wiki",
description: "[Legacy] Scan the campaign wiki (use /lint instead)",
prompt: "Scan the campaign wiki for lore inconsistencies and stale data. Use the lore-lint skill. Scope:",
},
];
export const DEFAULT_SETTINGS: HermesChatSettings = {
gatewayUrl: "http://192.168.1.12:8642/v1",
apiKey: "0f224b188b52e8728d135ec339f8db8dfbf82d89a60021dcfe20d91c6174cf9b",
showModelInfo: true,
enableContextInjection: true,
chatFontSize: 14,
customCommands: [],
chatMessages: [],
chatSessionId: "",
};
export class HermesChatSettingTab extends PluginSettingTab {
plugin: HermesChatPlugin;
constructor(app: App, plugin: HermesChatPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Hermes Chat Settings" });
new Setting(containerEl)
.setName("Gateway URL")
.setDesc("Base URL of your Hermes Gateway API server (e.g. http://192.168.1.12:8642/v1)")
.addText((text) =>
text
.setPlaceholder("http://192.168.1.12:8642/v1")
.setValue(this.plugin.settings.gatewayUrl)
.onChange(async (value) => {
this.plugin.settings.gatewayUrl = value;
await this.plugin.saveData(this.plugin.settings);
})
);
new Setting(containerEl)
.setName("API Key")
.setDesc("API key for authenticating with the Hermes Gateway")
.addText((text) =>
text
.setPlaceholder("Enter your API key")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveData(this.plugin.settings);
})
);
new Setting(containerEl)
.setName("Show model info")
.setDesc("Display the current model name in the status bar")
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.showModelInfo)
.onChange(async (value) => {
this.plugin.settings.showModelInfo = value;
await this.plugin.saveData(this.plugin.settings);
})
);
new Setting(containerEl)
.setName("Enable context injection")
.setDesc("Automatically send the active note as context with each message")
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.enableContextInjection)
.onChange(async (value) => {
this.plugin.settings.enableContextInjection = value;
await this.plugin.saveData(this.plugin.settings);
})
);
new Setting(containerEl)
.setName("Chat font size")
.setDesc("Font size for chat messages and input (10—24 px)")
.addSlider((slider) =>
slider
.setLimits(10, 24, 1)
.setValue(this.plugin.settings.chatFontSize)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.chatFontSize = value;
await this.plugin.saveSettings();
})
);
// --- Slash commands ---
containerEl.createEl("h3", { text: "Slash Commands" });
containerEl.createEl("p", {
text: "Type / in the chat input to see available commands. Built-in: /prep-session, /sync-content, /lint-wiki. Add custom commands below as JSON.",
});
const commandsDesc = containerEl.createDiv("setting-item-description");
commandsDesc.setText(
'Format: [{"trigger":"/my-cmd","description":"What it does","prompt":"Full prompt text..."}]'
);
const textarea = containerEl.createEl("textarea");
textarea.setAttribute("rows", "6");
textarea.style.width = "100%";
textarea.style.fontFamily = "monospace";
textarea.style.fontSize = "12px";
textarea.value = JSON.stringify(this.plugin.settings.customCommands, null, 2);
textarea.addEventListener("change", async () => {
try {
const parsed = JSON.parse(textarea.value);
if (Array.isArray(parsed)) {
this.plugin.settings.customCommands = parsed;
await this.plugin.saveData(this.plugin.settings);
}
} catch {
// Invalid JSON — don't save, user is still typing
}
});
}
}