From 3b91ce3068c116ac1ab485f39766b7983bead24e Mon Sep 17 00:00:00 2001 From: keligrubb Date: Sun, 11 Jan 2026 21:41:30 -0500 Subject: [PATCH] improve and fix ci stuff. cleanup debug --- dungeonGenerator.js | 24 +++++++++++++++--- dungeonTemplate.js | 60 +++++++++++++++++++++++++++++++++++++++------ index.js | 7 +----- ollamaClient.js | 55 +++-------------------------------------- 4 files changed, 78 insertions(+), 68 deletions(-) diff --git a/dungeonGenerator.js b/dungeonGenerator.js index 8849715..509fb7d 100644 --- a/dungeonGenerator.js +++ b/dungeonGenerator.js @@ -21,11 +21,29 @@ function parseList(raw) { function parseObjects(raw, type = "rooms") { const cleanedRaw = raw.replace(/Intermediate Rooms:/i, "").replace(/Climax Room:/i, "").trim(); const mapper = (entry) => { + if (type === "encounters") { + // For encounters, format is "Encounter Name: Location Name: details" + // We want to keep the encounter name separate and preserve the location:details structure + const parts = entry.split(/:/); + if (parts.length >= 3) { + // Format: "Encounter Name: Location Name: details" + return { + name: parts[0].trim(), + details: parts.slice(1).join(":").trim() // Keep "Location Name: details" + }; + } else if (parts.length === 2) { + // Format: "Encounter Name: details" (fallback) + return { + name: parts[0].trim(), + details: parts[1].trim() + }; + } + } + // For other types, use original logic const [name, ...descParts] = entry.split(/[-–—:]/); const desc = descParts.join(" ").trim(); const obj = { name: name.trim() }; if (type === "rooms") return { ...obj, description: desc }; - if (type === "encounters") return { ...obj, details: desc }; if (type === "npcs") return { ...obj, trait: desc }; if (type === "treasure") return { ...obj, description: desc }; return entry; @@ -373,7 +391,7 @@ Generate the rest of the dungeon's content to fill the space between the entranc - Hidden aspects discoverable through interaction or investigation Format as "Name: description" using colons, NOT em-dashes. -- **Strictly 6 Encounters:** Numbered 1-6 (for d6 rolling). Name (max 6 words) and details (90-120 words per encounter). Each encounter must: +- **Strictly 6 Encounters:** Numbered 1-6 (for d6 rolling). Name (max 6 words) and details (2-3 sentences MAX, approximately 30-50 words). Each encounter must: - Start with the room/location name followed by a colon, then the details (e.g., "Location Name: Details text") - The location name must match one of the actual room names from this dungeon - Include environmental hazards/opportunities (cover, elevation, traps, interactable objects, terrain features) @@ -491,7 +509,7 @@ Each resolution should: - Integrate NPCs, faction dynamics, and player actions - Include failure states or unexpected outcomes as options - Reflect different approaches players might take -Keep each item to 50-60 words (2-3 sentences). Output as a numbered list, plain text only. Do not use em-dashes (—) anywhere in the output. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`, +Keep each item to 1-2 sentences MAX (approximately 20-30 words). Be extremely concise. Output as a numbered list, plain text only. Do not use em-dashes (—) anywhere in the output. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`, undefined, 5, "Step 6: Plot Resolutions" ); const plotResolutions = parseList(plotResolutionsRaw); diff --git a/dungeonTemplate.js b/dungeonTemplate.js index b571022..6959a91 100644 --- a/dungeonTemplate.js +++ b/dungeonTemplate.js @@ -169,6 +169,8 @@ export function dungeonTemplate(data) { vertical-align: top; line-height: 1.3em; border-bottom: 1px solid #1a1a1a; + word-wrap: break-word; + overflow-wrap: break-word; } table tr:last-child td { border-bottom: 1px solid #1a1a1a; @@ -181,12 +183,14 @@ export function dungeonTemplate(data) { } .encounters-table td:nth-child(2) { font-weight: bold; - width: 30%; + width: 25%; padding-right: 0.5em; border-right: 1px solid #1a1a1a; } .encounters-table td:nth-child(3) { width: auto; + font-size: 0.75em; + line-height: 1.2em; } .map-page { display: flex; @@ -265,13 +269,35 @@ export function dungeonTemplate(data) {

Encounters (d6)

- ${data.encounters.map((encounter, index) => ` + ${data.encounters.map((encounter, index) => { + // Truncate details to 2-3 sentences max to prevent overflow + let details = encounter.details || ''; + // Remove location prefix from details if present (format: "Location Name: details") + details = details.replace(/^[^:]+:\s*/, ''); + // Split into sentences and keep only first 3 + const sentences = details.match(/[^.!?]+[.!?]+/g) || [details]; + if (sentences.length > 3) { + details = sentences.slice(0, 3).join(' ').trim(); + } + // Also limit by character count as fallback (max ~250 chars) + if (details.length > 250) { + details = details.substring(0, 247).trim(); + // Try to end at a sentence boundary + const lastPeriod = details.lastIndexOf('.'); + if (lastPeriod > 200) { + details = details.substring(0, lastPeriod + 1); + } else { + details += '...'; + } + } + return ` - + - `).join('')} + `; + }).join('')}
${index + 1} ${encounter.name}${encounter.details}${details}
@@ -304,11 +330,31 @@ export function dungeonTemplate(data) { ${data.plotResolutions && data.plotResolutions.length > 0 ? `

Plot Resolutions

- ${data.plotResolutions.map(resolution => ` + ${data.plotResolutions.map(resolution => { + // Truncate to 1-2 sentences max to prevent overflow + let text = resolution || ''; + // Split into sentences and keep only first 2 + const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]; + if (sentences.length > 2) { + text = sentences.slice(0, 2).join(' ').trim(); + } + // Also limit by character count as fallback (max ~150 chars) + if (text.length > 150) { + text = text.substring(0, 147).trim(); + // Try to end at a sentence boundary + const lastPeriod = text.lastIndexOf('.'); + if (lastPeriod > 100) { + text = text.substring(0, lastPeriod + 1); + } else { + text += '...'; + } + } + return `
- ${resolution} + ${text}
- `).join('')} + `; + }).join('')}
` : ''} diff --git a/index.js b/index.js index bcdd36e..9082d2b 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ import "dotenv/config"; import { generateDungeon } from "./dungeonGenerator.js"; import { generateDungeonImages } from "./imageGenerator.js"; import { generatePDF } from "./generatePDF.js"; -import { OLLAMA_MODEL, listOpenWebUIModels } from "./ollamaClient.js"; +import { OLLAMA_MODEL } from "./ollamaClient.js"; // Utility to create a filesystem-safe filename from the dungeon title function slugify(text) { @@ -20,11 +20,6 @@ function slugify(text) { console.log("Using Ollama API URL:", process.env.OLLAMA_API_URL); console.log("Using Ollama model:", OLLAMA_MODEL); - // Try to list available models if using Open WebUI - if (process.env.OLLAMA_API_URL?.includes("/api/chat/completions")) { - await listOpenWebUIModels(); - } - // Generate the dungeon data const dungeonData = await generateDungeon(); diff --git a/ollamaClient.js b/ollamaClient.js index 0d42e42..4251956 100644 --- a/ollamaClient.js +++ b/ollamaClient.js @@ -47,13 +47,6 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) { ? { model, messages: [{ role: "user", content: prompt }] } : { model, prompt, stream: false }; - // Debug logging for Open WebUI - if (isUsingOpenWebUI) { - console.log(`[${stepName}] Using Open WebUI API`); - console.log(`[${stepName}] Model name: "${model}"`); - console.log(`[${stepName}] API URL: ${OLLAMA_API_URL}`); - } - const response = await fetch(OLLAMA_API_URL, { method: "POST", headers, @@ -68,14 +61,9 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) { } catch { // Ignore errors reading error response } - const errorMsg = `Ollama request failed: ${response.status} ${response.statusText}${errorDetails}`; - if (isUsingOpenWebUI) { - console.error(`[${stepName}] Request details:`); - console.error(` URL: ${OLLAMA_API_URL}`); - console.error(` Model: "${model}"`); - console.error(` Body: ${JSON.stringify(body, null, 2)}`); - } - throw new Error(errorMsg); + throw new Error( + `Ollama request failed: ${response.status} ${response.statusText}${errorDetails}`, + ); } const data = await response.json(); @@ -122,40 +110,3 @@ export async function callOllamaExplicit( ) { return callOllamaBase(prompt, model, retries, stepName, apiType); } - -// Helper function to list available models from Open WebUI -export async function listOpenWebUIModels() { - if (!OLLAMA_API_URL || !OLLAMA_API_URL.includes("/api/chat/completions")) { - console.log("Not using Open WebUI API, skipping model list"); - return null; - } - - try { - const headers = { "Content-Type": "application/json" }; - if (OLLAMA_API_KEY) { - headers["Authorization"] = `Bearer ${OLLAMA_API_KEY}`; - } - - // Try to get models from Open WebUI's models endpoint - // Open WebUI might have a /api/v1/models endpoint - const baseUrl = OLLAMA_API_URL.replace("/api/chat/completions", ""); - const modelsUrl = `${baseUrl}/api/v1/models`; - - const response = await fetch(modelsUrl, { - method: "GET", - headers, - }); - - if (response.ok) { - const data = await response.json(); - console.log("Available models from Open WebUI:", JSON.stringify(data, null, 2)); - return data; - } else { - console.log(`Could not list models: ${response.status} ${response.statusText}`); - return null; - } - } catch (err) { - console.log(`Error listing models: ${err.message}`); - return null; - } -}