-
Notifications
You must be signed in to change notification settings - Fork 1.7k
improve sse performance #2851
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
improve sse performance #2851
Changes from all commits
e4ab69f
573eaf8
7142ce2
5ec2207
e03d802
acdd487
7f18e89
6ecdd61
9f63d35
ad6f53f
cdaa7e6
dcf4f9d
5a17df7
dd0f12d
f870bc5
fd5cf9f
3d4ffbf
ad7820d
282951a
042c24e
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 | ||||
|---|---|---|---|---|---|---|
| @@ -1,8 +1,9 @@ | ||||||
|
|
||||||
|
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. 🧩 Analysis chainMust mark as Client Component (uses hooks, events, observer). This file runs client-only code; without a client boundary it will fail in Next.js. Apply at top: + 'use client';🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Looking for the target file (exact path)..."
target="apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx"
if [ -f "$target" ]; then
echo "FOUND: $target"
files=("$target")
else
echo "Not found at exact path. Searching for 'chat-input/index.tsx' under apps/web/client/src/app..."
mapfile -t files < <(rg --hidden --files apps/web/client/src/app 2>/dev/null | rg 'chat-input/index.tsx' || true)
if [ ${#files[@]} -eq 0 ]; then
echo "No matching files found. Listing top-level tree for apps/web/client/src/app/project (maxdepth 4) to help locate file..."
if [ -d "apps/web/client/src/app/project" ]; then
find apps/web/client/src/app/project -maxdepth 4 -type f | sed -n '1,200p'
else
echo "Directory apps/web/client/src/app/project does not exist."
fi
exit 0
fi
fi
for file in "${files[@]}"; do
echo
echo "===== FILE: $file ====="
echo "---- head (1..200) ----"
sed -n '1,200p' "$file" || true
echo
echo "---- 'use client' directive (exact match) ----"
rg -n "^['\"]use client['\"]" "$file" || echo "(none)"
echo
echo "---- client-only hooks / events / observer usage ----"
rg -n -C2 --hidden -S '\b(useState|useEffect|useRef|useCallback|useMemo|useContext|useTransition|onClick|onSubmit|onChange|onBlur|observer)\b' "$file" || echo "(none found)"
echo
echo "---- imports referencing observer/mobx/react hooks ----"
rg -n "import .*observer|from (\"|')mobx|mobx-react|mobx-react-lite|react-use|@hookstate" "$file" || echo "(none found)"
echo "=============================="
done
echo
echo "---- Files under apps/web/client/src/app that include a top-level 'use client' directive ----"
rg -n --hidden "^['\"]use client['\"]" apps/web/client/src/app || echo "(none)"Length of output: 20299 Add 'use client' at the top of this file (component uses React hooks, mobx observer, and browser/DOM events). File: apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx + 'use client';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| import { useChatContext } from '@/app/project/[id]/_hooks/use-chat'; | ||||||
| import type { SendMessage } from '@/app/project/[id]/_hooks/use-chat'; | ||||||
| import { useEditorEngine } from '@/components/store/editor'; | ||||||
| import { FOCUS_CHAT_INPUT_EVENT } from '@/components/store/editor/chat'; | ||||||
| import { transKeys } from '@/i18n/keys'; | ||||||
| import type { ChatMessage, ChatSuggestion } from '@onlook/models'; | ||||||
| import { ChatType, EditorTabValue, type ImageMessageContext } from '@onlook/models'; | ||||||
| import { MessageContextType } from '@onlook/models/chat'; | ||||||
| import { Button } from '@onlook/ui/button'; | ||||||
|
|
@@ -21,21 +22,29 @@ import { Suggestions, type SuggestionsRef } from '../suggestions'; | |||||
| import { ActionButtons } from './action-buttons'; | ||||||
| import { ChatModeToggle } from './chat-mode-toggle'; | ||||||
|
|
||||||
| interface ChatInputProps { | ||||||
| messages: ChatMessage[]; | ||||||
| suggestions: ChatSuggestion[]; | ||||||
| isStreaming: boolean; | ||||||
| onStop: () => Promise<void>; | ||||||
| onSendMessage: SendMessage; | ||||||
| } | ||||||
|
|
||||||
| export const ChatInput = observer(({ | ||||||
| inputValue, | ||||||
| setInputValue, | ||||||
| }: { | ||||||
| inputValue: string; | ||||||
| setInputValue: React.Dispatch<React.SetStateAction<string>>; | ||||||
| }) => { | ||||||
| const { sendMessageToChat, stop, isWaiting } = useChatContext(); | ||||||
| messages, | ||||||
| suggestions, | ||||||
| isStreaming, | ||||||
| onStop, | ||||||
| onSendMessage, | ||||||
| }: ChatInputProps) => { | ||||||
| const editorEngine = useEditorEngine(); | ||||||
| const t = useTranslations(); | ||||||
| const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
| const [isComposing, setIsComposing] = useState(false); | ||||||
| const [actionTooltipOpen, setActionTooltipOpen] = useState(false); | ||||||
| const [isDragging, setIsDragging] = useState(false); | ||||||
| const chatMode = editorEngine.state.chatMode; | ||||||
| const [inputValue, setInputValue] = useState(''); | ||||||
|
|
||||||
| const focusInput = () => { | ||||||
| requestAnimationFrame(() => { | ||||||
|
|
@@ -44,10 +53,10 @@ export const ChatInput = observer(({ | |||||
| }; | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| if (textareaRef.current && !isWaiting) { | ||||||
| if (textareaRef.current && !isStreaming) { | ||||||
| focusInput(); | ||||||
| } | ||||||
| }, [editorEngine.chat.conversation.current?.messages]); | ||||||
| }, [isStreaming, messages]); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| if (editorEngine.state.rightPanelTab === EditorTabValue.CHAT) { | ||||||
|
|
@@ -57,7 +66,7 @@ export const ChatInput = observer(({ | |||||
|
|
||||||
| useEffect(() => { | ||||||
| const focusHandler = () => { | ||||||
| if (textareaRef.current && !isWaiting) { | ||||||
| if (textareaRef.current && !isStreaming) { | ||||||
| focusInput(); | ||||||
| } | ||||||
| }; | ||||||
|
|
@@ -83,7 +92,7 @@ export const ChatInput = observer(({ | |||||
| return () => window.removeEventListener('keydown', handleGlobalKeyDown, true); | ||||||
| }, []); | ||||||
|
|
||||||
| const disabled = isWaiting | ||||||
| const disabled = isStreaming | ||||||
| const inputEmpty = !inputValue || inputValue.trim().length === 0; | ||||||
|
|
||||||
| function handleInput(e: React.ChangeEvent<HTMLTextAreaElement>) { | ||||||
|
|
@@ -116,7 +125,7 @@ export const ChatInput = observer(({ | |||||
| } | ||||||
|
|
||||||
| if (!inputEmpty) { | ||||||
| sendMessage(); | ||||||
| void sendMessage(); | ||||||
| } | ||||||
| } | ||||||
| }; | ||||||
|
|
@@ -126,17 +135,13 @@ export const ChatInput = observer(({ | |||||
| console.warn('Empty message'); | ||||||
| return; | ||||||
| } | ||||||
| if (isWaiting) { | ||||||
| if (isStreaming) { | ||||||
| console.warn('Already waiting for response'); | ||||||
| return; | ||||||
| } | ||||||
| const savedInput = inputValue.trim(); | ||||||
| try { | ||||||
| const message = chatMode === ChatType.ASK | ||||||
| ? await editorEngine.chat.addAskMessage(savedInput) | ||||||
| : await editorEngine.chat.addEditMessage(savedInput); | ||||||
|
|
||||||
| await sendMessageToChat(chatMode); | ||||||
| await onSendMessage(savedInput, chatMode); | ||||||
| setInputValue(''); | ||||||
| } catch (error) { | ||||||
| console.error('Error sending message', error); | ||||||
|
|
@@ -162,7 +167,7 @@ export const ChatInput = observer(({ | |||||
| if (!file) { | ||||||
| continue; | ||||||
| } | ||||||
| handleImageEvent(file, 'Pasted image'); | ||||||
| void handleImageEvent(file, 'Pasted image'); | ||||||
| break; | ||||||
| } | ||||||
Kitenite marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| } | ||||||
|
|
@@ -179,7 +184,7 @@ export const ChatInput = observer(({ | |||||
| if (!file) { | ||||||
| continue; | ||||||
| } | ||||||
| handleImageEvent(file, 'Dropped image'); | ||||||
| void handleImageEvent(file, 'Dropped image'); | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -198,14 +203,14 @@ export const ChatInput = observer(({ | |||||
| const reader = new FileReader(); | ||||||
| reader.onload = async (event) => { | ||||||
| const compressedImage = await compressImageInBrowser(file); | ||||||
| const base64URL = compressedImage || (event.target?.result as string); | ||||||
| const base64URL = compressedImage ?? (event.target?.result as string); | ||||||
| const contextImage: ImageMessageContext = { | ||||||
| type: MessageContextType.IMAGE, | ||||||
| content: base64URL, | ||||||
| mimeType: file.type, | ||||||
| displayName: displayName ?? file.name, | ||||||
| }; | ||||||
| editorEngine.chat.context.context.push(contextImage); | ||||||
| editorEngine.chat.context.addContexts([contextImage]); | ||||||
| }; | ||||||
| reader.readAsDataURL(file); | ||||||
| }; | ||||||
|
|
@@ -218,15 +223,13 @@ export const ChatInput = observer(({ | |||||
|
|
||||||
| const { success, errorMessage } = validateImageLimit(currentImages, 1); | ||||||
| if (!success) { | ||||||
| toast.error(errorMessage); | ||||||
| return; | ||||||
| throw new Error(errorMessage); | ||||||
| } | ||||||
|
|
||||||
| const framesWithViews = editorEngine.frames.getAll().filter(f => !!f.view); | ||||||
|
|
||||||
| if (framesWithViews.length === 0) { | ||||||
| toast.error('No active frame available for screenshot'); | ||||||
| return; | ||||||
| throw new Error('No active frame available for screenshot'); | ||||||
| } | ||||||
|
|
||||||
| let screenshotData = null; | ||||||
|
|
@@ -250,8 +253,7 @@ export const ChatInput = observer(({ | |||||
| } | ||||||
|
|
||||||
| if (!screenshotData) { | ||||||
| toast.error('Failed to capture screenshot. Please refresh the page and try again.'); | ||||||
| return; | ||||||
| throw new Error('No screenshot data'); | ||||||
| } | ||||||
|
|
||||||
| const contextImage: ImageMessageContext = { | ||||||
|
|
@@ -260,10 +262,10 @@ export const ChatInput = observer(({ | |||||
| mimeType: mimeType, | ||||||
| displayName: 'Screenshot', | ||||||
| }; | ||||||
| editorEngine.chat.context.context.push(contextImage); | ||||||
| editorEngine.chat.context.addContexts([contextImage]); | ||||||
| toast.success('Screenshot added to chat'); | ||||||
| } catch (error) { | ||||||
| toast.error('Failed to capture screenshot. Please try again.'); | ||||||
| toast.error('Failed to capture screenshot. Error: ' + error); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
|
|
@@ -326,6 +328,8 @@ export const ChatInput = observer(({ | |||||
| > | ||||||
| <Suggestions | ||||||
| ref={suggestionRef} | ||||||
| suggestions={suggestions} | ||||||
| isStreaming={isStreaming} | ||||||
| disabled={disabled} | ||||||
| inputValue={inputValue} | ||||||
| setInput={(suggestion) => { | ||||||
|
|
@@ -395,7 +399,7 @@ export const ChatInput = observer(({ | |||||
| handleImageEvent={handleImageEvent} | ||||||
| handleScreenshot={handleScreenshot} | ||||||
| /> | ||||||
| {isWaiting ? ( | ||||||
| {isStreaming ? ( | ||||||
| <Tooltip open={actionTooltipOpen} onOpenChange={setActionTooltipOpen}> | ||||||
| <TooltipTrigger asChild> | ||||||
| <Button | ||||||
|
|
@@ -404,7 +408,7 @@ export const ChatInput = observer(({ | |||||
| className="text-smallPlus w-fit h-full py-0.5 px-2.5 text-primary" | ||||||
| onClick={() => { | ||||||
| setActionTooltipOpen(false); | ||||||
| stop(); | ||||||
| void onStop(); | ||||||
| }} | ||||||
| > | ||||||
| <Icons.Stop /> | ||||||
|
|
@@ -418,7 +422,7 @@ export const ChatInput = observer(({ | |||||
| variant={'secondary'} | ||||||
| className="text-smallPlus w-fit h-full py-0.5 px-2.5 text-primary" | ||||||
| disabled={inputEmpty || disabled} | ||||||
| onClick={sendMessage} | ||||||
| onClick={() => void sendMessage()} | ||||||
| > | ||||||
| <Icons.ArrowRight /> | ||||||
| </Button> | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.