2275 lines
No EOL
162 KiB
HTML
2275 lines
No EOL
162 KiB
HTML
<!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> |