mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +08:00
fix(frontend): block duplicate sends during uploads (#1165)
* fix(frontend): block duplicate sends during uploads Expose pre-submit upload work as a busy state so the chat input does not allow a second send while the first attachment is still uploading. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs(frontend): document upload and stream ownership Record that thread hooks own upload-before-submit state while the chat page owns composer busy wiring, so future changes do not reintroduce duplicate socket or upload state handling. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(frontend): separate upload busy state from streaming Keep uploads from reusing the streaming stop state so duplicate submits are blocked without turning the composer into a stop button during file uploads. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -76,6 +76,12 @@ src/
|
||||
- **MagicUI** - Magic UI components
|
||||
- **React Bits** - React bits components
|
||||
|
||||
### Interaction Ownership
|
||||
|
||||
- `src/app/workspace/chats/[thread_id]/page.tsx` owns composer busy-state wiring.
|
||||
- `src/core/threads/hooks.ts` owns pre-submit upload state and thread submission.
|
||||
- `src/hooks/usePoseStream.ts` is a passive store selector; global WebSocket lifecycle stays in `App.tsx`.
|
||||
|
||||
## Resources
|
||||
|
||||
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function ChatPage() {
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
const [thread, sendMessage, isUploading] = useThreadStream({
|
||||
threadId: isNewThread ? undefined : threadId,
|
||||
context: settings.context,
|
||||
isMock,
|
||||
@@ -127,7 +127,7 @@ export default function ChatPage() {
|
||||
extraHeader={
|
||||
isNewThread && <Welcome mode={settings.context.mode} />
|
||||
}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading}
|
||||
onContextChange={(context) => setSettings("context", context)}
|
||||
onSubmit={handleSubmit}
|
||||
onStop={handleStop}
|
||||
|
||||
@@ -156,6 +156,8 @@ export function useThreadStream({
|
||||
|
||||
// Optimistic messages shown before the server stream responds
|
||||
const [optimisticMessages, setOptimisticMessages] = useState<Message[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const sendInFlightRef = useRef(false);
|
||||
// Track message count before sending so we know when server has responded
|
||||
const prevMsgCountRef = useRef(thread.messages.length);
|
||||
|
||||
@@ -175,6 +177,11 @@ export function useThreadStream({
|
||||
message: PromptInputMessage,
|
||||
extraContext?: Record<string, unknown>,
|
||||
) => {
|
||||
if (sendInFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
sendInFlightRef.current = true;
|
||||
|
||||
const text = message.text.trim();
|
||||
|
||||
// Capture current count before showing optimistic messages
|
||||
@@ -217,6 +224,7 @@ export function useThreadStream({
|
||||
try {
|
||||
// Upload files first if any
|
||||
if (message.files && message.files.length > 0) {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
// Convert FileUIPart to File objects by fetching blob URLs
|
||||
const filePromises = message.files.map(async (fileUIPart) => {
|
||||
@@ -293,6 +301,8 @@ export function useThreadStream({
|
||||
toast.error(errorMessage);
|
||||
setOptimisticMessages([]);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +352,10 @@ export function useThreadStream({
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
} catch (error) {
|
||||
setOptimisticMessages([]);
|
||||
setIsUploading(false);
|
||||
throw error;
|
||||
} finally {
|
||||
sendInFlightRef.current = false;
|
||||
}
|
||||
},
|
||||
[thread, _handleOnStart, t.uploads.uploadingFiles, context, queryClient],
|
||||
@@ -357,7 +370,7 @@ export function useThreadStream({
|
||||
} as typeof thread)
|
||||
: thread;
|
||||
|
||||
return [mergedThread, sendMessage] as const;
|
||||
return [mergedThread, sendMessage, isUploading] as const;
|
||||
}
|
||||
|
||||
export function useThreads(
|
||||
|
||||
Reference in New Issue
Block a user