├── hooks ├── useColorScheme.ts ├── useColorScheme.web.ts └── useThemeColor.ts ├── image.png ├── assets ├── images │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ ├── react-logo.png │ ├── adaptive-icon.png │ ├── mistral-logo.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── partial-react-logo.png └── fonts │ └── SpaceMono-Regular.ttf ├── ios ├── Podfile.properties.json ├── HelloWorld2 │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── SplashScreen.imageset │ │ │ ├── image.png │ │ │ └── Contents.json │ │ ├── SplashScreenBackground.imageset │ │ │ ├── image.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ └── Contents.json │ ├── HelloWorld2-Bridging-Header.h │ ├── noop-file.swift │ ├── AppDelegate.h │ ├── HelloWorld22.entitlements │ ├── main.m │ ├── Supporting │ │ └── Expo.plist │ ├── PrivacyInfo.xcprivacy │ ├── AppDelegate.mm │ ├── Info.plist │ └── SplashScreen.storyboard ├── HelloWorld2.xcworkspace │ └── contents.xcworkspacedata ├── .gitignore ├── .xcode.env ├── Podfile ├── HelloWorld2.xcodeproj │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── HelloWorld2.xcscheme │ └── project.pbxproj └── Podfile.lock ├── babel.config.js ├── .gitignore ├── index.js ├── app ├── (tabs) │ ├── history │ │ ├── chat │ │ │ └── [id].tsx │ │ ├── settings │ │ │ └── index.tsx │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── _layout.tsx │ └── index.tsx ├── about │ └── index.tsx └── _layout.tsx ├── tsconfig.json ├── types ├── conversation.ts ├── message.ts └── models.ts ├── README.md ├── components ├── navigation │ └── TabBarIcon.tsx ├── ThemedView.tsx ├── InputBar.tsx └── MessageList.tsx ├── constants └── Colors.ts ├── app.json ├── package.json └── scripts └── reset-project.js /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/image.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/splash.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/mistral-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/mistral-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true" 4 | } 5 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /ios/HelloWorld2/HelloWorld2-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /ios/HelloWorld2/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /ios/HelloWorld2/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/SplashScreen.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/ios/HelloWorld2/Images.xcassets/SplashScreen.imageset/image.png -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/SplashScreenBackground.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/ios/HelloWorld2/Images.xcassets/SplashScreenBackground.imageset/image.png -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/MistralExpoBot/main/ios/HelloWorld2/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | .expo 4 | .DS_Store 5 | 6 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 7 | # The following patterns were generated by expo-cli 8 | 9 | expo-env.d.ts 10 | # @end expo-cli 11 | -------------------------------------------------------------------------------- /ios/HelloWorld2/HelloWorld22.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | import 'react-native-get-random-values'; 3 | import 'react-native-url-polyfill/auto'; 4 | import { ReadableStream } from 'web-streams-polyfill'; 5 | import 'expo-router/entry' 6 | 7 | globalThis.ReadableStream = ReadableStream; 8 | -------------------------------------------------------------------------------- /ios/HelloWorld2/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /app/(tabs)/history/chat/[id].tsx: -------------------------------------------------------------------------------- 1 | import ChatScreen from '../..'; 2 | import { useLocalSearchParams } from 'expo-router'; 3 | 4 | export default function ChatHistoryScreen() { 5 | const { id } = useLocalSearchParams(); 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /ios/HelloWorld2.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "version": 1, 12 | "author": "expo" 13 | } 14 | } -------------------------------------------------------------------------------- /types/conversation.ts: -------------------------------------------------------------------------------- 1 | export interface IConversation { 2 | id: string; 3 | preview: string; 4 | date: Date; 5 | } 6 | 7 | export class Conversation implements IConversation { 8 | id: string; 9 | preview: string; 10 | date: Date; 11 | 12 | constructor(id: string, preview: string, date: Date) { 13 | this.id = id; 14 | this.preview = preview; 15 | this.date = date 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/SplashScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /ios/HelloWorld2/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | 12 | -------------------------------------------------------------------------------- /ios/HelloWorld2/Images.xcassets/SplashScreenBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /app/about/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | import { ThemedView } from '@/components/ThemedView'; 3 | import Stack from 'expo-router/stack'; 4 | 5 | export default function SettingsScreen() { 6 | return ( 7 | 8 | 13 | About Screen 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mistral Expo Bot 2 | 3 | 4 | 5 | This is a simple hello world project I did in order to train myself with React Native / Expo 6 | 7 | # Features 8 | 9 | * Chat with various Mistral models. 10 | * Polished chat interface. 11 | * Automatically save chat history. 12 | * Restore chat context. 13 | 14 | # How to build & run 15 | 16 | Search for `YOUR_PAY_KEY_HERE` and replace it with your own Mistral API KEY. 17 | 18 | Then in the folder, run `expo run:ios` 19 | -------------------------------------------------------------------------------- /app/(tabs)/history/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | import { ThemedView } from '@/components/ThemedView'; 3 | import Stack from 'expo-router/stack'; 4 | 5 | export default function SettingsScreen() { 6 | return ( 7 | 8 | 13 | Setting Screen 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/navigation/TabBarIcon.tsx: -------------------------------------------------------------------------------- 1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ 2 | 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet'; 5 | import { type ComponentProps } from 'react'; 6 | 7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // NOTE: The default React Native styling doesn't support server rendering. 2 | // Server rendered styles should not change between the first render of the HTML 3 | // and the first render on the client. Typically, web developers will use CSS media queries 4 | // to render different styles on the client and server, these aren't directly supported in React Native 5 | // but can be achieved using a styling library like Nativewind. 6 | export function useColorScheme() { 7 | return 'dark'; 8 | } 9 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | }; 9 | 10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { 11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const tintColor = '#EA580B'; 7 | 8 | export const Colors = { 9 | text: '#D4D4D4', 10 | background: '#171412', 11 | tint: tintColor, 12 | icon: '#9BA1A6', 13 | tabIconDefault: 'gray', 14 | tabIconSelected: tintColor, 15 | }; 16 | -------------------------------------------------------------------------------- /hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { useColorScheme } from 'react-native'; 7 | 8 | import { Colors } from '@/constants/Colors'; 9 | 10 | export function useThemeColor( 11 | props: { light?: string; dark?: string }, 12 | colorName: keyof typeof Colors 13 | ) { 14 | const theme = useColorScheme() ?? 'light'; 15 | const colorFromProps = props[theme]; 16 | 17 | if (colorFromProps) { 18 | return colorFromProps; 19 | } else { 20 | return Colors[colorName]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /types/message.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export interface IMessage { 4 | id: string; 5 | text: string; 6 | role: MessageRole; 7 | date: Date; 8 | } 9 | 10 | export enum MessageRole { 11 | User = 'user', 12 | System = 'system', 13 | Assistant = 'assistant', 14 | Tool = 'tool' 15 | } 16 | 17 | export class Message implements IMessage { 18 | id: string; 19 | text: string; 20 | role: MessageRole; 21 | date: Date; 22 | 23 | constructor(text: string, role: MessageRole) { 24 | this.id = Message.newMessageId(); 25 | this.text = text; 26 | this.role = role; 27 | this.date = new Date(); 28 | } 29 | 30 | private static newMessageId(): string { 31 | return uuidv4(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/(tabs)/history/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from 'expo-router/stack'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { Colors } from '@/constants/Colors'; 5 | 6 | export default function HistoryLayout() { 7 | const queryClient = new QueryClient(); 8 | 9 | return ( 10 | 11 | 12 | 17 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /types/models.ts: -------------------------------------------------------------------------------- 1 | export interface IModel { 2 | id: string; 3 | title: string; 4 | description: string; 5 | icon: string; 6 | } 7 | 8 | export class Model implements IModel { 9 | id: string; 10 | title: string; 11 | description: string; 12 | icon: string; 13 | 14 | constructor(id: string, title: string, description: string, icon: string) { 15 | this.id = id; 16 | this.title = title; 17 | this.description = description; 18 | this.icon = icon; 19 | } 20 | } 21 | 22 | export const MODELS: Model[] = [ 23 | new Model("mistral-small-latest", "Mistral Small", "Designed to be fast and efficient.", "robotic.vacuum"), 24 | new Model("mistral-large-latest", "Mistral Large", "Designed to be accurate and efficient.", "plus.bubble"), 25 | new Model("codestral-latest", "CodeStral", "Designed for coding tasks.", "keyboard") 26 | ] 27 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "HelloWorld22", 4 | "slug": "HelloWorld22", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "dark", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true, 17 | "bundleIdentifier": "com.anonymous.HelloWorld22" 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/images/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | } 24 | }, 25 | "web": { 26 | "bundler": "metro", 27 | "output": "static", 28 | "favicon": "./assets/images/favicon.png" 29 | }, 30 | "plugins": [ 31 | "expo-router" 32 | ], 33 | "experiments": { 34 | "typedRoutes": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 2 | import { useFonts } from 'expo-font'; 3 | import * as SplashScreen from 'expo-splash-screen'; 4 | import { useEffect } from 'react'; 5 | import 'react-native-reanimated'; 6 | import { Stack } from 'expo-router'; 7 | import { StatusBar } from 'react-native'; 8 | 9 | // Prevent the splash screen from auto-hiding before asset loading is complete. 10 | SplashScreen.preventAutoHideAsync(); 11 | 12 | export default function RootLayout() { 13 | const [loaded] = useFonts({ 14 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), 15 | }); 16 | 17 | useEffect(() => { 18 | if (loaded) { 19 | SplashScreen.hideAsync(); 20 | } 21 | }, [loaded]); 22 | 23 | if (!loaded) { 24 | return null; 25 | } 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { TabBarIcon } from '@/components/navigation/TabBarIcon'; 2 | import { Colors } from '@/constants/Colors'; 3 | import { Tabs } from 'expo-router'; 4 | import { Button } from 'swiftui-react-native'; 5 | 6 | export default function TabLayout() { 7 | return ( 8 | 14 | ( 20 | 21 | ), 22 | }} 23 | /> 24 | ( 30 | 31 | ), 32 | }} 33 | /> 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /ios/HelloWorld2/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryFileTimestamp 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | 0A2A.1 21 | 3B52.1 22 | C617.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategoryDiskSpace 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | E174.1 31 | 85F4.1 32 | 33 | 34 | 35 | NSPrivacyAccessedAPIType 36 | NSPrivacyAccessedAPICategorySystemBootTime 37 | NSPrivacyAccessedAPITypeReasons 38 | 39 | 35F9.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /components/InputBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native'; 3 | import Icon from 'react-native-vector-icons/Ionicons'; // Import the icon library 4 | import { BlurView } from '@react-native-community/blur'; // Import BlurView 5 | 6 | interface InputBarProps { 7 | inputText: string; 8 | setInputText: (text: string) => void; 9 | sendMessage: () => void; 10 | isSending: boolean; 11 | textInputRef: React.RefObject; 12 | } 13 | 14 | export const InputBar: React.FC = ({ inputText, setInputText, sendMessage, isSending, textInputRef }) => ( 15 | 16 | 17 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | const styles = StyleSheet.create({ 36 | inputContainer: { 37 | flexDirection: 'row', 38 | alignItems: 'center', 39 | margin: 10, 40 | }, 41 | blurView: { 42 | flex: 1, 43 | flexDirection: 'row', 44 | alignItems: 'center', 45 | padding: 10, 46 | borderRadius: 10, 47 | backgroundColor: 'transparent', // Ensure background is transparent 48 | }, 49 | textInput: { 50 | flex: 1, 51 | borderWidth: 1, 52 | color: 'white', 53 | borderColor: 'gray', 54 | padding: 10, 55 | borderRadius: 5, 56 | }, 57 | sendButton: { 58 | padding: 10, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helloworld2", 3 | "main": "./index.js", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@expo/vector-icons": "^14.0.2", 19 | "@mistralai/mistralai": "^1.1.0", 20 | "@react-native-async-storage/async-storage": "1.23.1", 21 | "@react-native-community/blur": "^4.4.1", 22 | "@react-native-menu/menu": "^1.1.3", 23 | "@react-navigation/bottom-tabs": "^6.6.1", 24 | "@react-navigation/native": "^6.1.18", 25 | "@react-navigation/stack": "^6.4.1", 26 | "@tanstack/react-query": "^5.59.12", 27 | "date-fns": "^4.1.0", 28 | "expo": "^51.0.38", 29 | "expo-constants": "~16.0.2", 30 | "expo-dev-client": "~4.0.28", 31 | "expo-font": "~12.0.9", 32 | "expo-linking": "~6.3.1", 33 | "expo-router": "~3.5.23", 34 | "expo-splash-screen": "~0.27.5", 35 | "expo-status-bar": "~1.12.1", 36 | "expo-system-ui": "~3.0.7", 37 | "expo-web-browser": "~13.0.3", 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0", 40 | "react-native": "0.74.5", 41 | "react-native-gesture-handler": "~2.16.1", 42 | "react-native-get-random-values": "^1.11.0", 43 | "react-native-markdown-display": "^7.0.2", 44 | "react-native-reanimated": "~3.10.1", 45 | "react-native-safe-area-context": "^4.10.5", 46 | "react-native-screens": "^3.31.1", 47 | "react-native-url-polyfill": "^2.0.0", 48 | "react-native-vector-icons": "^10.2.0", 49 | "react-native-web": "~0.19.10", 50 | "swiftui-react-native": "^6.3.0", 51 | "uuid": "^10.0.0", 52 | "web-streams-polyfill": "^4.0.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.20.0", 56 | "@types/jest": "^29.5.12", 57 | "@types/react": "~18.2.45", 58 | "@types/react-test-renderer": "^18.0.7", 59 | "jest": "^29.2.1", 60 | "jest-expo": "~51.0.3", 61 | "react-test-renderer": "18.2.0", 62 | "typescript": "~5.3.3" 63 | }, 64 | "private": true 65 | } 66 | -------------------------------------------------------------------------------- /scripts/reset-project.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script is used to reset the project to a blank state. 5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file. 6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 | */ 8 | 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | 12 | const root = process.cwd(); 13 | const oldDirPath = path.join(root, 'app'); 14 | const newDirPath = path.join(root, 'app-example'); 15 | const newAppDirPath = path.join(root, 'app'); 16 | 17 | const indexContent = `import { Text, View } from "react-native"; 18 | 19 | export default function Index() { 20 | return ( 21 | 28 | Edit app/index.tsx to edit this screen. 29 | 30 | ); 31 | } 32 | `; 33 | 34 | const layoutContent = `import { Stack } from "expo-router"; 35 | 36 | export default function RootLayout() { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | } 43 | `; 44 | 45 | fs.rename(oldDirPath, newDirPath, (error) => { 46 | if (error) { 47 | return console.error(`Error renaming directory: ${error}`); 48 | } 49 | console.log('/app moved to /app-example.'); 50 | 51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => { 52 | if (error) { 53 | return console.error(`Error creating new app directory: ${error}`); 54 | } 55 | console.log('New /app directory created.'); 56 | 57 | const indexPath = path.join(newAppDirPath, 'index.tsx'); 58 | fs.writeFile(indexPath, indexContent, (error) => { 59 | if (error) { 60 | return console.error(`Error creating index.tsx: ${error}`); 61 | } 62 | console.log('app/index.tsx created.'); 63 | 64 | const layoutPath = path.join(newAppDirPath, '_layout.tsx'); 65 | fs.writeFile(layoutPath, layoutContent, (error) => { 66 | if (error) { 67 | return console.error(`Error creating _layout.tsx: ${error}`); 68 | } 69 | console.log('app/_layout.tsx created.'); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /ios/HelloWorld2/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 9 | { 10 | self.moduleName = @"main"; 11 | 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = @{}; 15 | 16 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 17 | } 18 | 19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 20 | { 21 | return [self bundleURL]; 22 | } 23 | 24 | - (NSURL *)bundleURL 25 | { 26 | #if DEBUG 27 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; 28 | #else 29 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 30 | #endif 31 | } 32 | 33 | // Linking API 34 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 35 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 36 | } 37 | 38 | // Universal Links 39 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 40 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 41 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 42 | } 43 | 44 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 45 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 46 | { 47 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 48 | } 49 | 50 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 51 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 52 | { 53 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 54 | } 55 | 56 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 57 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 58 | { 59 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /ios/HelloWorld2/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | HelloWorld22 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | myapp 31 | com.anonymous.HelloWorld2 32 | 33 | 34 | 35 | CFBundleURLSchemes 36 | 37 | exp+helloworld22 38 | 39 | 40 | 41 | CFBundleVersion 42 | 1 43 | LSRequiresIPhoneOS 44 | 45 | NSAppTransportSecurity 46 | 47 | NSAllowsArbitraryLoads 48 | 49 | NSAllowsLocalNetworking 50 | 51 | 52 | NSUserActivityTypes 53 | 54 | $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route 55 | 56 | UILaunchStoryboardName 57 | SplashScreen 58 | UIRequiredDeviceCapabilities 59 | 60 | arm64 61 | 62 | UIRequiresFullScreen 63 | 64 | UIStatusBarStyle 65 | UIStatusBarStyleDefault 66 | UISupportedInterfaceOrientations 67 | 68 | UIInterfaceOrientationPortrait 69 | UIInterfaceOrientationPortraitUpsideDown 70 | 71 | UISupportedInterfaceOrientations~ipad 72 | 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationPortraitUpsideDown 75 | UIInterfaceOrientationLandscapeLeft 76 | UIInterfaceOrientationLandscapeRight 77 | 78 | UIUserInterfaceStyle 79 | Automatic 80 | UIViewControllerBasedStatusBarAppearance 81 | 82 | 83 | -------------------------------------------------------------------------------- /components/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { FlatList, StyleSheet, View, Image } from 'react-native'; 3 | import { Message } from '@/types/message'; 4 | import Markdown from 'react-native-markdown-display'; 5 | 6 | interface MessageListProps { 7 | messages: Message[]; 8 | } 9 | 10 | export const MessageList: React.FC = ({ messages }) => { 11 | const flatListRef = useRef>(null); 12 | 13 | const scrollToEnd = () => { 14 | setTimeout(() => { 15 | if (flatListRef.current) { 16 | flatListRef.current.scrollToEnd({ animated: true }); 17 | } 18 | }, 250); 19 | }; 20 | 21 | return ( 22 | ( 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | {item.text} 37 | 38 | 39 | 40 | )} 41 | keyExtractor={item => item.id} 42 | style={styles.messageList} 43 | contentContainerStyle={styles.messageListContent} 44 | contentInset={{ bottom: 100 }} 45 | onContentSizeChange={scrollToEnd} 46 | keyboardDismissMode="interactive" 47 | /> 48 | ); 49 | } 50 | 51 | const markdownStyles = StyleSheet.create({ 52 | body: {color: 'white'}, 53 | root: {color: 'white'}, 54 | code_inline: {backgroundColor: '#333', color: 'white'}, 55 | code_block: {backgroundColor: '#333', color: 'white'}, 56 | fence: {backgroundColor: '#333', color: 'white'} 57 | }); 58 | 59 | const styles = StyleSheet.create({ 60 | messageContainer: { 61 | flexDirection: 'row', 62 | alignItems: 'flex-start', 63 | marginVertical: 5, 64 | }, 65 | avatar: { 66 | width: 20, 67 | height: 20, 68 | marginRight: 10, 69 | marginTop: 10, 70 | }, 71 | avatarContainer: { 72 | alignItems: 'center', 73 | }, 74 | threadLine: { 75 | width: 1, 76 | backgroundColor: 'gray', 77 | flex: 1, 78 | marginTop: 5, 79 | marginLeft: -8 80 | }, 81 | message: { 82 | color: 'white', 83 | padding: 10, 84 | maxWidth: '80%', 85 | alignSelf: 'flex-start', 86 | marginBottom: 20, 87 | marginTop: -10, 88 | flex: 1, 89 | }, 90 | messageListContent: { 91 | flexGrow: 1, 92 | justifyContent: 'flex-end', 93 | marginBottom: 100, 94 | }, 95 | messageList: { 96 | flex: 1, 97 | padding: 10, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | 4 | require 'json' 5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 6 | 7 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' 8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] 9 | 10 | use_autolinking_method_symbol = ('use' + '_native' + '_modules!').to_sym 11 | origin_autolinking_method = self.method(use_autolinking_method_symbol) 12 | self.define_singleton_method(use_autolinking_method_symbol) do |*args| 13 | if ENV['EXPO_UNSTABLE_CORE_AUTOLINKING'] == '1' 14 | Pod::UI.puts('Using expo-modules-autolinking as core autolinking source'.green) 15 | config_command = [ 16 | 'node', 17 | '--no-warnings', 18 | '--eval', 19 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 20 | 'react-native-config', 21 | '--json', 22 | '--platform', 23 | 'ios' 24 | ] 25 | origin_autolinking_method.call(config_command) 26 | else 27 | origin_autolinking_method.call() 28 | end 29 | end 30 | 31 | platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4' 32 | install! 'cocoapods', 33 | :deterministic_uuids => false 34 | 35 | prepare_react_native_project! 36 | 37 | target 'HelloWorld2' do 38 | use_expo_modules! 39 | config = use_native_modules! 40 | 41 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 42 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 43 | 44 | use_react_native!( 45 | :path => config[:reactNativePath], 46 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 47 | # An absolute path to your application root. 48 | :app_path => "#{Pod::Config.instance.installation_root}/..", 49 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', 50 | ) 51 | 52 | post_install do |installer| 53 | react_native_post_install( 54 | installer, 55 | config[:reactNativePath], 56 | :mac_catalyst_enabled => false, 57 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', 58 | ) 59 | 60 | # This is necessary for Xcode 14, because it signs resource bundles by default 61 | # when building for devices. 62 | installer.target_installation_results.pod_target_installation_results 63 | .each do |pod_name, target_installation_result| 64 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 65 | resource_bundle_target.build_configurations.each do |config| 66 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 67 | end 68 | end 69 | end 70 | end 71 | 72 | post_integrate do |installer| 73 | begin 74 | expo_patch_react_imports!(installer) 75 | rescue => e 76 | Pod::UI.warn e 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /ios/HelloWorld2.xcodeproj/xcshareddata/xcschemes/HelloWorld2.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/(tabs)/history/index.tsx: -------------------------------------------------------------------------------- 1 | import { FlatList, StyleSheet, Text, View, Image } from 'react-native'; 2 | import { Link } from 'expo-router'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import { Conversation } from '@/types/conversation'; 5 | import { useQuery } from '@tanstack/react-query'; 6 | import { ThemedView } from '@/components/ThemedView'; 7 | import { Colors } from '@/constants/Colors'; 8 | import { formatDistanceToNow } from 'date-fns'; 9 | 10 | 11 | export default function HistoryScreen() { 12 | 13 | const moreSections = [ 14 | { title: 'Settings', path: 'history/settings'}, 15 | { title: 'About', path: 'about'} 16 | ] 17 | 18 | const fetchConversations = async (): Promise => { 19 | const keys = await AsyncStorage.getAllKeys(); 20 | const conversationKeys = keys.filter(key => key.startsWith('chatMessages-')); 21 | const loadedConversations: Conversation[] = []; 22 | for (const key of conversationKeys) { 23 | const messagesString = await AsyncStorage.getItem(key); 24 | if (messagesString) { 25 | const messages = JSON.parse(messagesString); 26 | if (messages.length > 0) { 27 | loadedConversations.push({ 28 | id: key.replace('chatMessages-', ''), 29 | preview: messages[0].text, 30 | date: messages[0].date, 31 | }); 32 | } 33 | } 34 | } 35 | loadedConversations.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); 36 | console.log(loadedConversations); 37 | return loadedConversations; 38 | }; 39 | 40 | const { data: conversations = [] } = useQuery({ 41 | queryKey: ['conversations'], 42 | queryFn: fetchConversations, 43 | }); 44 | 45 | const renderConversation = ({ item }: { item: Conversation }) => ( 46 | 52 | 53 | 54 | 55 | {item.preview} 58 | 59 | {formatDistanceToNow(new Date(item.date), { addSuffix: true })} 60 | 61 | 62 | 63 | 64 | ); 65 | 66 | const renderMoreSection = ({ item }: { item: { title: string, path: string } }) => ( 67 | 70 | 71 | {item.title} 72 | 73 | 74 | ); 75 | 76 | return ( 77 | 78 | ( 83 | item.title} 87 | /> 88 | )} 89 | /> 90 | 91 | ); 92 | } 93 | 94 | const styles = StyleSheet.create({ 95 | container: { 96 | flex: 1, 97 | paddingHorizontal: 16, 98 | }, 99 | list: { 100 | flex: 1, 101 | paddingVertical: 16, 102 | }, 103 | itemContainer: { 104 | flexDirection: "row", 105 | marginVertical: 8, 106 | marginHorizontal: 16, 107 | padding: 10, 108 | borderWidth: 1, 109 | borderColor: 'gray', 110 | borderRadius: 8, 111 | width: '100%' 112 | }, 113 | itemTextContainer: { 114 | flexGrow: 1, 115 | }, 116 | itemImage: { 117 | marginRight: 12, 118 | width: 24, 119 | height: 24, 120 | }, 121 | itemText: { 122 | fontSize: 16, 123 | fontWeight: 'semibold', 124 | color: 'white', 125 | marginEnd: 10, 126 | }, 127 | itemDate: { 128 | fontSize: 12, 129 | color: 'gray', 130 | paddingTop: 4, 131 | }, 132 | moreSectionContainer: { 133 | marginVertical: 8, 134 | marginHorizontal: 16, 135 | padding: 10, 136 | backgroundColor: '#f0f0f0', 137 | borderRadius: 5, 138 | }, 139 | moreSectionText: { 140 | fontSize: 16, 141 | color: 'black', 142 | }, 143 | }); 144 | -------------------------------------------------------------------------------- /ios/HelloWorld2/SplashScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Platform, TextInput, KeyboardAvoidingView, View, Text, Button } from 'react-native'; 2 | import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; 3 | import { Mistral } from "@mistralai/mistralai"; 4 | import { ThemedView } from '@/components/ThemedView'; 5 | import { MessageList } from '@/components/MessageList'; 6 | import { InputBar } from '@/components/InputBar'; 7 | import { Message, MessageRole } from '@/types/message'; 8 | import { Model, MODELS } from '@/types/models'; 9 | import { v4 as uuidv4 } from 'uuid'; 10 | import AsyncStorage from '@react-native-async-storage/async-storage'; 11 | import { useNavigation } from '@react-navigation/native'; 12 | import { MenuView } from '@react-native-menu/menu'; 13 | import { Colors } from '@/constants/Colors'; 14 | 15 | interface ChatScreenProps { 16 | chatId?: string; 17 | } 18 | 19 | export default function ChatScreen({ chatId }: ChatScreenProps) { 20 | const [conversationId, setConversationId] = useState(chatId || uuidv4()); 21 | const [model, setModel] = useState(MODELS[0]); 22 | const [messages, setMessages] = useState([]); 23 | const [inputText, setInputText] = useState(''); 24 | const [isSending, setIsSending] = useState(false); 25 | const textInputRef = useRef(null); 26 | const mistral = new Mistral({ 27 | apiKey: "YOUR_PAY_KEY_HERE", 28 | }); 29 | const navigation = useNavigation(); 30 | 31 | useEffect(() => { 32 | loadMessages(); 33 | }, []); 34 | 35 | useEffect(() => { 36 | saveMessages(); 37 | }, [messages]); 38 | 39 | useLayoutEffect(() => { 40 | navigation.setOptions({ 41 | headerRight: () => ( 42 | modelMenuButton() 43 | ) 44 | }); 45 | }, [navigation, model]); 46 | 47 | const loadMessages = async () => { 48 | try { 49 | const id = conversationId[0]; 50 | const savedMessages = await AsyncStorage.getItem(`chatMessages-${id}`); 51 | if (savedMessages) { 52 | setMessages(JSON.parse(savedMessages)); 53 | } 54 | } catch (error) { 55 | console.error("Failed to load messages", error); 56 | } 57 | }; 58 | 59 | async function saveMessages(): Promise { 60 | try { 61 | const id = conversationId[0]; 62 | await AsyncStorage.setItem(`chatMessages-${id}`, JSON.stringify(messages)); 63 | } catch (error) { 64 | console.error("Failed to save messages", error); 65 | } 66 | }; 67 | 68 | async function getMistralResponse(message: string): Promise { 69 | const chatHistory = messages.map(msg => ({ 70 | content: msg.text, 71 | role: msg.role, 72 | })); 73 | 74 | const requestMessages = [ 75 | ...chatHistory, 76 | { 77 | content: message, 78 | role: MessageRole.User, 79 | }, 80 | ]; 81 | 82 | const result = await mistral.chat.complete({ 83 | model: model.id, 84 | messages: requestMessages.map(msg => ({ 85 | ...msg, 86 | role: msg.role as "system" | "user" | "assistant" | "tool" 87 | })), 88 | }); 89 | 90 | if (result.choices && result.choices.length > 0) { 91 | return result.choices[0].message.content ?? ""; 92 | } 93 | return ""; 94 | } 95 | 96 | const modelMenuButton = () => { 97 | return ( 98 | { 101 | if (nativeEvent.event === 'new-chat') { 102 | setConversationId(uuidv4()); 103 | setMessages([]); 104 | setInputText(''); 105 | } else { 106 | setModel(MODELS.find(model => model.id == nativeEvent.event) || MODELS[0]); 107 | } 108 | }} 109 | actions={[ 110 | ...MODELS.map((model) => ({ 111 | id: model.id, 112 | title: model.title, 113 | titleColor: '#46F289', 114 | subtitle: model.description, 115 | image: Platform.select({ 116 | ios: model.icon, 117 | android: model.icon, 118 | }), 119 | })), 120 | { id: 'new-chat', title: 'Start a New Chat', attributes: { destructive: true } } 121 | ]} 122 | shouldOpenOnLongPress={false} 123 | > 124 |