mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -972,14 +972,16 @@ export default function DraftAgentScreen() {
|
||||
</View>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
<AgentInputArea
|
||||
agentId={DRAFT_AGENT_ID}
|
||||
serverId={selectedServerId ?? ""}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
isSubmitLoading={isLoading}
|
||||
value={promptText}
|
||||
onChangeText={setPromptText}
|
||||
/>
|
||||
<View style={styles.inputAreaWrapper}>
|
||||
<AgentInputArea
|
||||
agentId={DRAFT_AGENT_ID}
|
||||
serverId={selectedServerId ?? ""}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
isSubmitLoading={isLoading}
|
||||
value={promptText}
|
||||
onChangeText={setPromptText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1024,6 +1026,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
inputAreaWrapper: {
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
configScrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
|
||||
@@ -171,10 +171,6 @@ export function AgentInputArea({
|
||||
const agentIdRef = useRef(agentId);
|
||||
const sendAgentMessageRef = useRef(sendAgentMessage);
|
||||
const onSubmitMessageRef = useRef(onSubmitMessage);
|
||||
const agentStatusRef = useRef<string | undefined>(undefined);
|
||||
const updateQueueRef = useRef<
|
||||
((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null
|
||||
>(null);
|
||||
const submitMessage = useCallback(
|
||||
async (text: string, images?: Array<{ uri: string; mimeType: string }>) => {
|
||||
if (onSubmitMessageRef.current) {
|
||||
@@ -190,33 +186,12 @@ export function AgentInputArea({
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const shouldQueue = agentStatusRef.current === "running";
|
||||
if (shouldQueue) {
|
||||
updateQueueRef.current?.((current) => [
|
||||
...current,
|
||||
{
|
||||
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,
|
||||
},
|
||||
]);
|
||||
}
|
||||
})();
|
||||
const current = userInput;
|
||||
const shouldPad = current.length > 0 && !/\s$/.test(current);
|
||||
const nextValue = `${current}${shouldPad ? " " : ""}${text}`;
|
||||
setUserInput(nextValue);
|
||||
},
|
||||
[submitMessage]
|
||||
[setUserInput, userInput]
|
||||
);
|
||||
|
||||
const handleDictationError = useCallback((error: Error) => {
|
||||
@@ -225,24 +200,24 @@ export function AgentInputArea({
|
||||
|
||||
const canStartDictation = useCallback(() => {
|
||||
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", {
|
||||
allowed,
|
||||
isRealtimeMode,
|
||||
wsConnected: socketConnected,
|
||||
});
|
||||
return allowed;
|
||||
}, [agent, isRealtimeMode, wsOrInert]);
|
||||
}, [agent, isRealtimeMode, onSubmitMessage, wsOrInert]);
|
||||
|
||||
const canConfirmDictation = useCallback(() => {
|
||||
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", {
|
||||
allowed,
|
||||
wsConnected: socketConnected,
|
||||
});
|
||||
return allowed;
|
||||
}, [agent, wsOrInert]);
|
||||
}, [agent, onSubmitMessage, wsOrInert]);
|
||||
|
||||
const {
|
||||
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";
|
||||
agentStatusRef.current = agent?.status;
|
||||
const hasText = userInput.trim().length > 0;
|
||||
const hasImages = selectedImages.length > 0;
|
||||
const hasSendableContent = hasText || hasImages;
|
||||
const shouldShowSendButton = !isAgentRunning && hasSendableContent;
|
||||
const shouldShowVoiceControls = !hasSendableContent;
|
||||
const shouldShowVoiceControls = true;
|
||||
const shouldHandleDesktopSubmit = IS_WEB;
|
||||
|
||||
const updateQueue = useCallback(
|
||||
@@ -676,11 +660,6 @@ export function AgentInputArea({
|
||||
},
|
||||
[agentId, serverId, setQueuedMessages],
|
||||
);
|
||||
useEffect(() => {
|
||||
updateQueueRef.current = updateQueue;
|
||||
}, [updateQueue]);
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!IS_WEB) {
|
||||
return;
|
||||
@@ -1003,6 +982,13 @@ export function AgentInputArea({
|
||||
editable={!isDictating && wsOrInert.isConnected}
|
||||
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 */}
|
||||
<View style={styles.buttonRow}>
|
||||
@@ -1058,27 +1044,30 @@ export function AgentInputArea({
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
) : shouldShowSendButton ? (
|
||||
<Pressable
|
||||
onPress={handleSendMessage}
|
||||
disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
|
||||
style={[
|
||||
styles.sendButton as any,
|
||||
(!wsOrInert.isConnected || isProcessing || isSubmitLoading ? styles.buttonDisabled : undefined) as any,
|
||||
]}
|
||||
>
|
||||
{isSubmitLoading ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<ArrowUp size={20} color="white" />
|
||||
) : (
|
||||
<>
|
||||
{shouldShowSendButton && (
|
||||
<Pressable
|
||||
onPress={handleSendMessage}
|
||||
disabled={!wsOrInert.isConnected || isProcessing || isSubmitLoading}
|
||||
style={[
|
||||
styles.sendButton as any,
|
||||
(!wsOrInert.isConnected || isProcessing || isSubmitLoading
|
||||
? styles.buttonDisabled
|
||||
: undefined) as any,
|
||||
]}
|
||||
>
|
||||
{isSubmitLoading ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<ArrowUp size={20} color="white" />
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
</Pressable>
|
||||
) : shouldShowVoiceControls ? (
|
||||
<>
|
||||
{voiceButton}
|
||||
{realtimeButton}
|
||||
</>
|
||||
) : null}
|
||||
{shouldShowVoiceControls && voiceButton}
|
||||
{realtimeButton}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</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: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -320,6 +320,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
||||
pendingRequestIdRef.current = null;
|
||||
setPendingRequestId(null);
|
||||
setIsProcessing(false);
|
||||
setDuration(0);
|
||||
setStatus("idle");
|
||||
setRetryAttempt(0);
|
||||
setRetryInfo(null);
|
||||
@@ -493,7 +494,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
||||
|
||||
setError(null);
|
||||
stopDurationTracking();
|
||||
setDuration(0);
|
||||
setIsProcessing(true);
|
||||
setRetryInfo(null);
|
||||
setRetryAttempt(0);
|
||||
@@ -566,6 +566,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
||||
pendingRequestIdRef.current = null;
|
||||
setPendingRequestId(null);
|
||||
setIsProcessing(false);
|
||||
setDuration(0);
|
||||
setFailedRecording(null);
|
||||
setStatus("idle");
|
||||
setRetryAttempt(0);
|
||||
|
||||
Reference in New Issue
Block a user