Add visual loading indicator to new agent page
- Add isSubmitLoading prop to AgentInputArea component to allow parent to control loading state externally - When isSubmitLoading is true: - Show ActivityIndicator instead of ArrowUp icon in send button - Disable send button with reduced opacity - Pass isLoading state from new.tsx to AgentInputArea via isSubmitLoading prop Files changed: - packages/app/src/components/agent-input-area.tsx:53-54, 98, 1050-1060 - packages/app/src/app/agent/new.tsx:472 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
58769e7ece
commit
bd7752b4f4
@@ -469,6 +469,7 @@ export default function DraftAgentScreen() {
|
|||||||
agentId={DRAFT_AGENT_ID}
|
agentId={DRAFT_AGENT_ID}
|
||||||
serverId={selectedServerId ?? ""}
|
serverId={selectedServerId ?? ""}
|
||||||
onSubmitMessage={handleCreateFromInput}
|
onSubmitMessage={handleCreateFromInput}
|
||||||
|
isSubmitLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ type QueuedMessage = {
|
|||||||
interface AgentInputAreaProps {
|
interface AgentInputAreaProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
onSubmitMessage?: (payload: {
|
||||||
|
text: string;
|
||||||
|
images?: Array<{ uri: string; mimeType: string }>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
/** Externally controlled loading state. When true, disables the submit button. */
|
||||||
|
isSubmitLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_INPUT_HEIGHT = 50;
|
const MIN_INPUT_HEIGHT = 50;
|
||||||
@@ -85,7 +91,12 @@ type DictationToastConfig = {
|
|||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
export function AgentInputArea({
|
||||||
|
agentId,
|
||||||
|
serverId,
|
||||||
|
onSubmitMessage,
|
||||||
|
isSubmitLoading = false,
|
||||||
|
}: AgentInputAreaProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
@@ -153,10 +164,21 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
const shouldShowAudioDebug = AUDIO_DEBUG_ENABLED;
|
const shouldShowAudioDebug = AUDIO_DEBUG_ENABLED;
|
||||||
const agentIdRef = useRef(agentId);
|
const agentIdRef = useRef(agentId);
|
||||||
const sendAgentMessageRef = useRef(sendAgentMessage);
|
const sendAgentMessageRef = useRef(sendAgentMessage);
|
||||||
|
const onSubmitMessageRef = useRef(onSubmitMessage);
|
||||||
const agentStatusRef = useRef<string | undefined>(undefined);
|
const agentStatusRef = useRef<string | undefined>(undefined);
|
||||||
const updateQueueRef = useRef<
|
const updateQueueRef = useRef<
|
||||||
((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null
|
((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const submitMessage = useCallback(
|
||||||
|
async (text: string, images?: Array<{ uri: string; mimeType: string }>) => {
|
||||||
|
if (onSubmitMessageRef.current) {
|
||||||
|
await onSubmitMessageRef.current({ text, images });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendAgentMessageRef.current?.(agentIdRef.current, text, images);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
const handleDictationTranscript = useCallback(
|
const handleDictationTranscript = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -175,7 +197,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
}
|
}
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await sendAgentMessageRef.current?.(agentIdRef.current, text);
|
await submitMessage(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgentInput] Failed to send transcribed message:", error);
|
console.error("[AgentInput] Failed to send transcribed message:", error);
|
||||||
updateQueueRef.current?.((current) => [
|
updateQueueRef.current?.((current) => [
|
||||||
@@ -188,7 +210,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
[]
|
[submitMessage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDictationError = useCallback((error: Error) => {
|
const handleDictationError = useCallback((error: Error) => {
|
||||||
@@ -197,24 +219,24 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
|
|
||||||
const canStartDictation = useCallback(() => {
|
const canStartDictation = useCallback(() => {
|
||||||
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
|
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
|
||||||
const allowed = !isRealtimeMode && socketConnected;
|
const allowed = !isRealtimeMode && socketConnected && Boolean(agent);
|
||||||
console.log("[AgentInput] canStartDictation", {
|
console.log("[AgentInput] canStartDictation", {
|
||||||
allowed,
|
allowed,
|
||||||
isRealtimeMode,
|
isRealtimeMode,
|
||||||
wsConnected: socketConnected,
|
wsConnected: socketConnected,
|
||||||
});
|
});
|
||||||
return allowed;
|
return allowed;
|
||||||
}, [isRealtimeMode, wsOrInert]);
|
}, [agent, isRealtimeMode, wsOrInert]);
|
||||||
|
|
||||||
const canConfirmDictation = useCallback(() => {
|
const canConfirmDictation = useCallback(() => {
|
||||||
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
|
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
|
||||||
const allowed = socketConnected;
|
const allowed = socketConnected && Boolean(agent);
|
||||||
console.log("[AgentInput] canConfirmDictation", {
|
console.log("[AgentInput] canConfirmDictation", {
|
||||||
allowed,
|
allowed,
|
||||||
wsConnected: socketConnected,
|
wsConnected: socketConnected,
|
||||||
});
|
});
|
||||||
return allowed;
|
return allowed;
|
||||||
}, [wsOrInert]);
|
}, [agent, wsOrInert]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isRecording: isDictating,
|
isRecording: isDictating,
|
||||||
@@ -334,9 +356,14 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
sendAgentMessageRef.current = sendAgentMessage;
|
sendAgentMessageRef.current = sendAgentMessage;
|
||||||
}, [sendAgentMessage]);
|
}, [sendAgentMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSubmitMessageRef.current = onSubmitMessage;
|
||||||
|
}, [onSubmitMessage]);
|
||||||
|
|
||||||
async function handleSendMessage() {
|
async function handleSendMessage() {
|
||||||
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
|
const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
|
||||||
if (!userInput.trim() || !socketConnected || !sendAgentMessage) return;
|
if (!userInput.trim() || !socketConnected) return;
|
||||||
|
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
|
||||||
|
|
||||||
const message = userInput.trim();
|
const message = userInput.trim();
|
||||||
const imageAttachments = selectedImages.length > 0 ? selectedImages : undefined;
|
const imageAttachments = selectedImages.length > 0 ? selectedImages : undefined;
|
||||||
@@ -347,7 +374,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendAgentMessage(agentIdRef.current, message, imageAttachments);
|
await submitMessage(message, imageAttachments);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgentInput] Failed to send message:", error);
|
console.error("[AgentInput] Failed to send message:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -834,13 +861,14 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
|
|
||||||
async function handleSendQueuedNow(id: string) {
|
async function handleSendQueuedNow(id: string) {
|
||||||
const item = queuedMessages.find((q) => q.id === id);
|
const item = queuedMessages.find((q) => q.id === id);
|
||||||
if (!item || !wsOrInert.isConnected || !sendAgentMessage) return;
|
if (!item || !wsOrInert.isConnected) return;
|
||||||
|
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
|
||||||
|
|
||||||
updateQueue((current) => current.filter((q) => q.id !== id));
|
updateQueue((current) => current.filter((q) => q.id !== id));
|
||||||
|
|
||||||
// Cancels current agent run before sending queued prompt
|
// Cancels current agent run before sending queued prompt
|
||||||
handleCancelAgent();
|
handleCancelAgent();
|
||||||
await sendAgentMessage(agentIdRef.current, item.text, item.images);
|
await submitMessage(item.text, item.images);
|
||||||
}
|
}
|
||||||
|
|
||||||
const realtimeButton = (
|
const realtimeButton = (
|
||||||
@@ -1019,13 +1047,17 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
) : shouldShowSendButton ? (
|
) : shouldShowSendButton ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleSendMessage}
|
onPress={handleSendMessage}
|
||||||
disabled={!wsOrInert.isConnected || isProcessing}
|
disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
|
||||||
style={[
|
style={[
|
||||||
styles.sendButton as any,
|
styles.sendButton as any,
|
||||||
(!wsOrInert.isConnected || isProcessing ? styles.buttonDisabled : undefined) as any,
|
(!wsOrInert.isConnected || isProcessing || isSubmitLoading ? styles.buttonDisabled : undefined) as any,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={20} color="white" />
|
{isSubmitLoading ? (
|
||||||
|
<ActivityIndicator size="small" color="white" />
|
||||||
|
) : (
|
||||||
|
<ArrowUp size={20} color="white" />
|
||||||
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : shouldShowVoiceControls ? (
|
) : shouldShowVoiceControls ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user