+ )}
+
+ )}
diff --git a/src/components/FirstRunWizard.tsx b/src/components/FirstRunWizard.tsx
new file mode 100644
index 0000000..3d0ac63
--- /dev/null
+++ b/src/components/FirstRunWizard.tsx
@@ -0,0 +1,300 @@
+import { useEffect, useState } from "react";
+import { invoke } from "@tauri-apps/api/core";
+import { useToast } from "./Toast";
+
+export function FirstRunWizard({ onComplete }: { onComplete: () => void }) {
+ const [step, setStep] = useState("welcome");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [models, setModels] = useState([]);
+ const [selectedModel, setSelectedModel] = useState("");
+ // ponytail: wizard defaults to local Ollama; no API-key UI here (Settings covers remote).
+ const apiUrl = "http://localhost:11434";
+ const apiKey = "";
+ const { addToast } = useToast();
+
+ useEffect(() => {
+ if (step === "check") {
+ checkOllama();
+ }
+ }, [step]);
+
+ async function checkOllama() {
+ setLoading(true);
+ setError("");
+ try {
+ // Test connection to the default Ollama URL
+ const result = await invoke<{ ok: boolean; models: string[]; error: string }>(
+ "test_connection",
+ { config: { api_url: apiUrl, api_key: apiKey, model: "", temperature: 0.7, max_tokens: 512, top_p: 0.9, image_model: "", embed_model: "" } }
+ );
+ if (result.ok) {
+ setModels(result.models);
+ setStep("model");
+ } else {
+ setError(result.error || "Failed to connect to Ollama");
+ setStep("error");
+ }
+ } catch (e) {
+ setError(String(e));
+ setStep("error");
+ }
+ setLoading(false);
+ }
+
+ async function pullModel(model: string) {
+ setLoading(true);
+ setError("");
+ try {
+ // We don't have a direct pull command, but we can suggest the user to run `ollama pull` in terminal.
+ // For now, we'll just set the model in config and hope it exists.
+ // Alternatively, we could invoke a generate command to trigger a pull? Not sure.
+ // We'll just set the config and complete.
+ await invoke("set_llm_config", {
+ config: {
+ api_url: apiUrl,
+ api_key: apiKey,
+ model: model,
+ temperature: 0.7,
+ max_tokens: 512,
+ top_p: 0.9,
+ image_model: "x/flux2-klein:4b",
+ embed_model: "nomic-embed-text",
+ }
+ });
+ addToast(`Model ${model} selected. You may need to run 'ollama pull ${model}' if not already downloaded.`, "success");
+ setStep("done");
+ } catch (e) {
+ setError(String(e));
+ setStep("error");
+ }
+ setLoading(false);
+ }
+
+ function skip() {
+ // Skip wizard and go to settings
+ onComplete();
+ }
+
+ function back() {
+ if (step === "model") {
+ setStep("welcome");
+ } else if (step === "error") {
+ setStep("welcome");
+ } else if (step === "done") {
+ // Should not happen
+ }
+ }
+
+ async function handleSubmit() {
+ if (step === "welcome") {
+ setStep("check");
+ return;
+ }
+ if (step === "model") {
+ if (!selectedModel) {
+ setError("Please select a model");
+ return;
+ }
+ await pullModel(selectedModel);
+ return;
+ }
+ if (step === "done") {
+ onComplete();
+ return;
+ }
+ }
+
+ if (step === "welcome") {
+ return (
+
+
+ Welcome to DM-Pal
+
+
+ To generate content, DM-Pal needs a language model. Let's set up a local model using Ollama (recommended for privacy and offline use).
+
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+ );
+ }
+
+ if (step === "check") {
+ return (
+
+
+ Checking for Ollama
+
+
+ We're checking if Ollama is running at {apiUrl}...
+
+ {loading ? (
+
+
+
Checking…
+
+ ) : (
+ <>
+ {error ? (
+
{error}
+ ) : (
+
Ollama is running!
+ )}
+
+
+
+
+ >
+ )}
+
+ );
+ }
+
+ if (step === "model") {
+ return (
+
+
+ Select a Model
+
+
+ Ollama is running. Choose a model to use for text generation.
+
+
+
+
+
+
+
+
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+ If you don't see your model, you may need to download it first. In a terminal, run:{' '}
+ ollama pull llama3.2
+
+
+ );
+ }
+
+ if (step === "done") {
+ return (
+
+
+ Setup Complete
+
+
+ DM-Pal is now configured to use {selectedModel} for text generation.
+ Image generation uses x/flux2-klein:4b (macOS-only via Ollama).
+ Embedding model for lore search is set to nomic-embed-text.
+
+
+
+
+
+
+ );
+ }
+
+ if (step === "error") {
+ return (
+
+
+ Error
+
+
+ {error}
+
+
+
+
+
+
+ );
+ }
+
+ return null;
+}
\ No newline at end of file
diff --git a/src/components/WorldBuilder.tsx b/src/components/WorldBuilder.tsx
index 347e1df..4b5f4c9 100644
--- a/src/components/WorldBuilder.tsx
+++ b/src/components/WorldBuilder.tsx
@@ -7,6 +7,7 @@ import { addGeneration, extractTitle, type Generation } from "../lib/generations
import { usePrefillEffect } from "../lib/usePrefill";
import { usePersistentState } from "../lib/usePersistentState";
import { WorldMap } from "./WorldMap";
+import { GeneratedImage } from "./GeneratedImage";
interface WorldResponse {
name: string;
@@ -31,6 +32,8 @@ export function WorldBuilder({ prefill, onPrefillConsumed }: Props = {}) {
const [world, setWorld] = usePersistentState("world.last", null);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState(null);
+ const [mapPrompt, setMapPrompt] = useState(null);
+ const [mapNonce, setMapNonce] = useState(0);
const { addToast } = useToast();
// ponytail: record a dragged pin's position into the world so it persists.
@@ -98,6 +101,10 @@ IMPORTANT: Return ONLY a raw JSON object. NO markdown fences, NO code blocks, NO
`World: ${parsed.name || theme}`,
`${parsed.name || "World"} (${theme}). ${parsed.description}\nRegions: ${regions}\nLandmarks: ${landmarks}\nConflicts: ${conflicts}\nCultures: ${cultures}`,
);
+ // Generate map image prompt
+ const mapPrompt = `fantasy map of ${parsed.name}, ${parsed.description}, parchment texture, ink cartography, top-down`;
+ setMapPrompt(mapPrompt);
+ setMapNonce((n) => n + 1);
} else {
addToast("LLM returned an unparseable response", "error");
}
@@ -157,6 +164,11 @@ IMPORTANT: Return ONLY a raw JSON object. NO markdown fences, NO code blocks, NO
{world.description}
+ {mapPrompt && (
+
+
+
+ )}
{/* Map + side details. Two-pane on wide screens; stacked on narrow. */}
diff --git a/src/lib/encounter-budget.test.ts b/src/lib/encounter-budget.test.ts
new file mode 100644
index 0000000..39b2500
--- /dev/null
+++ b/src/lib/encounter-budget.test.ts
@@ -0,0 +1,6 @@
+// ponytail: reuses the in-source demo() self-check so there's one set of
+// assertions, not two. Run: node scripts/check-encounter-budget.ts (or any
+// TS runner). No test framework, no @types/node dependency.
+import { demo } from "./encounter-budget";
+
+demo();