improve and fix ci stuff. cleanup debug
This commit is contained in:
@@ -21,11 +21,29 @@ function parseList(raw) {
|
|||||||
function parseObjects(raw, type = "rooms") {
|
function parseObjects(raw, type = "rooms") {
|
||||||
const cleanedRaw = raw.replace(/Intermediate Rooms:/i, "").replace(/Climax Room:/i, "").trim();
|
const cleanedRaw = raw.replace(/Intermediate Rooms:/i, "").replace(/Climax Room:/i, "").trim();
|
||||||
const mapper = (entry) => {
|
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 [name, ...descParts] = entry.split(/[-–—:]/);
|
||||||
const desc = descParts.join(" ").trim();
|
const desc = descParts.join(" ").trim();
|
||||||
const obj = { name: name.trim() };
|
const obj = { name: name.trim() };
|
||||||
if (type === "rooms") return { ...obj, description: desc };
|
if (type === "rooms") return { ...obj, description: desc };
|
||||||
if (type === "encounters") return { ...obj, details: desc };
|
|
||||||
if (type === "npcs") return { ...obj, trait: desc };
|
if (type === "npcs") return { ...obj, trait: desc };
|
||||||
if (type === "treasure") return { ...obj, description: desc };
|
if (type === "treasure") return { ...obj, description: desc };
|
||||||
return entry;
|
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
|
- Hidden aspects discoverable through interaction or investigation
|
||||||
Format as "Name: description" using colons, NOT em-dashes.
|
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")
|
- 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
|
- 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)
|
- 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
|
- Integrate NPCs, faction dynamics, and player actions
|
||||||
- Include failure states or unexpected outcomes as options
|
- Include failure states or unexpected outcomes as options
|
||||||
- Reflect different approaches players might take
|
- 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"
|
undefined, 5, "Step 6: Plot Resolutions"
|
||||||
);
|
);
|
||||||
const plotResolutions = parseList(plotResolutionsRaw);
|
const plotResolutions = parseList(plotResolutionsRaw);
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ export function dungeonTemplate(data) {
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
border-bottom: 1px solid #1a1a1a;
|
border-bottom: 1px solid #1a1a1a;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
table tr:last-child td {
|
table tr:last-child td {
|
||||||
border-bottom: 1px solid #1a1a1a;
|
border-bottom: 1px solid #1a1a1a;
|
||||||
@@ -181,12 +183,14 @@ export function dungeonTemplate(data) {
|
|||||||
}
|
}
|
||||||
.encounters-table td:nth-child(2) {
|
.encounters-table td:nth-child(2) {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 30%;
|
width: 25%;
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
border-right: 1px solid #1a1a1a;
|
border-right: 1px solid #1a1a1a;
|
||||||
}
|
}
|
||||||
.encounters-table td:nth-child(3) {
|
.encounters-table td:nth-child(3) {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 1.2em;
|
||||||
}
|
}
|
||||||
.map-page {
|
.map-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -265,13 +269,35 @@ export function dungeonTemplate(data) {
|
|||||||
<h2>Encounters (d6)</h2>
|
<h2>Encounters (d6)</h2>
|
||||||
<table class="encounters-table">
|
<table class="encounters-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
${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 `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td><strong>${encounter.name}</strong></td>
|
<td><strong>${encounter.name}</strong></td>
|
||||||
<td>${encounter.details}</td>
|
<td>${details}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,11 +330,31 @@ export function dungeonTemplate(data) {
|
|||||||
${data.plotResolutions && data.plotResolutions.length > 0 ? `
|
${data.plotResolutions && data.plotResolutions.length > 0 ? `
|
||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<h2>Plot Resolutions</h2>
|
<h2>Plot Resolutions</h2>
|
||||||
${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 `
|
||||||
<div class="plot-resolution">
|
<div class="plot-resolution">
|
||||||
${resolution}
|
${text}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
index.js
7
index.js
@@ -2,7 +2,7 @@ import "dotenv/config";
|
|||||||
import { generateDungeon } from "./dungeonGenerator.js";
|
import { generateDungeon } from "./dungeonGenerator.js";
|
||||||
import { generateDungeonImages } from "./imageGenerator.js";
|
import { generateDungeonImages } from "./imageGenerator.js";
|
||||||
import { generatePDF } from "./generatePDF.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
|
// Utility to create a filesystem-safe filename from the dungeon title
|
||||||
function slugify(text) {
|
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 API URL:", process.env.OLLAMA_API_URL);
|
||||||
console.log("Using Ollama model:", OLLAMA_MODEL);
|
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
|
// Generate the dungeon data
|
||||||
const dungeonData = await generateDungeon();
|
const dungeonData = await generateDungeon();
|
||||||
|
|
||||||
|
|||||||
@@ -47,13 +47,6 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) {
|
|||||||
? { model, messages: [{ role: "user", content: prompt }] }
|
? { model, messages: [{ role: "user", content: prompt }] }
|
||||||
: { model, prompt, stream: false };
|
: { 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, {
|
const response = await fetch(OLLAMA_API_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
@@ -68,14 +61,9 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) {
|
|||||||
} catch {
|
} catch {
|
||||||
// Ignore errors reading error response
|
// Ignore errors reading error response
|
||||||
}
|
}
|
||||||
const errorMsg = `Ollama request failed: ${response.status} ${response.statusText}${errorDetails}`;
|
throw new Error(
|
||||||
if (isUsingOpenWebUI) {
|
`Ollama request failed: ${response.status} ${response.statusText}${errorDetails}`,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -122,40 +110,3 @@ export async function callOllamaExplicit(
|
|||||||
) {
|
) {
|
||||||
return callOllamaBase(prompt, model, retries, stepName, apiType);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user