475 lines
No EOL
28 KiB
HTML
475 lines
No EOL
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<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 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/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>
|
|
<div x-data="app" x-init="init()">
|
|
<!-- debug banner removed -->
|
|
<!-- Main App -->
|
|
<template x-if="currentProject && !showModelLoading">
|
|
<div class="app-container">
|
|
<!-- Sidebar -->
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>Writingway</h1>
|
|
<p>AI-Powered Creative Writing</p>
|
|
</div>
|
|
|
|
<!-- AI Status -->
|
|
<div class="ai-status">
|
|
<div class="ai-status-item" style="cursor:pointer;" @click="showAISettings = true">
|
|
<div class="status-dot" :class="aiStatus"></div>
|
|
<span x-text="aiStatusText"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="project-info" x-data="{ openMenu:false }" style="position:relative;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
|
<h2 x-text="currentProject.name" style="margin:0;padding-right:8px;"></h2>
|
|
|
|
<div style="display:flex;align-items:center;gap:6px;">
|
|
<button class="btn btn-secondary" @click.stop="showCodexPanel = true"
|
|
title="Codex">Codex</button>
|
|
<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="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 @click="chapter.expanded = !chapter.expanded; currentChapter = chapter"
|
|
style="flex:1;cursor:pointer;display:flex;align-items:center;gap:8px;">
|
|
<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>
|
|
<span class="caret" :class="{ 'rotated': chapter.expanded }">▾</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="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)"
|
|
style="display:flex;justify-content:space-between;align-items:center;">
|
|
<div>
|
|
<div class="scene-title" x-text="scene.title"></div>
|
|
<div class="scene-meta" x-text="`${scene.wordCount || 0} words`"></div>
|
|
</div>
|
|
<div class="item-actions" style="margin-left:12px;"
|
|
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;">Move
|
|
to
|
|
<select class="move-select"
|
|
@change.stop="moveSceneToChapter(scene.id, $event.target.value); openMenu=false;">
|
|
<option value="" disabled selected>Choose chapter
|
|
</option>
|
|
<template x-for="ch in chapters" :key="ch.id">
|
|
<option :value="ch.id"
|
|
:selected="ch.id === scene.chapterId"
|
|
x-text="ch.title"></option>
|
|
</template>
|
|
</select>
|
|
</label>
|
|
</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="deleteScene(scene.id); openMenu=false">Delete
|
|
scene</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div style="display:flex;flex-direction:column;padding:10px;gap:8px;">
|
|
<button class="add-scene-btn" @click="openNewSceneModal()">
|
|
+ New Scene
|
|
</button>
|
|
|
|
<button class="add-scene-btn" @click="openNewChapterModal()">
|
|
+ New Chapter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor -->
|
|
<div class="editor-container">
|
|
<div class="editor-header">
|
|
<div class="editor-title" x-text="currentScene ? currentScene.title : 'No scene selected'">
|
|
</div>
|
|
<div class="editor-stats">
|
|
<span x-text="`${currentSceneWords} words`"></span>
|
|
<span class="save-indicator" :class="{ 'saving': isSaving }" x-text="saveStatus"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="editor-main">
|
|
<template x-if="currentScene">
|
|
<textarea class="editor-textarea" x-model="currentScene.content" @input="onEditorChange"
|
|
placeholder="Start writing your scene here..."></textarea>
|
|
</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-panel">
|
|
<h3>Generate from Beat</h3>
|
|
<div class="beat-input-group">
|
|
<textarea class="beat-input" x-model="beatInput"
|
|
placeholder="Describe what happens next (e.g., 'She grabs her bag and leaves the apartment. She senses tension in the corridor.')"
|
|
rows="2"></textarea>
|
|
|
|
<div style="display:flex;flex-direction:column;gap:8px;align-items:flex-start;">
|
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
<button class="generate-btn" @click="generateFromBeat"
|
|
:disabled="!beatInput || isGenerating || aiStatus !== 'ready'">
|
|
<span x-text="isGenerating ? 'Generating...' : 'Generate'"></span>
|
|
</button>
|
|
|
|
<button class="btn btn-secondary" @click="showSceneOptions = !showSceneOptions"
|
|
title="Scene options">
|
|
⚙️
|
|
</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;">
|
|
<div style="display:flex;flex-direction:column;gap:8px;">
|
|
<label style="font-size:12px;color:var(--text-secondary);">POV CHARACTER
|
|
<input type="text" x-model="povCharacter" placeholder="e.g., Alice"
|
|
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
|
|
</label>
|
|
|
|
<label style="font-size:12px;color:var(--text-secondary);">POV
|
|
<select x-model="pov"
|
|
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"
|
|
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>
|
|
|
|
<div style="display:flex;gap:8px;margin-top:6px;">
|
|
<button class="btn btn-secondary"
|
|
@click="showPromptsPanel = true">Prompts</button>
|
|
</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 20px;">
|
|
<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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- AI Settings Modal (placeholder) -->
|
|
<template x-if="showAISettings">
|
|
<div class="modal-overlay" @click.self="showAISettings = false">
|
|
<div class="modal">
|
|
<h3>AI Provider Settings</h3>
|
|
<p style="color:var(--text-secondary);">Configure AI providers and API keys here (placeholder).</p>
|
|
<div style="margin-top:12px;">
|
|
<label style="font-size:12px;color:var(--text-secondary);">Provider
|
|
<input type="text" placeholder="e.g., local / remote"
|
|
style="width:100%;margin-top:6px;padding:8px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);">
|
|
</label>
|
|
</div>
|
|
<div class="modal-buttons" style="margin-top:18px;">
|
|
<button class="btn btn-secondary" @click="showAISettings = false">Close</button>
|
|
<button class="btn btn-primary" @click="showAISettings = false">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Prompts Slide Panel -->
|
|
<div class="slide-panel-right" :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>
|
|
<button class="btn btn-secondary" @click="createPrompt('prose')">+ New</button>
|
|
</div>
|
|
|
|
<template x-for="cat in promptCategories" :key="cat">
|
|
<div style="margin-bottom:12px;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer;"
|
|
@click="promptCollapsed[cat] = !promptCollapsed[cat]">
|
|
<div style="text-transform:capitalize;font-weight:700;color:var(--text-primary);"
|
|
x-text="cat"></div>
|
|
<div style="font-size:12px;color:var(--text-secondary);"
|
|
x-text="`${(prompts.filter(p => p.category === cat) || []).length}`"></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 @click="openPrompt(pr.id)"
|
|
:class="{ 'active': currentPrompt && currentPrompt.id === pr.id }"
|
|
style="padding:8px;border-radius:6px;background:var(--bg-tertiary);margin-bottom:6px;cursor:pointer;">
|
|
<div style="font-weight:600;color:var(--text-primary);" x-text="pr.title">
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-secondary);"
|
|
x-text="new Date(pr.modified).toLocaleString()"></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>
|
|
</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>
|
|
|
|
<!-- Codex Slide Panel -->
|
|
<div class="slide-panel-right" :class="{ 'open': showCodexPanel }" x-cloak aria-hidden="true">
|
|
<div class="slide-panel-header">
|
|
<h3 style="margin:0;">Codex</h3>
|
|
<div>
|
|
<button class="btn btn-secondary" @click="showCodexPanel = false">Close</button>
|
|
</div>
|
|
</div>
|
|
<div class="slide-panel-body">
|
|
<p style="color:var(--text-secondary);">Story bible placeholder. Add character bios and world notes here
|
|
later.</p>
|
|
<textarea
|
|
style="width:100%;height:100%;padding:8px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);border-radius:6px;">Codex notes go here.</textarea>
|
|
</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>
|
|
</div>
|
|
|
|
|
|
</body>
|
|
|
|
</html> |