diff --git a/.changeset/ai-plan-approve-feedback.md b/.changeset/ai-plan-approve-feedback.md new file mode 100644 index 000000000..65edf2c81 --- /dev/null +++ b/.changeset/ai-plan-approve-feedback.md @@ -0,0 +1,12 @@ +--- +'@object-ui/plugin-chatbot': patch +'@object-ui/app-shell': patch +--- + +Plan-card approval gives immediate in-card feedback (#2627): clicking +"Build it" flips the clicked card to a spinning "Building…" badge right away +(the approval's chat-level effects land at the bottom of the thread, outside +the viewport, so the card looked untouched for ~10s and users double-clicked). +The durable Built state still derives from the message stream; an approval +that never left the client (rate limit / offline) rolls the badge back so the +button returns. New `planBuildingLabel` prop (AiChatPage passes zh). diff --git a/packages/app-shell/src/console/ai/AiChatPage.tsx b/packages/app-shell/src/console/ai/AiChatPage.tsx index e18d0f97f..294cd6bf6 100644 --- a/packages/app-shell/src/console/ai/AiChatPage.tsx +++ b/packages/app-shell/src/console/ai/AiChatPage.tsx @@ -2158,6 +2158,9 @@ export function ChatPane({ planApproveLabel={t('console.ai.planApprove', { defaultValue: 'Build it' })} planAdjustLabel={t('console.ai.planAdjust', { defaultValue: 'Adjust' })} planBuiltLabel={t('console.ai.planBuilt', { defaultValue: 'Built' })} + planBuildingLabel={ + convZh ? '正在搭建…' : t('console.ai.planBuilding', { defaultValue: 'Building…' }) + } planReadyLabel={t('console.ai.planReady', { defaultValue: 'The plan is ready. Build it now, or tell me what to adjust.', })} diff --git a/packages/plugin-chatbot/src/ChatbotEnhanced.tsx b/packages/plugin-chatbot/src/ChatbotEnhanced.tsx index ddb1ae312..9997cdb6e 100644 --- a/packages/plugin-chatbot/src/ChatbotEnhanced.tsx +++ b/packages/plugin-chatbot/src/ChatbotEnhanced.tsx @@ -666,6 +666,8 @@ export interface ChatbotEnhancedProps extends React.HTMLAttributes( planApproveLabel = 'Build it', planAdjustLabel = 'Adjust', planBuiltLabel = 'Built', + planBuildingLabel = 'Building…', planReadyLabel = 'The plan is ready. Build it now, or tell me what to adjust.', planApproveMessage = 'Looks good — build it as proposed.', planApproveDefaultsMessage = 'Build it with your best assumptions; use sensible defaults for the open questions.', @@ -1501,6 +1504,10 @@ const ChatbotEnhanced = React.forwardRef( // clears any prior send-failure restore guard. lastSubmittedRef.current = text; restoredErrorRef.current = null; + // A newer send supersedes any pending approve as "the last send" — an + // unsent failure of THIS message must not roll back a plan card whose + // approval already reached the server (#2627). + lastApprovedPlanIdRef.current = null; onSendMessage?.(text, files); }, [onSendMessage] @@ -1508,6 +1515,7 @@ const ChatbotEnhanced = React.forwardRef( const handleSuggestionClick = React.useCallback( (text: string) => { + lastApprovedPlanIdRef.current = null; onSendMessage?.(text); }, [onSendMessage] @@ -1519,8 +1527,28 @@ const ChatbotEnhanced = React.forwardRef( // approval explicitly authorizes sensible defaults so a click never silently // drops them. const promptInputWrapRef = React.useRef(null); + // Optimistic per-card feedback (#2627): the approval's visible effect (a + // new user bubble + a streaming turn) lands at the BOTTOM of the thread — + // outside the viewport when the plan card is scrolled into view — so the + // card itself looked untouched for the ~10s until apply_blueprint started + // and users clicked again. Flip the clicked card to a "Building…" badge + // immediately; the durable Built state still derives from the message + // stream (builtPlanIds). A send that never left the client (rate limit / + // offline) rolls the flip back so the buttons return. + const [approvedPlanIds, setApprovedPlanIds] = React.useState>( + () => new Set(), + ); + const lastApprovedPlanIdRef = React.useRef(null); const handlePlanApprove = React.useCallback( - (hasOpenQuestions: boolean) => { + (hasOpenQuestions: boolean, toolCallId?: string) => { + if (toolCallId) { + lastApprovedPlanIdRef.current = toolCallId; + setApprovedPlanIds((prev) => { + const next = new Set(prev); + next.add(toolCallId); + return next; + }); + } onSendMessage?.(hasOpenQuestions ? planApproveDefaultsMessage : planApproveMessage); }, [onSendMessage, planApproveMessage, planApproveDefaultsMessage] @@ -1544,6 +1572,18 @@ const ChatbotEnhanced = React.forwardRef( if (!error || !isUnsentSendError(error)) return; if (restoredErrorRef.current === error) return; restoredErrorRef.current = error; + // The approval message never left the client — roll the optimistic + // "Building…" badge back so the Build-it button returns (#2627). + if (lastApprovedPlanIdRef.current) { + const failedId = lastApprovedPlanIdRef.current; + lastApprovedPlanIdRef.current = null; + setApprovedPlanIds((prev) => { + if (!prev.has(failedId)) return prev; + const next = new Set(prev); + next.delete(failedId); + return next; + }); + } const text = lastSubmittedRef.current; if (!text) return; const textarea = promptInputWrapRef.current?.querySelector('textarea'); @@ -2149,6 +2189,16 @@ const ChatbotEnhanced = React.forwardRef( {planBuiltLabel} + ) : approvedPlanIds.has(tool.toolCallId) ? ( +
+ + + {planBuildingLabel} + +
) : (
(
+ ) : approvedPlanIds.has(tool.toolCallId) ? ( +
+ + + {planBuildingLabel} + +
) : (
(