-
-
Notifications
You must be signed in to change notification settings - Fork 6
fix: cancel suggestion stream on submit to prevent chat blur #565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| $ next dev --turbo | ||
| ▲ Next.js 15.3.8 (Turbopack) | ||
| - Local: http://localhost:3000 | ||
| - Network: http://192.168.0.2:3000 | ||
| - Environments: .env | ||
|
|
||
| ✓ Starting... | ||
| ✓ Compiled middleware in 386ms | ||
| ✓ Ready in 1880ms | ||
| ○ Compiling / ... | ||
| ✓ Compiled / in 28.6s | ||
| Chat DB actions loaded. Ensure getCurrentUserId() is correctly implemented for server-side usage if applicable. | ||
| GET / 200 in 33121ms | ||
| GET / 200 in 976ms | ||
| [Auth] Supabase URL or Anon Key is not set for server-side auth. | ||
| POST / 200 in 1775ms | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |
| const inputRef = useRef<HTMLTextAreaElement>(null) | ||
| const formRef = useRef<HTMLFormElement>(null) | ||
| const fileInputRef = useRef<HTMLInputElement>(null) | ||
| const activeSuggestionRef = useRef<string>('') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a unique request token instead of the query text. Clearing 🛠️ Suggested fix- const activeSuggestionRef = useRef<string>('')
+ const activeSuggestionRef = useRef<symbol | null>(null)
- activeSuggestionRef.current = ''
+ activeSuggestionRef.current = null
- const currentQuery = value
- activeSuggestionRef.current = currentQuery
+ const requestToken = Symbol('suggestions')
+ activeSuggestionRef.current = requestToken
debounceTimeoutRef.current = setTimeout(async () => {
- if (activeSuggestionRef.current !== currentQuery) return
+ if (activeSuggestionRef.current !== requestToken) return
try {
const suggestionsStream = await getSuggestions(value, mapData)
for await (const partialSuggestions of readStreamableValue(
suggestionsStream
)) {
- if (activeSuggestionRef.current !== currentQuery) break
+ if (activeSuggestionRef.current !== requestToken) break
if (partialSuggestions) {
setSuggestions(partialSuggestions as PartialRelated)
}
}Also applies to: 98-98, 140-140, 156-156, 160-172 🤖 Prompt for AI Agents |
||
|
|
||
| useImperativeHandle(ref, () => ({ | ||
| handleAttachmentClick() { | ||
|
|
@@ -91,6 +92,12 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |
| return | ||
| } | ||
|
|
||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
| activeSuggestionRef.current = '' | ||
| setSuggestions(null) | ||
|
|
||
| const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = [] | ||
| if (input) { | ||
| content.push({ type: 'text', text: input }) | ||
|
|
@@ -119,14 +126,20 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |
| formData.append('drawnFeatures', JSON.stringify(mapData.drawnFeatures || [])) | ||
|
|
||
| setInput('') | ||
| setSuggestions(null) | ||
| clearAttachment() | ||
|
|
||
| const responseMessage = await submit(formData) | ||
| setMessages(currentMessages => [...currentMessages, responseMessage as any]) | ||
| } | ||
|
|
||
| const handleClear = async () => { | ||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
| activeSuggestionRef.current = '' | ||
| setMessages([]) | ||
| setSuggestions(null) | ||
| clearAttachment() | ||
| await clearChat() | ||
| } | ||
|
|
@@ -140,17 +153,27 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |
| const wordCount = value.trim().split(/\s+/).filter(Boolean).length | ||
| if (wordCount < 2) { | ||
| setSuggestions(null) | ||
| activeSuggestionRef.current = '' | ||
| return | ||
| } | ||
|
|
||
| const currentQuery = value | ||
| activeSuggestionRef.current = currentQuery | ||
|
|
||
| debounceTimeoutRef.current = setTimeout(async () => { | ||
| const suggestionsStream = await getSuggestions(value, mapData) | ||
| for await (const partialSuggestions of readStreamableValue( | ||
| suggestionsStream | ||
| )) { | ||
| if (partialSuggestions) { | ||
| setSuggestions(partialSuggestions as PartialRelated) | ||
| if (activeSuggestionRef.current !== currentQuery) return | ||
| try { | ||
| const suggestionsStream = await getSuggestions(value, mapData) | ||
| for await (const partialSuggestions of readStreamableValue( | ||
| suggestionsStream | ||
| )) { | ||
| if (activeSuggestionRef.current !== currentQuery) break | ||
| if (partialSuggestions) { | ||
| setSuggestions(partialSuggestions as PartialRelated) | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error(error) | ||
| } | ||
| }, 500) // 500ms debounce delay | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| const fs = require('fs'); | ||
|
|
||
| const path = 'components/chat-panel.tsx'; | ||
| let content = fs.readFileSync(path, 'utf8'); | ||
|
|
||
| // We will add activeSuggestionRef to the component | ||
| content = content.replace( | ||
| 'const fileInputRef = useRef<HTMLInputElement>(null)', | ||
| 'const fileInputRef = useRef<HTMLInputElement>(null)\n const activeSuggestionRef = useRef<string>(\'\')' | ||
| ); | ||
|
|
||
| // We will update debouncedGetSuggestions | ||
| const oldDebounce = ` const debouncedGetSuggestions = useCallback( | ||
| (value: string) => { | ||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
|
|
||
| const wordCount = value.trim().split(/\\s+/).filter(Boolean).length | ||
| if (wordCount < 2) { | ||
| setSuggestions(null) | ||
| return | ||
| } | ||
|
|
||
| debounceTimeoutRef.current = setTimeout(async () => { | ||
| const suggestionsStream = await getSuggestions(value, mapData) | ||
| for await (const partialSuggestions of readStreamableValue( | ||
| suggestionsStream | ||
| )) { | ||
| if (partialSuggestions) { | ||
| setSuggestions(partialSuggestions as PartialRelated) | ||
| } | ||
| } | ||
| }, 500) // 500ms debounce delay | ||
| }, | ||
| [mapData, setSuggestions] | ||
| )`; | ||
|
|
||
| const newDebounce = ` const debouncedGetSuggestions = useCallback( | ||
| (value: string) => { | ||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
|
|
||
| const wordCount = value.trim().split(/\\s+/).filter(Boolean).length | ||
| if (wordCount < 2) { | ||
| setSuggestions(null) | ||
| activeSuggestionRef.current = '' | ||
| return | ||
| } | ||
|
|
||
| const currentQuery = value | ||
| activeSuggestionRef.current = currentQuery | ||
|
|
||
| debounceTimeoutRef.current = setTimeout(async () => { | ||
| if (activeSuggestionRef.current !== currentQuery) return | ||
| try { | ||
| const suggestionsStream = await getSuggestions(value, mapData) | ||
| for await (const partialSuggestions of readStreamableValue( | ||
| suggestionsStream | ||
| )) { | ||
| if (activeSuggestionRef.current !== currentQuery) break | ||
| if (partialSuggestions) { | ||
| setSuggestions(partialSuggestions as PartialRelated) | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error(error) | ||
| } | ||
| }, 500) // 500ms debounce delay | ||
| }, | ||
| [mapData, setSuggestions] | ||
| )`; | ||
|
|
||
| content = content.replace(oldDebounce, newDebounce); | ||
|
|
||
| const oldHandleSubmit = ` const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault() | ||
| if (!input.trim() && !selectedFile) { | ||
| return | ||
| }`; | ||
|
|
||
| const newHandleSubmit = ` const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault() | ||
| if (!input.trim() && !selectedFile) { | ||
| return | ||
| } | ||
|
|
||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
| activeSuggestionRef.current = '' | ||
| setSuggestions(null)`; | ||
|
|
||
| content = content.replace(oldHandleSubmit, newHandleSubmit); | ||
|
|
||
| const oldHandleClear = ` const handleClear = async () => { | ||
| setMessages([]) | ||
| setSuggestions(null)`; | ||
|
|
||
| const newHandleClear = ` const handleClear = async () => { | ||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
| activeSuggestionRef.current = '' | ||
| setMessages([]) | ||
| setSuggestions(null)`; | ||
|
|
||
| content = content.replace(oldHandleClear, newHandleClear); | ||
|
|
||
| fs.writeFileSync(path, content); | ||
|
Comment on lines
+7
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the patcher fail fast and idempotent. These replacements are exact-string rewrites, but none of them verify that a match occurred. A formatting drift turns this into a silent partial/no-op, and several replacements rewrite 🛠️ Suggested fix+function replaceOrThrow(source, from, to, label) {
+ if (source.includes(to)) return source; // already patched
+ if (!source.includes(from)) {
+ throw new Error(`Could not find ${label} in ${path}`);
+ }
+ return source.replace(from, to);
+}
+
-content = content.replace(
+content = replaceOrThrow(
+ content,
'const fileInputRef = useRef<HTMLInputElement>(null)',
- 'const fileInputRef = useRef<HTMLInputElement>(null)\n const activeSuggestionRef = useRef<string>(\'\')'
-);
+ 'const fileInputRef = useRef<HTMLInputElement>(null)\n const activeSuggestionRef = useRef<string>(\'\')',
+ 'activeSuggestionRef declaration'
+);Apply the same helper to the debounce, submit, and clear replacements. 🤖 Prompt for AI Agents |
||
| console.log("Patched suggestions with active tracking"); | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1 @@ | ||||||||
| grep -rnw -A 10 -B 5 "const handleSubmit = async" components/chat-panel.tsx | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a shebang so this runs under a known shell.
🛠️ Suggested fix+#!/usr/bin/env bash
grep -rnw -A 10 -B 5 "const handleSubmit = async" components/chat-panel.tsx📝 Committable suggestion
Suggested change
🧰 Tools🪛 Shellcheck (0.11.0)[error] 1-1: Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive. (SC2148) 🤖 Prompt for AI Agents |
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the local dev log from source control.
This is transient runtime output, not a reproducible test artifact. It adds review noise and exposes machine-specific details like the LAN address and local auth warning without helping validate the chat-panel fix.
🤖 Prompt for AI Agents