Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental local ai model #254

Merged
merged 13 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/easypid/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ const config = {
],
},
associatedDomains: associatedDomains.map((host) => `applinks:${host}`),
entitlements: {
'com.apple.developer.kernel.increased-memory-limit': true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any thing we also need to configure in appstoreconnect?

},
},
android: {
adaptiveIcon: {
Expand Down
2 changes: 2 additions & 0 deletions apps/easypid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"expo-blur": "^13.0.2",
"expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.16",
"expo-device": "~6.0.2",
"expo-font": "~12.0.7",
"expo-haptics": "~13.0.1",
"expo-image": "~1.13.0",
Expand All @@ -55,6 +56,7 @@
"react": "catalog:",
"react-native": "catalog:",
"react-native-argon2": "^2.0.1",
"react-native-executorch": "^0.1.2",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.16.2",
"react-native-get-random-values": "~1.11.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/easypid/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { Slot } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'

import { useCheckIncompleteDownload } from '@easypid/llm'
import tamaguiConfig from '../../tamagui.config'

void SplashScreen.preventAutoHideAsync()
Expand All @@ -17,6 +18,7 @@ export const unstable_settings = {

export default function RootLayout() {
useTransparentNavigationBar()
useCheckIncompleteDownload()

return (
<Provider config={tamaguiConfig}>
Expand Down
55 changes: 33 additions & 22 deletions apps/easypid/src/features/menu/FunkeSettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
import { Button, FlexPage, Heading, HeroIcons, Paragraph, ScrollView, Stack, YStack } from '@package/ui'
import { Button, FlexPage, Heading, HeroIcons, ScrollView, Stack, YStack } from '@package/ui'
import React from 'react'
import { useRouter } from 'solito/router'

import { useScrollViewPosition } from '@package/app/src/hooks'
import { LocalAiContainer } from './components/LocalAiContainer'

export function FunkeSettingsScreen() {
const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition()
const router = useRouter()

return (
<FlexPage gap="$0" paddingHorizontal="$0">
<YStack w="100%" top={0} borderBottomWidth="$0.5" borderColor={isScrolledByOffset ? '$grey-200' : '$background'}>
<YStack gap="$4" p="$4">
<Stack h="$1" />
<Heading variant="h1" fontWeight="$bold">
Settings
</Heading>
<>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is <> </> wrapper component is not used it seems?

<FlexPage gap="$0" paddingHorizontal="$0">
<YStack
w="100%"
top={0}
borderBottomWidth="$0.5"
borderColor={isScrolledByOffset ? '$grey-200' : '$background'}
>
<YStack gap="$4" p="$4">
<Stack h="$1" />
<Heading variant="h1" fontWeight="$bold">
Settings
</Heading>
</YStack>
</YStack>
</YStack>
<ScrollView
onScroll={handleScroll}
scrollEventThrottle={scrollEventThrottle}
contentContainerStyle={{ minHeight: '85%' }}
>
<YStack fg={1} px="$4" jc="space-between">
<Paragraph color="$grey-700">This page is under construction.</Paragraph>
<Button.Text color="$primary-500" fontWeight="$semiBold" fontSize="$4" onPress={() => router.back()}>
<HeroIcons.ArrowLeft mr={-4} color="$primary-500" size={20} /> Back
</Button.Text>
</YStack>
</ScrollView>
</FlexPage>
<ScrollView
onScroll={handleScroll}
scrollEventThrottle={scrollEventThrottle}
contentContainerStyle={{ minHeight: '85%' }}
>
<YStack fg={1} px="$4" jc="space-between">
<YStack gap="$4" py="$2">
<LocalAiContainer />
</YStack>

<Button.Text color="$primary-500" fontWeight="$semiBold" fontSize="$4" onPress={() => router.back()}>
<HeroIcons.ArrowLeft mr={-4} color="$primary-500" size={20} /> Back
</Button.Text>
</YStack>
</ScrollView>
</FlexPage>
</>
)
}
99 changes: 99 additions & 0 deletions apps/easypid/src/features/menu/components/LocalAiContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { HeroIcons } from '@package/ui/src/content/Icon'

import { Switch } from '@package/ui/src/base/Switch'

import { useIsDeviceCapable, useLLM } from '@easypid/llm'
import { ConfirmationSheet } from '@package/app/src/components/ConfirmationSheet'
import { useHasInternetConnection, useIsConnectedToWifi } from 'packages/app/src/hooks'
import { useToastController } from 'packages/ui/src'
import React, { useState } from 'react'

export function LocalAiContainer() {
const toast = useToastController()
const isConnectedToWifi = useIsConnectedToWifi()
const hasInternetConnection = useHasInternetConnection()
const isDeviceCapable = useIsDeviceCapable()

const [isAiModelConfirmationOpen, setIsAiModelConfirmationOpen] = useState(false)
const { loadModel, isModelReady, downloadProgress, removeModel, isModelActivated, isModelDownloading } = useLLM()

const onActivateModel = () => {
if (!isDeviceCapable) {
toast.show('Device not supported', {
message: 'This device is not powerful enough to run local AI models',
customData: {
preset: 'warning',
},
})
setIsAiModelConfirmationOpen(false)
return
}
if (!isConnectedToWifi && !hasInternetConnection) {
toast.show('WiFi connection required', {
message: 'Please connect to WiFi to activate and download the model',
customData: {
preset: 'warning',
},
})
setIsAiModelConfirmationOpen(false)
return
}

setIsAiModelConfirmationOpen(false)
loadModel()
}

const handleAiModelChange = (value: boolean) => {
if (isModelDownloading) {
toast.show('Model download in progress', {
message: 'Force close the app to cancel the download',
customData: {
preset: 'warning',
},
})
return
}

if (value) {
setIsAiModelConfirmationOpen(true)
} else {
removeModel()
}
}

return (
<>
<Switch
id="local-ai-model"
label="Use local AI model"
icon={<HeroIcons.CpuChipFilled />}
value={isModelActivated}
description={
isModelActivated
? isModelReady
? 'Model active and ready to use'
: downloadProgress
? `Downloading model ${(downloadProgress * 100).toFixed(2)}%`
: 'Getting ready...'
: ''
}
onChange={handleAiModelChange}
beta
/>
<ConfirmationSheet
type="floating"
variant="regular"
isOpen={isAiModelConfirmationOpen}
setIsOpen={setIsAiModelConfirmationOpen}
title="Enable local AI model"
confirmText="Enable"
cancelText="Cancel"
description={[
'This will download a local AI model to your device which will take up to around 1.3GB of space.',
'This is an experimental feature. Only supported on high-end devices.',
]}
onConfirm={onActivateModel}
/>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import React, { useEffect, useState, useCallback } from 'react'

import { useAppAgent } from '@easypid/agent'
import { InvalidPinError } from '@easypid/crypto/error'
import { analyzeVerification } from '@easypid/use-cases/ValidateVerification'
import type { VerificationAnalysisResponse } from '@easypid/use-cases/ValidateVerification'
import { useOverAskingAi } from '@easypid/hooks'
import { usePushToWallet } from '@package/app/src/hooks/usePushToWallet'
import { setWalletServiceProviderPin } from '../../crypto/WalletServiceProviderClient'
import { useShouldUsePinForSubmission } from '../../hooks/useShouldUsePinForPresentation'
Expand Down Expand Up @@ -58,26 +57,24 @@ export function FunkeOpenIdPresentationNotificationScreen() {
})
}, [credentialsForRequest, params.data, params.uri, toast.show, agent, pushToWallet, toast])

const [verificationAnalysis, setVerificationAnalysis] = useState<{
isLoading: boolean
result: VerificationAnalysisResponse | undefined
}>({
isLoading: false,
result: undefined,
})
const { checkForOverAsking, isProcessingOverAsking, overAskingResponse } = useOverAskingAi()

useEffect(() => {
if (!credentialsForRequest?.formattedSubmission || !credentialsForRequest?.formattedSubmission.areAllSatisfied) {
if (!credentialsForRequest?.formattedSubmission) {
return
}

if (isProcessingOverAsking || overAskingResponse) {
// Already generating or already has result
return
}
setVerificationAnalysis((prev) => ({ ...prev, isLoading: true }))

const submission = credentialsForRequest.formattedSubmission
const requestedCards = submission.entries
.filter((entry): entry is FormattedSubmissionEntrySatisfied => entry.isSatisfied)
.flatMap((entry) => entry.credentials)

analyzeVerification({
void checkForOverAsking({
verifier: {
name: credentialsForRequest.verifier.name ?? 'No name provided',
domain: credentialsForRequest.verifier.hostName ?? 'No domain provided',
Expand All @@ -89,8 +86,8 @@ export function FunkeOpenIdPresentationNotificationScreen() {
subtitle: credential.credential.display.description ?? 'Card description',
requestedAttributes: getDisclosedAttributeNamesForDisplay(credential),
})),
}).then((result) => setVerificationAnalysis((prev) => ({ ...prev, isLoading: false, result })))
}, [credentialsForRequest])
})
}, [credentialsForRequest, checkForOverAsking, isProcessingOverAsking, overAskingResponse])

const onProofAccept = useCallback(
async (pin?: string): Promise<PresentationRequestResult> => {
Expand Down Expand Up @@ -206,7 +203,7 @@ export function FunkeOpenIdPresentationNotificationScreen() {
trustedEntities={credentialsForRequest?.verifier.trustedEntities}
lastInteractionDate={lastInteractionDate}
onComplete={() => pushToWallet('replace')}
verificationAnalysis={verificationAnalysis}
overAskingResponse={overAskingResponse}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DisplayImage, FormattedSubmission, TrustedEntity } from '@package/agent'

import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification'
import type { OverAskingResponse } from '@easypid/use-cases/OverAskingApi'
import { type SlideStep, SlideWizard } from '@package/app'
import { LoadingRequestSlide } from '../receive/slides/LoadingRequestSlide'
import { VerifyPartySlide } from '../receive/slides/VerifyPartySlide'
Expand All @@ -14,7 +14,7 @@ interface FunkePresentationNotificationScreenProps {
verifierName?: string
logo?: DisplayImage
lastInteractionDate?: string
verificationAnalysis: VerificationAnalysisResult
overAskingResponse?: OverAskingResponse
trustedEntities?: Array<TrustedEntity>
submission?: FormattedSubmission
usePin: boolean
Expand All @@ -35,7 +35,7 @@ export function FunkePresentationNotificationScreen({
isAccepting,
submission,
onComplete,
verificationAnalysis,
overAskingResponse,
trustedEntities,
}: FunkePresentationNotificationScreenProps) {
return (
Expand Down Expand Up @@ -74,7 +74,7 @@ export function FunkePresentationNotificationScreen({
logo={logo}
submission={submission}
isAccepting={isAccepting}
verificationAnalysis={verificationAnalysis}
overAskingResponse={overAskingResponse}
/>
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification'
import type { OverAskingResponse } from '@easypid/use-cases/OverAskingApi'
import {
AnimatedStack,
Circle,
Expand All @@ -7,24 +7,25 @@ import {
Image,
InfoSheet,
MessageBox,
Spinner,
Stack,
XStack,
YStack,
useScaleAnimation,
} from '@package/ui'
import type { DisplayImage } from 'packages/agent/src'
import { isAndroid } from 'packages/app/src'
import { useState } from 'react'
import React from 'react'
import { FadeIn, ZoomIn } from 'react-native-reanimated'
import { VerificationAnalysisIcon } from './VerificationAnalysisIcon'

interface RequestPurposeSectionProps {
purpose: string
logo?: DisplayImage
verificationAnalysis?: VerificationAnalysisResult
overAskingResponse?: OverAskingResponse
}

export function RequestPurposeSection({ purpose, logo, verificationAnalysis }: RequestPurposeSectionProps) {
export function RequestPurposeSection({ purpose, logo, overAskingResponse }: RequestPurposeSectionProps) {
const [isAnalysisModalOpen, setIsAnalysisModalOpen] = useState(false)

const { handlePressIn, handlePressOut, pressStyle } = useScaleAnimation()
Expand All @@ -34,14 +35,14 @@ export function RequestPurposeSection({ purpose, logo, verificationAnalysis }: R
return (
<>
<YStack gap="$2">
{verificationAnalysis?.result?.validRequest === 'no' && (
{overAskingResponse?.validRequest === 'no' && (
<AnimatedStack
entering={FadeIn}
style={pressStyle}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={toggleAnalysisModal}
mt="$-2"
mt={isAndroid() ? '$0' : '$-2'}
mb="$4"
>
<MessageBox
Expand All @@ -54,11 +55,15 @@ export function RequestPurposeSection({ purpose, logo, verificationAnalysis }: R
<XStack gap="$2" jc="space-between" ai="center">
<Heading variant="sub2">PURPOSE</Heading>
<Stack h="$2" w="$2" ai="center" jc="center">
{verificationAnalysis && (
<AnimatedStack key={verificationAnalysis.result?.validRequest} entering={ZoomIn}>
<VerificationAnalysisIcon verificationAnalysis={verificationAnalysis} />
</AnimatedStack>
)}
<AnimatedStack key={overAskingResponse?.validRequest} entering={ZoomIn}>
{!overAskingResponse ? (
<Spinner scale={0.8} />
) : overAskingResponse.validRequest === 'yes' ? (
<HeroIcons.CheckCircleFilled size={26} color="$positive-500" />
) : overAskingResponse.validRequest === 'no' ? (
<HeroIcons.ExclamationTriangleFilled size={26} color="$danger-500" />
) : null}
</AnimatedStack>
</Stack>
</XStack>
<MessageBox
Expand Down
Loading