From 9354d61292a466750c6d06d2f5e33642cd0d6044 Mon Sep 17 00:00:00 2001 From: Harsh Kumar Date: Tue, 28 Jul 2026 14:41:53 -0400 Subject: [PATCH 1/2] feat(chat): refine assistant response guidance Favor complete everyday sentences, concrete passage-grounded options, and one easy next step so responses remain readable under time pressure. Clarify locator excerpts as two or three nearby words in total with only the target bolded. Preserve the longer-response exception and avoid examples drawn from text outside the active passage. --- src/components/chatbot/Chatbot.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/chatbot/Chatbot.tsx b/src/components/chatbot/Chatbot.tsx index 292c919..c8df27d 100644 --- a/src/components/chatbot/Chatbot.tsx +++ b/src/components/chatbot/Chatbot.tsx @@ -21,6 +21,11 @@ interface ChatTabProps { chatReady?: boolean; } +/** + * Keep the locator-excerpt convention identical across both stages so users + * learn one predictable way to distinguish a suggested word from its context. + * Stage addenda change the assistant's goal, not its output format. + */ const systemMessageDefault = ` You are a helpful AI assistant embedded in a blackout poetry web app. You are helping the user create a blackout poem from a fixed passage. @@ -28,15 +33,19 @@ Blackout poetry: the poet starts with an existing passage and creates a poem by Grounding: - Work only with the passage provided below. Never reference or substitute any other text. -- When suggesting a specific word choice, show it in a short excerpt containing 2–3 nearby words from the passage when available. Bold only the suggested word so it is clear which word to select; the unbolded context is only a locator, not part of the suggested poem. -- Quote suggested words exactly as written in the passage, preserve passage order, and suggest at most five at a time. -- Use bold only for suggested words from the passage, never for general emphasis. +- When you point to a specific passage word, show it in a short excerpt containing two or three nearby passage words in total when available. Bold only the word you are pointing to. The unbolded words are only a locator to help the user find it; they are not part of the suggestion. +- Quote passage words exactly as written, keep multiple suggested words in passage order, and point to at most five words in a single response. +- Use bold only for passage words you are pointing to, never for general emphasis. - Never suggest a word that does not appear in the passage. Style and behavior: - Be warm, natural, and conversational, like a capable writing partner. Avoid ungrounded or sycophantic flattery. -- The user is working under time pressure. Keep responses under 80 words unless the user asks for more. Use plain prose; no headers or tables; use a short list only when presenting options. -- If the user's direction is unclear, ask one brief clarifying question. When offering creative directions, present two or three distinct options rather than a single recommendation. +- Write in complete, everyday sentences. Never compress responses into fragments, labels, or note-style phrasing. If a response is running long, cut options rather than grammar. +- Use plain language a casual reader can understand on the first pass: prefer feelings and concrete images over literary-analysis terminology unless the user uses that terminology first. +- The user is working under time pressure. Keep responses under 80 words unless the user explicitly asks for more; most turns should be two to four short sentences. +- When offering creative directions, give two, or at most three, distinct options. Make each option a complete sentence grounded in something concrete from the passage. A short bulleted list is fine, but do not use headers or tables. +- End with at most one easy question or next step that the user can answer with a quick choice or reaction. +- If the user's direction is unclear, ask one brief clarifying question instead of guessing. - Do not write a complete poem unless the user explicitly asks you to. If they explicitly ask, do it. - You are text-only. You cannot generate images, browse the web, run code, or use any external tools, and you should not offer to. Do not include images or hyperlinks in your responses. - If the user asks for help unrelated to this task, briefly steer them back to the poem. @@ -45,9 +54,9 @@ Style and behavior: const stageMessages: Record = { SPARK: - "The user is currently reading the passage and brainstorming—exploring themes, moods, and directions, and taking notes. Help them figure out what they might want to express. You may point to evocative words in the passage, but do not push them to finalize word selections yet.", + "The user is reading the passage and brainstorming—figuring out what they might want to express and taking notes for later. Talk about the passage's images, moments, and feelings. You may point out striking words as material worth noting, but nothing is final yet. Do not pressure the user to commit to a direction or start building lines.", WRITE: - "The user is now composing—selecting words from the passage to build the poem. Their current selections appear below and update as they work. Help them find words that realize their intent, refine or trim what they have, and get unstuck if they stall. When suggesting sequences of words, respect the passage-order rule.", + "The user is now composing—selecting words from the passage to build the poem. Their current selections appear below and update as they work. Help them find words that realize their intent, refine or trim what they have, and get unstuck if they stall.", }; const promptSuggestions: Record = { From e907f0429011a9effaec73b22a177407820a27e2 Mon Sep 17 00:00:00 2001 From: Harsh Kumar Date: Tue, 28 Jul 2026 14:43:01 -0400 Subject: [PATCH 2/2] fix(chat): recover safely from failed sends Prevent overlapping requests and route the send button through the form's single submit path. Show a recoverable connection error, remove the failed optimistic message, and restore unsent text without overwriting anything the user typed while waiting. Send only role and content fields to the API. --- src/components/chatbot/Chatbot.tsx | 47 ++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/components/chatbot/Chatbot.tsx b/src/components/chatbot/Chatbot.tsx index c8df27d..25a9d82 100644 --- a/src/components/chatbot/Chatbot.tsx +++ b/src/components/chatbot/Chatbot.tsx @@ -1,4 +1,11 @@ -import { useEffect, useState, useRef, useMemo, useContext } from "react"; +import { + useEffect, + useState, + useRef, + useMemo, + useContext, + type KeyboardEvent, +} from "react"; import { FiSend } from "react-icons/fi"; import { Button, Textarea } from "@chakra-ui/react"; import { nanoid } from "nanoid"; @@ -142,8 +149,8 @@ export default function ChatTab({ const [input, setInput] = useState(""); // const [lastResponseId] = useState(null); const [markdownOutput, setMarkdownOutput] = useState(""); - const [hasUsedFirstPrompt, setHasUsedFirstPrompt] = useState(false); const [hasShownIdleNudge, setHasShownIdleNudge] = useState(false); + const [sendError, setSendError] = useState(null); const hasUserMessageInStage = messages .slice(stageStartMessageCountRef.current) @@ -206,7 +213,7 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, const sendMessage = async (messageContent?: string) => { const content = messageContent || input; - if (!content.trim()) return; + if (!content.trim() || isLLMLoading) return; if (timeoutRef.current) { clearTimeout(timeoutRef.current); @@ -220,9 +227,13 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, timestamp: new Date(), }; - const strippedMessages = messages.map(({ id, timestamp, ...rest }) => rest); + const strippedMessages = messages.map(({ role, content }) => ({ + role, + content, + })); setMarkdownOutput(""); + setSendError(null); setMessages((prev) => [...prev, artistMessage]); setInput(""); setIsLLMLoading(true); @@ -253,12 +264,23 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, setMarkdownOutput(""); // Clear this so the streaming UI disappears } catch (error) { console.error("LLM response failed", error); + setMarkdownOutput(""); + setSendError( + "The assistant had a connection problem. Send your message again.", + ); + // Remove the optimistic message before restoring it for a clean retry. + setMessages((previousMessages) => + previousMessages.filter((message) => message.id !== artistMessage.id), + ); + setInput((currentInput) => + currentInput.trim() ? currentInput : content, + ); } finally { setIsLLMLoading(false); } }; - const handleKeyDown = (e: any) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); @@ -266,8 +288,6 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, }; const handlePromptSelection = (prompt: string) => { - setInput(prompt); - setHasUsedFirstPrompt(true); sendMessage(prompt); }; @@ -294,7 +314,6 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, {chatReady && !hasUserMessageInStage && - !hasUsedFirstPrompt && !isLLMLoading && (

Try asking me:

@@ -339,6 +358,11 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, {/* Input area */} {
+ {sendError && ( +

+ {sendError} +

+ )}
{ e.preventDefault(); @@ -353,7 +377,12 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`, placeholder="Type a message..." className="text-main bg-white flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-grey" /> -