diff --git a/frontend/src/pages/EditorPage.jsx b/frontend/src/pages/EditorPage.jsx index ad68b35..5c4fe38 100644 --- a/frontend/src/pages/EditorPage.jsx +++ b/frontend/src/pages/EditorPage.jsx @@ -14,6 +14,33 @@ import NotesPanel from '../components/NotesPanel' const MIN_FONT = 13 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() { const { id } = useParams() const navigate = useNavigate() @@ -33,7 +60,6 @@ export default function EditorPage() { const [promptText, setPromptText] = useState(null) const [promptSource, setPromptSource] = useState(null) // 'ollama' | 'local' const [promptModel, setPromptModel] = useState(null) - const [promptLoading, setPromptLoading] = useState(false) const [fontSize, setFontSize] = useState( () => 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 prevWordCount = useRef(0) const coverRef = useRef() - // Holds the in-flight prompt request so the click can await it rather than - // starting a brand-new fetch (which would have to wait all over again). - const pendingPrompt = useRef(null) + // Prompt queue — up to 5 AI prompts pre-fetched and ready to go. + // pendingFetches tracks in-flight requests so we know how many more to start. + const promptQueue = useRef([]) + const pendingFetches = useRef([]) const editorRef = useRef() const saveRef = useRef(null) @@ -62,9 +89,8 @@ export default function EditorPage() { .catch(() => setLoadError(true)) }, [id]) - // Kick off a prompt fetch in the background as soon as the page loads so - // Ollama has the full response time before the user ever clicks the button. - useEffect(() => { kickPromptFetch() }, []) // eslint-disable-line react-hooks/exhaustive-deps + // Start filling the prompt queue as soon as the page loads. + useEffect(() => { refillPrompts() }, []) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { localStorage.setItem('sw-fontsize', fontSize) @@ -167,32 +193,28 @@ export default function EditorPage() { await api.updateStory(id, { cover_image: null }) } - // Returns the in-flight prompt Promise, or starts a fresh one. - // Calling this twice before the first resolves hands back the same Promise. - function kickPromptFetch() { - if (!pendingPrompt.current) { - pendingPrompt.current = api.getPrompt() + // Keep up to 5 AI prompts pre-fetched. Each request is tracked individually + // so we always know exactly how many are in-flight vs. ready. + function refillPrompts() { + const needed = 5 - promptQueue.current.length - pendingFetches.current.length + 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() { - setPromptLoading(true) - setPromptText(null) - const promise = kickPromptFetch() - try { - const data = await promise - pendingPrompt.current = null // clear so the next kick starts fresh - 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 fetchPrompt() { + // Use an AI prompt if one is ready, otherwise fall back to a built-in instantly + const item = promptQueue.current.shift() ?? LOCAL_PROMPTS.next() + setPromptText(item.prompt) + setPromptSource(item.source ?? 'local') + setPromptModel(item.model ?? null) + refillPrompts() // top the queue back up to 5 } function insertPrompt() { @@ -251,19 +273,20 @@ export default function EditorPage() {