docs: update tmux commands formatting and add Android screenshot instructions

This commit is contained in:
Mohamed Boudra
2025-10-22 16:50:07 +02:00
parent 89641a18e7
commit de5aba4186
18 changed files with 1217 additions and 1983 deletions

View File

@@ -18,6 +18,11 @@ This is an npm workspace monorepo:
We run the mobile app and server in the tmux session `voice-dev`:
tmux capture-pane -t voice-dev:mobile -p
`tmux capture-pane -t voice-dev:mobile -p`
`tmux capture-pane -t voice-dev:server -p`
tmux capture-pane -t voice-dev:server -p
Don't run them anywhere else. Check logs there.
## Andriod
Take screensots like this: `adb exec-out screencap -p > screenshot.png`

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
.claude/CLAUDE.md

2524
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,
"softwareKeyboardLayoutMode": "pan",
"softwareKeyboardLayoutMode": "resize",
"permissions": [
"RECORD_AUDIO",
"android.permission.RECORD_AUDIO",

View File

@@ -1,3 +0,0 @@
/// <reference types="react-native-css/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by react-native-css. If you need to move or disable this file, please see the documentation.

View File

@@ -43,6 +43,7 @@
"react-native-css": "^3.0.1",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "^1.19.2",
"react-native-markdown-display": "^7.0.2",
"react-native-nitro-modules": "^0.30.0",
"react-native-permissions": "^5.4.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,14 +1,17 @@
import { Stack } from 'expo-router';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { KeyboardProvider } from 'react-native-keyboard-controller';
export default function RootLayout() {
return (
<SafeAreaProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="settings" />
<Stack.Screen name="audio-test" />
</Stack>
<KeyboardProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="settings" />
<Stack.Screen name="audio-test" />
</Stack>
</KeyboardProvider>
</SafeAreaProvider>
);
}

View File

@@ -8,7 +8,10 @@ import {
KeyboardAvoidingView,
ScrollView,
Animated,
Keyboard,
} from "react-native";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import ReanimatedAnimated, { useAnimatedStyle } from "react-native-reanimated";
import { router } from "expo-router";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -131,9 +134,21 @@ export default function VoiceAssistantScreen() {
const [isRealtimeMode, setIsRealtimeMode] = useState(false);
const pulseAnim = useRef(new Animated.Value(1)).current;
const insets = useSafeAreaInsets();
const audioRecorder = useAudioRecorder();
const audioPlayer = useAudioPlayer();
const insets = useSafeAreaInsets();
// Keyboard animation
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
const bottomInset = insets.bottom;
const animatedKeyboardStyle = useAnimatedStyle(() => {
'worklet';
const absoluteHeight = Math.abs(keyboardHeight.value);
const padding = Math.max(0, absoluteHeight - bottomInset);
return {
paddingBottom: padding,
};
});
// Realtime audio with Speechmatics (echo cancellation)
const realtimeAudio = useSpeechmaticsAudio({
@@ -960,197 +975,198 @@ export default function VoiceAssistantScreen() {
// Render orchestrator view (main chat)
return (
<KeyboardAvoidingView
behavior="padding"
style={styles.container}
keyboardVerticalOffset={Platform.OS === "ios" ? insets.top : 0}
>
{/* Connection status header with buttons */}
<View style={{ paddingTop: insets.top + 16 }}>
<View style={styles.headerRow}>
<View style={styles.headerLeft}>
<ConnectionStatus isConnected={ws.isConnected} />
<View style={styles.container}>
{/* Fixed Header */}
<View style={styles.header}>
<View style={{ paddingTop: insets.top + 16 }}>
<View style={styles.headerRow}>
<View style={styles.headerLeft}>
<ConnectionStatus isConnected={ws.isConnected} />
</View>
<View style={styles.headerRight}>
<ConversationSelector
currentConversationId={conversationId}
onSelectConversation={handleSelectConversation}
websocket={ws}
/>
<Pressable
onPress={() => router.push("/settings")}
style={styles.settingsButton}
>
<Settings size={20} color="white" />
</Pressable>
</View>
</View>
<View style={styles.headerRight}>
<ConversationSelector
currentConversationId={conversationId}
onSelectConversation={handleSelectConversation}
websocket={ws}
</View>
{/* Active processes bar */}
<ActiveProcesses
agents={Array.from(agents.values())}
commands={Array.from(commands.values())}
activeProcessId={activeAgentId}
activeProcessType={activeAgentId ? "agent" : null}
onSelectAgent={handleSelectAgent}
onBackToOrchestrator={handleBackToOrchestrator}
/>
</View>
{/* Content Area with Keyboard Handling */}
<ReanimatedAnimated.View
style={[styles.contentArea, animatedKeyboardStyle]}
>
{/* Scrollable Messages Area */}
<ScrollView
ref={scrollViewRef}
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
>
{messages.map((msg) => {
if (msg.type === "user") {
return (
<UserMessage
key={msg.id}
message={msg.message}
timestamp={msg.timestamp}
/>
);
}
if (msg.type === "assistant") {
return (
<AssistantMessage
key={msg.id}
message={msg.message}
timestamp={msg.timestamp}
/>
);
}
if (msg.type === "activity") {
return (
<ActivityLog
key={msg.id}
type={msg.activityType}
message={msg.message}
timestamp={msg.timestamp}
metadata={msg.metadata}
onArtifactClick={handleArtifactClick}
/>
);
}
if (msg.type === "artifact") {
return (
<ActivityLog
key={msg.id}
type="artifact"
message=""
timestamp={msg.timestamp}
artifactId={msg.artifactId}
artifactType={msg.artifactType}
title={msg.title}
onArtifactClick={handleArtifactClick}
/>
);
}
if (msg.type === "tool_call") {
return (
<ToolCall
key={msg.id}
toolName={msg.toolName}
args={msg.args}
result={msg.result}
error={msg.error}
status={msg.status}
/>
);
}
return null;
})}
{/* Streaming assistant message */}
{currentAssistantMessage && (
<AssistantMessage
message={currentAssistantMessage}
timestamp={Date.now()}
isStreaming={true}
/>
)}
</ScrollView>
{/* Fixed Footer */}
<View
style={[
styles.inputArea,
{ paddingBottom: Math.max(insets.bottom, 8) },
]}
>
{/* Text input */}
<TextInput
value={userInput}
onChangeText={setUserInput}
placeholder="Say something..."
placeholderTextColor={defaultTheme.colors.mutedForeground}
style={styles.textInput}
multiline
editable={!isRecording && ws.isConnected}
/>
{/* Buttons */}
<View style={styles.buttonRow}>
{/* Realtime mode button */}
<Pressable
onPress={() => router.push("/settings")}
style={styles.settingsButton}
onPress={handleRealtimeToggle}
disabled={!ws.isConnected}
style={[
styles.realtimeButton,
!ws.isConnected && styles.buttonDisabled,
isRealtimeMode && styles.realtimeButtonActive,
]}
>
<Settings size={20} color="white" />
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<AudioLines size={20} color="white" />
</Animated.View>
</Pressable>
{/* Main action button */}
<Pressable
onPress={handleButtonClick}
disabled={!ws.isConnected}
style={[
styles.mainButton,
!ws.isConnected && styles.buttonDisabled,
isRecording && styles.mainButtonRecording,
isInProgress && styles.mainButtonInProgress,
userInput.trim() &&
!isRecording &&
!isInProgress &&
styles.mainButtonWithText,
]}
>
{isInProgress ? (
<Square size={18} color="white" fill="white" />
) : isRecording ? (
<Square size={14} color="white" fill="white" />
) : userInput.trim() ? (
<ArrowUp size={20} color="white" />
) : (
<Mic size={20} color="white" />
)}
</Pressable>
</View>
</View>
</View>
{/* Active processes bar */}
<ActiveProcesses
agents={Array.from(agents.values())}
commands={Array.from(commands.values())}
activeProcessId={activeAgentId}
activeProcessType={activeAgentId ? "agent" : null}
onSelectAgent={handleSelectAgent}
onBackToOrchestrator={handleBackToOrchestrator}
/>
{/* Messages */}
<ScrollView
ref={scrollViewRef}
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
>
{messages.map((msg) => {
if (msg.type === "user") {
return (
<UserMessage
key={msg.id}
message={msg.message}
timestamp={msg.timestamp}
/>
);
}
if (msg.type === "assistant") {
return (
<AssistantMessage
key={msg.id}
message={msg.message}
timestamp={msg.timestamp}
/>
);
}
if (msg.type === "activity") {
return (
<ActivityLog
key={msg.id}
type={msg.activityType}
message={msg.message}
timestamp={msg.timestamp}
metadata={msg.metadata}
onArtifactClick={handleArtifactClick}
/>
);
}
if (msg.type === "artifact") {
return (
<ActivityLog
key={msg.id}
type="artifact"
message=""
timestamp={msg.timestamp}
artifactId={msg.artifactId}
artifactType={msg.artifactType}
title={msg.title}
onArtifactClick={handleArtifactClick}
/>
);
}
if (msg.type === "tool_call") {
return (
<ToolCall
key={msg.id}
toolName={msg.toolName}
args={msg.args}
result={msg.result}
error={msg.error}
status={msg.status}
/>
);
}
return null;
})}
{/* Streaming assistant message */}
{currentAssistantMessage && (
<AssistantMessage
message={currentAssistantMessage}
timestamp={Date.now()}
isStreaming={true}
/>
)}
</ScrollView>
{/* Input area */}
<View
style={[
styles.inputArea,
{
paddingBottom: Math.max(insets.bottom, 32),
},
]}
>
{/* Text input */}
<TextInput
value={userInput}
onChangeText={setUserInput}
placeholder="Say something..."
placeholderTextColor={defaultTheme.colors.mutedForeground}
style={styles.textInput}
multiline
editable={!isRecording && ws.isConnected}
/>
{/* Buttons */}
<View style={styles.buttonRow}>
{/* Realtime mode button */}
<Pressable
onPress={handleRealtimeToggle}
disabled={!ws.isConnected}
style={[
styles.realtimeButton,
!ws.isConnected && styles.buttonDisabled,
isRealtimeMode && styles.realtimeButtonActive,
]}
>
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<AudioLines size={20} color="white" />
</Animated.View>
</Pressable>
{/* Main action button */}
<Pressable
onPress={handleButtonClick}
disabled={!ws.isConnected}
style={[
styles.mainButton,
!ws.isConnected && styles.buttonDisabled,
isRecording && styles.mainButtonRecording,
isInProgress && styles.mainButtonInProgress,
userInput.trim() &&
!isRecording &&
!isInProgress &&
styles.mainButtonWithText,
]}
>
{isInProgress ? (
<Square size={18} color="white" fill="white" />
) : isRecording ? (
<Square size={14} color="white" fill="white" />
) : userInput.trim() ? (
<ArrowUp size={20} color="white" />
) : (
<Mic size={20} color="white" />
)}
</Pressable>
</View>
</View>
</ReanimatedAnimated.View>
{/* Artifact drawer */}
<ArtifactDrawer
artifact={currentArtifact}
onClose={handleCloseArtifact}
/>
</KeyboardAvoidingView>
</View>
);
}
@@ -1168,12 +1184,21 @@ const styles = StyleSheet.create((theme) => ({
agentNotFoundText: {
color: theme.colors.foreground,
},
header: {
backgroundColor: theme.colors.background,
},
contentArea: {
flex: 1,
minHeight: 0,
},
headerRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: theme.spacing[6],
paddingBottom: theme.spacing[4],
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
},
headerLeft: {
flex: 1,
@@ -1190,9 +1215,11 @@ const styles = StyleSheet.create((theme) => ({
},
scrollView: {
flex: 1,
minHeight: 0,
},
scrollContent: {
paddingBottom: theme.spacing[4],
flexGrow: 1,
},
inputArea: {
paddingTop: theme.spacing[4],
@@ -1215,6 +1242,7 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "flex-end",
gap: theme.spacing[3],
marginBottom: theme.spacing[2],
},
realtimeButton: {
width: 48,

View File

@@ -1,5 +1,5 @@
import { View, Text } from 'react-native';
import { StyleSheet } from 'react-native-unistyles';
import { View, Text } from "react-native";
import { StyleSheet } from "react-native-unistyles";
interface ConnectionStatusProps {
isConnected: boolean;
@@ -7,14 +7,11 @@ interface ConnectionStatusProps {
const styles = StyleSheet.create((theme) => ({
container: {
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
// No padding or border - parent handles layout
},
row: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
},
dot: {
width: 8,
@@ -43,9 +40,19 @@ export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
return (
<View style={styles.container}>
<View style={styles.row}>
<View style={[styles.dot, isConnected ? styles.dotConnected : styles.dotDisconnected]} />
<Text style={[styles.text, isConnected ? styles.textConnected : styles.textDisconnected]}>
{isConnected ? 'Connected' : 'Disconnected'}
<View
style={[
styles.dot,
isConnected ? styles.dotConnected : styles.dotDisconnected,
]}
/>
<Text
style={[
styles.text,
isConnected ? styles.textConnected : styles.textDisconnected,
]}
>
{isConnected ? "Connected" : "Disconnected"}
</Text>
</View>
</View>

View File

@@ -1,12 +1,21 @@
import { useState, useEffect } from 'react';
import { Modal, View, Text, Pressable, ScrollView, Alert, KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MessageSquare, X, Plus, Trash2 } from 'lucide-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { StyleSheet } from 'react-native-unistyles';
import type { UseWebSocketReturn } from '../hooks/use-websocket';
import { useState, useEffect } from "react";
import {
Modal,
View,
Text,
Pressable,
ScrollView,
Alert,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { MessageSquare, X, Plus, Trash2 } from "lucide-react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { StyleSheet } from "react-native-unistyles";
import type { UseWebSocketReturn } from "../hooks/use-websocket";
const STORAGE_KEY = '@voice-dev:conversation-id';
const STORAGE_KEY = "@voice-dev:conversation-id";
interface Conversation {
id: string;
@@ -31,32 +40,41 @@ export function ConversationSelector({
// Listen for conversation list responses
useEffect(() => {
const unsubscribe = websocket.on('list_conversations_response', (message) => {
if (message.type !== 'list_conversations_response') return;
setConversations(message.payload.conversations);
setIsLoading(false);
});
const unsubscribe = websocket.on(
"list_conversations_response",
(message) => {
if (message.type !== "list_conversations_response") return;
setConversations(message.payload.conversations);
setIsLoading(false);
}
);
return unsubscribe;
}, [websocket]);
// Listen for delete conversation responses
useEffect(() => {
const unsubscribe = websocket.on('delete_conversation_response', (message) => {
if (message.type !== 'delete_conversation_response') return;
console.log('[ConversationSelector] Delete response:', message.payload);
if (message.payload.success) {
// Refresh conversations list
fetchConversations();
const unsubscribe = websocket.on(
"delete_conversation_response",
(message) => {
if (message.type !== "delete_conversation_response") return;
console.log("[ConversationSelector] Delete response:", message.payload);
if (message.payload.success) {
// Refresh conversations list
fetchConversations();
// If we deleted the current conversation, start a new one
if (message.payload.conversationId === currentConversationId) {
handleNewConversation();
// If we deleted the current conversation, start a new one
if (message.payload.conversationId === currentConversationId) {
handleNewConversation();
}
} else {
Alert.alert(
"Error",
`Failed to delete conversation: ${message.payload.error}`
);
}
} else {
Alert.alert('Error', `Failed to delete conversation: ${message.payload.error}`);
}
});
);
return unsubscribe;
}, [websocket, currentConversationId]);
@@ -70,30 +88,35 @@ export function ConversationSelector({
function fetchConversations() {
setIsLoading(true);
console.log('[ConversationSelector] Requesting conversations via WebSocket');
console.log(
"[ConversationSelector] Requesting conversations via WebSocket"
);
websocket.send({
type: 'session',
type: "session",
message: {
type: 'list_conversations_request',
type: "list_conversations_request",
},
});
}
function handleDeleteConversation(id: string) {
Alert.alert(
'Delete Conversation',
'Are you sure you want to delete this conversation?',
"Delete Conversation",
"Are you sure you want to delete this conversation?",
[
{ text: 'Cancel', style: 'cancel' },
{ text: "Cancel", style: "cancel" },
{
text: 'Delete',
style: 'destructive',
text: "Delete",
style: "destructive",
onPress: () => {
console.log('[ConversationSelector] Deleting conversation via WebSocket:', id);
console.log(
"[ConversationSelector] Deleting conversation via WebSocket:",
id
);
websocket.send({
type: 'session',
type: "session",
message: {
type: 'delete_conversation_request',
type: "delete_conversation_request",
conversationId: id,
},
});
@@ -105,21 +128,23 @@ export function ConversationSelector({
function handleClearAll() {
Alert.alert(
'Clear All Conversations',
'Are you sure you want to delete all conversations?',
"Clear All Conversations",
"Are you sure you want to delete all conversations?",
[
{ text: 'Cancel', style: 'cancel' },
{ text: "Cancel", style: "cancel" },
{
text: 'Clear All',
style: 'destructive',
text: "Clear All",
style: "destructive",
onPress: () => {
console.log('[ConversationSelector] Clearing all conversations via WebSocket');
console.log(
"[ConversationSelector] Clearing all conversations via WebSocket"
);
// Delete all conversations
conversations.forEach((conv) => {
websocket.send({
type: 'session',
type: "session",
message: {
type: 'delete_conversation_request',
type: "delete_conversation_request",
conversationId: conv.id,
},
});
@@ -144,7 +169,10 @@ export function ConversationSelector({
onSelectConversation(id);
setIsOpen(false);
} catch (error) {
console.error('[ConversationSelector] Failed to save conversation ID:', error);
console.error(
"[ConversationSelector] Failed to save conversation ID:",
error
);
}
}
@@ -155,7 +183,10 @@ export function ConversationSelector({
onSelectConversation(null);
setIsOpen(false);
} catch (error) {
console.error('[ConversationSelector] Failed to clear conversation ID:', error);
console.error(
"[ConversationSelector] Failed to clear conversation ID:",
error
);
}
}
@@ -167,7 +198,7 @@ export function ConversationSelector({
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
@@ -176,10 +207,7 @@ export function ConversationSelector({
return (
<View>
<Pressable
onPress={() => setIsOpen(true)}
style={styles.triggerButton}
>
<Pressable onPress={() => setIsOpen(true)} style={styles.triggerButton}>
<MessageSquare size={20} color="white" />
</Pressable>
@@ -199,7 +227,8 @@ export function ConversationSelector({
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>
Conversations {conversations.length > 0 && `(${conversations.length})`}
Conversations{" "}
{conversations.length > 0 && `(${conversations.length})`}
</Text>
<Pressable onPress={() => setIsOpen(false)}>
<X size={24} color="white" />
@@ -236,34 +265,40 @@ export function ConversationSelector({
</View>
)}
{!isLoading && conversations.map((conversation) => (
<View
key={conversation.id}
style={[
styles.conversationItem,
conversation.id === currentConversationId && styles.conversationItemActive
]}
>
<Pressable
style={styles.conversationContent}
onPress={() => handleSelectConversation(conversation.id)}
{!isLoading &&
conversations.map((conversation) => (
<View
key={conversation.id}
style={[
styles.conversationItem,
conversation.id === currentConversationId &&
styles.conversationItemActive,
]}
>
<Text style={styles.conversationTitle}>
{conversation.messageCount} messages
</Text>
<Text style={styles.conversationDate}>
{formatDate(conversation.lastUpdated)}
</Text>
</Pressable>
<Pressable
onPress={() => handleDeleteConversation(conversation.id)}
style={styles.deleteButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Trash2 size={20} color="#ef4444" />
</Pressable>
</View>
))}
<Pressable
style={styles.conversationContent}
onPress={() =>
handleSelectConversation(conversation.id)
}
>
<Text style={styles.conversationTitle}>
{conversation.messageCount} messages
</Text>
<Text style={styles.conversationDate}>
{formatDate(conversation.lastUpdated)}
</Text>
</Pressable>
<Pressable
onPress={() =>
handleDeleteConversation(conversation.id)
}
style={styles.deleteButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Trash2 size={20} color="#ef4444" />
</Pressable>
</View>
))}
{!isLoading && conversations.length > 0 && (
<View style={styles.clearAllContainer}>
@@ -288,13 +323,12 @@ export function ConversationSelector({
const styles = StyleSheet.create((theme) => ({
triggerButton: {
backgroundColor: theme.colors.muted,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[2],
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
backgroundColor: "rgba(0,0,0,0.5)",
},
modalBackdrop: {
flex: 1,
@@ -303,15 +337,15 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.card,
borderTopLeftRadius: theme.spacing[6],
borderTopRightRadius: theme.spacing[6],
height: '80%',
height: "80%",
},
modalInner: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: theme.spacing[6],
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
@@ -327,9 +361,9 @@ const styles = StyleSheet.create((theme) => ({
borderBottomColor: theme.colors.border,
},
newButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: theme.spacing[2],
backgroundColor: theme.colors.muted,
paddingVertical: theme.spacing[3],
@@ -347,26 +381,26 @@ const styles = StyleSheet.create((theme) => ({
},
loadingContainer: {
padding: theme.spacing[8],
alignItems: 'center',
alignItems: "center",
},
loadingText: {
color: theme.colors.mutedForeground,
},
emptyContainer: {
padding: theme.spacing[8],
alignItems: 'center',
alignItems: "center",
},
emptyText: {
color: theme.colors.mutedForeground,
},
conversationItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: theme.spacing[4],
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
backgroundColor: 'transparent',
backgroundColor: "transparent",
},
conversationItemActive: {
backgroundColor: theme.colors.muted,
@@ -391,11 +425,11 @@ const styles = StyleSheet.create((theme) => ({
padding: theme.spacing[4],
},
clearAllButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: theme.spacing[2],
backgroundColor: 'rgba(127, 29, 29, 0.2)',
backgroundColor: "rgba(127, 29, 29, 0.2)",
paddingVertical: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
borderWidth: theme.borderWidth[1],

BIN
screenshot-keyboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
screenshot-now.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
screenshot-test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
screenshot-test2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
screenshot_before.png Normal file

Binary file not shown.