Improve dictation UX: transcribe-only mode with UI fixes
- Dictation now appends transcribed text to input instead of auto-sending - Voice button stays visible even with attachments or existing text - Dictation works on create agent screen (not just agent chat) - Input auto-grows when dictation appends text - New agent screen input has solid background (no transparency) - Timer preserves duration after confirming (no scary reset to zero) 🤖 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
09d4acce4c
commit
0cb8bbcf62
@@ -972,14 +972,16 @@ export default function DraftAgentScreen() {
|
|||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<AgentInputArea
|
<View style={styles.inputAreaWrapper}>
|
||||||
agentId={DRAFT_AGENT_ID}
|
<AgentInputArea
|
||||||
serverId={selectedServerId ?? ""}
|
agentId={DRAFT_AGENT_ID}
|
||||||
onSubmitMessage={handleCreateFromInput}
|
serverId={selectedServerId ?? ""}
|
||||||
isSubmitLoading={isLoading}
|
onSubmitMessage={handleCreateFromInput}
|
||||||
value={promptText}
|
isSubmitLoading={isLoading}
|
||||||
onChangeText={setPromptText}
|
value={promptText}
|
||||||
/>
|
onChangeText={setPromptText}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -1024,6 +1026,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
inputAreaWrapper: {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
},
|
||||||
configScrollContent: {
|
configScrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -171,10 +171,6 @@ export function AgentInputArea({
|
|||||||
const agentIdRef = useRef(agentId);
|
const agentIdRef = useRef(agentId);
|
||||||
const sendAgentMessageRef = useRef(sendAgentMessage);
|
const sendAgentMessageRef = useRef(sendAgentMessage);
|
||||||
const onSubmitMessageRef = useRef(onSubmitMessage);
|
const onSubmitMessageRef = useRef(onSubmitMessage);
|
||||||
const agentStatusRef = useRef<string | undefined>(undefined);
|
|
||||||
const updateQueueRef = useRef<
|
|
||||||
((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null
|
|
||||||
>(null);
|
|
||||||
const submitMessage = useCallback(
|
const submitMessage = useCallback(
|
||||||
async (text: string, images?: Array<{ uri: string; mimeType: string }>) => {
|
async (text: string, images?: Array<{ uri: string; mimeType: string }>) => {
|
||||||
if (onSubmitMessageRef.current) {
|
if (onSubmitMessageRef.current) {
|
||||||
@@ -190,33 +186,12 @@ export function AgentInputArea({
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const shouldQueue = agentStatusRef.current === "running";
|
const current = userInput;
|
||||||
if (shouldQueue) {
|
const shouldPad = current.length > 0 && !/\s$/.test(current);
|
||||||
updateQueueRef.current?.((current) => [
|
const nextValue = `${current}${shouldPad ? " " : ""}${text}`;
|
||||||
...current,
|
setUserInput(nextValue);
|
||||||
{
|
|
||||||
id: generateMessageId(),
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await submitMessage(text);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[AgentInput] Failed to send transcribed message:", error);
|
|
||||||
updateQueueRef.current?.((current) => [
|
|
||||||
...current,
|
|
||||||
{
|
|
||||||
id: generateMessageId(),
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
},
|
},
|
||||||
[submitMessage]
|
[setUserInput, userInput]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDictationError = useCallback((error: Error) => {
|
const handleDictationError = useCallback((error: Error) => {
|
||||||
@@ -225,24 +200,24 @@ export function AgentInputArea({
|
|||||||
|
|
||||||
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 && Boolean(agent);
|
const allowed = !isRealtimeMode && socketConnected && (Boolean(agent) || Boolean(onSubmitMessage));
|
||||||
console.log("[AgentInput] canStartDictation", {
|
console.log("[AgentInput] canStartDictation", {
|
||||||
allowed,
|
allowed,
|
||||||
isRealtimeMode,
|
isRealtimeMode,
|
||||||
wsConnected: socketConnected,
|
wsConnected: socketConnected,
|
||||||
});
|
});
|
||||||
return allowed;
|
return allowed;
|
||||||
}, [agent, isRealtimeMode, wsOrInert]);
|
}, [agent, isRealtimeMode, onSubmitMessage, 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 && Boolean(agent);
|
const allowed = socketConnected && (Boolean(agent) || Boolean(onSubmitMessage));
|
||||||
console.log("[AgentInput] canConfirmDictation", {
|
console.log("[AgentInput] canConfirmDictation", {
|
||||||
allowed,
|
allowed,
|
||||||
wsConnected: socketConnected,
|
wsConnected: socketConnected,
|
||||||
});
|
});
|
||||||
return allowed;
|
return allowed;
|
||||||
}, [agent, wsOrInert]);
|
}, [agent, onSubmitMessage, wsOrInert]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isRecording: isDictating,
|
isRecording: isDictating,
|
||||||
@@ -657,13 +632,22 @@ export function AgentInputArea({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNativeSizerLayout = useCallback(
|
||||||
|
(event: { nativeEvent: { layout: { height: number } } }) => {
|
||||||
|
if (IS_WEB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyMeasuredHeight(event.nativeEvent.layout.height, "native-sizer");
|
||||||
|
},
|
||||||
|
[applyMeasuredHeight]
|
||||||
|
);
|
||||||
|
|
||||||
const isAgentRunning = agent?.status === "running";
|
const isAgentRunning = agent?.status === "running";
|
||||||
agentStatusRef.current = agent?.status;
|
|
||||||
const hasText = userInput.trim().length > 0;
|
const hasText = userInput.trim().length > 0;
|
||||||
const hasImages = selectedImages.length > 0;
|
const hasImages = selectedImages.length > 0;
|
||||||
const hasSendableContent = hasText || hasImages;
|
const hasSendableContent = hasText || hasImages;
|
||||||
const shouldShowSendButton = !isAgentRunning && hasSendableContent;
|
const shouldShowSendButton = !isAgentRunning && hasSendableContent;
|
||||||
const shouldShowVoiceControls = !hasSendableContent;
|
const shouldShowVoiceControls = true;
|
||||||
const shouldHandleDesktopSubmit = IS_WEB;
|
const shouldHandleDesktopSubmit = IS_WEB;
|
||||||
|
|
||||||
const updateQueue = useCallback(
|
const updateQueue = useCallback(
|
||||||
@@ -676,11 +660,6 @@ export function AgentInputArea({
|
|||||||
},
|
},
|
||||||
[agentId, serverId, setQueuedMessages],
|
[agentId, serverId, setQueuedMessages],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
|
||||||
updateQueueRef.current = updateQueue;
|
|
||||||
}, [updateQueue]);
|
|
||||||
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
return;
|
return;
|
||||||
@@ -1003,6 +982,13 @@ export function AgentInputArea({
|
|||||||
editable={!isDictating && wsOrInert.isConnected}
|
editable={!isDictating && wsOrInert.isConnected}
|
||||||
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined}
|
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined}
|
||||||
/>
|
/>
|
||||||
|
{!IS_WEB && (
|
||||||
|
<View pointerEvents="none" style={styles.textInputSizerContainer}>
|
||||||
|
<Text style={styles.textInputSizer} onLayout={handleNativeSizerLayout}>
|
||||||
|
{userInput.length > 0 ? userInput : " "}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Button row below input */}
|
{/* Button row below input */}
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
@@ -1058,27 +1044,30 @@ export function AgentInputArea({
|
|||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
) : shouldShowSendButton ? (
|
) : (
|
||||||
<Pressable
|
<>
|
||||||
onPress={handleSendMessage}
|
{shouldShowSendButton && (
|
||||||
disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
|
<Pressable
|
||||||
style={[
|
onPress={handleSendMessage}
|
||||||
styles.sendButton as any,
|
disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
|
||||||
(!wsOrInert.isConnected || isProcessing || isSubmitLoading ? styles.buttonDisabled : undefined) as any,
|
style={[
|
||||||
]}
|
styles.sendButton as any,
|
||||||
>
|
(!wsOrInert.isConnected || isProcessing || isSubmitLoading
|
||||||
{isSubmitLoading ? (
|
? styles.buttonDisabled
|
||||||
<ActivityIndicator size="small" color="white" />
|
: undefined) as any,
|
||||||
) : (
|
]}
|
||||||
<ArrowUp size={20} color="white" />
|
>
|
||||||
|
{isSubmitLoading ? (
|
||||||
|
<ActivityIndicator size="small" color="white" />
|
||||||
|
) : (
|
||||||
|
<ArrowUp size={20} color="white" />
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
{shouldShowVoiceControls && voiceButton}
|
||||||
) : shouldShowVoiceControls ? (
|
{realtimeButton}
|
||||||
<>
|
</>
|
||||||
{voiceButton}
|
)}
|
||||||
{realtimeButton}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -1197,6 +1186,20 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
|
textInputSizerContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
opacity: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
},
|
||||||
|
textInputSizer: {
|
||||||
|
width: "100%",
|
||||||
|
paddingHorizontal: theme.spacing[4],
|
||||||
|
paddingVertical: theme.spacing[3],
|
||||||
|
fontSize: theme.fontSize.lg,
|
||||||
|
lineHeight: theme.fontSize.lg * 1.4,
|
||||||
|
},
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
|||||||
pendingRequestIdRef.current = null;
|
pendingRequestIdRef.current = null;
|
||||||
setPendingRequestId(null);
|
setPendingRequestId(null);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
setDuration(0);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
setRetryAttempt(0);
|
setRetryAttempt(0);
|
||||||
setRetryInfo(null);
|
setRetryInfo(null);
|
||||||
@@ -493,7 +494,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
|||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
stopDurationTracking();
|
stopDurationTracking();
|
||||||
setDuration(0);
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setRetryInfo(null);
|
setRetryInfo(null);
|
||||||
setRetryAttempt(0);
|
setRetryAttempt(0);
|
||||||
@@ -566,6 +566,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
|||||||
pendingRequestIdRef.current = null;
|
pendingRequestIdRef.current = null;
|
||||||
setPendingRequestId(null);
|
setPendingRequestId(null);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
setDuration(0);
|
||||||
setFailedRecording(null);
|
setFailedRecording(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
setRetryAttempt(0);
|
setRetryAttempt(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user