diff --git a/functions/main.py b/functions/main.py index 81ba54d..af1ee72 100644 --- a/functions/main.py +++ b/functions/main.py @@ -1,9 +1,5 @@ -# Welcome to Cloud Functions for Firebase for Python! -# To get started, simply uncomment the below code or create your own. -# Deploy with `firebase deploy` - from firebase_admin import initialize_app -from firebase_functions import https_fn +from firebase_functions import https_fn, options from langchain.chains.query_constructor.base import AttributeInfo from langchain.retrievers.self_query.base import SelfQueryRetriever from langchain_astradb import AstraDBVectorStore @@ -16,16 +12,15 @@ # Initialize embeddings and vector store -def initialize_vector_store(api_key, token): - embeddings = OpenAIEmbeddings(api_key=api_key) - vstore = AstraDBVectorStore( +def initialize_vector_store(token): + embeddings = OpenAIEmbeddings(api_key='') + return AstraDBVectorStore( embedding=embeddings, collection_name='test_collection_2', api_endpoint='', token=token, namespace='test', ) - return vstore # Metadata field info @@ -140,22 +135,16 @@ def initialize_vector_store(api_key, token): # Create a function to get response from the chain -def get_health_ai_response(question): - api_key = '' +def get_health_ai_response(question, llm): token = '' - - vstore = initialize_vector_store(api_key, token) - - llm = ChatOpenAI(api_key=api_key, temperature=0) + vector_store = initialize_vector_store(token) retriever = SelfQueryRetriever.from_llm( llm=llm, - vectorstore=vstore, + vectorstore=vector_store, document_content_description=document_content_description, metadata_field_info=metadata_field_info, document_contents='', ) - - # Prompt Template for Health AI Agent health_ai_template = """ You are a health AI agent equipped with access to diverse sources of health data, including research articles, nutritional information, medical archives, and more. @@ -171,28 +160,48 @@ def get_health_ai_response(question): YOUR ANSWER: """ - - # Create a ChatPromptTemplate instance from the template health_ai_prompt = ChatPromptTemplate.from_template(health_ai_template) - - # Integration example: - # In your retrieval and generation pipeline, integrate this prompt template - # Replace 'retriever' and 'llm' with appropriate retrieval and language models - chain = ( {'context': retriever, 'question': RunnablePassthrough()} | health_ai_prompt | llm | StrOutputParser() ) - response = chain.invoke(question) return response -@https_fn.on_request() -def on_request_example(req: https_fn.Request) -> https_fn.Response: - response = get_health_ai_response( - 'What are the key points of the nutrition article on healthy eating?' - ) - return https_fn.Response([response]) +def get_response_from_llm(query, llm): + api_key = '' + llm_model = None + if llm == 'gpt-4': + llm_model = ChatOpenAI(api_key=api_key, temperature=0) + if llm == 'gpt-3.5-turbo-instruct': + llm_model = ChatOpenAI(name='gpt-3.5-turbo-instruct', api_key=api_key, temperature=0) + if llm_model is not None: + response = get_health_ai_response(query, llm_model) + return response + else: + return 'Model Not Found' + + +@https_fn.on_request(cors=options.CorsOptions(cors_origins=['*'], cors_methods=['get', 'post'])) +def get_response_url(req: https_fn.Request) -> https_fn.Response: + query = req.get_json().get('query', '') + llms = req.get_json().get('llms', ['gpt-4']) + responses = [] + for llm in llms: + response = get_response_from_llm(query, llm) + responses.append(response) + return https_fn.Response(responses) + + +@https_fn.on_call() +def get_response(req: https_fn.CallableRequest): + query = req.data.get('query', '') + llms = req.data.get('llms', ['gpt-4']) + responses = [] + for llm in llms: + response = get_response_from_llm(query, llm) + responses.append(response) + return response diff --git a/package.json b/package.json index a3177d9..90f047c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@react-native-firebase/app": "^20.1.0", "@react-native-firebase/auth": "^20.1.0", "@react-native-firebase/dynamic-links": "^20.1.0", + "@react-native-firebase/functions": "^20.1.0", "@react-native-google-signin/google-signin": "^12.2.0", "@react-native-voice/voice": "^3.2.4", "@react-navigation/drawer": "^7.0.0-alpha.21", @@ -57,6 +58,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.16.1", "react-native-keyboard-aware-scroll-view": "^0.9.5", + "react-native-markdown-display": "^7.0.2", "react-native-paper": "^5.12.3", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.1", diff --git a/src/frontend/components/FirebaseProvider/index.tsx b/src/frontend/components/FirebaseProvider/index.tsx index caf8662..cac290d 100644 --- a/src/frontend/components/FirebaseProvider/index.tsx +++ b/src/frontend/components/FirebaseProvider/index.tsx @@ -1,10 +1,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import Constants from 'expo-constants'; -import { type FirebaseOptions, getApps, initializeApp } from 'firebase/app'; +import { initializeApp } from 'firebase/app'; import { initializeAuth as getAuth, getReactNativePersistence as store } from 'firebase/auth'; import { getFirestore } from 'firebase/firestore'; -import { type ReactNode, useMemo } from 'react'; -import { AuthProvider, FirebaseAppProvider, FirestoreProvider } from 'reactfire'; +import { getFunctions } from 'firebase/functions'; +import { type ReactNode, useEffect, useMemo } from 'react'; +import { AuthProvider, FirebaseAppProvider, FirestoreProvider, FunctionsProvider } from 'reactfire'; type FirebaseProviderProps = { children: ReactNode; @@ -16,6 +17,7 @@ export function FirebaseProvider(props: FirebaseProviderProps) { const fireApp = useMemo(() => initializeApp(Constants.expoConfig?.extra?.firebase), []); const fireAuth = useMemo(() => getAuth(fireApp, { persistence: store(AsyncStorage) }), [fireApp]); const fireStore = useMemo(() => getFirestore(fireApp, 'ailixir-users'), [fireApp]); + const fireFunction = useMemo(() => getFunctions(fireApp), [fireApp]); return ( - {children} + + {children} + ); diff --git a/src/frontend/components/Header/index.tsx b/src/frontend/components/Header/index.tsx index 06102ea..bc7d55f 100644 --- a/src/frontend/components/Header/index.tsx +++ b/src/frontend/components/Header/index.tsx @@ -1,26 +1,21 @@ import type { DrawerHeaderProps } from '@react-navigation/drawer'; -import { DrawerActions, type RouteProp, useRoute } from '@react-navigation/native'; +import { DrawerActions } from '@react-navigation/native'; import * as Clipboard from 'expo-clipboard'; -import * as FileSystem from 'expo-file-system'; import * as MediaLibrary from 'expo-media-library'; import React, { useEffect } from 'react'; import { Alert, Platform, Pressable, View } from 'react-native'; import RNFS from 'react-native-fs'; import { IconButton, Surface, Text, useTheme } from 'react-native-paper'; -import { useActiveChatId, useGetChat, useLLMs } from 'src/frontend/hooks'; +import { useActiveChatId, useGetChat } from 'src/frontend/hooks'; import { AilixirLogo } from 'src/frontend/icons'; -import type { MainDrawerParams } from 'src/frontend/routes/MainRoutes'; -import { styles } from 'src/frontend/screens/ChatUI/style'; import { DropdownMenu } from '../DropdownMenu'; import { Style } from './style'; export function Header(props: DrawerHeaderProps) { const { colors } = useTheme(); const { navigation } = props; - - const { activeChatId, setActiveChatId } = useActiveChatId(); - const { activeLLMs, toggleLLM } = useLLMs(activeChatId || 'default'); - const { chat, status, error } = useGetChat(activeChatId); + const { activeChatId } = useActiveChatId(); + const { chat } = useGetChat(activeChatId); // Determine if the button should be disabled const isButtonDisabled = chat === undefined; diff --git a/src/frontend/components/RenderChat/index.tsx b/src/frontend/components/RenderChat/index.tsx new file mode 100644 index 0000000..02957cb --- /dev/null +++ b/src/frontend/components/RenderChat/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import Markdown from 'react-native-markdown-display'; +import { ActivityIndicator, Avatar, useTheme } from 'react-native-paper'; +import { useUser } from 'reactfire'; +import { useActiveChatId, useGetChat } from 'src/frontend/hooks'; +import type { conversationMessage } from 'src/frontend/types'; +import { Style } from './style'; + +export function RenderChat() { + const { activeChatId } = useActiveChatId(); + const { chat, status } = useGetChat(activeChatId); + const { colors } = useTheme(); + const { data: user } = useUser(); + + if (status === 'loading') return ; + + return ( + <> + {chat?.conversation.map((item: conversationMessage) => { + const { type, message } = item; + return ( + + + + + {type === 'AI' ? 'AiLixir' : user?.displayName || 'User'} + + + {message} + + ); + })} + + ); +} diff --git a/src/frontend/components/RenderChat/style.ts b/src/frontend/components/RenderChat/style.ts new file mode 100644 index 0000000..0aba4c9 --- /dev/null +++ b/src/frontend/components/RenderChat/style.ts @@ -0,0 +1,11 @@ +import { StyleSheet } from "react-native"; + +export const Style = StyleSheet.create({ + bubble: { + maxWidth: '80%', + borderRadius: 4, + padding: 8, + display: 'flex', + flexDirection: 'column' + } +}) \ No newline at end of file diff --git a/src/frontend/components/index.ts b/src/frontend/components/index.ts index f43a5c9..082ab0f 100644 --- a/src/frontend/components/index.ts +++ b/src/frontend/components/index.ts @@ -8,3 +8,4 @@ export * from './DropdownMenu'; export * from './Header'; export * from './ActiveChatProvider'; export * from './ChatBubble'; +export * from './RenderChat'; diff --git a/src/frontend/hooks/index.ts b/src/frontend/hooks/index.ts index 8661445..e8363a4 100644 --- a/src/frontend/hooks/index.ts +++ b/src/frontend/hooks/index.ts @@ -6,3 +6,4 @@ export * from './useLLMs'; export * from './useActiveChatId'; export * from './useCreateChat'; export * from './useLLMsTypes'; +export * from './useGetResponse'; diff --git a/src/frontend/hooks/useGetResponse.tsx b/src/frontend/hooks/useGetResponse.tsx new file mode 100644 index 0000000..26f1367 --- /dev/null +++ b/src/frontend/hooks/useGetResponse.tsx @@ -0,0 +1,7 @@ +import { httpsCallable } from 'firebase/functions'; +import { useFunctions } from 'reactfire'; + +export function useGetResponse() { + const fireFunction = useFunctions(); + return httpsCallable(fireFunction, 'get_response'); +} diff --git a/src/frontend/screens/ChatUI/index.tsx b/src/frontend/screens/ChatUI/index.tsx index 095e151..8db3f5d 100644 --- a/src/frontend/screens/ChatUI/index.tsx +++ b/src/frontend/screens/ChatUI/index.tsx @@ -3,79 +3,54 @@ import Voice, { type SpeechStartEvent, type SpeechRecognizedEvent } from '@react-native-voice/voice'; -import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; -import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import Constants from 'expo-constants'; -import * as Speech from 'expo-speech'; -import { Timestamp } from 'firebase/firestore'; +import type { FirebaseError } from 'firebase/app'; +import { FieldValue, arrayUnion } from 'firebase/firestore'; import React from 'react'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { useEffect, useRef } from 'react'; -import { ScrollView, Text, TextInput, View } from 'react-native'; +import { ScrollView, TextInput, View } from 'react-native'; import { Keyboard } from 'react-native'; import { Vibration } from 'react-native'; -import { ActivityIndicator, Button, IconButton } from 'react-native-paper'; +import { ActivityIndicator, IconButton } from 'react-native-paper'; import { useTheme } from 'react-native-paper'; -import { ChatBubble } from 'src/frontend/components'; -import { Screens, getLLMResponse } from 'src/frontend/helpers'; +import { RenderChat } from 'src/frontend/components'; import { - LLM_MODELS, useActiveChatId, useCreateChat, useGetChat, - useLLMs, + useGetResponse, useUpdateChat } from 'src/frontend/hooks'; -import type { AppRoutesParams } from 'src/frontend/routes'; -import type { MainDrawerParams } from 'src/frontend/routes/MainRoutes'; -import type { Chat, conversationMessage } from 'src/frontend/types'; import { styles } from './style'; export type ChatUiProps = { chatId: string; }; -export function ChatUI(/*props: ChatUiProps*/) { +export function ChatUI() { const { colors } = useTheme(); const scrollViewRef = useRef(null); - - const { createChat, isCreating } = useCreateChat(); const { activeChatId, setActiveChatId } = useActiveChatId(); - const { chat, status, error } = useGetChat(activeChatId); + const { chat } = useGetChat(activeChatId); const [isRecording, setIsRecording] = useState(false); - - const { activeLLMs: LLMs, toggleLLM } = useLLMs(activeChatId); - const [responses, setResponses] = useState([]); - //for chatbubble - const [responseIndex, setResponseIndex] = useState(0); - const [text, setText] = useState(''); - const { - updateChat, - isUpdating, - error: updateError, - isSuccess: updatedChatSuccessfully - } = useUpdateChat(activeChatId || ''); - const [recognized, setRecognized] = useState(''); const [started, setStarted] = useState(''); const [results, setResults] = useState([]); - - //const {LLMResponse, isGenerating, LLMResponseError} = useGetLLMResponse(''); const [isSendButtonDisabled, setSendButtonDisabled] = useState(false); - //const isSendButtonDisabled = false; + const getResponse = useGetResponse(); + const { updateChat } = useUpdateChat(activeChatId); + const { createChat } = useCreateChat(); + const [isChatTitleUpdated, setIsChatTitleUpdated] = useState(false); // ------------- Keyboard and scrolling ------------- - useEffect(() => { const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { scrollViewRef.current?.scrollToEnd({ animated: true }); }); - const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { scrollViewRef.current?.scrollToEnd({ animated: true }); }); - return () => { keyboardDidShowListener.remove(); keyboardDidHideListener.remove(); @@ -86,145 +61,7 @@ export function ChatUI(/*props: ChatUiProps*/) { scrollViewRef.current?.scrollToEnd({ animated: true }); }, [chat?.conversation.length]); - useEffect(() => { - renderMessages(); - }, [chat?.conversation, activeChatId, responseIndex]); - - // ------------- End keyboard and scrolling ------------- - - // ------------- Render Chat from firebase ------------- - - const renderMessages = () => { - if (status === 'loading' || isCreating) return ; - if (chat === undefined) - return ( - - Write a message to begin. - - ); - - let i = 0; - try { - return chat?.conversation.map((message, index) => ( - //ChatBubble(message, (chat.id + (i++).toString()), colors, responseIndex, setResponseIndex) - - )); - } catch (error) { - return ; - } - }; - - // ------------- End render Chat from firebase ------------- - - // ------------- Sending new message to firebase ------------- - - async function sendMessage() { - // Create new Chat - if (chat === undefined && text.trim()) { - try { - setSendButtonDisabled(true); - - const msg: conversationMessage = { user: text }; - const newChatData: Chat = { - title: text, - model: [LLM_MODELS[0].key], - conversation: [msg], - createdAt: Timestamp.now() - }; - setText(''); - - const result = await createChat(newChatData); - if (!result) throw new Error('Failed to create new chat'); - - const { id: newId, chat: newChat } = result; - setActiveChatId(newId); - - await getLLMResponseAndUpdateFirestore(newChat.model[0], newChat); //TODO: receive answers from multiple LLMS - } catch (error) { - console.error('Error: ', error); - } finally { - setSendButtonDisabled(false); - } - // Send user message in existing chat - } else if (chat?.id && text.trim()) { - try { - setSendButtonDisabled(true); - - const msg: conversationMessage = { user: text }; - const updatedConversation = [...chat.conversation, msg]; - await updateChat({ conversation: updatedConversation }); - //chat?.conversation.push(msg); - setText(''); - - getLLMResponseAndUpdateFirestore(getActiveLLMs(LLMs)[0], { - ...chat, - conversation: updatedConversation - }); //TODO: receive answers from multiple LLMS - } catch (error) { - console.error('Error updating existing chat:', error); - } finally { - setSendButtonDisabled(false); - } - } - } - - function getActiveLLMs(LLMs: { [key: string]: { name: string; active: boolean } }) { - const activeLLMList = []; - for (const [key, value] of Object.entries(LLMs)) { - if (value.active) { - activeLLMList.push(key); - } - } - return activeLLMList; - } - - async function getLLMResponseAndUpdateFirestore(model: string, currentChat: Chat) { - if (currentChat === undefined || !currentChat.id) { - console.error('Trying to save LLM response but chat is undefined or has no id.'); - return; - } - - // Retry updating chat in case of failure TODO: currently doesnt work - const retryUpdate = async (updateData: Partial, maxRetries = 5) => { - for (let i = 0; i < maxRetries; i++) { - updateChat(updateData); - - while (isUpdating) { - await new Promise((resolve) => setTimeout(resolve, 500)); - } - - if (updatedChatSuccessfully) return; - - if (i === maxRetries - 1) { - throw new Error(`Failed to update chat after ${maxRetries} attempts`); - } - - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait 1 second before retrying - } - }; - - try { - // smooth chat UI displays loading bubble until LLM response is received - await retryUpdate({ - id: currentChat.id, - conversation: [...currentChat.conversation, { loading: 'loading' }] - }); - - const response = await getLLMResponse(model, currentChat.conversation); - const msg: conversationMessage = { [model]: response }; - const updatedConversation = [...currentChat.conversation, msg]; - - await retryUpdate({ id: currentChat.id, conversation: updatedConversation }); - //await updateChat({ id: currentChat.id, conversation: updatedConversation }); - } catch (error) { - console.error('Error in getLLMResponseAndUpdateFirestore: ', error); - } - } - - // ------------- End sending new message to firebase ------------- - // ------------- Voice Recognition Setup ------------- - useEffect(() => { Voice.onSpeechStart = onSpeechStart; Voice.onSpeechRecognized = onSpeechRecognized; @@ -273,6 +110,40 @@ export function ChatUI(/*props: ChatUiProps*/) { setIsRecording(false); // Reset recording state }; + useEffect(() => { + const create = async () => { + const { id } = await createChat({ + title: 'NewChat', + model: ['gpt-4'], + conversation: [] + }); + setActiveChatId(id || ''); + }; + if (activeChatId === 'default') create(); + }, [activeChatId]); + + async function sendMessage() { + setSendButtonDisabled(true); + setText(''); + try { + if (!isChatTitleUpdated) { + updateChat({ title: text }); + setIsChatTitleUpdated(true); + } + } catch (error) { + console.error(error); + } + try { + await updateChat({ conversation: arrayUnion({ type: 'USER', message: text }) }); + const { data } = await getResponse({ query: text, llms: ['gpt-4'] }); + await updateChat({ conversation: arrayUnion({ type: 'AI', message: data }) }); + } catch (error) { + console.error(error as FirebaseError); + } finally { + setSendButtonDisabled(false); + } + } + // ------------- End Voice Recognition Setup ------------- return ( @@ -282,7 +153,8 @@ export function ChatUI(/*props: ChatUiProps*/) { contentContainerStyle={styles.scrollViewContent} ref={scrollViewRef} > - {renderMessages()} + + {isSendButtonDisabled && } + conversation: any; }; export type User = { diff --git a/yarn.lock b/yarn.lock index 50a976d..054b6ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2372,6 +2372,11 @@ resolved "https://registry.yarnpkg.com/@react-native-firebase/dynamic-links/-/dynamic-links-20.1.0.tgz#831821e821fdbb527fe62c613852e9aa54604c0e" integrity sha512-7pStvtln1UI1D76hvtl2uPG+RZaPE/+kDI6v4JrUba4rDxUx8n4UWX/4B7/8zrY+JXcdUiRk2KMnD3k0lPmnLA== +"@react-native-firebase/functions@^20.1.0": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@react-native-firebase/functions/-/functions-20.1.0.tgz#717932c0d57787c21fe997e2808e5a3caa2bcbf2" + integrity sha512-A0z3kVnbDKLkHEze4sY0iZpZ6qiUKNFbxwXjCP8AVPwanIyjQhea2o6yMxn5gMcY4W4CAihp6H5qrHu63lx7ig== + "@react-native-google-signin/google-signin@^12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@react-native-google-signin/google-signin/-/google-signin-12.2.0.tgz#1a5e646915f882e8beea7815ffd4ca0ca30d577b" @@ -3291,6 +3296,11 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + caniuse-lite@^1.0.30001587: version "1.0.30001625" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz#ead1b155ea691d6a87938754d3cb119c24465b03" @@ -3627,6 +3637,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + css-in-js-utils@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb" @@ -3645,6 +3660,15 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + css-tree@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" @@ -3919,6 +3943,11 @@ entities@^4.2.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@~2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + env-editor@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-0.4.2.tgz#4e76568d0bd8f5c2b6d314a9412c8fe9aa3ae861" @@ -5593,6 +5622,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" + integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== + dependencies: + uc.micro "^1.0.1" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -5710,6 +5746,17 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +markdown-it@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" + integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== + dependencies: + argparse "^1.0.7" + entities "~2.0.0" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + marky@^1.2.2: version "1.2.5" resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" @@ -5750,6 +5797,11 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -6557,7 +6609,7 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -6632,7 +6684,7 @@ prompts@^2.3.2, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6779,6 +6831,13 @@ react-native-element-dropdown@^2.12.0: dependencies: lodash "^4.17.21" +react-native-fit-image@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz#c660d1ad74b9dcaa1cba27a0d9c23837e000226c" + integrity sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg== + dependencies: + prop-types "^15.5.10" + react-native-fs@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" @@ -6811,6 +6870,16 @@ react-native-keyboard-aware-scroll-view@^0.9.5: prop-types "^15.6.2" react-native-iphone-x-helper "^1.0.3" +react-native-markdown-display@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz#b6584cec8d6670c0141fb8780bc2f0710188a4c2" + integrity sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ== + dependencies: + css-to-react-native "^3.0.0" + markdown-it "^10.0.0" + prop-types "^15.7.2" + react-native-fit-image "^1.5.5" + react-native-paper@^5.12.3: version "5.12.3" resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-5.12.3.tgz#d583119722ebbfbb7fe40400181d63748cca3683" @@ -8010,6 +8079,11 @@ ua-parser-js@^1.0.35: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2" integrity sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"