Prompt queue: pre-fetch 5, instant local fallback, emoji source icon
- Refill a queue of 5 AI prompts in the background on page load - Track in-flight fetches with pendingFetches ref so we never over-request - fetchPrompt() is now synchronous: pops from queue or instantly returns a shuffled built-in (LOCAL_PROMPTS, 12 entries, no-repeat cycling) - Source badge replaced with a small emoji in the top-right corner of the popover: 🤖 for AI prompts, 📚 for built-ins - Removed promptLoading state and spinner entirely Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c5b7d7f774
commit
48d3533bfc
@ -14,6 +14,33 @@ import NotesPanel from '../components/NotesPanel'
|
|||||||
const MIN_FONT = 13
|
const MIN_FONT = 13
|
||||||
const MAX_FONT = 26
|
const MAX_FONT = 26
|
||||||
|
|
||||||
|
// Small client-side fallback pool — used instantly when the AI queue is empty.
|
||||||
|
// Shuffled so the same prompt doesn't appear twice before cycling through all.
|
||||||
|
const LOCAL_PROMPTS = (() => {
|
||||||
|
const list = [
|
||||||
|
"A door appears in your bedroom wall overnight. It wasn't there when you went to sleep.",
|
||||||
|
"You find a letter in your pocket written in your own handwriting — dated next week.",
|
||||||
|
"The last dragon in the world lands in your garden. It is very small and very scared.",
|
||||||
|
"Your shadow starts leaving notes under your pillow.",
|
||||||
|
"Every story you write comes true — but always with one small, wrong detail.",
|
||||||
|
"A train arrives at your station that isn't on any timetable. The conductor says you have been expected.",
|
||||||
|
"You discover you have one magical power: you can always find things that are lost. Not everything that is lost wants to be found.",
|
||||||
|
"The stars rearrange themselves every night. Last night you finally worked out what language they're writing in.",
|
||||||
|
"A bookshop appears on your street that wasn't there yesterday. Inside are books about your life — including things that haven't happened yet.",
|
||||||
|
"Everyone in your town wakes up speaking a different language. Everyone except you.",
|
||||||
|
"The person chosen to save the world is seventy-three years old, very tired, and was just about to have a cup of tea.",
|
||||||
|
"You are the only one who can hear the sea talking. It is furious about something.",
|
||||||
|
]
|
||||||
|
let queue = []
|
||||||
|
const shuffle = arr => [...arr].sort(() => Math.random() - 0.5)
|
||||||
|
return {
|
||||||
|
next() {
|
||||||
|
if (!queue.length) queue = shuffle(list)
|
||||||
|
return { prompt: queue.pop(), source: 'local', model: null }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
export default function EditorPage() {
|
export default function EditorPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -33,7 +60,6 @@ export default function EditorPage() {
|
|||||||
const [promptText, setPromptText] = useState(null)
|
const [promptText, setPromptText] = useState(null)
|
||||||
const [promptSource, setPromptSource] = useState(null) // 'ollama' | 'local'
|
const [promptSource, setPromptSource] = useState(null) // 'ollama' | 'local'
|
||||||
const [promptModel, setPromptModel] = useState(null)
|
const [promptModel, setPromptModel] = useState(null)
|
||||||
const [promptLoading, setPromptLoading] = useState(false)
|
|
||||||
const [fontSize, setFontSize] = useState(
|
const [fontSize, setFontSize] = useState(
|
||||||
() => Math.max(MIN_FONT, Math.min(MAX_FONT, parseInt(localStorage.getItem('sw-fontsize')) || 17))
|
() => Math.max(MIN_FONT, Math.min(MAX_FONT, parseInt(localStorage.getItem('sw-fontsize')) || 17))
|
||||||
)
|
)
|
||||||
@ -43,9 +69,10 @@ export default function EditorPage() {
|
|||||||
const latestContent = useRef({})
|
const latestContent = useRef({})
|
||||||
const prevWordCount = useRef(0)
|
const prevWordCount = useRef(0)
|
||||||
const coverRef = useRef()
|
const coverRef = useRef()
|
||||||
// Holds the in-flight prompt request so the click can await it rather than
|
// Prompt queue — up to 5 AI prompts pre-fetched and ready to go.
|
||||||
// starting a brand-new fetch (which would have to wait all over again).
|
// pendingFetches tracks in-flight requests so we know how many more to start.
|
||||||
const pendingPrompt = useRef(null)
|
const promptQueue = useRef([])
|
||||||
|
const pendingFetches = useRef([])
|
||||||
const editorRef = useRef()
|
const editorRef = useRef()
|
||||||
const saveRef = useRef(null)
|
const saveRef = useRef(null)
|
||||||
|
|
||||||
@ -62,9 +89,8 @@ export default function EditorPage() {
|
|||||||
.catch(() => setLoadError(true))
|
.catch(() => setLoadError(true))
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
// Kick off a prompt fetch in the background as soon as the page loads so
|
// Start filling the prompt queue as soon as the page loads.
|
||||||
// Ollama has the full response time before the user ever clicks the button.
|
useEffect(() => { refillPrompts() }, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
useEffect(() => { kickPromptFetch() }, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('sw-fontsize', fontSize)
|
localStorage.setItem('sw-fontsize', fontSize)
|
||||||
@ -167,32 +193,28 @@ export default function EditorPage() {
|
|||||||
await api.updateStory(id, { cover_image: null })
|
await api.updateStory(id, { cover_image: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the in-flight prompt Promise, or starts a fresh one.
|
// Keep up to 5 AI prompts pre-fetched. Each request is tracked individually
|
||||||
// Calling this twice before the first resolves hands back the same Promise.
|
// so we always know exactly how many are in-flight vs. ready.
|
||||||
function kickPromptFetch() {
|
function refillPrompts() {
|
||||||
if (!pendingPrompt.current) {
|
const needed = 5 - promptQueue.current.length - pendingFetches.current.length
|
||||||
pendingPrompt.current = api.getPrompt()
|
for (let i = 0; i < needed; i++) {
|
||||||
|
const p = api.getPrompt()
|
||||||
|
.then(data => { promptQueue.current.push(data) })
|
||||||
|
.catch(() => {}) // server always returns something; this is a safety net
|
||||||
|
.finally(() => {
|
||||||
|
pendingFetches.current = pendingFetches.current.filter(x => x !== p)
|
||||||
|
})
|
||||||
|
pendingFetches.current = [...pendingFetches.current, p]
|
||||||
}
|
}
|
||||||
return pendingPrompt.current
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPrompt() {
|
function fetchPrompt() {
|
||||||
setPromptLoading(true)
|
// Use an AI prompt if one is ready, otherwise fall back to a built-in instantly
|
||||||
setPromptText(null)
|
const item = promptQueue.current.shift() ?? LOCAL_PROMPTS.next()
|
||||||
const promise = kickPromptFetch()
|
setPromptText(item.prompt)
|
||||||
try {
|
setPromptSource(item.source ?? 'local')
|
||||||
const data = await promise
|
setPromptModel(item.model ?? null)
|
||||||
pendingPrompt.current = null // clear so the next kick starts fresh
|
refillPrompts() // top the queue back up to 5
|
||||||
setPromptText(data.prompt)
|
|
||||||
setPromptSource(data.source ?? null)
|
|
||||||
setPromptModel(data.model ?? null)
|
|
||||||
kickPromptFetch() // pre-load the next one immediately
|
|
||||||
} catch {
|
|
||||||
pendingPrompt.current = null
|
|
||||||
toast('Could not fetch a prompt', 'error')
|
|
||||||
} finally {
|
|
||||||
setPromptLoading(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertPrompt() {
|
function insertPrompt() {
|
||||||
@ -251,19 +273,20 @@ export default function EditorPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="topbar-right">
|
<div className="topbar-right">
|
||||||
<div className="prompt-wrap">
|
<div className="prompt-wrap">
|
||||||
<button className="btn btn-ghost" onClick={fetchPrompt} disabled={promptLoading} title="Get a writing prompt">
|
<button className="btn btn-ghost" onClick={fetchPrompt} title="Get a writing prompt">
|
||||||
{promptLoading ? '…' : <>💡 <span>Prompt</span></>}
|
💡 <span>Prompt</span>
|
||||||
</button>
|
</button>
|
||||||
{promptText && (
|
{promptText && (
|
||||||
<div className="prompt-popover">
|
<div className="prompt-popover">
|
||||||
<p className="prompt-text">{promptText}</p>
|
|
||||||
{promptSource && (
|
{promptSource && (
|
||||||
<div className={`prompt-source prompt-source--${promptSource}`}>
|
<span
|
||||||
{promptSource === 'ollama'
|
className={`prompt-source-icon prompt-source--${promptSource}`}
|
||||||
? <>🤖 AI · {promptModel || 'ollama'}</>
|
title={promptSource === 'ollama' ? `AI · ${promptModel || 'ollama'}` : 'Built-in prompt'}
|
||||||
: <>📚 Built-in prompt</>}
|
>
|
||||||
</div>
|
{promptSource === 'ollama' ? '🤖' : '📚'}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<p className="prompt-text">{promptText}</p>
|
||||||
<div className="prompt-actions">
|
<div className="prompt-actions">
|
||||||
<button className="btn btn-ghost" onClick={fetchPrompt}>↻ Another</button>
|
<button className="btn btn-ghost" onClick={fetchPrompt}>↻ Another</button>
|
||||||
<button className="btn btn-primary" onClick={insertPrompt}>Insert</button>
|
<button className="btn btn-primary" onClick={insertPrompt}>Insert</button>
|
||||||
|
|||||||
@ -1488,18 +1488,14 @@ button { cursor: pointer; font-family: inherit; }
|
|||||||
margin-bottom: 0.875rem;
|
margin-bottom: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-source {
|
.prompt-source-icon {
|
||||||
font-size: 0.7rem;
|
position: absolute;
|
||||||
letter-spacing: 0.05em;
|
top: 0.6rem;
|
||||||
font-family: var(--font-head);
|
right: 0.7rem;
|
||||||
margin-bottom: 0.6rem;
|
font-size: 0.85rem;
|
||||||
display: inline-block;
|
line-height: 1;
|
||||||
padding: 0.15rem 0.5rem;
|
opacity: 0.55;
|
||||||
border-radius: 99px;
|
|
||||||
border: 1px solid currentColor;
|
|
||||||
}
|
}
|
||||||
.prompt-source--ollama { color: var(--accent-hi); }
|
|
||||||
.prompt-source--local { color: var(--text-muted); }
|
|
||||||
|
|
||||||
.prompt-actions {
|
.prompt-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user