feat(37-04): wire VoiceMicButton, VoiceModeToggle, ChatVoiceBadge, voiceMode into chat UI
- ChatInput: replace VoiceRecordButton with VoiceMicButton (VAD-powered) - ChatInput: add VoiceModeToggle above input when enableVoiceInput=true - ChatMessage: add ChatVoiceBadge render for voice_input and voice_full messageTypes - ChatMessage: auto-play reads from localStorage nexus:voice:autoplay key - ChatPanel: import and call useVoiceMode, extract mode as voiceMode - ChatPanel: pass voiceMode as third arg to all startStream calls (5 call sites)
This commit is contained in:
parent
39bfec7fd8
commit
90efd342ef
3 changed files with 43 additions and 7 deletions
|
|
@ -4,7 +4,8 @@ import { Button } from "@/components/ui/button";
|
|||
import { ChatSlashCommandPopover } from "./ChatSlashCommandPopover";
|
||||
import { ChatMentionPopover } from "./ChatMentionPopover";
|
||||
import { ChatFileDropZone } from "./ChatFileDropZone";
|
||||
import { VoiceRecordButton } from "./VoiceRecordButton";
|
||||
import { VoiceMicButton } from "./VoiceMicButton";
|
||||
import { VoiceModeToggle } from "./VoiceModeToggle";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import type { PendingFile } from "../hooks/useChatFileUpload";
|
||||
|
|
@ -171,6 +172,7 @@ export function ChatInput({
|
|||
onFilesDropped={(files) => files.forEach((f) => onFilesPicked?.([f]))}
|
||||
disabled={disabled}
|
||||
>
|
||||
{enableVoiceInput && <VoiceModeToggle />}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -243,8 +245,8 @@ export function ChatInput({
|
|||
|
||||
{/* Voice input button */}
|
||||
{enableVoiceInput && (
|
||||
<VoiceRecordButton
|
||||
onTranscription={handleTranscription}
|
||||
<VoiceMicButton
|
||||
onTranscript={handleTranscription}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ChatHandoffIndicator } from "./ChatHandoffIndicator";
|
|||
import { ChatTaskCreatedBadge } from "./ChatTaskCreatedBadge";
|
||||
import { ChatStatusUpdateBadge } from "./ChatStatusUpdateBadge";
|
||||
import { ChatFilePreview } from "./ChatFilePreview";
|
||||
import { ChatVoiceBadge } from "./ChatVoiceBadge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { AgentRole, ChatFile } from "@paperclipai/shared";
|
||||
|
|
@ -85,6 +86,37 @@ export function ChatMessage({
|
|||
return null;
|
||||
}
|
||||
}
|
||||
if (messageType === "voice_input" || messageType === "voice_full") {
|
||||
const autoPlay = typeof window !== "undefined"
|
||||
? localStorage.getItem("nexus:voice:autoplay") === "true"
|
||||
: false;
|
||||
return (
|
||||
<div className="max-w-full group relative">
|
||||
{agentName && (
|
||||
<ChatMessageIdentityBar
|
||||
agentName={agentName}
|
||||
agentIcon={agentIcon}
|
||||
agentRole={agentRole}
|
||||
timestamp={timestamp}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
)}
|
||||
<ChatVoiceBadge
|
||||
content={content}
|
||||
messageType={messageType}
|
||||
autoPlayVoice={autoPlay}
|
||||
/>
|
||||
{isStreaming && <ChatStreamingCursor />}
|
||||
<ChatMessageActions
|
||||
role="assistant"
|
||||
isStreaming={isAnyStreaming}
|
||||
onRetry={id && onRetry ? () => onRetry(id) : undefined}
|
||||
onBookmark={id && onBookmark ? () => onBookmark(id) : undefined}
|
||||
isBookmarked={isBookmarked}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Fall through to default system message rendering (plain markdown)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { useMediaQuery } from "../hooks/useMediaQuery";
|
|||
import { useOfflineQueue } from "../hooks/useOfflineQueue";
|
||||
import { useOnlineStatus } from "../hooks/useOnlineStatus";
|
||||
import { resolveAgentFromContent } from "../lib/slash-commands";
|
||||
import { useVoiceMode } from "../hooks/useVoiceMode";
|
||||
import type { AgentRole } from "@paperclipai/shared";
|
||||
|
||||
export function ChatPanel() {
|
||||
|
|
@ -41,6 +42,7 @@ export function ChatPanel() {
|
|||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [bookmarksOpen, setBookmarksOpen] = useState(false);
|
||||
|
||||
const { mode: voiceMode } = useVoiceMode();
|
||||
const { messages } = useChatMessages(activeConversationId);
|
||||
const { streamingContent, isStreaming, startStream, stop } = useStreamingChat(activeConversationId);
|
||||
const { pendingFiles, addFile, removeFile, clearCompleted, completedFileIds } = useChatFileUpload(activeConversationId);
|
||||
|
|
@ -178,7 +180,7 @@ export function ChatPanel() {
|
|||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConvo.id] });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["chat"] });
|
||||
startStream(content, resolvedAgentId ?? undefined);
|
||||
startStream(content, resolvedAgentId ?? undefined, voiceMode);
|
||||
} else {
|
||||
// Path 2: Active conversation -- post user message then stream
|
||||
const message = await chatApi.postMessage(activeConversationId, { role: "user", content });
|
||||
|
|
@ -188,7 +190,7 @@ export function ChatPanel() {
|
|||
clearCompleted();
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", activeConversationId] });
|
||||
startStream(content, resolvedAgentId ?? undefined);
|
||||
startStream(content, resolvedAgentId ?? undefined, voiceMode);
|
||||
}
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
|
|
@ -216,7 +218,7 @@ export function ChatPanel() {
|
|||
await chatApi.truncateMessagesAfter(newConv.id, messageId);
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConv.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "search"] });
|
||||
startStream(newContent, activeAgentId ?? undefined);
|
||||
startStream(newContent, activeAgentId ?? undefined, voiceMode);
|
||||
} catch {
|
||||
pushToast({ title: "Could not create branch. Try again.", tone: "error" });
|
||||
}
|
||||
|
|
@ -267,7 +269,7 @@ export function ChatPanel() {
|
|||
queryClient.invalidateQueries({ queryKey: ["chat", "search"] });
|
||||
|
||||
// Re-stream using the actual user message content
|
||||
startStream(lastUserContent, activeAgentId ?? undefined);
|
||||
startStream(lastUserContent, activeAgentId ?? undefined, voiceMode);
|
||||
};
|
||||
|
||||
// On mobile, render the full-screen MobileChatView instead of the desktop slide-in panel
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue