display prompt separately from story text

This commit is contained in:
2026-06-08 19:54:49 +10:00
parent df8cd67c90
commit 3b8cae9baa
5 changed files with 54 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ export interface StoryMeta {
wordCountTarget?: number
tags?: string[]
notes?: string
prompt?: string
}
export interface Market {

View File

@@ -72,13 +72,12 @@ export function PromptHero(): JSX.Element {
}
const name = `Story ${stories.length + 1}`
const created = await window.api.createStory(name)
await window.api.setStoryMeta(created.id, { prompt })
const refreshed = await window.api.listStories()
setStories(refreshed)
const story = refreshed.find((s) => s.path === created.path)
if (story) {
const initial = `> ${prompt}\n\n`
await window.api.writeStory(story.path, initial)
setActiveStory(story.path, story.id, initial)
setActiveStory(story.path, story.id, '')
}
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import { EditorView, keymap, Decoration, type DecorationSet, ViewPlugin, type ViewUpdate } from '@codemirror/view'
import { EditorState, StateField, StateEffect, RangeSetBuilder, Compartment } from '@codemirror/state'
import { markdown } from '@codemirror/lang-markdown'
@@ -53,11 +53,11 @@ const annPlugin = ViewPlugin.fromClass(class {
const themeCompartment = new Compartment()
const fontSizeCompartment = new Compartment()
function buildTheme(fontSize: number, isDark: boolean): ReturnType<typeof EditorView.theme> {
function buildTheme(fontSize: number, isDark: boolean, hasPrompt = false): ReturnType<typeof EditorView.theme> {
return EditorView.theme({
'&': { height: '100%', background: 'transparent' },
'.cm-scroller': { fontFamily: 'Georgia, "Times New Roman", serif', fontSize: `${fontSize}px`, lineHeight: '1.85', overflow: 'auto' },
'.cm-content': { maxWidth: '680px', margin: '0 auto', padding: '48px 24px 24px', caretColor: 'var(--accent)' },
'.cm-content': { maxWidth: '680px', margin: '0 auto', padding: `${hasPrompt ? '24px' : '48px'} 24px 24px`, caretColor: 'var(--accent)' },
'.cm-line': { padding: '0' },
'.cm-cursor': { borderLeftColor: 'var(--accent)', borderLeftWidth: '2px' },
'.cm-selectionBackground': { background: 'rgba(200,169,110,0.25)' },
@@ -85,6 +85,7 @@ export function MarkdownEditor(): JSX.Element {
const viewRef = useRef<EditorView | null>(null)
const lastPathRef = useRef<string | null>(null)
const storeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [storyPrompt, setStoryPrompt] = useState<string | null>(null)
// Initialize editor
@@ -159,6 +160,14 @@ export function MarkdownEditor(): JSX.Element {
}
}, [activeStoryPath, activeStoryContent, setRevisions])
// Load prompt from meta when story changes
useEffect(() => {
if (!activeStoryId) { setStoryPrompt(null); return }
window.api.getStoryMeta(activeStoryId).then((meta) => {
setStoryPrompt(meta.prompt ?? null)
}).catch(() => setStoryPrompt(null))
}, [activeStoryId])
// Sync annotations into editor
useEffect(() => {
const view = viewRef.current
@@ -168,8 +177,8 @@ export function MarkdownEditor(): JSX.Element {
// Update theme
useEffect(() => {
viewRef.current?.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
}, [theme, fontSize])
viewRef.current?.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark', !!storyPrompt)) })
}, [theme, fontSize, storyPrompt])
if (!activeStoryId) {
return <div className="no-story-placeholder">Select a story or create a new one</div>
@@ -178,6 +187,14 @@ export function MarkdownEditor(): JSX.Element {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative' }}>
<WordCountBar />
{storyPrompt && (
<div className="story-prompt-banner">
<div className="story-prompt-banner-inner">
<span className="story-prompt-banner-label">Prompt</span>
{storyPrompt}
</div>
</div>
)}
<div
style={{ flex: 1, overflow: 'hidden', position: 'relative' }}
onMouseDown={(e) => {

View File

@@ -799,3 +799,31 @@ textarea { resize: vertical; }
/* Misc */
.inline-edit { background: none; border: none; padding: 0; font: inherit; color: inherit; width: 100%; outline: none; border-bottom: 1px solid var(--accent); }
.no-story-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text3); font-size: 14px; }
/* ── Story prompt banner ─────────────────────────────────────────────────── */
.story-prompt-banner {
flex-shrink: 0;
overflow: hidden;
font-family: Georgia, "Times New Roman", serif;
}
.story-prompt-banner-inner {
max-width: 680px;
margin: 0 auto;
padding: 28px 24px 0;
font-size: 13px;
font-style: italic;
color: var(--text3);
line-height: 1.6;
}
.story-prompt-banner-label {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-style: normal;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text3);
margin-bottom: 6px;
opacity: 0.7;
}

View File

@@ -3,6 +3,7 @@ export interface StoryMeta {
wordCountTarget?: number
tags?: string[]
notes?: string
prompt?: string
}
export interface StoryFile {