| .. | .. |
|---|
| 11 | 11 | import { ImageCaptionModal } from "../components/chat/ImageCaptionModal"; |
|---|
| 12 | 12 | import { StatusDot } from "../components/ui/StatusDot"; |
|---|
| 13 | 13 | import { SessionDrawer } from "../components/SessionDrawer"; |
|---|
| 14 | | -import { playSingle, stopPlayback, isPlaying, onPlayingChange } from "../services/audio"; |
|---|
| 14 | +import { playAudio, stopPlayback, isPlaying, onPlayingChange } from "../services/audio"; |
|---|
| 15 | 15 | |
|---|
| 16 | 16 | interface StagedImage { |
|---|
| 17 | 17 | base64: string; |
|---|
| .. | .. |
|---|
| 20 | 20 | } |
|---|
| 21 | 21 | |
|---|
| 22 | 22 | export default function ChatScreen() { |
|---|
| 23 | | - const { messages, sendTextMessage, sendVoiceMessage, sendImageMessage, clearMessages, requestScreenshot, sessions } = |
|---|
| 23 | + const { messages, sendTextMessage, sendVoiceMessage, sendImageMessage, deleteMessage, clearMessages, isTyping, requestScreenshot, sessions } = |
|---|
| 24 | 24 | useChat(); |
|---|
| 25 | 25 | const { status } = useConnection(); |
|---|
| 26 | 26 | const { colors, mode, cycleMode } = useTheme(); |
|---|
| .. | .. |
|---|
| 130 | 130 | [stagedImage, sendImageMessage], |
|---|
| 131 | 131 | ); |
|---|
| 132 | 132 | |
|---|
| 133 | | - const handleReplay = useCallback(() => { |
|---|
| 133 | + const handleReplay = useCallback(async () => { |
|---|
| 134 | 134 | if (isPlaying()) { |
|---|
| 135 | 135 | stopPlayback(); |
|---|
| 136 | 136 | return; |
|---|
| 137 | 137 | } |
|---|
| 138 | + // Find the last assistant voice message, then walk back to the first chunk in that group |
|---|
| 139 | + let lastIdx = -1; |
|---|
| 138 | 140 | for (let i = messages.length - 1; i >= 0; i--) { |
|---|
| 139 | | - const msg = messages[i]; |
|---|
| 140 | | - if (msg.role === "assistant" && msg.audioUri) { |
|---|
| 141 | | - playSingle(msg.audioUri).catch(() => {}); |
|---|
| 142 | | - return; |
|---|
| 141 | + if (messages[i].role === "assistant" && messages[i].type === "voice" && messages[i].audioUri) { |
|---|
| 142 | + lastIdx = i; |
|---|
| 143 | + break; |
|---|
| 143 | 144 | } |
|---|
| 145 | + } |
|---|
| 146 | + if (lastIdx === -1) return; |
|---|
| 147 | + |
|---|
| 148 | + // Walk back to find the start of this chunk group |
|---|
| 149 | + let startIdx = lastIdx; |
|---|
| 150 | + while (startIdx > 0) { |
|---|
| 151 | + const prev = messages[startIdx - 1]; |
|---|
| 152 | + if (prev.role === "assistant" && prev.type === "voice" && prev.audioUri) { |
|---|
| 153 | + startIdx--; |
|---|
| 154 | + } else { |
|---|
| 155 | + break; |
|---|
| 156 | + } |
|---|
| 157 | + } |
|---|
| 158 | + |
|---|
| 159 | + // Queue all chunks from start to last |
|---|
| 160 | + await stopPlayback(); |
|---|
| 161 | + for (let i = startIdx; i <= lastIdx; i++) { |
|---|
| 162 | + const m = messages[i]; |
|---|
| 163 | + if (m.audioUri) playAudio(m.audioUri); |
|---|
| 144 | 164 | } |
|---|
| 145 | 165 | }, [messages]); |
|---|
| 146 | 166 | |
|---|
| .. | .. |
|---|
| 267 | 287 | </View> |
|---|
| 268 | 288 | </View> |
|---|
| 269 | 289 | ) : ( |
|---|
| 270 | | - <MessageList messages={messages} /> |
|---|
| 290 | + <MessageList messages={messages} isTyping={isTyping} onDeleteMessage={deleteMessage} /> |
|---|
| 271 | 291 | )} |
|---|
| 272 | 292 | </View> |
|---|
| 273 | 293 | |
|---|