Writingway2/main.html

2275 lines
No EOL
162 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writingway 2.0</title>
<!-- Note: This file should be named main.html and opened via start.bat -->
<script src="https://cdn.jsdelivr.net/npm/dexie@3.2.4/dist/dexie.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script defer src="src/prompts.js"></script>
<script defer src="src/generation.js"></script>
<script defer src="src/ai.js"></script>
<script defer src="src/save-utils.js"></script>
<script defer src="src/modules/project-manager.js"></script>
<script defer src="src/modules/chapter-manager.js"></script>
<script defer src="src/modules/scene-manager.js"></script>
<script defer src="src/modules/beat-mentions.js"></script>
<script defer src="src/modules/compendium-manager.js"></script>
<script defer src="src/modules/ai-settings.js"></script>
<script defer src="src/modules/github-backup.js"></script>
<script defer src="src/modules/tab-sync.js"></script>
<script defer src="src/workshop.js"></script>
<script defer src="src/update-checker.js"></script>
<script defer src="src/tts.js"></script>
<script defer src="src/w1-importer.js"></script>
<script defer src="src/app.js"></script>
<script defer src="src/compendium.js"></script>
<!-- Local vendor Alpine (use a real alpine.min.js here for offline runs) -->
<script defer src="src/vendor/alpine.min.js"></script>
<link rel="stylesheet" href="src/styles.css">
</head>
<body>
<!-- Initial Loading Screen -->
<div id="loadingScreen"
style="position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg, #1a1f2e 0%, #2d1b3d 100%);display:flex;align-items:center;justify-content:center;z-index:99999;transition:opacity 0.5s ease-out;">
<div style="text-align:center;max-width:500px;padding:40px;">
<div style="font-size:48px;margin-bottom:24px;">✍️</div>
<h1 style="margin:0 0 16px 0;color:#4a9eff;font-size:32px;font-weight:700;">Writingway</h1>
<p id="loadingStatus" style="color:rgba(255,255,255,0.8);font-size:16px;margin:16px 0;min-height:24px;">
Starting up...</p>
<!-- Loading bar -->
<div
style="width:100%;height:4px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;margin:24px 0;">
<div id="loadingBar"
style="height:100%;background:linear-gradient(90deg, #4a9eff, #60a5fa);width:0%;transition:width 0.3s ease-out;">
</div>
</div>
<p style="color:rgba(255,255,255,0.5);font-size:13px;line-height:1.6;margin:16px 0 0 0;">
<span id="loadingTip">Loading your workspace...</span>
</p>
</div>
</div>
<div x-data="app" x-init="init()">
<!-- Projects Landing Page -->
<template x-if="showProjectsView && !showModelLoading">
<div
style="min-height:100vh;background:linear-gradient(135deg, #1a1f2e 0%, #2d1b3d 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;position:relative;">
<!-- Header with Settings -->
<div style="position:absolute;top:20px;right:20px;">
<button class="btn btn-secondary" @click="showAISettings = true" style="padding:8px 16px;"
title="Settings">⚙️ Settings</button>
</div>
<!-- Title -->
<div style="text-align:center;margin-bottom:40px;">
<div style="font-size:64px;margin-bottom:16px;">✍️</div>
<h1 style="margin:0 0 8px 0;color:#4a9eff;font-size:42px;font-weight:700;">Writingway 2</h1>
<p style="color:rgba(255,255,255,0.6);font-size:16px;margin:0;">Select a project to begin writing
</p>
</div>
<!-- Project Carousel -->
<div style="position:relative;width:100%;max-width:800px;">
<template x-if="projects.length === 0">
<div
style="text-align:center;padding:60px 20px;background:rgba(0,0,0,0.3);border-radius:16px;border:2px dashed rgba(255,255,255,0.2);">
<div style="font-size:48px;margin-bottom:16px;opacity:0.5;">📚</div>
<p style="color:rgba(255,255,255,0.7);font-size:18px;margin:0 0 24px 0;">No projects yet</p>
<button class="btn btn-primary" @click="showNewProjectModal = true"
style="padding:12px 24px;font-size:16px;"> Create Your First Project</button>
</div>
</template>
<template x-if="projects.length > 0">
<div>
<!-- Navigation Arrows -->
<template x-if="projects.length > 1">
<div>
<button
@click="currentProjectCarouselIndex = (currentProjectCarouselIndex - 1 + projects.length) % projects.length"
class="btn btn-secondary"
style="position:absolute;left:-60px;top:50%;transform:translateY(-50%);padding:12px 16px;font-size:20px;z-index:10;"
title="Previous project"></button>
<button
@click="currentProjectCarouselIndex = (currentProjectCarouselIndex + 1) % projects.length"
class="btn btn-secondary"
style="position:absolute;right:-60px;top:50%;transform:translateY(-50%);padding:12px 16px;font-size:20px;z-index:10;"
title="Next project"></button>
</div>
</template>
<!-- Project Card -->
<template x-for="(project, index) in projects" :key="project.id">
<div x-show="index === currentProjectCarouselIndex"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
x-data="{ menuOpen: false }"
style="background:rgba(0,0,0,0.4);border:2px solid rgba(74,158,255,0.3);border-radius:16px;padding:40px;position:relative;backdrop-filter:blur(10px);">
<!-- Card Menu Button -->
<div style="position:absolute;top:8px;right:8px;padding:8px;">
<button @click.stop="menuOpen = !menuOpen" class="btn btn-secondary"
style="padding:10px 16px;font-size:20px;min-width:48px;min-height:48px;"
title="Project options"></button>
<div x-show="menuOpen" x-cloak @click.away="menuOpen = false" x-transition
class="action-menu" style="right:8px;left:auto;min-width:180px;">
<button @click.stop="openProject(project.id); menuOpen=false">📖
Open</button>
<button @click.stop="renameProject(project); menuOpen=false">✏️
Rename</button>
<button
@click.stop="document.getElementById('coverInput-' + project.id).click(); menuOpen=false">🖼️
Change Cover</button>
<button x-show="project.coverImage"
@click.stop="removeProjectCover(project.id); menuOpen=false">🗑️
Remove Cover</button>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<button @click.stop="deleteProject(project.id); menuOpen=false"
style="color:#ff6b6b;">🗑️ Delete</button>
</div>
</div>
<!-- Hidden file input for cover upload -->
<input type="file" :id="'coverInput-' + project.id" accept="image/*"
@change="updateProjectCover(project.id, $event.target.files[0]); $event.target.value = null"
style="display:none;">
<!-- Click card to open -->
<div @click="openProject(project.id)" style="cursor:pointer;text-align:center;"
@mouseenter="$el.style.transform = 'scale(1.02)'"
@mouseleave="$el.style.transform = 'scale(1)'"
style="transition:transform 0.2s ease;">
<!-- Cover Image or Fallback -->
<div style="margin-bottom:24px;">
<template x-if="project.coverImage">
<img :src="project.coverImage"
style="max-width:240px;max-height:360px;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);"
alt="Project cover">
</template>
<template x-if="!project.coverImage">
<div style="font-size:72px;">📖</div>
</template>
</div>
<h2 style="margin:0 0 16px 0;color:#fff;font-size:32px;font-weight:700;"
x-text="project.name"></h2>
<p style="color:rgba(255,255,255,0.6);font-size:14px;margin:0;">
<span
x-text="'Last modified: ' + new Date(project.modified).toLocaleDateString()"></span>
</p>
<p style="color:rgba(255,255,255,0.4);font-size:13px;margin:8px 0 0 0;">Click to
open</p>
</div>
</div>
</template>
<!-- Project Counter -->
<template x-if="projects.length > 1">
<div
style="text-align:center;margin-top:24px;color:rgba(255,255,255,0.5);font-size:14px;">
<span x-text="(currentProjectCarouselIndex + 1) + ' / ' + projects.length"></span>
</div>
</template>
</div>
</template>
</div>
<!-- New Project Button -->
<div style="margin-top:40px;display:flex;gap:16px;align-items:center;justify-content:center;">
<button class="btn btn-primary" @click="showNewProjectModal = true"
style="padding:12px 24px;font-size:16px;"> New Project</button>
<button class="btn btn-secondary" @click="showW1ImportModal = true"
style="padding:12px 24px;font-size:16px;" title="Migrate from Writingway 1">📥 Import from
W1</button>
</div>
</div>
</template>
<!-- Main App -->
<template x-if="currentProject && !showModelLoading && !showProjectsView">
<div class="app-container">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header" style="position:relative;">
<h1>Writingway</h1>
<p>AI-Powered Creative Writing</p>
<!-- Main Menu Button -->
<div x-data="{ mainMenuOpen: false }" style="position:absolute;top:12px;right:12px;">
<button class="btn btn-secondary" @click.stop="mainMenuOpen = !mainMenuOpen"
style="padding:6px 12px;font-size:18px;" title="Main Menu"></button>
<div x-show="mainMenuOpen" x-cloak @click.away="mainMenuOpen = false" x-transition
class="action-menu" style="right:0;left:auto;min-width:200px;">
<button @click.stop="backToProjects(); mainMenuOpen=false">◄ Back to Projects</button>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<button @click.stop="showNewProjectModal = true; mainMenuOpen=false"> New
Project</button>
<button
@click.stop="document.getElementById('importProjectInput').click(); mainMenuOpen=false">📥
Import Project</button>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<button @click.stop="exportProject(); mainMenuOpen=false" :disabled="!currentProject">📦
Export Current Project</button>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<button @click.stop="openCompendium(); mainMenuOpen=false">📚 Compendium</button>
<button @click.stop="showWorkshopChat = true; mainMenuOpen=false">💬 Workshop
Chat</button>
<button @click.stop="showAISettings = true; mainMenuOpen=false">⚙️ AI Settings</button>
<button @click.stop="showPromptsPanel = true; mainMenuOpen=false">📝 Manage
Prompts</button>
<button @click.stop="openBackupSettings(); mainMenuOpen=false">☁️ Cloud Backup</button>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<button @click.stop="checkForUpdates(); mainMenuOpen=false"
:disabled="checkingForUpdates">
<span x-show="!checkingForUpdates">🔄 Check for Updates</span>
<span x-show="checkingForUpdates">⏳ Checking...</span>
</button>
</div>
</div>
</div>
<!-- Hidden file input for import (global scope) -->
<input type="file" id="importProjectInput" accept=".zip"
@change="importProject($event); $event.target.value = null" style="display:none;">
<!-- AI Status -->
<div class="ai-status">
<div class="ai-status-item"
style="cursor:pointer;padding:10px 16px;border-radius:8px;transition:background 0.2s;"
@click="showAISettings = !showAISettings"
@mouseenter="$el.style.background='rgba(74,158,255,0.1)'"
@mouseleave="$el.style.background='transparent'" title="Click to toggle AI settings"
aria-label="Toggle AI settings" role="button">
<div class="status-dot" :class="aiStatus"></div>
<span x-text="aiStatusText"></span>
<span style="margin-left:8px;opacity:0.5;font-size:12px;"></span>
</div>
<!-- Backup Status -->
<div x-show="backupEnabled" class="ai-status-item"
style="cursor:pointer;padding:8px 16px;border-radius:8px;transition:background 0.2s;font-size:12px;"
@click="openBackupSettings()" @mouseenter="$el.style.background='rgba(76,175,80,0.1)'"
@mouseleave="$el.style.background='transparent'" title="Click for backup settings">
<span>☁️</span>
<span x-text="backupStatus || 'Auto-backup active'"></span>
</div>
</div>
<div class="project-info" x-data="{ openMenu:false }"
style="position:relative;padding:16px 12px;border-bottom:2px solid rgba(74,158,255,0.3);">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
<h2 style="margin:0;padding-right:8px;">
<button class="project-name-button" @click.stop="openCompendium()"
title="Click to toggle Compendium" aria-controls="compendium-panel"
:aria-expanded="showCodexPanel ? 'true' : 'false'"
style="background:none;border:none;padding:8px 12px 8px 0;font-size:18px;font-weight:700;color:#4a9eff;cursor:pointer;border-radius:6px;transition:all 0.2s;"
@mouseenter="$el.style.background='rgba(74,158,255,0.15)'"
@mouseleave="$el.style.background='transparent'">
<span x-text="currentProject.name"></span>
<span style="margin-left:8px;opacity:0.7;font-size:16px;">📚</span>
</button>
</h2>
<div style="display:flex;align-items:center;gap:6px;">
<button class="menu-button" @click.stop="openMenu = !openMenu"
title="Project menu"></button>
<div x-show="openMenu" x-cloak @click.away="openMenu = false" x-transition
class="action-menu">
<button
@click.stop="showRenameProjectModal = true; renameProjectName = currentProject.name; openMenu=false">Rename</button>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<div style="max-height:200px;overflow:auto;margin-bottom:6px;">
<strong
style="display:block;padding:6px 10px;color:var(--text-secondary);font-size:12px;">Switch
project</strong>
<template x-for="p in projects" :key="p.id">
<button @click.stop="selectProject(p.id); openMenu=false"
style="padding-left:10px;">
<span x-text="p.name"></span>
</button>
</template>
</div>
<div style="border-top:1px solid var(--border);margin:6px 0"></div>
<button @click.stop="exportProject(); openMenu=false">📦 Export Project</button>
<div style="margin-top:6px"></div>
<button @click.stop="showNewProjectModal = true; openMenu=false"> New
Project</button>
</div>
</div>
</div>
<div class="word-count" x-text="`${totalWords} words`" style="margin-top:6px;"></div>
</div>
<div class="scene-list">
<template x-for="chapter in chapters" :key="chapter.id">
<div class="chapter-item">
<div class="chapter-header" style="align-items:center;gap:8px;"
x-data="{ openMenu:false }">
<div
style="flex:1;display:flex;justify-content:space-between;align-items:center;position:relative;">
<div style="flex:1;display:flex;align-items:center;gap:8px;">
<span class="summary-dot"
:class="{ 'has': chapter.summary && !chapter.summaryStale, 'stale': chapter.summaryStale, 'none': !chapter.summary }"
:title="`${chapter.title}: ${chapter.summary ? (chapter.summaryStale ? 'Summary outdated' : 'Has summary') : 'No summary yet'}`"
:aria-label="`${chapter.title}: ${chapter.summary ? (chapter.summaryStale ? 'Summary outdated' : 'Has summary') : 'No summary yet'}`"></span>
<div style="flex:1;" x-text="chapter.title"></div>
<div
style="font-size:12px;color:var(--text-secondary);display:flex;align-items:center;gap:6px;">
<span x-text="`${chapter.scenes ? chapter.scenes.length : 0}`"></span>
</div>
</div>
<div class="item-actions" style="margin-left:8px;">
<button class="menu-button" @click.stop="openMenu = !openMenu" title="More">
</button>
<div x-show="openMenu" x-cloak @click.away="openMenu = false" x-transition
class="action-menu">
<button
@click.stop="chapter.expanded = !chapter.expanded; openMenu=false"
x-text="chapter.expanded ? 'Collapse' : 'Expand'"></button>
<button
@click.stop="openRenameChapterModal(chapter.id); openMenu=false">Rename
chapter</button>
<button
@click.stop="openChapterSummary(chapter.id); openMenu=false">Summary</button>
<button @click.stop="moveChapterUp(chapter.id); openMenu=false">Move
chapter up</button>
<button @click.stop="moveChapterDown(chapter.id); openMenu=false">Move
chapter down</button>
<button @click.stop="deleteChapter(chapter.id); openMenu=false">Delete
chapter</button>
</div>
</div>
</div>
</div>
<div class="chapter-scenes" x-show="chapter.expanded">
<template x-for="scene in chapter.scenes" :key="scene.id">
<div class="scene-item"
:class="{ 'active': currentScene && currentScene.id === scene.id }"
@click="loadScene(scene.id)"
:title="`${scene.title}${scene.povCharacter ? ' (POV: ' + scene.povCharacter + ')' : ''}\n${scene.summary ? (scene.summaryStale ? 'Summary outdated' : 'Has summary') : 'No summary yet'}`"
:aria-label="`${scene.title}: ${scene.summary ? (scene.summaryStale ? 'Summary outdated' : 'Has summary') : 'No summary yet'}`"
style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;align-items:center;gap:8px;">
<span class="summary-dot"
:class="{ 'has': scene.summary && !scene.summaryStale, 'stale': scene.summaryStale, 'none': !scene.summary }"
:title="`${scene.title}: ${scene.summary ? (scene.summaryStale ? 'Summary outdated' : 'Has summary') : 'No summary yet'}`"
:aria-label="`${scene.title}: ${scene.summary ? (scene.summaryStale ? 'Summary outdated' : 'Has summary') : 'No summary yet'}`"></span>
<div>
<div class="scene-title" x-text="scene.title"></div>
<div class="scene-meta" x-text="`${scene.wordCount || 0} words`">
</div>
</div>
</div>
<div class="item-actions" style="margin-left:12px;"
x-data="{ openMenu:false }">
<!-- Backdrop overlay -->
<div x-show="openMenu" x-cloak @click="openMenu = false"
style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:9998;"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
</div>
<button class="menu-button" @click.stop="openMenu = !openMenu"
title="More">
</button>
<div x-show="openMenu" x-cloak @mouseenter.stop @mousemove.stop
x-transition class="action-menu" @click.stop x-ref="actionMenu">
<div
style="font-size:13px;color:var(--text-secondary);padding:4px 0 8px 0;border-bottom:1px solid var(--border);margin-bottom:8px;">
Move to:
</div>
<template x-for="ch in chapters" :key="ch.id">
<button
@click.stop="moveSceneToChapter(scene.id, ch.id); openMenu=false"
:style="ch.id === scene.chapterId ? 'background: var(--bg-tertiary);' : ''"
x-text="ch.title">
</button>
</template>
<div
style="border-top:1px solid var(--border);margin-top:8px;padding-top:8px;">
</div>
<button @click.stop="moveSceneUp(scene.id); openMenu=false">Move
up</button>
<button @click.stop="moveSceneDown(scene.id); openMenu=false">Move
down</button>
<button
@click.stop="openRenameSceneModal(scene.id); openMenu=false">Rename
scene</button>
<button
@click.stop="openSceneSummary(scene.id); openMenu=false">Summary</button>
<button @click.stop="deleteScene(scene.id); openMenu=false">Delete
scene</button>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
<div class="sidebar-actions"
style="display:flex;flex-direction:row;padding:10px;gap:8px;align-items:center;">
<button class="add-scene-btn" @click="openNewChapterModal()" aria-label="New chapter"
title="New Chapter">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"
fill="currentColor" />
</svg>
</button>
<button class="add-scene-btn" @click="openNewSceneModal()" aria-label="New scene"
title="New Scene">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM14 3.5L18.5 8H15a1 1 0 0 1-1-1V3.5z"
fill="currentColor" />
<path d="M8 12h8v2H8v-2zm0 4h5v2H8v-2z" fill="currentColor" />
</svg>
</button>
</div>
</div>
<!-- Editor -->
<div class="editor-container">
<div class="editor-header">
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;">
<div class="editor-title" x-text="currentScene ? currentScene.title : 'No scene selected'">
</div>
<div class="editor-stats" style="white-space:nowrap;">
<span x-text="`${currentSceneWords} words`"></span>
<span class="save-indicator" :class="{ 'saving': isSaving }" x-text="saveStatus"></span>
</div>
</div>
</div>
<!-- Formatting Toolbar (compact dropdown) -->
<div x-show="currentScene" x-cloak x-data="{ formatMenuOpen: false }"
style="padding:8px 30px;background:var(--bg-secondary);border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:center;">
<span style="font-size:12px;color:var(--text-secondary);">Format:</span>
<button @click="formatMenuOpen = !formatMenuOpen" class="btn btn-secondary"
style="position:relative;">
✎ Text Formatting
</button>
<!-- Preview Toggle Button -->
<button @click="showMarkdownPreview = !showMarkdownPreview" class="btn btn-secondary"
:class="{ 'btn-primary': showMarkdownPreview }"
:title="showMarkdownPreview ? 'Switch to edit mode' : 'Preview formatted text'">
<span x-show="!showMarkdownPreview">👁️ Preview</span>
<span x-show="showMarkdownPreview">✏️ Edit</span>
</button>
<!-- Rewrite Selection Button (only visible when text is selected) -->
<button x-show="showRewriteBtn && !showMarkdownPreview" @click="handleRewriteButtonClick()"
class="btn btn-secondary" title="Rewrite selected text with AI">
✨ Rewrite
</button>
<!-- TTS Read Aloud Button (only in preview mode) -->
<button x-show="showMarkdownPreview" @click="toggleTTS()" class="btn btn-secondary"
:class="{ 'btn-primary': isReading }"
:title="isReading ? 'Stop reading' : 'Read scene aloud'">
<span x-show="!isReading">🔊 Read Aloud</span>
<span x-show="isReading">⏸️ Stop</span>
</button>
<div x-show="formatMenuOpen" @click.away="formatMenuOpen = false" x-cloak
style="position:absolute;top:45px;left:80px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:1000;display:grid;grid-template-columns:1fr 1fr;gap:4px;min-width:280px;">
<button @click="applyFormatting('bold'); formatMenuOpen=false" class="btn btn-secondary"
style="justify-content:flex-start;font-weight:bold;" title="Wrap selection in **bold**">
<span style="width:20px;display:inline-block;">B</span> Bold
</button>
<button @click="applyFormatting('italic'); formatMenuOpen=false" class="btn btn-secondary"
style="justify-content:flex-start;font-style:italic;"
title="Wrap selection in *italic*">
<span style="width:20px;display:inline-block;">I</span> Italic
</button>
<button @click="applyFormatting('strikethrough'); formatMenuOpen=false"
class="btn btn-secondary"
style="justify-content:flex-start;text-decoration:line-through;"
title="Wrap selection in ~~strikethrough~~">
<span style="width:20px;display:inline-block;">S</span> Strike
</button>
<button @click="applyFormatting('heading'); formatMenuOpen=false" class="btn btn-secondary"
style="justify-content:flex-start;" title="Make selection a ## Heading">
<span style="width:20px;display:inline-block;">H</span> Heading
</button>
<button @click="applyFormatting('quote'); formatMenuOpen=false" class="btn btn-secondary"
style="justify-content:flex-start;" title="Make selection a > quote">
<span style="width:20px;display:inline-block;">"</span> Quote
</button>
</div>
</div>
<div class="editor-main">
<!-- Export Reminder Banner -->
<template x-if="showExportReminder && !exportReminderDismissed">
<div
style="background:linear-gradient(135deg, rgba(255,193,7,0.15), rgba(255,152,0,0.1));border-bottom:2px solid rgba(255,193,7,0.4);padding:12px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px;">
<div style="display:flex;align-items:center;gap:12px;flex:1;">
<span style="font-size:24px;">💾</span>
<div>
<div style="font-weight:600;color:var(--text-primary);margin-bottom:4px;">
Don't forget to backup your work!
</div>
<div style="font-size:12px;color:var(--text-secondary);line-height:1.4;">
Your project is saved in your browser. Export it regularly to keep a backup
on your computer.
</div>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button class="btn btn-primary" @click="exportProject()"
style="padding:8px 16px;font-size:13px;white-space:nowrap;">
📦 Export Now
</button>
<button class="btn btn-secondary" @click="dismissExportReminder()"
style="padding:8px 12px;font-size:13px;">
Dismiss
</button>
</div>
</div>
</template>
<template x-if="currentScene">
<div style="position:relative;height:100%;display:flex;flex-direction:column;">
<!-- Edit Mode: Textarea -->
<textarea x-show="!showMarkdownPreview" class="editor-textarea"
:class="{ 'has-generated-text': showGeneratedHighlight }"
@input="onEditorChange($event); handleAutoReplace($event)" @paste="handlePaste"
x-model="currentScene.content" style="outline:none;direction:ltr;resize:none;"
dir="ltr" placeholder="Start writing your scene here..."></textarea>
<!-- Preview Mode: Rendered HTML -->
<div x-show="showMarkdownPreview" class="editor-preview"
x-html="markdownToHtml(currentScene.content)"
style="flex:1;padding:50px 60px;background:var(--bg-primary);overflow-y:auto;direction:ltr;font-family:'Georgia',serif;font-size:17px;line-height:2.0;color:#e8e8e8;">
</div>
</div>
</template> <template x-if="!currentScene">
<div class="welcome-screen">
<div class="welcome-content">
<h2>Ready to Write</h2>
<p>Select a scene from the sidebar or create a new one to get started.</p>
</div>
</div>
</template>
</div>
<!-- Beat Panel -->
<div class="beat-separator" aria-hidden="true"></div>
<div class="beat-panel">
<h3>Generate from Beat</h3>
<div class="beat-input-group" style="position:relative;">
<textarea class="beat-input" x-model="beatInput" @input="onBeatInput($event)"
@keydown="onBeatKey($event)"
placeholder="Describe what happens next (e.g., 'She grabs her bag and leaves'). Use @name for compendium entries, #scene for scenes."
spellcheck="true" rows="2"></textarea>
<div class="quick-search-dropdown" x-show="showQuickSearch" x-cloak
style="position:relative;width:100%;">
<div
style="position:absolute;bottom:100%;left:0;right:0;z-index:5000;background:var(--bg-primary);border:1px solid var(--accent);border-radius:6px;padding:6px;max-height:240px;overflow:auto;box-shadow:0 8px 24px rgba(0,0,0,0.4);margin-bottom:8px;">
<template x-for="(m,mi) in quickSearchMatches" :key="m.id">
<div class="quick-search-item"
:class="{ 'active': mi === quickSearchSelectedIndex }"
@click="selectQuickMatch(m)"
style="padding:6px;border-radius:4px;cursor:pointer;">
<div style="font-weight:600" x-text="m.title"></div>
<div style="font-size:12px;color:var(--text-secondary);"
x-text="(m.category || '')"></div>
</div>
</template>
</div>
</div>
<div class="scene-search-dropdown" x-show="showSceneSearch" x-cloak
style="position:relative;width:100%;">
<div
style="position:absolute;bottom:100%;left:0;right:0;z-index:5000;background:var(--bg-primary);border:1px solid var(--accent-blue,#60a5fa);border-radius:6px;padding:6px;max-height:240px;overflow:auto;box-shadow:0 8px 24px rgba(0,0,0,0.4);margin-bottom:8px;">
<template x-for="(scene,si) in sceneSearchMatches" :key="scene.id">
<div class="scene-search-item"
:class="{ 'active': si === sceneSearchSelectedIndex }"
@click="selectSceneMatch(scene)"
style="padding:6px;border-radius:4px;cursor:pointer;">
<div style="display:flex;align-items:center;gap:6px;">
<span
x-text="scene.hasSummary ? (scene.summaryStale ? '⚠️' : '✅') : '❌'"
:title="scene.hasSummary ? (scene.summaryStale ? 'Summary is outdated' : 'Summary available') : 'No summary'"></span>
<span style="font-weight:600" x-text="scene.title"></span>
</div>
<div style="font-size:12px;color:var(--text-secondary);"
x-text="scene.chapterName || 'Unknown Chapter'"></div>
</div>
</template>
</div>
</div>
</div>
<div class="beat-controls"
style="display:flex;flex-direction:column;gap:8px;align-items:flex-start;margin-bottom:0;">
<div class="beat-actions" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<div style="position:relative;">
<button class="btn btn-secondary" @click="showSpecialChars = !showSpecialChars"
title="Insert special character"
:disabled="!currentScene || showMarkdownPreview"
style="font-size:14px;padding:6px 12px;">
</button>
<div x-show="showSpecialChars" @click.outside="showSpecialChars = false" x-cloak
style="position:absolute;left:0;bottom:100%;margin-bottom:4px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;padding:8px;min-width:280px;z-index:1000;box-shadow:0 4px 12px rgba(0,0,0,0.3);">
<div
style="font-size:11px;color:var(--text-secondary);margin-bottom:6px;font-weight:600;">
INSERT SPECIAL CHARACTER</div>
<button @click="insertSpecialChar('—')" class="special-char-btn">Em dash — (long
dash)</button>
<button @click="insertSpecialChar('')" class="special-char-btn">En dash
(short dash)</button>
<button @click="insertSpecialChar('\u00A0')"
class="special-char-btn">Non-breaking space</button>
<button @click="insertSpecialChar('«')" class="special-char-btn">French quote «
(opening)</button>
<button @click="insertSpecialChar('»')" class="special-char-btn">French quote »
(closing)</button>
<button @click="insertSpecialChar('„')" class="special-char-btn">Low-high quote
„ (opening)</button>
<button @click="insertSpecialChar('"')" class="special-char-btn">Low-high quote " (closing)</button>
<button @click="insertSpecialChar(''' )" class="special-char-btn">Single quote
' (opening)</button>
<button @click="insertSpecialChar(''')" class="special-char-btn">Single quote '
(closing)</button>
<button @click="insertSpecialChar('…')" class="special-char-btn">Ellipsis
</button>
<button @click="insertSpecialChar('\u2003')" class="special-char-btn">Em space
</button>
<button @click="insertSpecialChar('\u2002')" class="special-char-btn">En space
</button>
<button @click="insertSpecialChar('\u2009')" class="special-char-btn">Thin space
</button>
<div
style="font-size:10px;color:var(--text-secondary);margin-top:8px;padding-top:8px;border-top:1px solid var(--border);">
Tip: Type -- for em dash (—)</div>
</div>
</div>
<button class="btn btn-secondary" @click="loadPromptHistory(); showPromptHistory = true"
title="View Prompt History" aria-label="View prompt history">
🕐
</button>
<button class="btn btn-secondary" @click="showSceneOptions = !showSceneOptions"
title="Scene options">
⚙️
</button>
<button class="btn btn-secondary" @click="showContextPanel = !showContextPanel"
title="Context Panel" aria-label="Toggle context panel">
📚
</button>
<button class="btn btn-secondary" @click="previewPrompt"
:disabled="!beatInput || aiStatus !== 'ready'" title="Preview Prompt">
👁️
</button>
<button class="btn btn-secondary" @click="generateFromBeat"
:disabled="!beatInput || isGenerating || aiStatus !== 'ready'"
title="Generate Text">
</button>
</div>
<div class="scene-options" x-show="showSceneOptions"
style="background:var(--bg-tertiary);padding:10px;border-radius:6px;border:1px solid var(--border);width:320px;">
<label style="font-size:12px;color:var(--text-secondary);">POV CHARACTER
<input type="text" x-model="povCharacter" placeholder="e.g., Alice"
list="character-list" @blur="saveScene()" @keydown.enter.prevent="saveScene()"
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
<datalist id="character-list">
<template
x-for="char in compendiumList.filter(c => c.category === 'characters')"
:key="char.id">
<option :value="char.title"></option>
</template>
</datalist>
</label>
<label style="font-size:12px;color:var(--text-secondary);">POV
<select x-model="pov" @change="saveScene()"
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
<option>3rd person limited</option>
<option>1st person</option>
<option>omniscient</option>
</select>
</label>
<label style="font-size:12px;color:var(--text-secondary);">TENSE
<select x-model="tense" @change="saveScene()"
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
<option value="past">past tense</option>
<option value="present">present tense</option>
</select>
</label>
<label style="font-size:12px;color:var(--text-secondary);">Prose Prompt
<select x-model="selectedProsePromptId"
@change="saveSelectedProsePrompt(selectedProsePromptId)"
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
<option value="">Use current prompt (fallback)</option>
<template x-for="pr in prompts.filter(p => p.category === 'prose')"
:key="pr.id">
<option :value="pr.id" x-text="pr.title"></option>
</template>
</select>
</label>
<!-- Generation Settings -->
<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);">
<div
style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">
Generation Settings
</div>
<!-- Model Selection -->
<label style="font-size:12px;color:var(--text-secondary);"
title="The AI model used for generation. Different models have different capabilities and costs.">
MODEL
<select x-model="aiModel" @change="saveGenerationParams()"
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
<!-- API mode models -->
<template
x-for="model in (aiMode === 'api' ? (providerModels[aiProvider] || []) : [])"
:key="model.id">
<option :value="model.id"
x-text="model.name + (model.recommended ? ' ⭐' : '')">
</option>
</template>
<!-- Local mode models -->
<template
x-for="modelName in (aiMode === 'local' ? availableLocalModels : [])"
:key="modelName">
<option :value="modelName" x-text="modelName"></option>
</template>
<!-- Show current model if not in lists -->
<option
x-show="aiModel && ((aiMode === 'api' && !(providerModels[aiProvider] || []).find(m => m.id === aiModel)) || (aiMode === 'local' && !availableLocalModels.includes(aiModel)))"
:value="aiModel" x-text="aiModel"></option>
<!-- Fallback for local mode with no models -->
<option
x-show="aiMode === 'local' && availableLocalModels.length === 0 && !aiModel"
value="" disabled>Local model (via llama-server)</option>
</select>
</label>
<!-- Temperature -->
<label
style="font-size:12px;color:var(--text-secondary);margin-top:12px;display:block;"
title="Controls randomness. Lower (0.3-0.6) = more focused and consistent. Higher (0.8-1.2) = more creative and varied.">
TEMPERATURE: <span x-text="temperature.toFixed(1)"></span>
<input type="range" x-model.number="temperature" min="0.1" max="1.5" step="0.1"
@input="saveGenerationParams()" style="width:100%;margin-top:6px;">
<div
style="display:flex;justify-content:space-between;font-size:10px;color:var(--text-tertiary);margin-top:2px;">
<span>Focused</span>
<span>Balanced</span>
<span>Creative</span>
</div>
</label>
<!-- Max Tokens -->
<label
style="font-size:12px;color:var(--text-secondary);margin-top:12px;display:block;"
title="Maximum length of generated text. 100-200 = short paragraph, 300-500 = multiple paragraphs, 800-1500 = longer scenes, 2000-5000 = very long, 10000+ = thinking models with reasoning.">
MAX LENGTH: <span x-text="maxTokens"></span> tokens
<input type="range" x-model.number="maxTokens" min="100" max="20000" step="100"
@input="saveGenerationParams()" style="width:100%;margin-top:6px;">
<div
style="display:flex;justify-content:space-between;font-size:10px;color:var(--text-tertiary);margin-top:2px;">
<span>Short</span>
<span>Medium</span>
<span>Very Long</span>
</div>
</label>
<!-- Quick link to full AI Settings -->
<button class="btn btn-secondary"
@click="showSceneOptions = false; showAISettings = true"
style="width:100%;margin-top:12px;font-size:11px;">
⚙️ Advanced AI Settings
</button>
</div>
</div>
<!-- Standalone Context Panel -->
<div class="scene-options" x-show="showContextPanel" x-data="{ allCompendiumEntries: [] }"
x-init="$watch('showContextPanel', async (value) => { if (value) { allCompendiumEntries = await getAllCompendiumEntries(); } })"
style="background:var(--bg-tertiary);padding:16px;border-radius:6px;border:1px solid var(--border);width:900px;max-width:95vw;max-height:600px;overflow-y:auto;">
<div style="display:flex;flex-direction:column;gap:12px;">
<div>
<div
style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:4px;">
Scene Context Settings
</div>
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;">
Select what context to include in every generation prompt
</div>
</div>
<!-- Two Column Layout -->
<div style="display:flex;gap:16px;align-items:flex-start;">
<!-- Left Column: Compendium -->
<div style="flex:0 0 350px;display:flex;flex-direction:column;gap:12px;">
<!-- Compendium Entries Section -->
<div
style="background:var(--bg-secondary);padding:12px;border-radius:6px;border:1px solid var(--border);display:flex;flex-direction:column;height:100%;">
<div
style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">
📚 Compendium Entries
</div>
<div style="flex:1;overflow-y:auto;">
<template x-if="allCompendiumEntries.length === 0">
<div
style="font-size:12px;color:var(--text-tertiary);padding:8px;text-align:center;">
No compendium entries yet
</div>
</template>
<template x-for="category in compendiumCategories" :key="category">
<div x-show="allCompendiumEntries.filter(e => e.category === category).length > 0"
style="margin-bottom:12px;">
<div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;margin-bottom:6px;font-weight:600;"
x-text="category"></div>
<template
x-for="entry in allCompendiumEntries.filter(e => e.category === category)"
:key="entry.id">
<label
style="display:flex;align-items:center;gap:8px;padding:6px;cursor:pointer;border-radius:4px;font-size:13px;"
@mouseenter="$el.style.background = 'var(--bg-tertiary)'"
@mouseleave="$el.style.background = 'transparent'">
<input type="checkbox"
:checked="contextPanel.compendiumIds.includes(entry.id)"
@change="toggleContextCompendium(entry.id)"
style="cursor:pointer;">
<span x-text="entry.title"></span>
</label>
</template>
</div>
</template>
</div>
</div>
</div>
<!-- Right Column: Scenes & Tags -->
<div style="flex:1;display:flex;flex-direction:column;gap:12px;">
<!-- Scenes Section -->
<div
style="background:var(--bg-secondary);padding:12px;border-radius:6px;border:1px solid var(--border);">
<div
style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">
📄 Scenes
</div>
<div style="max-height:300px;overflow-y:auto;">
<template x-if="chapters.length === 0">
<div
style="font-size:12px;color:var(--text-tertiary);padding:8px;text-align:center;">
No scenes yet
</div>
</template>
<template x-for="chapter in chapters" :key="chapter.id">
<div style="margin-bottom:16px;">
<!-- Chapter Header -->
<div
style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding:6px;background:var(--bg-tertiary);border-radius:4px;">
<div style="font-size:13px;font-weight:600;"
x-text="chapter.title"></div>
<div style="display:flex;gap:6px;">
<button
@click="setContextChapter(chapter.id, contextPanel.chapters[chapter.id] === 'full' ? null : 'full')"
:style="contextPanel.chapters[chapter.id] === 'full' ? 'background:var(--accent);color:white;font-weight:600;' : 'background:var(--bg-primary);color:var(--text-primary);'"
style="padding:6px 12px;border:1px solid var(--border);border-radius:4px;font-size:12px;cursor:pointer;transition:all 0.2s;"
title="Include full text of all scenes (click again to deselect)">
📄 Full Text
</button>
<button
@click="setContextChapter(chapter.id, contextPanel.chapters[chapter.id] === 'summary' ? null : 'summary')"
:style="contextPanel.chapters[chapter.id] === 'summary' ? 'background:var(--accent);color:white;font-weight:600;' : 'background:var(--bg-primary);color:var(--text-primary);'"
style="padding:6px 12px;border:1px solid var(--border);border-radius:4px;font-size:12px;cursor:pointer;transition:all 0.2s;"
title="Include summaries of all scenes (click again to deselect)">
📝 Summary
</button>
</div>
</div>
<!-- Scenes List -->
<template x-for="scene in chapter.scenes" :key="scene.id">
<div style="margin-left:16px;margin-bottom:4px;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:4px 6px;border-radius:4px;"
@mouseenter="$el.style.background = 'var(--bg-tertiary)'"
@mouseleave="$el.style.background = 'transparent'">
<div
style="font-size:12px;display:flex;align-items:center;gap:6px;">
<span class="summary-dot"
:class="{ 'has': scene.summary && !scene.summaryStale, 'stale': scene.summaryStale, 'none': !scene.summary }"
style="width:8px;height:8px;"></span>
<span x-text="scene.title"></span>
</div>
<div style="display:flex;gap:4px;"
x-show="!contextPanel.chapters[chapter.id]">
<button
@click="setContextScene(scene.id, contextPanel.scenes[scene.id] === 'full' ? null : 'full')"
:style="contextPanel.scenes[scene.id] === 'full' ? 'background:var(--accent);color:white;font-weight:600;' : 'background:var(--bg-primary);color:var(--text-primary);'"
style="padding:3px 8px;border:1px solid var(--border);border-radius:3px;font-size:11px;cursor:pointer;transition:all 0.2s;"
title="Include full text (click again to deselect)">
Full
</button>
<button
@click="setContextScene(scene.id, contextPanel.scenes[scene.id] === 'summary' ? null : 'summary')"
:style="contextPanel.scenes[scene.id] === 'summary' ? 'background:var(--accent);color:white;font-weight:600;' : 'background:var(--bg-primary);color:var(--text-primary);'"
style="padding:3px 8px;border:1px solid var(--border);border-radius:3px;font-size:11px;cursor:pointer;transition:all 0.2s;"
title="Include summary (click again to deselect)"
:disabled="!scene.summary">
Summary
</button>
</div>
<div style="font-size:10px;color:var(--text-tertiary);padding:2px 6px;"
x-show="contextPanel.chapters[chapter.id]"
x-text="'(via chapter: ' + contextPanel.chapters[chapter.id] + ')'">
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
<!-- Tags Section -->
<div
style="background:var(--bg-secondary);padding:12px;border-radius:6px;border:1px solid var(--border);">
<div
style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">
🏷️ Tags
</div>
<template x-if="getAllTags().length === 0">
<div
style="font-size:12px;color:var(--text-tertiary);padding:8px;text-align:center;">
No tags yet. Add tags to scenes in their summary panel.
</div>
</template>
<div x-show="getAllTags().length > 0"
style="display:flex;flex-wrap:wrap;gap:8px;">
<template x-for="tag in getAllTags()" :key="tag">
<button @click="toggleContextTag(tag)"
:style="contextPanel.tags.includes(tag) ? 'background:var(--accent);color:white;border-color:var(--accent);font-weight:600;' : 'background:var(--bg-primary);color:var(--text-primary);border-color:var(--border);'"
style="padding:8px 16px;border:2px solid;border-radius:16px;font-size:13px;cursor:pointer;transition:all 0.2s;font-weight:500;"
@mouseenter="if (!contextPanel.tags.includes(tag)) $el.style.borderColor = 'var(--accent)'"
@mouseleave="if (!contextPanel.tags.includes(tag)) $el.style.borderColor = 'var(--border)'"
x-text="tag">
</button>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="context-info">
<span x-show="aiStatus === 'ready'">✓ AI ready • Will use current scene as context</span>
<span x-show="aiStatus === 'loading'">⏳ Loading model...</span>
<span x-show="aiStatus === 'error'">⚠️ Model not loaded (using placeholder text for
now)</span>
</div>
<!-- Generation actions (accept/retry/discard) -->
<div style="position:relative;padding:12px 30px 6px;">
<div x-show="showGenActions" x-cloak
style="position:absolute;right:30px;top:-12px;display:flex;gap:8px;">
<button class="btn btn-primary" @click="acceptGeneration">Accept</button>
<button class="btn btn-secondary" @click="retryGeneration">Retry</button>
<button class="btn btn-secondary" @click="discardGeneration">Discard</button>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Rewrite Modal -->
<div x-show="showRewriteModal" x-cloak class="modal-overlay" @click.self="discardRewrite()">
<div class="rewrite-modal" @click.stop>
<div class="modal-header">
<h3 style="margin:0;">Rewrite Selection</h3>
<button class="btn btn-secondary" @click="discardRewrite()">Close</button>
</div>
<div class="modal-body">
<div style="margin-bottom:16px;">
<label
style="display:block;margin-bottom:6px;font-size:13px;font-weight:600;color:var(--text-primary);">Original
text</label>
<textarea readonly :value="rewriteOriginalText"
style="width:100%;height:120px;padding:10px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;resize:vertical;"></textarea>
</div>
<div style="display:flex;gap:8px;margin-bottom:16px;">
<button class="btn btn-secondary" @click="buildRewritePrompt()"
:disabled="rewriteInProgress">Prompt</button>
<button class="btn btn-primary" @click="performRewrite()" :disabled="rewriteInProgress">
<span x-show="!rewriteInProgress">Rewrite</span>
<span x-show="rewriteInProgress">Rewriting...</span>
</button>
</div>
<div x-show="showRewritePromptList" style="margin-bottom:16px;">
<label
style="display:block;margin-bottom:6px;font-size:13px;font-weight:600;color:var(--text-primary);">Select
rewrite prompt</label>
<select x-model="selectedRewritePromptId" @change="buildRewritePrompt()"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<option value="">Default rewrite prompt</option>
<template x-for="pr in prompts.filter(p => p.category === 'rewrite')" :key="pr.id">
<option :value="pr.id" x-text="pr.title"></option>
</template>
</select>
</div>
<div style="margin-bottom:16px;">
<label
style="display:block;margin-bottom:6px;font-size:13px;font-weight:600;color:var(--text-primary);">Rewritten
text</label>
<textarea readonly :value="rewriteOutput"
style="width:100%;height:180px;padding:10px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;resize:vertical;"
placeholder="Rewritten text will appear here..."></textarea>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button class="btn btn-primary" @click="acceptRewrite()"
x-show="rewriteOutput && !rewriteInProgress">Accept</button>
<button class="btn btn-secondary" @click="retryRewrite()"
x-show="rewriteOutput && !rewriteInProgress">Retry</button>
<button class="btn btn-secondary" @click="discardRewrite()">Discard</button>
</div>
</div>
</div>
</div>
<!-- Model Loading Screen -->
<template x-if="showModelLoading">
<div class="welcome-screen">
<div class="welcome-content">
<h2>Setting Up AI</h2>
<p x-text="loadingMessage"></p>
<div class="loading-bar">
<div class="loading-bar-fill" :style="`width: ${loadingProgress}%`"></div>
</div>
<p style="margin-top: 10px; font-size: 14px;" x-text="`${loadingProgress}%`"></p>
<div class="note" x-show="loadingProgress < 100">
This may take a minute on first run while the AI engine initializes...
</div>
</div>
</div>
</template>
<!-- Rewrite modal removed: selecting text shows only the floating Rewrite button -->
<!-- Welcome Screen (No Project) -->
<template x-if="!currentProject && !showModelLoading">
<div class="welcome-screen">
<div class="welcome-content">
<h2>Welcome to Writingway 2.0</h2>
<p>A free, open-source creative writing tool with AI assistance. Everything runs locally on your
computer.</p>
<button class="btn btn-primary" @click="showNewProjectModal = true">
Create Your First Project
</button>
<div class="note">
<strong>Note:</strong> AI integration is currently being set up. The app will work without AI,
and generation features will use placeholder text until the local model is fully configured.
</div>
</div>
</div>
</template>
<!-- New Project Modal -->
<template x-if="showNewProjectModal">
<div class="modal-overlay" @click.self="showNewProjectModal = false">
<div class="modal">
<h3>New Project</h3>
<input type="text" x-model="newProjectName" placeholder="Project name (e.g., 'My Novel')"
@keydown.enter="createProject">
<div class="modal-buttons">
<button class="btn btn-secondary" @click="showNewProjectModal = false">Cancel</button>
<button class="btn btn-primary" @click="createProject">Create</button>
</div>
</div>
</div>
</template>
<!-- Rename Project Modal -->
<template x-if="showRenameProjectModal">
<div class="modal-overlay" @click.self="showRenameProjectModal = false">
<div class="modal">
<h3>Rename Project</h3>
<input type="text" x-model="renameProjectName" placeholder="Project name"
@keydown.enter="renameCurrentProject">
<div class="modal-buttons">
<button class="btn btn-secondary" @click="showRenameProjectModal = false">Cancel</button>
<button class="btn btn-primary" @click="renameCurrentProject">Rename</button>
</div>
</div>
</div>
</template>
<!-- Import from Writingway 1 Modal -->
<template x-if="showW1ImportModal">
<div class="modal-overlay" @click.self="showW1ImportModal = false">
<div class="modal" style="max-width:600px;">
<h3>Import from Writingway 1</h3>
<div
style="background:rgba(255,200,100,0.1);border:1px solid rgba(255,200,100,0.3);border-radius:8px;padding:16px;margin:16px 0;line-height:1.6;">
<p style="margin:0 0 12px 0;font-weight:600;color:#ffcc66;">⚠️ Import Notes:</p>
<p style="margin:0 0 8px 0;font-size:14px;"><strong>What will be imported:</strong></p>
<ul style="margin:0 0 12px 0;padding-left:24px;font-size:13px;">
<li>Project structure (Acts flattened to Chapters)</li>
<li>Scene content (latest backup used)</li>
<li>Chapter and scene summaries</li>
<li>Basic compendium entries (name + content)</li>
<li>Prompts</li>
</ul>
<p style="margin:0 0 8px 0;font-size:14px;"><strong>What will NOT be imported:</strong></p>
<ul style="margin:0 0 0 0;padding-left:24px;font-size:13px;">
<li>Compendium relationships, templates, custom fields</li>
<li>Compendium images (must be re-uploaded)</li>
<li>Scene status markers (To Do, In Progress, etc.)</li>
<li>Project cover images (must be re-uploaded)</li>
</ul>
</div>
<p style="margin:16px 0 8px 0;font-size:14px;color:rgba(255,255,255,0.8);">Select your Writingway 1
project folder:</p>
<input type="file" webkitdirectory directory multiple @change="importFromW1($event)"
style="margin:8px 0;padding:8px;background:rgba(255,255,255,0.05);border:1px solid var(--border);border-radius:4px;color:inherit;width:100%;">
<div class="modal-buttons" style="margin-top:20px;">
<button class="btn btn-secondary" @click="showW1ImportModal = false"
:disabled="w1ImportInProgress">Cancel</button>
</div>
</div>
</div>
</template>
<!-- Scene/Chapter Summary Panel -->
<div class="slide-panel-right summary-panel" :class="{ 'open': showSummaryPanel }" x-cloak aria-hidden="true">
<div class="slide-panel-header">
<h3 style="margin:0;" x-text="summaryTargetChapterId ? 'Chapter Summary' : 'Scene Summary'"></h3>
<div>
<button class="btn btn-secondary" @click="showSummaryPanel = false">Close</button>
</div>
</div>
<div class="slide-panel-body">
<div style="display:flex;flex-direction:column;height:100%;">
<div style="flex:1;overflow:auto;">
<!-- Tags input (scenes only) -->
<div x-show="summaryTargetSceneId" style="margin-bottom:12px;">
<label
style="display:block;margin-bottom:6px;font-size:13px;color:var(--text-secondary);font-weight:600;">Tags
(comma-separated)</label>
<input type="text" x-model="sceneTags" placeholder="e.g., chase, action, romance"
style="width:100%;padding:10px 12px;background:var(--bg-primary);border:1px solid var(--border);color:var(--text-primary);border-radius:6px;font-size:14px;" />
<div style="font-size:12px;color:var(--text-tertiary);margin-top:4px;">Tags help organize
and reference scenes in your context panel</div>
</div>
<label
style="display:block;margin-bottom:6px;font-size:13px;color:var(--text-secondary);font-weight:600;"
x-text="summaryTargetChapterId ? 'Chapter Summary' : 'Scene Summary'"></label>
<textarea x-model="summaryText" rows="12"
style="width:100%;padding:12px;background:var(--bg-primary);border:1px solid var(--border);color:var(--text-primary);border-radius:6px;resize:vertical;"></textarea>
</div>
<div
style="flex:0 0 auto;display:flex;gap:8px;align-items:center;justify-content:flex-end;margin-top:12px;position:relative;">
<!-- Prompt Selection Button -->
<div style="position:relative;">
<button class="btn btn-secondary" @click="showSummaryPromptList = !showSummaryPromptList"
title="Choose a summary prompt">
Prompt
</button>
<!-- Prompt Dropdown -->
<div x-show="showSummaryPromptList" @click.away="showSummaryPromptList = false" x-cloak
style="position:absolute;bottom:100%;left:0;margin-bottom:8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;padding:8px;min-width:280px;max-height:300px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,0.4);z-index:1000;">
<div
style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;padding:4px 8px;">
Select Summary Prompt:
</div>
<!-- Default option -->
<div @click="selectedSummaryPromptId = null; showSummaryPromptList = false"
:style="!selectedSummaryPromptId ? 'background:var(--accent);color:white;' : ''"
style="padding:8px 12px;cursor:pointer;border-radius:4px;margin-bottom:4px;transition:background 0.15s;"
:class="!selectedSummaryPromptId ? '' : 'hover:bg-tertiary'">
<div style="font-weight:600;font-size:13px;">
Default
<span x-show="prompts.filter(p => p.category === 'summary').length === 0"
style="font-size:11px;opacity:0.7;">(Heuristic)</span>
</div>
<div style="font-size:11px;opacity:0.8;">
<span x-show="prompts.filter(p => p.category === 'summary').length > 0">First
summary prompt or fallback</span>
</div>
</div>
<!-- Summary category prompts -->
<template x-for="prompt in prompts.filter(p => p.category === 'summary')"
:key="prompt.id">
<div @click="selectedSummaryPromptId = prompt.id; showSummaryPromptList = false"
:style="selectedSummaryPromptId === prompt.id ? 'background:var(--accent);color:white;' : ''"
style="padding:8px 12px;cursor:pointer;border-radius:4px;margin-bottom:4px;transition:background 0.15s;"
@mouseenter="$el.style.background = selectedSummaryPromptId === prompt.id ? 'var(--accent)' : 'var(--bg-secondary)'"
@mouseleave="$el.style.background = selectedSummaryPromptId === prompt.id ? 'var(--accent)' : 'transparent'">
<div style="font-weight:600;font-size:13px;" x-text="prompt.title"></div>
<div style="font-size:11px;opacity:0.8;"
x-text="(prompt.content || '').slice(0, 60) + '...'"></div>
</div>
</template>
<!-- No prompts message -->
<div x-show="prompts.filter(p => p.category === 'summary').length === 0"
style="padding:12px;text-align:center;color:var(--text-secondary);font-size:12px;">
No summary prompts yet.<br>
<span style="font-size:11px;opacity:0.7;">Add one in the Prompts panel</span>
</div>
</div>
</div>
<button class="btn btn-secondary"
@click="summaryTargetChapterId ? summarizeChapter() : summarizeScene()">Summarize</button>
<button class="btn btn-primary"
@click="summaryTargetChapterId ? saveChapterSummary() : saveSceneSummary()">Save</button>
</div>
</div>
</div>
</div>
<!-- AI Settings Panel -->
<div class="slide-panel-right" :class="{ 'open': showAISettings }" x-cloak
style="width:600px;border-left:1px solid rgba(74,158,255,0.08);box-shadow:-18px 0 40px rgba(0,0,0,0.55);">
<div class="slide-panel-header">
<h3 style="margin:0;">AI Configuration</h3>
<button class="btn btn-secondary" @click="showAISettings = false">Close</button>
</div>
<div class="slide-panel-body" style="overflow-y:auto;">
<!-- Mode Selection -->
<div style="margin-bottom:20px;">
<label style="display:block;font-weight:600;margin-bottom:8px;color:var(--text-primary);">AI
Mode</label>
<div style="display:flex;gap:12px;">
<button class="btn" :class="aiMode === 'api' ? 'btn-primary' : 'btn-secondary'"
@click="aiMode = 'api'; if (aiProvider === 'openrouter' && !aiModel) aiModel = 'google/gemini-2.0-flash-exp:free';"
style="flex:1;display:flex;flex-direction:column;align-items:center;padding:12px;">
<div style="font-size:20px;margin-bottom:4px;">☁️</div>
<div style="font-weight:600;">API (Cloud)</div>
<div style="font-size:11px;opacity:0.7;">No setup required</div>
</button>
<button class="btn" :class="aiMode === 'local' ? 'btn-primary' : 'btn-secondary'"
@click="aiMode = 'local'; if (availableLocalModels.length > 0 && !aiModel) aiModel = availableLocalModels[0]; scanLocalModels();"
style="flex:1;display:flex;flex-direction:column;align-items:center;padding:12px;">
<div style="font-size:20px;margin-bottom:4px;">💻</div>
<div style="font-weight:600;">Local Model</div>
<div style="font-size:11px;opacity:0.7;">Privacy & offline</div>
</button>
</div>
</div>
<!-- API Mode Settings -->
<div x-show="aiMode === 'api'" style="margin-bottom:20px;">
<h4 style="margin:0 0 12px 0;">API Provider Settings</h4>
<div style="margin-bottom:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">Provider</label>
<select x-model="aiProvider"
@change="aiModel = (providerModels[aiProvider] && providerModels[aiProvider].length > 0) ? providerModels[aiProvider].find(m => m.recommended)?.id || providerModels[aiProvider][0].id : ''; modelsFetched = false;"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<option value="openrouter">OpenRouter (Multi-model, has free options)</option>
<option value="anthropic">Anthropic (Claude)</option>
<option value="openai">OpenAI (GPT)</option>
<option value="google">Google AI (Gemini)</option>
<option value="custom">Custom API Endpoint</option>
</select>
</div>
<div style="margin-bottom:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">API
Key</label>
<input type="password" x-model="aiApiKey" placeholder="Enter your API key"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;">
<template x-if="aiProvider === 'openrouter'">Get a free key at <a
href="https://openrouter.ai/keys" target="_blank"
style="color:var(--accent);">openrouter.ai/keys</a></template>
<template x-if="aiProvider === 'anthropic'">Get your key at <a
href="https://console.anthropic.com/" target="_blank"
style="color:var(--accent);">console.anthropic.com</a></template>
<template x-if="aiProvider === 'openai'">Get your key at <a
href="https://platform.openai.com/api-keys" target="_blank"
style="color:var(--accent);">platform.openai.com/api-keys</a></template>
<template x-if="aiProvider === 'google'">Get your key at <a
href="https://makersuite.google.com/app/apikey" target="_blank"
style="color:var(--accent);">Google AI Studio</a></template>
</p>
</div>
<div style="margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<label
style="display:block;font-size:13px;font-weight:600;color:var(--text-primary);">Model</label>
<button class="btn btn-secondary" @click="fetchProviderModels()"
:disabled="!aiApiKey || fetchingModels" style="font-size:11px;padding:4px 8px;"
title="Refresh available models from provider">
<span x-show="!fetchingModels">🔄 Refresh</span>
<span x-show="fetchingModels">⏳ Loading...</span>
</button>
</div>
<select x-model="aiModel"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<option value="" disabled>Select a model...</option>
<template x-for="model in providerModels[aiProvider] || []" :key="model.id">
<option :value="model.id" x-text="model.name + (model.recommended ? ' ⭐' : '')">
</option>
</template>
<!-- Show current model if not in list -->
<option x-show="aiModel && !(providerModels[aiProvider] || []).find(m => m.id === aiModel)"
:value="aiModel" x-text="aiModel"></option>
</select>
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;"
x-show="modelsFetched && aiMode === 'api'">
✓ Model list updated from provider
</p>
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;"
x-show="!aiApiKey && aiMode === 'api'">
Enter API key above to see available models
</p>
</div>
<!-- Force Non-Streaming Toggle (for API mode only) -->
<div x-show="aiMode === 'api'" style="margin-bottom:16px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" x-model="forceNonStreaming" @change="saveGenerationParams()"
style="width:16px;height:16px;cursor:pointer;">
<span style="font-size:13px;color:var(--text-primary);">
Force non-streaming mode
</span>
</label>
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;padding-left:24px;">
Enable this if your model doesn't support streaming or returns empty responses. Most
thinking models (o1, o3, reasoning models) are auto-detected, but use this for any others
that need it.
</p>
</div>
<div x-show="aiProvider === 'custom'" style="margin-bottom:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">Custom
Endpoint URL</label>
<input type="text" x-model="aiEndpoint"
placeholder="https://api.example.com/v1/chat/completions"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
</div>
</div>
<!-- Local Mode Settings -->
<div x-show="aiMode === 'local'" style="margin-bottom:20px;">
<h4 style="margin:0 0 12px 0;">Local Model Settings</h4>
<div
style="background:rgba(74,158,255,0.05);border:1px solid rgba(74,158,255,0.2);border-radius:6px;padding:12px;margin-bottom:16px;">
<p style="font-size:12px;color:var(--text-secondary);margin:0;">📁 Place your GGUF model
files in the <code
style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;">/models</code>
folder, then click "Scan for Models" below.</p>
</div>
<button class="btn btn-secondary" @click="scanLocalModels()" style="margin-bottom:16px;">🔍 Scan for
Models</button>
<div style="margin-bottom:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">Local
Model Status</label>
<div
style="padding:12px;background:rgba(76,175,80,0.1);border:1px solid rgba(76,175,80,0.3);border-radius:6px;">
<p style="font-size:12px;color:var(--text-secondary);margin:0;">
✓ Model loaded by llama-server (started via start.bat)<br>
<span style="font-size:11px;opacity:0.8;">The actual model file is whichever .gguf file
start.bat found in your /models folder</span>
</p>
</div>
</div>
<div
style="margin-top:12px;padding:10px;background:rgba(33,150,243,0.1);border:1px solid rgba(33,150,243,0.3);border-radius:6px;">
<p style="font-size:11px;color:var(--text-secondary);margin:0;line-height:1.5;">
💡 <strong>Note:</strong> To change models, close Writingway, replace the .gguf file in
/models folder, and run start.bat again
</p>
</div>
<div style="margin-top:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">Server
URL</label>
<input type="text" x-model="aiEndpoint" value="http://localhost:8080"
placeholder="http://localhost:8080"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;">The llama-server
endpoint (usually localhost:8080)</p>
</div>
</div>
<!-- Save Button -->
<div style="display:flex;gap:8px;padding-top:16px;border-top:1px solid var(--border);">
<button class="btn btn-primary" @click="saveAISettings()">💾 Save & Test Connection</button>
<button class="btn btn-secondary" @click="showAISettings = false">Cancel</button>
</div>
<!-- TTS Settings Section -->
<div style="margin-top:32px;padding-top:24px;border-top:2px solid var(--border);">
<h4 style="margin:0 0 16px 0;color:var(--text-primary);">🔊 Text-to-Speech Settings</h4>
<div style="margin-bottom:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">
Voice
</label>
<select x-model="ttsVoiceName" @change="saveTTSSettings()"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<option value="">Default Voice</option>
<template x-for="voice in availableTTSVoices" :key="voice.name">
<option :value="voice.name" x-text="`${voice.name} (${voice.lang})`"></option>
</template>
</select>
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;">
💡 Chrome/Edge on Windows have excellent neural voices
</p>
</div>
<div style="margin-bottom:16px;">
<label
style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">
Reading Speed: <span x-text="ttsSpeed.toFixed(1) + 'x'"></span>
</label>
<input type="range" x-model.number="ttsSpeed" @input="saveTTSSettings()" min="0.5" max="2.0"
step="0.1" style="width:100%;margin-top:6px;">
<div
style="display:flex;justify-content:space-between;font-size:10px;color:var(--text-tertiary);margin-top:2px;">
<span>0.5x (Slower)</span>
<span>1.0x (Normal)</span>
<span>2.0x (Faster)</span>
</div>
<p style="font-size:11px;color:var(--text-secondary);margin:4px 0 0 0;">
Higher speeds are useful when editing - you can catch errors faster
</p>
</div>
<div
style="background:rgba(74,158,255,0.05);border:1px solid rgba(74,158,255,0.2);border-radius:6px;padding:12px;">
<p style="font-size:12px;color:var(--text-secondary);margin:0;line-height:1.5;">
💡 <strong>Editing Tip:</strong> Your eyes go "blind" to typos after reading text 2-3 times.
Listening with TTS helps catch errors you'd otherwise miss. Try 1.2x-1.5x speed for
efficient editing!
</p>
</div>
</div>
</div>
</div>
<!-- GitHub Backup Settings Panel -->
<div class="slide-panel-right" :class="{ 'open': showBackupSettings }" x-cloak>
<div class="slide-panel-header">
<h3 style="margin:0;">☁️ Cloud Backup</h3>
<button class="btn btn-secondary" @click="closeBackupSettings()">Close</button>
</div>
<div class="slide-panel-body" style="padding:20px;">
<div
style="background:rgba(74,158,255,0.1);border:1px solid rgba(74,158,255,0.3);border-radius:8px;padding:16px;margin-bottom:24px;">
<h4 style="margin:0 0 12px 0;color:var(--text-primary);">Setup Instructions</h4>
<ol style="margin:0;padding-left:20px;line-height:1.8;font-size:14px;">
<li>Go to <a href="https://github.com/settings/tokens" target="_blank"
style="color:#4a9eff;">GitHub Settings → Developer settings → Personal access tokens</a>
</li>
<li>Click "Generate new token (classic)"</li>
<li>Give it a name (e.g., "Writingway Backup")</li>
<li>Expiration: Select <strong>"No expiration"</strong> for convenience</li>
<li>Select the <strong>gist</strong> permission (only this one)</li>
<li>Click "Generate token" and copy the token</li>
<li>Paste it below</li>
</ol>
</div>
<div style="margin-bottom:20px;">
<label style="display:block;font-weight:600;margin-bottom:8px;">GitHub Token</label>
<input type="password" x-model="githubToken" placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
style="width:100%;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-family:monospace;font-size:13px;">
<p style="font-size:12px;color:var(--text-secondary);margin-top:8px;">
<span x-show="githubUsername" style="color:#4caf50;">✓ Connected as: <strong
x-text="githubUsername"></strong></span>
</p>
</div>
<div style="margin-bottom:20px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" x-model="backupEnabled" style="width:18px;height:18px;">
<span style="font-weight:600;">Enable automatic backup (every 5 minutes)</span>
</label>
</div>
<div style="display:flex;gap:12px;margin-bottom:20px;">
<button class="btn btn-primary" @click="saveBackupSettings()" style="flex:1;">
Save Settings
</button>
<button class="btn btn-secondary" @click="backupNow()" style="flex:1;">
Backup Now
</button>
</div>
<div x-show="backupStatus"
style="padding:12px;background:rgba(74,158,255,0.1);border:1px solid rgba(74,158,255,0.3);border-radius:6px;margin-bottom:20px;">
<p style="margin:0;font-size:14px;" x-text="backupStatus"></p>
<p x-show="lastBackupTime" style="margin:8px 0 0 0;font-size:12px;color:var(--text-secondary);">
Last backup: <span
x-text="lastBackupTime ? new Date(lastBackupTime).toLocaleString() : 'Never'"></span>
</p>
</div>
<div style="border-top:1px solid var(--border);padding-top:20px;">
<button class="btn btn-secondary" @click="openRestoreModal()"
:disabled="!githubToken || !currentProjectGistId" style="width:100%;">
📥 Restore from Backup
</button>
</div>
<div
style="margin-top:20px;padding:16px;background:rgba(255,193,7,0.1);border:1px solid rgba(255,193,7,0.3);border-radius:8px;">
<p style="margin:0;font-size:13px;line-height:1.6;">
<strong>Note:</strong> Each project is backed up to its own private GitHub Gist.
Backups include all chapters, scenes, content, compendium entries, and prompts.
</p>
</div>
</div>
</div>
<!-- Restore Backup Modal -->
<div x-show="showRestoreModal" class="modal-backdrop" @click.self="closeRestoreModal()" x-cloak
style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:10000;">
<div class="modal-content"
style="background:var(--bg-secondary);padding:24px;border-radius:12px;max-width:700px;width:90%;max-height:80vh;overflow-y:auto;">
<h3 style="margin:0 0 20px 0;">Restore from Backup</h3>
<div x-show="backupList.length === 0"
style="text-align:center;padding:40px;color:var(--text-secondary);">
<p>Loading backups...</p>
</div>
<div x-show="backupList.length > 0">
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:16px;">
Select a backup version to restore. This will replace your current project data.
</p>
<div style="display:flex;flex-direction:column;gap:12px;">
<template x-for="backup in backupList" :key="backup.version">
<div
style="padding:16px;background:var(--bg-primary);border:1px solid var(--border);border-radius:8px;">
<div
style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px;">
<div>
<div style="font-weight:600;color:var(--text-primary);"
x-text="new Date(backup.timestamp).toLocaleString()"></div>
<div style="font-size:12px;color:var(--text-secondary);margin-top:4px;">
Version: <span x-text="backup.version.substring(0, 7)"></span>
</div>
</div>
<button class="btn btn-primary" @click="restoreBackup(backup.url)"
style="padding:8px 16px;font-size:13px;">
Restore
</button>
</div>
</div>
</template>
</div>
</div>
<div style="margin-top:24px;display:flex;justify-content:flex-end;">
<button class="btn btn-secondary" @click="closeRestoreModal()">Cancel</button>
</div>
</div>
</div>
<!-- Prompts Slide Panel -->
<div class="slide-panel-right prompts-panel" :class="{ 'open': showPromptsPanel }" x-cloak aria-hidden="true">
<div class="slide-panel-header">
<h3 style="margin:0;">Prompts</h3>
<div>
<button class="btn btn-secondary" @click="showPromptsPanel = false">Close</button>
</div>
</div>
<div class="slide-panel-body">
<div style="display:flex;gap:12px;height:100%;">
<div style="width:40%;overflow:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<strong style="color:var(--text-primary);">Categories</strong>
</div>
<template x-for="cat in promptCategories" :key="cat">
<div style="margin-bottom:12px;">
<div class="comp-category" :class="{ 'active': promptCollapsed[cat] === false }"
tabindex="0" role="button" @click="promptCollapsed[cat] = !promptCollapsed[cat]">
<div class="comp-category-label" style="text-transform:capitalize;" x-text="cat">
</div>
<div style="display:flex;align-items:center;gap:6px;">
<div class="comp-category-count"
style="font-size:12px;color:var(--text-secondary);"
x-text="(prompts.filter(p => p.category === cat) || []).length"></div>
<button class="btn btn-secondary" style="padding:2px 8px;font-size:14px;"
@click.stop="createPrompt(cat)">+</button>
</div>
</div>
<div x-show="!promptCollapsed[cat]" x-cloak style="margin-top:6px;">
<template x-for="pr in prompts.filter(p => p.category === cat)" :key="pr.id">
<div class="comp-entry"
:class="{ 'active': currentPrompt && currentPrompt.id === pr.id }"
tabindex="0">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="flex:1;cursor:pointer;" @click.stop="openPrompt(pr.id)">
<div class="comp-entry-title" x-text="pr.title"></div>
<div class="comp-entry-meta"
style="font-size:11px;color:var(--text-secondary);"
x-text="new Date(pr.modified).toLocaleString()"></div>
</div>
<div class="item-actions" style="margin-left:8px;"
x-data="{ openMenu:false }">
<button class="menu-button" @click.stop="openMenu = !openMenu"
title="More"></button>
<div x-show="openMenu" x-cloak @click.away="openMenu = false"
x-transition class="action-menu">
<button @click.stop="movePromptUp(pr.id); openMenu=false">Move
up</button>
<button @click.stop="movePromptDown(pr.id); openMenu=false">Move
down</button>
<button
@click.stop="renamePrompt(pr.id); openMenu=false">Rename</button>
<button
@click.stop="deletePrompt(pr.id); openMenu=false">Delete</button>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
<div style="flex:1;display:flex;flex-direction:column;">
<div style="flex:0 0 auto;margin-bottom:6px;">
<div x-show="!currentPrompt" style="color:var(--text-secondary);">Select a prompt or create
a new one.</div>
<div x-show="currentPrompt">
<input type="text" x-model="currentPrompt.title"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);border-radius:6px;margin-bottom:6px;">
<div style="margin-bottom:6px;">
<label
style="font-size:12px;color:var(--text-secondary);display:block;margin-bottom:4px;">Category</label>
<select x-model="currentPrompt.category"
style="width:100%;padding:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);border-radius:6px;">
<template x-for="cat in promptCategories" :key="cat">
<option :value="cat" x-text="cat" style="text-transform:capitalize;">
</option>
</template>
</select>
</div>
</div>
</div>
<div style="flex:1;overflow:auto;">
<textarea x-model="promptEditorContent"
style="width:100%;height:100%;padding:8px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);border-radius:6px;"></textarea>
</div>
<div style="flex:0 0 auto;display:flex;gap:8px;margin-top:8px;">
<button class="btn btn-primary" @click="savePrompt" :disabled="!currentPrompt">Save</button>
<button class="btn btn-secondary" @click="deletePrompt(currentPrompt.id)"
x-show="currentPrompt">Delete</button>
</div>
</div>
</div>
</div>
</div>
<!-- Compendium Slide Panel -->
<div id="compendium-panel" class="slide-panel-right compendium-panel" :class="{ 'open': showCodexPanel }"
x-cloak :aria-hidden="!showCodexPanel">
<div class="slide-panel-header">
<h3 style="margin:0;">Compendium</h3>
<div>
<button class="btn btn-secondary" @click="showCodexPanel = false">Close</button>
</div>
</div>
<div class="slide-panel-body">
<div style="display:flex;gap:12px;height:100%;">
<div style="width:40%;overflow:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<strong style="color:var(--text-primary);">Categories</strong>
</div>
<template x-for="cat in compendiumCategories" :key="cat">
<div style="margin-bottom:12px;">
<div class="comp-category" :class="{ 'active': currentCompCategory === cat }"
tabindex="0" role="button" @click="loadCompendiumCategory(cat)"
@keydown.enter="loadCompendiumCategory(cat)">
<div class="comp-category-label"
style="text-transform:capitalize;font-weight:700;color:var(--text-primary);"
x-text="cat"></div>
<div style="display:flex;align-items:center;gap:6px;">
<div class="comp-category-count"
style="font-size:12px;color:var(--text-secondary);"
x-text="(compendiumCounts[cat] || 0)"></div>
<button class="btn btn-secondary" style="padding:2px 8px;font-size:14px;"
@click.stop="createCompendiumEntry(cat)"
:title="`New ${cat} entry`">+</button>
</div>
</div>
<div x-show="currentCompCategory === cat" x-cloak style="margin-top:6px;">
<template x-for="e in compendiumList" :key="e.id">
<div class="comp-entry"
:class="{ 'active': currentCompEntry && currentCompEntry.id === e.id }"
tabindex="0">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="flex:1;cursor:pointer;" @click="selectCompendiumEntry(e.id)"
@keydown.enter="selectCompendiumEntry(e.id)">
<div class="comp-entry-title" x-text="e.title"></div>
<div class="comp-entry-meta"
x-text="new Date(e.modified).toLocaleString()"></div>
</div>
<div class="item-actions" style="margin-left:8px;"
x-data="{ openMenu:false }">
<button class="menu-button" @click.stop="openMenu = !openMenu"
title="More"></button>
<div x-show="openMenu" x-cloak @click.away="openMenu = false"
x-transition class="action-menu">
<div class="menu-row">
<label
style="font-size:12px;color:var(--text-secondary);flex:1;display:flex;gap:6px;align-items:center;">
Move to
<select class="move-select"
@change.stop="moveCompendiumEntryToCategory(e.id, $event.target.value); openMenu=false;">
<option value="" disabled selected>Choose category
</option>
<template x-for="cat in compendiumCategories"
:key="cat">
<option :value="cat"
:selected="cat === e.category" x-text="cat">
</option>
</template>
</select>
</label>
</div>
<button
@click.stop="moveCompendiumEntryUp(e.id); openMenu=false">Move
up</button>
<button
@click.stop="moveCompendiumEntryDown(e.id); openMenu=false">Move
down</button>
<button
@click.stop="deleteCompendiumEntry(e.id); openMenu=false">Delete
entry</button>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
<div style="flex:1;display:flex;flex-direction:column;">
<div style="flex:0 0 auto;margin-bottom:6px;">
<template x-if="!currentCompEntry">
<div style="color:var(--text-secondary);">Select an entry or create a new one.</div>
</template>
<template x-if="currentCompEntry">
<div>
<input type="text" x-model="currentCompEntry.title" placeholder="Entry title"
style="width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);border-radius:6px;margin-bottom:6px;">
</div>
</template>
</div>
<div style="flex:1;overflow:auto;">
<template x-if="currentCompEntry">
<div class="comp-editor-vertical">
<!-- Image under the title -->
<label class="comp-image-preview comp-image-block" tabindex="0"
@click.prevent="$refs.compImageInput.click()" @dragover.prevent
@drop.prevent="setCompImageFromFile($event)">
<template x-if="currentCompEntry.imageUrl">
<img :src="currentCompEntry.imageUrl" alt="Entry image preview"
@click.stop="confirmRemoveCompImage()">
</template>
<template x-if="!currentCompEntry.imageUrl">
<div class="comp-image-placeholder">Drop an image here</div>
</template>
<input x-ref="compImageInput" type="file" accept="image/*"
@change="setCompImageFromFile($event)" style="display:none">
</label>
<!-- Body below the image -->
<textarea x-model="currentCompEntry.body"
placeholder="Write compendium entry body here" class="comp-editor-body"
style="margin-top:12px;"></textarea>
<!-- Tags and image remove button -->
<div
style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;gap:12px;">
<div class="comp-tags">
<div class="tag-list">
<template x-for="(t,ti) in (currentCompEntry.tags || [])" :key="ti">
<span class="tag-chip" @click.stop="removeCompTag(ti)"
title="Remove tag" x-text="t"></span>
</template>
</div>
<input class="tag-input" x-model="newCompTag"
placeholder="Add tag and press Enter"
@keydown.enter.prevent="addCompTag()">
</div>
<div style="display:flex;gap:8px;align-items:center;">
<!-- Image removal handled by clicking the image itself -->
</div>
</div>
<!-- Always in Context checkbox -->
<div
style="margin-top:12px;padding:12px;background:rgba(74,158,255,0.05);border:1px solid rgba(74,158,255,0.2);border-radius:6px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" x-model="currentCompEntry.alwaysInContext"
style="cursor:pointer;">
<span style="font-size:14px;color:rgba(255,255,255,0.9);">Always include in
prose prompts</span>
</label>
<p
style="margin:6px 0 0 26px;font-size:12px;color:rgba(255,255,255,0.5);line-height:1.4;">
When enabled, this entry will be automatically included in all prose
generation prompts (useful for style guides, world rules, etc.)
</p>
</div>
</div>
</template>
</div>
<div style="flex:0 0 auto;display:flex;gap:8px;margin-top:8px;align-items:center;">
<button class="btn btn-primary" @click="saveCompendiumEntry"
:disabled="!currentCompEntry">Save</button>
<span style="margin-left:8px;font-size:13px;color:var(--text-secondary);"
class="compendium-save-status"
:class="{ 'saving': compendiumSaveStatus === 'Saving...' }"
x-text="compendiumSaveStatus"></span>
</div>
</div>
</div>
</div>
</div>
<!-- New Scene Modal -->
<template x-if="showNewSceneModal">
<div class="modal-overlay" @click.self="showNewSceneModal = false">
<div class="modal">
<h3>New Scene</h3>
<input type="text" x-model="newSceneName" placeholder="Scene name (e.g., 'Opening')"
@keydown.enter="createScene">
<div class="modal-buttons">
<button class="btn btn-secondary" @click="showNewSceneModal = false">Cancel</button>
<button class="btn btn-primary" @click="createScene">Create</button>
</div>
</div>
</div>
</template>
<!-- New Chapter Modal -->
<template x-if="showNewChapterModal">
<div class="modal-overlay" @click.self="showNewChapterModal = false">
<div class="modal">
<h3>New Chapter</h3>
<input type="text" x-model="newChapterName" placeholder="Chapter name (e.g., 'Chapter 1')"
@keydown.enter="createChapter">
<div class="modal-buttons">
<button class="btn btn-secondary" @click="showNewChapterModal = false">Cancel</button>
<button class="btn btn-primary" @click="createChapter">Create</button>
</div>
</div>
</div>
</template>
<!-- Rename Chapter Modal -->
<template x-if="showRenameChapterModal">
<div class="modal-overlay" @click.self="showRenameChapterModal = false">
<div class="modal">
<h3>Rename Chapter</h3>
<input type="text" x-model="renameChapterName" placeholder="Chapter name"
@keydown.enter="renameChapter">
<div class="modal-buttons">
<button class="btn btn-secondary" @click="showRenameChapterModal = false">Cancel</button>
<button class="btn btn-primary" @click="renameChapter">Rename</button>
</div>
</div>
</div>
</template>
<!-- Rename Scene Modal -->
<template x-if="showRenameSceneModal">
<div class="modal-overlay" @click.self="showRenameSceneModal = false">
<div class="modal">
<h3>Rename Scene</h3>
<input type="text" x-model="renameSceneName" placeholder="Scene name" @keydown.enter="renameScene">
<div class="modal-buttons">
<button class="btn btn-secondary" @click="showRenameSceneModal = false">Cancel</button>
<button class="btn btn-primary" @click="renameScene">Rename</button>
</div>
</div>
</div>
</template>
<!-- Prompt History Modal -->
<template x-if="showPromptHistory">
<div class="modal-overlay" @click.self="showPromptHistory = false">
<div class="modal" style="max-width:900px;max-height:80vh;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h3 style="margin:0;">Prompt History</h3>
<button class="btn btn-secondary" @click="showPromptHistory = false">Close</button>
</div>
<div style="overflow:auto;max-height:calc(80vh - 100px);">
<template x-if="promptHistoryList.length === 0">
<div style="color:var(--text-secondary);text-align:center;padding:40px;">
No prompt history yet. Generate scenes to build your history.
</div>
</template>
<template x-for="(item, idx) in promptHistoryList" :key="item.id">
<div
style="border:1px solid var(--border);border-radius:6px;padding:12px;margin-bottom:12px;background:var(--bg-secondary);">
<div
style="display:flex;justify-content:space-between;align-items:start;margin-bottom:8px;">
<div style="flex:1;">
<div style="font-size:11px;color:var(--text-secondary);margin-bottom:4px;">
<span x-text="new Date(item.timestamp).toLocaleString()"></span>
<template x-if="item.sceneId">
<span> • Scene: <span
x-text="scenes.find(s => s.id === item.sceneId)?.title || 'Unknown'"></span></span>
</template>
</div>
<div style="font-weight:500;margin-bottom:8px;">
<strong style="color:var(--text-secondary);font-size:11px;">Beat:</strong>
<div style="color:var(--text-primary);font-size:13px;margin-top:2px;"
x-text="item.beat"></div>
</div>
</div>
<button class="btn btn-primary"
@click="beatInput = item.beat; showPromptHistory = false"
style="font-size:11px;padding:4px 8px;">
Reuse Beat
</button>
</div>
<details style="margin-top:8px;">
<summary
style="cursor:pointer;color:var(--text-secondary);font-size:11px;user-select:none;">
View Full Prompt
</summary>
<pre style="margin-top:8px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:4px;font-size:11px;overflow:auto;max-height:300px;white-space:pre-wrap;word-wrap:break-word;"
x-text="item.prompt"></pre>
</details>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- Update Available Dialog -->
<template x-if="showUpdateDialog && updateAvailable">
<div class="modal-overlay" @click.self="showUpdateDialog = false" style="z-index:10000;">
<div class="modal" style="max-width:600px;">
<div class="modal-header">
<h3 style="margin:0;display:flex;align-items:center;gap:8px;">
<span style="font-size:24px;">🎉</span>
Update Available!
</h3>
<button class="btn-icon" @click="showUpdateDialog = false">×</button>
</div>
<div class="modal-body">
<div
style="background:linear-gradient(135deg,rgba(74,158,255,0.1),rgba(74,158,255,0.05));border:1px solid rgba(74,158,255,0.3);border-radius:8px;padding:16px;margin-bottom:16px;">
<div style="font-size:18px;font-weight:600;margin-bottom:8px;color:var(--accent);">
New update available!
</div>
<div style="font-size:13px;color:var(--text-secondary);">
Latest commit (<span x-text="updateAvailable.version"></span>) from <span
x-text="updateAvailable.commitDate"></span>
</div>
</div>
<div style="margin-bottom:16px;">
<div style="font-weight:600;margin-bottom:8px;color:var(--text-primary);">Latest Change:
</div>
<div style="background:var(--bg-tertiary);padding:12px;border-radius:6px;">
<div style="margin:0;font-size:13px;line-height:1.6;color:var(--text-secondary);"
x-text="updateAvailable.message"></div>
</div>
</div>
<div
style="background:rgba(255,193,7,0.1);border:1px solid rgba(255,193,7,0.3);border-radius:6px;padding:12px;margin-bottom:16px;">
<div style="font-size:12px;color:var(--text-secondary);line-height:1.5;">
<strong>⚠️ Note:</strong> Writingway is a local application. To update:
<ol style="margin:8px 0 0 0;padding-left:20px;">
<li>Download the new version from GitHub</li>
<li>Extract to a new folder (don't overwrite current installation)</li>
<li>Your projects are stored in the browser database and will be available in the
new version</li>
<li>Optionally backup your database first (Export projects)</li>
</ol>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="showUpdateDialog = false">Maybe Later</button>
<button class="btn btn-primary"
@click="window.open(updateAvailable.url, '_blank'); showUpdateDialog = false;">
👉 View Release on GitHub
</button>
</div>
</div>
</div>
</template>
<!-- Workshop Chat Modal -->
<template x-if="showWorkshopChat">
<div class="modal-overlay" @click.self="showWorkshopChat = false" style="z-index:9000;"
x-init="(async () => { await loadWorkshopSessions(); if (!workshopSessions || workshopSessions.length === 0) { workshopSessions = [window.workshopChat.createNewSession($data)]; await saveWorkshopSessions(); } })();">
<div class="workshop-chat-container">
<!-- Left: Sessions List -->
<div class="workshop-sessions">
<div class="workshop-sessions-header">
<h4 style="margin:0;font-size:14px;">Conversations</h4>
<button class="btn btn-secondary btn-sm" @click="createWorkshopSession"
title="New Chat">+</button>
</div>
<div class="workshop-sessions-list">
<template x-for="(session, idx) in workshopSessions" :key="session.id">
<div class="workshop-session-item"
:class="{ 'active': idx === currentWorkshopSessionIndex }"
@click="currentWorkshopSessionIndex = idx" x-data="{ menuOpen: false }">
<span x-text="session.name"
style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
<div style="position:relative;">
<button class="btn-icon-sm" @click.stop="menuOpen = !menuOpen"
title="Options"></button>
<div x-show="menuOpen" @click.away="menuOpen = false" x-cloak
class="action-menu" style="right:0;top:100%;margin-top:4px;">
<button
@click.stop="renameWorkshopSession(idx); menuOpen=false">Rename</button>
<button @click.stop="clearWorkshopSession(idx); menuOpen=false">Clear
Messages</button>
<button @click.stop="exportWorkshopSession(idx); menuOpen=false">Export as
Markdown</button>
<button @click.stop="deleteWorkshopSession(idx); menuOpen=false"
style="color:var(--danger,#ef4444);">Delete</button>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- Center: Chat Area -->
<div class="workshop-chat-main">
<!-- Header -->
<div class="workshop-chat-header">
<div style="display:flex;align-items:center;gap:12px;flex:1;flex-wrap:wrap;">
<h3 style="margin:0;font-size:18px;">Workshop Chat</h3>
<select x-model="aiModel" @change="saveGenerationParams()" class="workshop-select"
title="AI Model for this chat"
x-show="aiMode === 'api' || availableLocalModels.length > 0">
<template
x-for="model in (aiMode === 'api' ? (providerModels[aiProvider] || []) : [])"
:key="model.id">
<option :value="model.id" x-text="model.name + (model.recommended ? ' ⭐' : '')">
</option>
</template>
<template x-for="modelName in (aiMode === 'local' ? availableLocalModels : [])"
:key="modelName">
<option :value="modelName" x-text="modelName"></option>
</template>
<!-- Show current model if not in lists -->
<option
x-show="aiModel && ((aiMode === 'api' && !(providerModels[aiProvider] || []).find(m => m.id === aiModel)) || (aiMode === 'local' && !availableLocalModels.includes(aiModel)))"
:value="aiModel" x-text="aiModel"></option>
</select>
<span x-show="aiMode === 'local' && availableLocalModels.length === 0"
style="padding:4px 12px;background:rgba(76,175,80,0.1);border:1px solid rgba(76,175,80,0.3);border-radius:6px;font-size:12px;">
✓ Local Model Active
</span>
<select x-model="selectedWorkshopPromptId" @change="saveSelectedWorkshopPrompt"
class="workshop-select">
<option value="">Default Workshop Prompt</option>
<template x-for="p in prompts.filter(p => p.category === 'workshop')" :key="p.id">
<option :value="p.id" x-text="p.title"></option>
</template>
</select>
<select x-model="workshopFidelityMode" class="workshop-select">
<option value="high">High Fidelity</option>
<option value="balanced">Balanced</option>
<option value="compressed">Compressed</option>
</select>
</div>
<button class="btn btn-secondary" @click="showWorkshopChat = false">Close</button>
</div>
<!-- Messages -->
<div class="workshop-messages" id="workshopMessages">
<template
x-if="workshopSessions[currentWorkshopSessionIndex] && workshopSessions[currentWorkshopSessionIndex].messages.length === 0">
<div class="workshop-empty-state">
<p style="color:var(--text-secondary);margin:0;">Start a conversation with the
AI!</p>
<p style="color:var(--text-secondary);font-size:13px;margin:8px 0 0 0;">
Use <code>@name</code> to reference compendium entries or <code>#scene</code>
to reference scenes.
</p>
</div>
</template>
<template x-if="workshopSessions[currentWorkshopSessionIndex]">
<template x-for="(msg, idx) in workshopSessions[currentWorkshopSessionIndex].messages"
:key="idx">
<div class="workshop-message"
:class="`workshop-message-${msg.role}` + (msg.isError ? ' workshop-message-error' : '')">
<div class="workshop-message-header">
<div class="workshop-message-icon">
<span x-show="msg.role === 'user'">👤</span>
<span x-show="msg.role === 'assistant' && !msg.isError">🤖</span>
<span x-show="msg.isError">⚠️</span>
</div>
<div class="workshop-message-role"
x-text="msg.role === 'user' ? 'You' : (msg.isError ? 'Error' : 'Assistant')">
</div>
</div>
<div class="workshop-message-content">
<template x-if="msg.content">
<span x-text="msg.content"></span>
</template>
<template
x-if="!msg.content && msg.role === 'assistant' && workshopIsGenerating">
<span class="typing-indicator">●●●</span>
</template>
</div>
</div>
</template>
</template>
</div>
<!-- Input Area -->
<div class="workshop-input-area">
<div class="workshop-input-wrapper" style="position:relative;">
<textarea x-model="workshopInput"
@input="window.workshopChat.onWorkshopInput($data, $event)"
@keydown="window.workshopChat.handleWorkshopKeydown($data, $event)"
@keydown.enter.prevent="
if (!$event.shiftKey && workshopInput.trim() && !showWorkshopQuickSearch && !showWorkshopSceneSearch) {
window.workshopChat.sendMessage($data, workshopInput);
}
" placeholder="Type your message... (Use @name for compendium, #scene for scenes)"
rows="3" class="workshop-textarea" :disabled="workshopIsGenerating"></textarea>
<!-- Compendium Quick Search Dropdown -->
<div x-show="showWorkshopQuickSearch" x-cloak class="quick-search-dropdown"
style="position:absolute;bottom:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--bg-primary);border:1px solid var(--border-primary);border-radius:4px;margin-bottom:4px;z-index:9999;">
<template x-for="(item, idx) in workshopQuickSearchMatches" :key="item.id">
<div class="quick-search-item"
@click="window.workshopChat.selectWorkshopQuickMatch($data, item)"
:class="{ 'active': idx === workshopQuickSearchSelectedIndex }"
style="padding:8px;cursor:pointer;font-size:13px;">
<div style="font-weight:600;" x-text="item.title"></div>
<div style="font-size:11px;color:var(--text-secondary);"
x-text="item.category">
</div>
</div>
</template>
</div>
<!-- Scene Search Dropdown -->
<div x-show="showWorkshopSceneSearch" x-cloak class="scene-search-dropdown"
style="position:absolute;bottom:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--bg-primary);border:1px solid var(--border-primary);border-radius:4px;margin-bottom:4px;z-index:9999;">
<template x-for="(scene, idx) in workshopSceneSearchMatches" :key="scene.id">
<div class="scene-search-item"
@click="window.workshopChat.selectWorkshopSceneMatch($data, scene)"
:class="{ 'active': idx === workshopSceneSearchSelectedIndex }"
style="padding:8px;cursor:pointer;font-size:13px;">
<div style="font-weight:600;" x-text="scene.title"></div>
<div style="font-size:11px;color:var(--text-secondary);">
<template x-if="scene.summary">
<span x-text="scene.summary.substring(0, 60) + '...'"></span>
</template>
</div>
</div>
</template>
</div>
<button class="btn btn-primary"
@click="window.workshopChat.sendMessage($data, workshopInput)"
:disabled="!workshopInput.trim() || workshopIsGenerating"
style="align-self:flex-end;margin-top:8px;">
<span x-show="!workshopIsGenerating">Send</span>
<span x-show="workshopIsGenerating">Sending...</span>
</button>
</div>
<div style="font-size:12px;color:var(--text-secondary);margin-top:6px;">
Press Enter to send, Shift+Enter for new line. Type @ for compendium, # for scenes.
</div>
</div>
</div>
<!-- Right: Context Panel (Optional - can be toggled) -->
<div class="workshop-context" x-show="showWorkshopContext" x-cloak>
<div class="workshop-context-header">
<h4 style="margin:0;font-size:14px;">Quick Reference</h4>
<button class="btn-icon-sm" @click="showWorkshopContext = false">×</button>
</div>
<div class="workshop-context-content">
<p style="font-size:13px;color:var(--text-secondary);">
Mentioned items will appear here for quick reference.
</p>
</div>
</div>
</div>
</div>
</template>
</div>
</body>
</html>