Initial commit (code only; models ignored)
This commit is contained in:
commit
e9fcb1b58a
21 changed files with 1102 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.gguf filter=lfs diff=lfs merge=lfs -text
|
||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# models / checkpoints / caches
|
||||
models/
|
||||
*.gguf
|
||||
*.bin
|
||||
*.pt
|
||||
*.safetensors
|
||||
*.zip
|
||||
*.7z
|
||||
*.tar*
|
||||
# typical Python/IDE junk
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
env/
|
||||
.venv/
|
||||
build/
|
||||
dist/
|
||||
.vscode/
|
||||
.idea/
|
||||
1
README.md
Normal file
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Writingway 2.0, readme is WIP ;)
|
||||
BIN
ggml-base.dll
Normal file
BIN
ggml-base.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-alderlake.dll
Normal file
BIN
ggml-cpu-alderlake.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-haswell.dll
Normal file
BIN
ggml-cpu-haswell.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-icelake.dll
Normal file
BIN
ggml-cpu-icelake.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-sandybridge.dll
Normal file
BIN
ggml-cpu-sandybridge.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-sapphirerapids.dll
Normal file
BIN
ggml-cpu-sapphirerapids.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-skylakex.dll
Normal file
BIN
ggml-cpu-skylakex.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-sse42.dll
Normal file
BIN
ggml-cpu-sse42.dll
Normal file
Binary file not shown.
BIN
ggml-cpu-x64.dll
Normal file
BIN
ggml-cpu-x64.dll
Normal file
Binary file not shown.
BIN
ggml-rpc.dll
Normal file
BIN
ggml-rpc.dll
Normal file
Binary file not shown.
BIN
ggml.dll
Normal file
BIN
ggml.dll
Normal file
Binary file not shown.
BIN
libcurl-x64.dll
Normal file
BIN
libcurl-x64.dll
Normal file
Binary file not shown.
BIN
libomp140.x86_64.dll
Normal file
BIN
libomp140.x86_64.dll
Normal file
Binary file not shown.
BIN
llama-server.exe
Normal file
BIN
llama-server.exe
Normal file
Binary file not shown.
BIN
llama.dll
Normal file
BIN
llama.dll
Normal file
Binary file not shown.
958
main.html
Normal file
958
main.html
Normal file
|
|
@ -0,0 +1,958 @@
|
|||
<!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 defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dexie@3.2.4/dist/dexie.min.js"></script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2a2a2a;
|
||||
--bg-tertiary: #3a3a3a;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--accent: #4a9eff;
|
||||
--accent-hover: #3a7fd9;
|
||||
--border: #404040;
|
||||
--success: #4ade80;
|
||||
--warning: #fbbf24;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.project-info {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.project-info h2 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.project-info .word-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ai-status {
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ai-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-dot.loading {
|
||||
background: var(--warning);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.status-dot.ready {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.scene-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.scene-item {
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 5px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.scene-item:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.scene-item.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.scene-item .scene-title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.scene-item .scene-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.add-scene-btn {
|
||||
margin: 10px;
|
||||
padding: 12px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.add-scene-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Editor Area */
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 15px 30px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editor-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.save-indicator {
|
||||
padding: 4px 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.save-indicator.saving {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-textarea {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
background: var(--bg-primary);
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-family: 'Georgia', serif;
|
||||
}
|
||||
|
||||
.editor-textarea::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Beat Panel */
|
||||
.beat-panel {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.beat-panel h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.beat-input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.beat-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.beat-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
padding: 12px 24px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.generate-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.generate-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.context-info {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Welcome Screen */
|
||||
.welcome-screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.welcome-content h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.welcome-content p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.welcome-content .note {
|
||||
font-size: 14px;
|
||||
color: var(--warning);
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
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: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div x-data="app" x-init="init()">
|
||||
<!-- 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">
|
||||
<div class="status-dot" :class="aiStatus"></div>
|
||||
<span x-text="aiStatusText"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-info">
|
||||
<h2 x-text="currentProject.name"></h2>
|
||||
<div class="word-count" x-text="`${totalWords} words`"></div>
|
||||
</div>
|
||||
|
||||
<div class="scene-list">
|
||||
<template x-for="scene in scenes" :key="scene.id">
|
||||
<div
|
||||
class="scene-item"
|
||||
:class="{ 'active': currentScene && currentScene.id === scene.id }"
|
||||
@click="loadScene(scene.id)">
|
||||
<div class="scene-title" x-text="scene.title"></div>
|
||||
<div class="scene-meta" x-text="`${scene.wordCount || 0} words`"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button class="add-scene-btn" @click="showNewSceneModal = true">
|
||||
+ New Scene
|
||||
</button>
|
||||
</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>
|
||||
<button
|
||||
class="generate-btn"
|
||||
@click="generateFromBeat"
|
||||
:disabled="!beatInput || isGenerating || aiStatus !== 'ready'">
|
||||
<span x-text="isGenerating ? 'Generating...' : 'Generate'"></span>
|
||||
</button>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Dexie Database
|
||||
const db = new Dexie('WritingwayDB');
|
||||
db.version(1).stores({
|
||||
projects: 'id, name, created, modified',
|
||||
scenes: 'id, projectId, title, order, created, modified',
|
||||
content: 'sceneId, text, wordCount'
|
||||
});
|
||||
|
||||
// Alpine.js App
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('app', () => ({
|
||||
// State
|
||||
currentProject: null,
|
||||
currentScene: null,
|
||||
scenes: [],
|
||||
beatInput: '',
|
||||
isGenerating: false,
|
||||
isSaving: false,
|
||||
saveStatus: 'Saved',
|
||||
saveTimeout: null,
|
||||
showNewProjectModal: false,
|
||||
showNewSceneModal: false,
|
||||
newProjectName: '',
|
||||
newSceneName: '',
|
||||
|
||||
// AI State
|
||||
aiWorker: null,
|
||||
aiStatus: 'loading', // loading, ready, error
|
||||
aiStatusText: 'Initializing...',
|
||||
showModelLoading: false,
|
||||
loadingMessage: 'Setting up AI...',
|
||||
loadingProgress: 0,
|
||||
|
||||
// Computed
|
||||
get currentSceneWords() {
|
||||
if (!this.currentScene || !this.currentScene.content) return 0;
|
||||
return this.countWords(this.currentScene.content);
|
||||
},
|
||||
|
||||
get totalWords() {
|
||||
return this.scenes.reduce((total, scene) => total + (scene.wordCount || 0), 0);
|
||||
},
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
await this.loadLastProject();
|
||||
await this.initializeAI();
|
||||
},
|
||||
|
||||
async initializeAI() {
|
||||
try {
|
||||
this.showModelLoading = true;
|
||||
this.loadingMessage = 'Connecting to AI server...';
|
||||
this.loadingProgress = 30;
|
||||
|
||||
// Check if llama-server is running
|
||||
const response = await fetch('http://localhost:8080/health');
|
||||
|
||||
if (response.ok) {
|
||||
this.loadingProgress = 100;
|
||||
this.loadingMessage = 'Connected to AI!';
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
this.aiStatus = 'ready';
|
||||
this.aiStatusText = 'AI Ready (Local Server)';
|
||||
this.showModelLoading = false;
|
||||
|
||||
console.log('✓ Connected to llama-server successfully');
|
||||
} else {
|
||||
throw new Error('Server not responding');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not connect to AI server:', error);
|
||||
this.aiStatus = 'error';
|
||||
this.aiStatusText = 'AI server not running';
|
||||
this.showModelLoading = false;
|
||||
|
||||
console.log('Make sure start.bat launched llama-server successfully');
|
||||
}
|
||||
},
|
||||
|
||||
async loadLastProject() {
|
||||
const projects = await db.projects.toArray();
|
||||
if (projects.length > 0) {
|
||||
this.currentProject = projects[0];
|
||||
await this.loadScenes();
|
||||
if (this.scenes.length > 0) {
|
||||
await this.loadScene(this.scenes[0].id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Project Management
|
||||
async createProject() {
|
||||
if (!this.newProjectName) return;
|
||||
|
||||
const project = {
|
||||
id: Date.now().toString(),
|
||||
name: this.newProjectName,
|
||||
created: new Date(),
|
||||
modified: new Date()
|
||||
};
|
||||
|
||||
await db.projects.add(project);
|
||||
this.currentProject = project;
|
||||
this.showNewProjectModal = false;
|
||||
this.newProjectName = '';
|
||||
|
||||
await this.createDefaultScene();
|
||||
},
|
||||
|
||||
async createDefaultScene() {
|
||||
const scene = {
|
||||
id: Date.now().toString(),
|
||||
projectId: this.currentProject.id,
|
||||
title: 'Scene 1',
|
||||
order: 0,
|
||||
created: new Date(),
|
||||
modified: new Date()
|
||||
};
|
||||
|
||||
await db.scenes.add(scene);
|
||||
await db.content.add({
|
||||
sceneId: scene.id,
|
||||
text: '',
|
||||
wordCount: 0
|
||||
});
|
||||
|
||||
await this.loadScenes();
|
||||
await this.loadScene(scene.id);
|
||||
},
|
||||
|
||||
// Scene Management
|
||||
async loadScenes() {
|
||||
this.scenes = await db.scenes
|
||||
.where('projectId')
|
||||
.equals(this.currentProject.id)
|
||||
.sortBy('order');
|
||||
|
||||
for (let scene of this.scenes) {
|
||||
const content = await db.content.get(scene.id);
|
||||
scene.wordCount = content ? content.wordCount : 0;
|
||||
}
|
||||
},
|
||||
|
||||
async createScene() {
|
||||
if (!this.newSceneName) return;
|
||||
|
||||
const scene = {
|
||||
id: Date.now().toString(),
|
||||
projectId: this.currentProject.id,
|
||||
title: this.newSceneName,
|
||||
order: this.scenes.length,
|
||||
created: new Date(),
|
||||
modified: new Date()
|
||||
};
|
||||
|
||||
await db.scenes.add(scene);
|
||||
await db.content.add({
|
||||
sceneId: scene.id,
|
||||
text: '',
|
||||
wordCount: 0
|
||||
});
|
||||
|
||||
this.showNewSceneModal = false;
|
||||
this.newSceneName = '';
|
||||
await this.loadScenes();
|
||||
await this.loadScene(scene.id);
|
||||
},
|
||||
|
||||
async loadScene(sceneId) {
|
||||
const scene = await db.scenes.get(sceneId);
|
||||
const content = await db.content.get(sceneId);
|
||||
|
||||
this.currentScene = {
|
||||
...scene,
|
||||
content: content ? content.text : ''
|
||||
};
|
||||
},
|
||||
|
||||
// Editor
|
||||
onEditorChange() {
|
||||
this.saveStatus = 'Unsaved';
|
||||
clearTimeout(this.saveTimeout);
|
||||
this.saveTimeout = setTimeout(() => {
|
||||
this.saveScene();
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
async saveScene() {
|
||||
if (!this.currentScene) return;
|
||||
|
||||
this.isSaving = true;
|
||||
this.saveStatus = 'Saving...';
|
||||
|
||||
const wordCount = this.countWords(this.currentScene.content);
|
||||
|
||||
await db.content.put({
|
||||
sceneId: this.currentScene.id,
|
||||
text: this.currentScene.content,
|
||||
wordCount: wordCount
|
||||
});
|
||||
|
||||
await db.scenes.update(this.currentScene.id, {
|
||||
modified: new Date()
|
||||
});
|
||||
|
||||
const sceneIndex = this.scenes.findIndex(s => s.id === this.currentScene.id);
|
||||
if (sceneIndex !== -1) {
|
||||
this.scenes[sceneIndex].wordCount = wordCount;
|
||||
}
|
||||
|
||||
this.isSaving = false;
|
||||
this.saveStatus = 'Saved';
|
||||
},
|
||||
|
||||
countWords(text) {
|
||||
if (!text) return 0;
|
||||
return text.trim().split(/\s+/).filter(word => word.length > 0).length;
|
||||
},
|
||||
|
||||
// AI Generation (Real llama-server)
|
||||
async generateFromBeat() {
|
||||
if (!this.beatInput || this.aiStatus !== 'ready') return;
|
||||
|
||||
this.isGenerating = true;
|
||||
|
||||
try {
|
||||
// Build the prompt
|
||||
const prompt = this.buildPrompt(this.beatInput, this.currentScene?.content || '');
|
||||
|
||||
console.log('Sending generation request to llama-server...');
|
||||
|
||||
// Call llama-server's completion endpoint
|
||||
const response = await fetch('http://localhost:8080/completion', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
n_predict: 300,
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
stop: ['<|im_end|>', '<|endoftext|>', '\n\n\n\n', 'USER:', 'HUMAN:'],
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status}`);
|
||||
}
|
||||
|
||||
// Stream the response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE format (Server-Sent Events)
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
if (data.content) {
|
||||
// Stream each token into the editor
|
||||
this.currentScene.content += data.content;
|
||||
}
|
||||
if (data.stop) {
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse errors for incomplete chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ Generation complete');
|
||||
|
||||
// Clear beat input
|
||||
this.beatInput = '';
|
||||
|
||||
// Auto-save after generation
|
||||
await this.saveScene();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Generation error:', error);
|
||||
alert('Failed to generate text. Make sure llama-server is running.\n\nError: ' + error.message);
|
||||
} finally {
|
||||
this.isGenerating = false;
|
||||
}
|
||||
},
|
||||
|
||||
buildPrompt(beat, sceneContext) {
|
||||
// Qwen3 uses ChatML format
|
||||
const systemPrompt = `You are a creative writing assistant. The author provides a BEAT (what happens next) and you expand it into vivid, engaging prose. Write 2-3 paragraphs that bring the beat to life. Match the author's tone and style. Use sensory details. Show, don't tell.`;
|
||||
|
||||
let contextText = '';
|
||||
if (sceneContext && sceneContext.length > 0) {
|
||||
// Use last 500 words of current scene as context
|
||||
const words = sceneContext.split(/\s+/);
|
||||
const contextWords = words.slice(-500).join(' ');
|
||||
contextText = `\n\nCURRENT SCENE SO FAR:\n${contextWords}`;
|
||||
}
|
||||
|
||||
const prompt = `<|im_start|>system
|
||||
${systemPrompt}<|im_end|>
|
||||
<|im_start|>user
|
||||
${contextText}
|
||||
|
||||
BEAT TO EXPAND:
|
||||
${beat}
|
||||
|
||||
Write the next 2-3 paragraphs:<|im_end|>
|
||||
<|im_start|>assistant
|
||||
`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
mtmd.dll
Normal file
BIN
mtmd.dll
Normal file
Binary file not shown.
124
start.bat
Normal file
124
start.bat
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
@echo off
|
||||
title Writingway 2.0
|
||||
color 0A
|
||||
|
||||
echo.
|
||||
echo ================================
|
||||
echo Starting Writingway 2.0...
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
REM Check if llama.cpp server exists
|
||||
if not exist "llama-server.exe" (
|
||||
echo [!] llama-server.exe not found!
|
||||
echo.
|
||||
echo Please download llama.cpp for Windows:
|
||||
echo 1. Go to: https://github.com/ggerganov/llama.cpp/releases
|
||||
echo 2. Download: llama-XXX-bin-win-cuda-cu12.2.0-x64.zip
|
||||
echo ^(or the non-CUDA version if you don't have NVIDIA GPU^)
|
||||
echo 3. Extract llama-server.exe to this folder
|
||||
echo.
|
||||
echo Expected location: %CD%\llama-server.exe
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if model exists
|
||||
if not exist "models\Qwen3-4B-Instruct-2507-IQ4_XS.gguf" (
|
||||
echo [!] Model file not found!
|
||||
echo.
|
||||
echo Expected location: %CD%\models\Qwen3-4B-Instruct-2507-IQ4_XS.gguf
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [OK] llama-server.exe found
|
||||
echo [OK] Model file found
|
||||
echo.
|
||||
|
||||
REM Check if Python is installed
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [!] Python not found!
|
||||
echo.
|
||||
echo Please install Python from: https://www.python.org/downloads/
|
||||
echo Make sure to check "Add Python to PATH" during install
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [OK] Python found
|
||||
echo.
|
||||
echo ================================
|
||||
echo Starting AI Model Server...
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
REM Start llama.cpp server in background
|
||||
start "Writingway AI Server" cmd /c "llama-server.exe -m models\Qwen3-4B-Instruct-2507-IQ4_XS.gguf -c 4096 -ngl 999 --port 8080 --host 127.0.0.1"
|
||||
|
||||
echo [*] AI server starting on port 8080...
|
||||
echo [*] Waiting for AI server to initialize...
|
||||
|
||||
REM Wait for llama server to be ready (check every second, max 30 seconds)
|
||||
set /a counter=0
|
||||
:wait_for_llama
|
||||
timeout /t 1 /nobreak >nul
|
||||
set /a counter+=1
|
||||
|
||||
REM Try to connect to the server
|
||||
curl -s http://localhost:8080/health >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo [OK] AI server is ready!
|
||||
goto start_web
|
||||
)
|
||||
|
||||
if %counter% lss 30 (
|
||||
echo Still waiting... ^(%counter%/30^)
|
||||
goto wait_for_llama
|
||||
)
|
||||
|
||||
echo [!] AI server took too long to start
|
||||
echo [*] Continuing anyway - you can reload the page once server is ready
|
||||
echo.
|
||||
|
||||
:start_web
|
||||
echo.
|
||||
echo ================================
|
||||
echo Starting Web Server...
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
REM Start Python HTTP server and open browser
|
||||
echo [*] Starting web server on port 8000...
|
||||
echo [*] Opening Writingway in your browser...
|
||||
echo.
|
||||
echo ================================
|
||||
echo Writingway is running!
|
||||
echo ================================
|
||||
echo.
|
||||
echo Web UI: http://localhost:8000/main.html
|
||||
echo AI API: http://localhost:8080
|
||||
echo.
|
||||
echo Keep this window open while using Writingway.
|
||||
echo Close this window to stop both servers.
|
||||
echo.
|
||||
echo Press Ctrl+C to stop manually.
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
REM Open browser
|
||||
start "" http://localhost:8000/main.html
|
||||
|
||||
REM Start Python web server (blocks here)
|
||||
python -m http.server 8000
|
||||
|
||||
REM Cleanup when Python server stops
|
||||
echo.
|
||||
echo [*] Shutting down servers...
|
||||
taskkill /FI "WindowTitle eq Writingway AI Server*" /T /F >nul 2>&1
|
||||
echo [*] All servers stopped.
|
||||
pause
|
||||
Loading…
Add table
Add a link
Reference in a new issue