Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions __mocks__/whisper.rn.ts

This file was deleted.

6 changes: 2 additions & 4 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSPhotoLibraryUsageDescription": "Whisper needs access to your photos to attach images in chat.",
"NSCameraUsageDescription": "Whisper needs camera access to take photos for chat.",
"NSMicrophoneUsageDescription": "Whisper needs microphone access to record audio messages."
"NSCameraUsageDescription": "Whisper needs camera access to take photos for chat."
},
"entitlements": {
"com.apple.developer.kernel.increased-memory-limit": true,
Expand All @@ -28,8 +27,7 @@
"package": "org.avatechnologies.whisper",
"permissions": [
"android.permission.READ_MEDIA_IMAGES",
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
"android.permission.CAMERA"
]
},
"web": {
Expand Down
4 changes: 1 addition & 3 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ export default function RootLayout() {
<Stack.Screen name="game" />
<Stack.Screen name="setup-ai" />
<Stack.Screen name="provider-setup/[providerId]" />
<Stack.Screen name="provider-setup/huggingface-download" />
<Stack.Screen name="callback/[provider]" />
</Stack>
<Stack.Screen name="provider-setup/huggingface-download" /> </Stack>
</ErrorBoundary>
</ThemeProvider>
</AIProviderProvider>
Expand Down
48 changes: 0 additions & 48 deletions app/callback/[provider].tsx

This file was deleted.

158 changes: 24 additions & 134 deletions app/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ChatBackground } from "@/components/chat-background";
import { createLogger } from "@/src/logger";
import { AttachmentButton } from "@/components/chat/attachment-button";
import { AttachmentPreview } from "@/components/chat/attachment-preview";
import { AudioRecorderOverlay } from "@/components/chat/audio-recorder-overlay";
import { ChatPageHeader } from "@/components/chat/chat-page-header";
import { ImageViewer } from "@/components/chat/image-viewer";
import { MoveToFolderSheet } from "@/components/move-to-folder-sheet";
Expand All @@ -14,24 +12,19 @@ import { Text } from "@/components/ui/text";
import { View } from "@/components/ui/view";
import { useAIProvider } from "@/contexts/AIProviderContext";
import { useAttachments } from "@/hooks/useAttachments";
import { useAudioRecorder } from "@/hooks/useAudioRecorder";
import { useChatCompletion } from "@/hooks/useChatCompletion";
import { useChatMessages } from "@/hooks/useChatMessages";
import { useChatRenderers } from "@/hooks/useChatRenderers";
import { useChatState } from "@/hooks/useChatState";
import { useColorScheme } from "@/hooks/useColorScheme";
import { useNetworkState } from "@/hooks/useNetworkState";
import { setMessageStatus } from "@/src/actions/message";
import {
NO_MULTIMODAL,
type PendingAttachment,
} from "@/src/ai-providers/types";
import { getTranscription } from "@/src/stt";
import { NO_MULTIMODAL } from "@/src/ai-providers/types";
import { wouldTruncate } from "@/src/utils/context-window";
import { Colors } from "@/theme/colors";
import * as ImagePicker from "expo-image-picker";
import { useLocalSearchParams, useRouter } from "expo-router";
import { Camera, Mic } from "lucide-react-native";
import { Camera } from "lucide-react-native";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Expand All @@ -44,8 +37,6 @@ import { GiftedChat, type IMessage } from "react-native-gifted-chat";
import { SafeAreaView } from "react-native-safe-area-context";
import { useCell, useSortedRowIds, useTable } from "tinybase/ui-react";

const logger = createLogger("Chat");

export default function ChatPage() {
const router = useRouter();
const { id: chatIdParam, folderId: folderIdParam } = useLocalSearchParams<{
Expand Down Expand Up @@ -121,11 +112,9 @@ export default function ChatPage() {
const {
attachments,
addImageAttachment,
addAudioAttachment,
removeAttachment,
clearAttachments,
canAddImage,
canAddAudio,
} = useAttachments();

const handleTakePhoto = useCallback(async () => {
Expand Down Expand Up @@ -158,12 +147,6 @@ export default function ChatPage() {
}
}, [addImageAttachment]);

// Audio recorder
const { recorderState, startRecording, stopRecording, cancelRecording } =
useAudioRecorder(multimodalCaps.constraints);

const [isTranscribing, setIsTranscribing] = useState(false);

// AI completion orchestration
const {
isAiTyping,
Expand All @@ -181,70 +164,6 @@ export default function ChatPage() {
folderId: folderIdParam || null,
});

const handleSendRecording = useCallback(async () => {
const uri = await stopRecording();
if (!uri) return;

// Build the audio attachment object directly (don't rely on async state updates)
const audioAtt: PendingAttachment = {
id: `rec_${Date.now()}`,
type: "audio",
uri,
mimeType: "audio/wav",
fileName: `recording_${Date.now()}.wav`,
fileSize: 0,
duration: recorderState.durationMs / 1000,
};

// Snapshot current input text and any pre-existing attachments
const capturedText = inputText.trim();
const priorAttachments = [...attachments];

// Also add to React state so the preview shows while transcribing
addAudioAttachment(
uri,
"audio/wav",
audioAtt.fileName,
0,
audioAtt.duration,
);

// Transcribe, then send.
// whisper.rn runs on its own native context (whisper.cpp), independent of
// llama.rn (llama.cpp). No need to suspend/reload the LLM — they coexist.
setIsTranscribing(true);

const transcribeAndSend = async () => {
try {
const transcription = await getTranscription(uri, recorderState.durationMs);

if (transcription?.trim()) {
audioAtt.transcription = transcription;
}
} catch (err) {
logger.warn("STT failed", { error: err instanceof Error ? err.message : String(err) });
} finally {
setIsTranscribing(false);
}

// Send message with our locally-built attachment list
const allAttachments = [...priorAttachments, audioAtt];
setInputText("");
clearAttachments();
await sendMessage(capturedText, allAttachments);
};

transcribeAndSend();
}, [
stopRecording,
addAudioAttachment,
recorderState.durationMs,
inputText,
attachments,
clearAttachments,
sendMessage,
]);

// Clear incompatible attachments when provider/model changes
const activeProviderId = activeProvider?.id;
useEffect(() => {
Expand All @@ -257,7 +176,6 @@ export default function ChatPage() {
// Remove attachments not supported by new provider
const hasIncompatible = attachments.some((att) => {
if (att.type === "image" && !caps.vision) return true;
if (att.type === "audio" && !caps.audio) return true;
if (att.type === "file" && !caps.files) return true;
return false;
});
Expand Down Expand Up @@ -497,41 +415,28 @@ export default function ChatPage() {
onChangeText={setInputText}
canSend={
!!(inputText.trim() || attachments.length > 0) &&
!recorderState.isRecording &&
!isProcessingMedia &&
!isAiTyping &&
!isTranscribing
!isAiTyping
}
onSend={() => {
if (
(inputText.trim() || attachments.length > 0) &&
!recorderState.isRecording &&
!isProcessingMedia &&
!isAiTyping &&
!isTranscribing
!isAiTyping
) {
onSend([{ text: inputText.trim() } as IMessage]);
}
}}
topAccessory={
<>
{recorderState.isRecording && (
<AudioRecorderOverlay
isRecording={recorderState.isRecording}
isStopped={recorderState.isStopped}
durationMs={recorderState.durationMs}
onSend={handleSendRecording}
onCancel={cancelRecording}
/>
)}
{attachments.length > 0 && !recorderState.isRecording && (
{attachments.length > 0 && (
<AttachmentPreview
attachments={attachments}
onRemove={removeAttachment}
isCloudProvider={isCloudProvider}
/>
)}
{(isTranscribing || isProcessingMedia) && (
{isProcessingMedia && (
<RNView
style={{
flexDirection: "row",
Expand All @@ -543,47 +448,32 @@ export default function ChatPage() {
>
<ActivityIndicator size="small" color={theme.tint} />
<Text style={{ fontSize: 13, color: theme.mutedForeground }}>
{isProcessingMedia
? "Processing media..."
: "Transcribing audio..."}
Processing media...
</Text>
</RNView>
)}
</>
}
leftAccessory={
!recorderState.isRecording ? (
<AttachmentButton
capabilities={multimodalCaps}
canAddImage={canAddImage}
disabled={isAiTyping}
onImageSelected={addImageAttachment}
/>
) : null
<AttachmentButton
capabilities={multimodalCaps}
canAddImage={canAddImage}
disabled={isAiTyping}
onImageSelected={addImageAttachment}
/>
}
rightAccessory={
!recorderState.isRecording && !isTranscribing ? (
<RNView style={{ flexDirection: "row", alignItems: "center" }}>
{multimodalCaps.vision && (
<TouchableOpacity
onPress={handleTakePhoto}
style={{ padding: 8 }}
activeOpacity={0.6}
>
<Camera size={22} color={theme.text} strokeWidth={2} />
</TouchableOpacity>
)}
{multimodalCaps.audio && canAddAudio && (
<TouchableOpacity
onPress={startRecording}
style={{ padding: 8 }}
activeOpacity={0.6}
>
<Mic size={22} color={theme.text} strokeWidth={2} />
</TouchableOpacity>
)}
</RNView>
) : null
<RNView style={{ flexDirection: "row", alignItems: "center" }}>
{multimodalCaps.vision && (
<TouchableOpacity
onPress={handleTakePhoto}
style={{ padding: 8 }}
activeOpacity={0.6}
>
<Camera size={22} color={theme.text} strokeWidth={2} />
</TouchableOpacity>
)}
</RNView>
}
/>
{currentChatId && (
Expand Down
4 changes: 0 additions & 4 deletions app/provider-setup/[providerId].tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { AppleModelsSetup } from "@/components/provider-setup/AppleModelsSetup";
import { CustomProviderSetup } from "@/components/provider-setup/CustomProviderSetup";
import { HuggingFaceSetup } from "@/components/provider-setup/HuggingFaceSetup";
import { OpenAISetup } from "@/components/provider-setup/OpenAISetup";
import { OpenRouterSetup } from "@/components/provider-setup/OpenRouterSetup";
import { WhisperAISetup } from "@/components/provider-setup/WhisperAISetup";
import { Text } from "@/components/ui/text";
import { View } from "@/components/ui/view";
Expand All @@ -14,8 +12,6 @@ export default function ProviderSetup() {
const onboardedAt = useValue("onboardedAt");

if (providerId === "whisper-ai") return <WhisperAISetup />;
if (providerId === "openrouter") return <OpenRouterSetup />;
if (providerId === "openai") return <OpenAISetup />;
if (providerId === "custom-provider") return <CustomProviderSetup />;
if (providerId === "huggingface")
return <HuggingFaceSetup initialSearch={search} onboarding={!onboardedAt} />;
Expand Down
Binary file removed assets/images/ai-providers/claude.png
Binary file not shown.
7 changes: 0 additions & 7 deletions assets/images/ai-providers/claude.svg

This file was deleted.

Binary file removed assets/images/ai-providers/gemini.png
Binary file not shown.
1 change: 0 additions & 1 deletion assets/images/ai-providers/gemini.svg

This file was deleted.

Binary file removed assets/images/ai-providers/openai.png
Binary file not shown.
Loading
Loading