diff --git a/example/assets/avatar.png b/example/assets/avatar.png new file mode 100644 index 0000000..b884783 Binary files /dev/null and b/example/assets/avatar.png differ diff --git a/example/ios/AiExample.xcodeproj/project.pbxproj b/example/ios/AiExample.xcodeproj/project.pbxproj index 4288423..74f150a 100644 --- a/example/ios/AiExample.xcodeproj/project.pbxproj +++ b/example/ios/AiExample.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7699B88040F8A987B510C191 /* libPods-AiExample-AiExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-AiExample-AiExampleTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + AC91505B2C4C37D100E4348A /* bundle in Resources */ = {isa = PBXBuildFile; fileRef = AC91505A2C4C37D100E4348A /* bundle */; }; FA9143FAE05E547962661C6F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1158F81499343192EE2DB0E3 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -46,6 +47,8 @@ 5DCACB8F33CDC322A6C60F78 /* libPods-AiExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AiExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = AiExample/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-AiExample-AiExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AiExample-AiExampleTests.release.xcconfig"; path = "Target Support Files/Pods-AiExample-AiExampleTests/Pods-AiExample-AiExampleTests.release.xcconfig"; sourceTree = ""; }; + AC9150592C4C347200E4348A /* AiExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = AiExample.entitlements; path = AiExample/AiExample.entitlements; sourceTree = ""; }; + AC91505A2C4C37D100E4348A /* bundle */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bundle; path = dist/bundle; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -89,6 +92,7 @@ 13B07FAE1A68108700A75B9A /* AiExample */ = { isa = PBXGroup; children = ( + AC9150592C4C347200E4348A /* AiExample.entitlements */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 13B07FB51A68108700A75B9A /* Images.xcassets */, @@ -121,6 +125,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + AC91505A2C4C37D100E4348A /* bundle */, 13B07FAE1A68108700A75B9A /* AiExample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* AiExampleTests */, @@ -246,6 +251,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + AC91505B2C4C37D100E4348A /* bundle in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, FA9143FAE05E547962661C6F /* PrivacyInfo.xcprivacy in Resources */, @@ -471,6 +477,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = AiExample/AiExample.entitlements; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ZK8L4ATDPD; ENABLE_BITCODE = NO; @@ -557,6 +564,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = AiExample/AiExample.entitlements; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ZK8L4ATDPD; INFOPLIST_FILE = AiExample/Info.plist; diff --git a/example/ios/AiExample/AiExample.entitlements b/example/ios/AiExample/AiExample.entitlements new file mode 100644 index 0000000..99f4716 --- /dev/null +++ b/example/ios/AiExample/AiExample.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.kernel.increased-memory-limit + + + diff --git a/example/ios/Podfile b/example/ios/Podfile index c451c3f..8878ba7 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,4 +1,4 @@ -ENV['RCT_NEW_ARCH_ENABLED'] = '1' +ENV['RCT_NEW_ARCH_ENABLED'] = '0' # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ad15db3..5dd0a21 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -978,6 +978,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-get-random-values (1.11.0): + - React-Core + - react-native-netinfo (11.3.2): + - React-Core - React-nativeconfig (0.74.2) - React-NativeModulesApple (0.74.2): - glog @@ -1244,6 +1248,8 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - react-native-ai (from `../..`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1337,6 +1343,10 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" react-native-ai: :path: "../.." + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" React-nativeconfig: :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1417,15 +1427,17 @@ SPEC CHECKSUMS: React-jsitracing: 0fa7f78d8fdda794667cb2e6f19c874c1cf31d7e React-logger: 29fa3e048f5f67fe396bc08af7606426d9bd7b5d React-Mapbuffer: bf56147c9775491e53122a94c423ac201417e326 - react-native-ai: 4c526ef3a4ab811772b4ef0a3300f7d6b26cd0b4 + react-native-ai: e1592baf0559a8b64e5f38dd2c0f2758ccea2d25 + react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 + react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc React-nativeconfig: 9f223cd321823afdecf59ed00861ab2d69ee0fc1 React-NativeModulesApple: ff7efaff7098639db5631236cfd91d60abff04c0 React-perflogger: 32ed45d9cee02cf6639acae34251590dccd30994 React-RCTActionSheet: 19f967ddaea258182b56ef11437133b056ba2adf React-RCTAnimation: d7f4137fc44a08bba465267ea7cb1dbdb7c4ec87 - React-RCTAppDelegate: dca95e1a6194f7ae06c2b5f1d5f891c61af00ec8 + React-RCTAppDelegate: 2b3f4d8009796af209a0d496e73276b743acee08 React-RCTBlob: c6c3e1e0251700b7bea036b893913f22e2b9cb47 - React-RCTFabric: a7874c54aea18f64677446efc5f839ec4fa5e931 + React-RCTFabric: 93a3ea55169d19294f07092013c1c9ea7a015c9b React-RCTImage: 40528ab74a4fef0f0e2ee797a074b26d120b6cc6 React-RCTLinking: 385b5beb96749aae9ae1606746e883e1c9f8a6a7 React-RCTNetwork: ffc9f05bd8fa5b3bce562199ba41235ad0af645c @@ -1444,6 +1456,6 @@ SPEC CHECKSUMS: SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 2f71ecf38d934aecb366e686278102a51679c308 -PODFILE CHECKSUM: bbc2c796a007c2b3b597f12669100724c622d914 +PODFILE CHECKSUM: c1ce3fcc38763d2aab0f2e5c56e464d324db8386 COCOAPODS: 1.15.2 diff --git a/example/package.json b/example/package.json index 20b3534..925ee63 100644 --- a/example/package.json +++ b/example/package.json @@ -11,10 +11,15 @@ "start": "react-native start" }, "dependencies": { + "@react-native-community/netinfo": "^11.3.2", + "@types/uuid": "^10.0.0", "ai": "^3.2.15", "react": "18.2.0", "react-native": "0.74.2", + "react-native-get-random-values": "^1.11.0", + "react-native-gifted-chat": "^2.4.0", "text-encoding": "^0.7.0", + "uuid": "^10.0.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/example/polyfills.js b/example/polyfills.js index 012b04d..d57f830 100644 --- a/example/polyfills.js +++ b/example/polyfills.js @@ -1,5 +1,12 @@ +import 'react-native-get-random-values'; + // @ts-ignore import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; +const webStreamPolyfills = require('web-streams-polyfill/ponyfill/es6'); polyfillGlobal('TextEncoder', () => require('text-encoding').TextEncoder); polyfillGlobal('TextDecoder', () => require('text-encoding').TextDecoder); +polyfillGlobal('ReadableStream', () => webStreamPolyfills.ReadableStream); +polyfillGlobal('TransformStream', () => webStreamPolyfills.TransformStream); +polyfillGlobal('WritableStream', () => webStreamPolyfills.WritableStream); +polyfillGlobal('TextEncoderStream', () => webStreamPolyfills.TextEncoderStream); diff --git a/example/src/App.tsx b/example/src/App.tsx index 2e4a548..94225fb 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,32 +1,69 @@ -import React from 'react'; -import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; -import { doGenerate } from 'react-native-ai'; - -export default function App() { - const askQuestion = async () => { - try { - const data = await doGenerate('ai', 'whats react native'); - console.log(data); - } catch (e) { - console.error(e); - } +import React, { useState } from 'react'; +import { SafeAreaView, StyleSheet } from 'react-native'; +import { GiftedChat, type IMessage } from 'react-native-gifted-chat'; +import { getModel } from 'react-native-ai'; +import { generateText } from 'ai'; +import { v4 as uuid } from 'uuid'; +import NetworkInfo from './NetworkInfo'; + +const modelId = 'Phi-3-mini-4k-instruct-q4f16_1-MLC'; + +const aiBot = { + _id: 2, + name: 'AI Chat Bot', + avatar: require('./../assets/avatar.png'), +}; + +export default function Example() { + const [messages, setMessages] = useState([ + { + _id: uuid(), + text: 'Hello! How can I help you today?', + createdAt: new Date(), + user: aiBot, + }, + ]); + + const onSendMessage = async (prompt: string) => { + const { text } = await generateText({ + model: getModel(modelId), + prompt, + }); + + setMessages((previousMessages) => + GiftedChat.append(previousMessages, { + // @ts-ignore + _id: uuid(), + text, + createdAt: new Date(), + user: aiBot, + }) + ); }; return ( - - - Ask a question - - + + + { + setMessages((previousMessages) => + GiftedChat.append(previousMessages, newMessage) + ); + + onSendMessage(newMessage[0]!.text); + }} + user={{ + _id: 1, + }} + /> + ); } const styles = StyleSheet.create({ container: { flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'darkblue', + backgroundColor: '#fff', }, - button: { width: 200, height: 200, backgroundColor: 'red' }, }); diff --git a/example/src/NetworkInfo.tsx b/example/src/NetworkInfo.tsx new file mode 100644 index 0000000..f868009 --- /dev/null +++ b/example/src/NetworkInfo.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { useNetInfo } from '@react-native-community/netinfo'; + +const NetworkInfo = () => { + const netInfo = useNetInfo(); + + const getStatusColor = () => { + if (netInfo.isConnected) return styles.connected; + return styles.disconnected; + }; + + return ( + + + + {netInfo.isConnected ? 'Connected ✅' : 'Disconnected ❌'} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + padding: 10, + backgroundColor: '#f0f0f0', + borderRadius: 8, + }, + statusIndicator: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: 10, + }, + connected: { + backgroundColor: '#4CAF50', + }, + disconnected: { + backgroundColor: '#F44336', + }, + textContainer: { + flex: 1, + }, + statusText: { + fontSize: 16, + fontWeight: 'bold', + }, + detailText: { + fontSize: 14, + color: '#666', + }, +}); + +export default NetworkInfo; diff --git a/ios/Ai.h b/ios/Ai.h index aca6864..c0a92f6 100644 --- a/ios/Ai.h +++ b/ios/Ai.h @@ -1,12 +1,13 @@ +#import #ifdef RCT_NEW_ARCH_ENABLED #import "RNAiSpec.h" -@interface Ai : NSObject +@interface Ai : RCTEventEmitter #else #import -@interface Ai : NSObject +@interface Ai : RCTEventEmitter #endif @end diff --git a/ios/Ai.mm b/ios/Ai.mm index cd0f2a4..047d9a7 100644 --- a/ios/Ai.mm +++ b/ios/Ai.mm @@ -13,26 +13,76 @@ @interface Ai () @implementation Ai +{ + bool hasListeners; +} + RCT_EXPORT_MODULE() +- (NSArray *)supportedEvents { + return @[@"onChatUpdate", @"onChatComplete"]; +} + +-(void)startObserving { + hasListeners = YES; + +} + +-(void)stopObserving { + hasListeners = NO; +} + + - (instancetype)init { self = [super init]; if (self) { _engine = [[MLCEngine alloc] init]; _bundleURL = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"bundle"]; - _modelPath = @"Llama-3-8B-Instruct-q3f16_1-MLC"; - _modelLib = @"llama_q3f16_1"; - _displayText = @""; + _modelPath = @"Phi-3-mini-4k-instruct-q4f16_1-MLC"; + _modelLib = @"phi3_q4f16_1_5c8034316e4d4f818718cf6bf5bf5e89"; } return self; } +- (NSDictionary *)parseResponseString:(NSString *)responseString { + NSData *jsonData = [responseString dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error) { + NSLog(@"Error parsing JSON: %@", error); + return nil; + } + + if (jsonArray.count > 0) { + NSDictionary *responseDict = jsonArray[0]; + NSArray *choices = responseDict[@"choices"]; + if (choices.count > 0) { + NSDictionary *choice = choices[0]; + NSDictionary *delta = choice[@"delta"]; + NSString *content = delta[@"content"]; + NSString *finishReason = choice[@"finish_reason"]; + + BOOL isFinished = (finishReason != nil && ![finishReason isEqual:[NSNull null]]); + + return @{ + @"content": content ?: @"", + @"isFinished": @(isFinished) + }; + } + } + + return nil; +} + RCT_EXPORT_METHOD(doGenerate:(NSString *)instanceId text:(NSString *)text resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { NSLog(@"Generating for instance ID: %@, with text: %@", instanceId, text); + _displayText = @""; + __block BOOL hasResolved = NO; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURL *modelLocalURL = [self.bundleURL URLByAppendingPathComponent:self.modelPath]; @@ -46,20 +96,32 @@ - (instancetype)init { }; [self.engine chatCompletionWithMessages:@[message] completion:^(id response) { - if ([response isKindOfClass:[NSDictionary class]]) { - NSDictionary *responseDictionary = (NSDictionary *)response; - if (responseDictionary[@"usage"]) { - NSString *usageText = [self getUsageTextFromExtra:responseDictionary[@"usage"][@"extra"]]; - self.displayText = [self.displayText stringByAppendingFormat:@"\n%@", usageText]; - resolve(self.displayText); - } else { - NSString *content = responseDictionary[@"choices"][0][@"delta"][@"content"]; + if ([response isKindOfClass:[NSString class]]) { + NSDictionary *parsedResponse = [self parseResponseString:response]; + if (parsedResponse) { + NSString *content = parsedResponse[@"content"]; + BOOL isFinished = [parsedResponse[@"isFinished"] boolValue]; + if (content) { self.displayText = [self.displayText stringByAppendingString:content]; } + + if (isFinished && !hasResolved) { + hasResolved = YES; + resolve(self.displayText); + } + + } else { + if (!hasResolved) { + hasResolved = YES; + reject(@"PARSE_ERROR", @"Failed to parse response", nil); + } + } + } else { + if (!hasResolved) { + hasResolved = YES; + reject(@"INVALID_RESPONSE", @"Received an invalid response type", nil); } - } else if ([response isKindOfClass:[NSString class]]) { - self.displayText = [self.displayText stringByAppendingString:(NSString *)response]; } }]; }); @@ -70,9 +132,12 @@ - (instancetype)init { resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"Streaming for instance ID: %@, with text: %@", instanceId, text); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __block BOOL hasResolved = NO; + NSURL *modelLocalURL = [self.bundleURL URLByAppendingPathComponent:self.modelPath]; NSString *modelLocalPath = [modelLocalURL path]; @@ -84,23 +149,40 @@ - (instancetype)init { }; [self.engine chatCompletionWithMessages:@[message] completion:^(id response) { - if ([response isKindOfClass:[NSDictionary class]]) { - NSDictionary *responseDictionary = (NSDictionary *)response; - if (responseDictionary[@"usage"]) { - NSString *usageText = [self getUsageTextFromExtra:responseDictionary[@"usage"][@"extra"]]; - self.displayText = [self.displayText stringByAppendingFormat:@"\n%@", usageText]; - resolve(self.displayText); - } else { - NSString *content = responseDictionary[@"choices"][0][@"delta"][@"content"]; + if ([response isKindOfClass:[NSString class]]) { + NSDictionary *parsedResponse = [self parseResponseString:response]; + if (parsedResponse) { + NSString *content = parsedResponse[@"content"]; + BOOL isFinished = [parsedResponse[@"isFinished"] boolValue]; + if (content) { self.displayText = [self.displayText stringByAppendingString:content]; -// [self sendEventWithName:@"onStreamProgress" body:@{@"text": content}]; + if (self->hasListeners) { + [self sendEventWithName:@"onChatUpdate" body:@{@"content": content}]; + } + } + + if (isFinished && !hasResolved) { + hasResolved = YES; + if (self->hasListeners) { + [self sendEventWithName:@"onChatComplete" body:nil]; + } + + resolve(@""); + + return; } + } else { + if (!hasResolved) { + hasResolved = YES; + reject(@"PARSE_ERROR", @"Failed to parse response", nil); + } + } + } else { + if (!hasResolved) { + hasResolved = YES; + reject(@"INVALID_RESPONSE", @"Received an invalid response type", nil); } - } else if ([response isKindOfClass:[NSString class]]) { - NSString *content = (NSString *)response; - self.displayText = [self.displayText stringByAppendingString:content]; -// [self sendEventWithName:@"onStreamProgress" body:@{@"text": content}]; } }]; }); @@ -112,8 +194,7 @@ - (instancetype)init { reject:(RCTPromiseRejectBlock)reject) { NSLog(@"Getting model: %@", name); - - // For now, we're just returning the model path and lib + // TODO: add a logic for fetching models if they're not presented in the `bundle/` directory. NSDictionary *modelInfo = @{ @"path": self.modelPath, @"lib": self.modelLib @@ -122,16 +203,6 @@ - (instancetype)init { resolve(modelInfo); } -- (NSString *)getUsageTextFromExtra:(NSDictionary *)extra { - // Implement this method to convert the extra dictionary to a string - // This is a placeholder implementation - return [extra description]; -} - -- (NSArray *)supportedEvents { - return @[@"onStreamProgress"]; -} - // Don't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: diff --git a/src/NativeAi.ts b/src/NativeAi.ts index 1bdab73..5dbf958 100644 --- a/src/NativeAi.ts +++ b/src/NativeAi.ts @@ -4,7 +4,7 @@ import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { getModel(name: string): Promise; // Returns JSON string of ModelInstance doGenerate(instanceId: string, text: string): Promise; - doStream(instanceId: string, text: string): Promise; + doStream(instanceId: string, text: string): Promise; } export default TurboModuleRegistry.getEnforcing('Ai'); diff --git a/src/index.tsx b/src/index.tsx index 64bf84c..d552e04 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,20 @@ -import { NativeModules, Platform } from 'react-native'; -import type { - LanguageModelV1, - LanguageModelV1CallOptions, - LanguageModelV1FinishReason, - LanguageModelV1FunctionToolCall, +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; +import { + UnsupportedFunctionalityError, + type LanguageModelV1, + type LanguageModelV1CallOptions, + type LanguageModelV1CallWarning, + type LanguageModelV1FinishReason, + type LanguageModelV1FunctionToolCall, + type LanguageModelV1ImagePart, + type LanguageModelV1Prompt, + type LanguageModelV1StreamPart, + type LanguageModelV1TextPart, + type LanguageModelV1ToolCallPart, + type LanguageModelV1ToolResultPart, } from '@ai-sdk/provider'; import './polyfills'; +import { ReadableStream } from 'web-streams-polyfill/ponyfill'; const LINKING_ERROR = `The package 'react-native-ai' doesn't seem to be linked. Make sure: \n\n` + @@ -31,29 +40,54 @@ const Ai = AiModule } ); -export async function getModel(name: string): Promise { - // const instanceDataJson = await Ai.getModel(name); - // console.log(instanceDataJson); - // const instanceData: ModelInstance = JSON.parse(instanceDataJson); - // return new AiModel(instanceData); -} +export default Ai; + +export interface AiModelSettings extends Record {} -export interface ModelInstance { - instanceId: string; +export interface Model { modelId: string; modelLib: string; - // Add other properties here as needed +} + +function getStringContent( + content: + | string + | (LanguageModelV1TextPart | LanguageModelV1ImagePart)[] + | (LanguageModelV1TextPart | LanguageModelV1ToolCallPart)[] + | LanguageModelV1ToolResultPart[] +): string { + if (typeof content === 'string') { + return content.trim(); + } else if (Array.isArray(content) && content.length > 0) { + const [first] = content; + if (first.type !== 'text') { + throw new UnsupportedFunctionalityError({ functionality: 'toolCall' }); + } + return first.text.trim(); + } else { + return ''; + } } class AiModel implements LanguageModelV1 { - private instanceId: string; + readonly specificationVersion = 'v1'; + readonly defaultObjectGenerationMode = 'json'; + readonly provider = 'gemini-nano'; public modelId: string; - public modelLib: string; + private options: AiModelSettings; + + constructor(modelId: string, options: AiModelSettings = {}) { + this.modelId = modelId; + this.options = options; + + console.debug('init:', this.modelId); + } + + private model!: Model; + async getModel() { + this.model = await Ai.getModel(this.modelId); - constructor(instanceData: ModelInstance) { - this.instanceId = instanceData.instanceId; - this.modelId = instanceData.modelId; - this.modelLib = instanceData.modelLib; + return this.model; } async doGenerate(options: LanguageModelV1CallOptions): Promise<{ @@ -69,18 +103,19 @@ class AiModel implements LanguageModelV1 { rawSettings: Record; }; }> { - console.log({ - role: options.prompt[0]?.role, - message: options.prompt[0]?.content[0]?.text, - }); + const model = await this.getModel(); + console.log({ model }); - // fix role issue, and implement streaming function and we'll be fine! - const text = await Ai.doGenerate( - this.instanceId, - options.prompt[0]?.content[0]?.text - ); + // TODO: poprawna obsluga tego zeby typescript nie narzekal + const message = + options.prompt[options.prompt.length - 1]!.content[0]!.text!; + console.log({ message }); + + let text = ''; - console.log(JSON.stringify(text)); + if (message.trim().length > 0) { + text = await Ai.doGenerate(model, message); + } return { text, @@ -96,10 +131,113 @@ class AiModel implements LanguageModelV1 { }; } - async doStream(text: string): Promise { - return Ai.doStream(this.instanceId, text); - } + stream = null; + controller = null; + streamId = null; + + public doStream = async ( + options: LanguageModelV1CallOptions + ): Promise<{ + stream: ReadableStream; + rawCall: { rawPrompt: unknown; rawSettings: Record }; + rawResponse?: { headers?: Record }; + warnings?: LanguageModelV1CallWarning[]; + }> => { + console.debug('stream options:', options); + + const model = await this.getModel(); + const message = + options.prompt[options.prompt.length - 1]!.content[0]!.text!; + + const eventEmitter = new NativeEventEmitter(NativeModules.Ai); + eventEmitter.addListener('onChatUpdate', (data) => { + console.log({ data }); + }); + + eventEmitter.addListener('onChatComplete', () => { + console.log('onChatComplete'); + }); + + // const stream = new ReadableStream({ + // start: async (controller) => { + // this.controller = controller; + + // try { + // this.streamId = + // await StreamingChatModule.streamChatCompletion(message); + + // this.updateListener = eventEmitter.addListener( + // 'chatUpdate', + // this.handleChatUpdate + // ); + // this.completeListener = eventEmitter.addListener( + // 'chatComplete', + // this.handleChatComplete + // ); + // this.errorListener = eventEmitter.addListener( + // 'chatError', + // this.handleChatError + // ); + // } catch (error) { + // controller.error(error); + // } + // }, + // cancel: () => { + // this.cleanup(); + // }, + // }); + + Ai.doStream(model.modelId, message); + + // const stream = new ReadableStream({ + // start: (controller) => { + // this.controller = controller; + + // this.chatCompleteListener = eventEmitter.addListener( + // 'chatComplete', + // (data) => { + // console.log(data); + // } + // ); + // this.chatErrorListener = eventEmitter.addListener( + // 'chatError', + // (data) => { + // console.log(data); + // } + // ); + + // Ai.doStream(model, message); // this should be called via model.doStream() + // }, + // cancel: () => { + // console.log('cancel'); + // console.log('cleanup?'); + // this.chatUpdateListener.remove(); + // this.chatCompleteListener.remove(); + // this.chatErrorListener.remove(); + // }, + // }); + + // const promptStream = session.promptStreaming(message); + // const transformStream = new StreamAI(options.abortSignal); + // const stream = promptStream.pipeThrough(transformStream); + + // TODO: how to convert event emitter to stream + + return { + stream: [], + rawCall: { rawPrompt: options.prompt, rawSettings: this.options }, + }; + }; + + // Add other methods here as needed } + +type ModelOptions = {}; + +export function getModel(modelId: string, options: ModelOptions = {}): AiModel { + return new AiModel(modelId, options); +} + const { doGenerate, doStream } = Ai; export { doGenerate, doStream }; diff --git a/src/polyfills.ts b/src/polyfills.ts index 012b04d..f9b6591 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,5 +1,9 @@ // @ts-ignore import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; +const webStreamPolyfills = require('web-streams-polyfill/ponyfill/es6'); + polyfillGlobal('TextEncoder', () => require('text-encoding').TextEncoder); polyfillGlobal('TextDecoder', () => require('text-encoding').TextDecoder); +polyfillGlobal('ReadableStream', () => webStreamPolyfills.ReadableStream); +polyfillGlobal('TransformStream', () => webStreamPolyfills.TransformStream); diff --git a/yarn.lock b/yarn.lock index 4b2a077..a784999 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2064,6 +2064,18 @@ __metadata: languageName: node linkType: hard +"@expo/react-native-action-sheet@npm:4.0.1": + version: 4.0.1 + resolution: "@expo/react-native-action-sheet@npm:4.0.1" + dependencies: + "@types/hoist-non-react-statics": ^3.3.1 + hoist-non-react-statics: ^3.3.0 + peerDependencies: + react: ">=16.3.0" + checksum: f33b8a3b7c6ca77a6e05e5b2284c0961cf1e27b954e87b76747afc990a9581b852326e1ab739ac84a2c1afce05a133627c85465418cad5292d162fbcb305d86e + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -2899,6 +2911,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/netinfo@npm:^11.3.2": + version: 11.3.2 + resolution: "@react-native-community/netinfo@npm:11.3.2" + peerDependencies: + react-native: ">=0.59" + checksum: 6ec29c1305655f66267a026d3af970a71aa77193b6ed6e6538613334c0470f75ae9d6e3fd7ad40f75d618d6a59d9e14d4986625666d6227f7273f756809f39d3 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.74.84": version: 0.74.84 resolution: "@react-native/assets-registry@npm:0.74.84" @@ -3412,6 +3433,16 @@ __metadata: languageName: node linkType: hard +"@types/hoist-non-react-statics@npm:^3.3.1": + version: 3.3.5 + resolution: "@types/hoist-non-react-statics@npm:3.3.5" + dependencies: + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + checksum: b645b062a20cce6ab1245ada8274051d8e2e0b2ee5c6bd58215281d0ec6dae2f26631af4e2e7c8abe238cdcee73fcaededc429eef569e70908f82d0cc0ea31d7 + languageName: node + linkType: hard + "@types/http-cache-semantics@npm:^4.0.2": version: 4.0.4 resolution: "@types/http-cache-semantics@npm:4.0.4" @@ -3547,6 +3578,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: e3958f8b0fe551c86c14431f5940c3470127293280830684154b91dc7eb3514aeb79fe3216968833cf79d4d1c67f580f054b5be2cd562bebf4f728913e73e944 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -5449,6 +5487,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:1.8.26": + version: 1.8.26 + resolution: "dayjs@npm:1.8.26" + checksum: d0f124aca89be97cb7ae00c336bf2e96de05dc541e74ba0e56c746cd6fa8b2eb07edd48ac3b034c63544143374eb3238ecdc8815912d2fbbef2e853d3ccfec4d + languageName: node + linkType: hard + "dayjs@npm:^1.8.15": version: 1.11.11 resolution: "dayjs@npm:1.11.11" @@ -6540,6 +6585,13 @@ __metadata: languageName: node linkType: hard +"fast-base64-decode@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-base64-decode@npm:1.0.0" + checksum: 4c59eb1775a7f132333f296c5082476fdcc8f58d023c42ed6d378d2e2da4c328c7a71562f271181a725dd17cdaa8f2805346cc330cdbad3b8e4b9751508bd0a3 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -7392,6 +7444,15 @@ __metadata: languageName: node linkType: hard +"hoist-non-react-statics@npm:^3.3.0": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: ^16.7.0 + checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -10969,7 +11030,18 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.8.1": +"prop-types@npm:15.7.2": + version: 15.7.2 + resolution: "prop-types@npm:15.7.2" + dependencies: + loose-envify: ^1.4.0 + object-assign: ^4.1.1 + react-is: ^16.8.1 + checksum: 5eef82fdda64252c7e75aa5c8cc28a24bbdece0f540adb60ce67c205cf978a5bd56b83e4f269f91c6e4dcfd80b36f2a2dec24d362e278913db2086ca9c6f9430 + languageName: node + linkType: hard + +"prop-types@npm:^15.7.x, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -11132,7 +11204,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -11153,14 +11225,19 @@ __metadata: "@babel/core": ^7.20.0 "@babel/preset-env": ^7.20.0 "@babel/runtime": ^7.20.0 + "@react-native-community/netinfo": ^11.3.2 "@react-native/babel-preset": 0.74.85 "@react-native/metro-config": 0.74.85 "@react-native/typescript-config": 0.74.85 + "@types/uuid": ^10.0.0 ai: ^3.2.15 babel-plugin-module-resolver: ^5.0.0 react: 18.2.0 react-native: 0.74.2 + react-native-get-random-values: ^1.11.0 + react-native-gifted-chat: ^2.4.0 text-encoding: ^0.7.0 + uuid: ^10.0.0 zod: ^3.23.8 languageName: unknown linkType: soft @@ -11224,6 +11301,87 @@ __metadata: languageName: node linkType: hard +"react-native-communications@npm:2.2.1": + version: 2.2.1 + resolution: "react-native-communications@npm:2.2.1" + checksum: 0b1a63a116a34866d06790b0dd1153ff622aba148a8ad7d87e18e0ba6c2cfa545210be0d04f3428341a72fbcbd09ac95ea38b92197856cb0fda67bca2321e07b + languageName: node + linkType: hard + +"react-native-get-random-values@npm:^1.11.0": + version: 1.11.0 + resolution: "react-native-get-random-values@npm:1.11.0" + dependencies: + fast-base64-decode: ^1.0.0 + peerDependencies: + react-native: ">=0.56" + checksum: 07729f70a007f7a3b8f98ebf687c1298ba288b87dd71d8ba385be6b5a377718b27b97547bbe1db6b225b83ee109dfce0b01721e6ed535d53892f3ac81e6bf975 + languageName: node + linkType: hard + +"react-native-gifted-chat@npm:^2.4.0": + version: 2.4.0 + resolution: "react-native-gifted-chat@npm:2.4.0" + dependencies: + "@expo/react-native-action-sheet": 4.0.1 + dayjs: 1.8.26 + prop-types: 15.7.2 + react-native-communications: 2.2.1 + react-native-iphone-x-helper: 1.3.1 + react-native-lightbox-v2: 0.9.0 + react-native-parsed-text: 0.0.22 + react-native-typing-animation: 0.1.7 + use-memo-one: 1.1.3 + uuid: 3.4.0 + peerDependencies: + react: "*" + react-native: "*" + checksum: 6573584ac5f010e86b8e4b4253e6fb0a133578eda5856e7d247511642091af123728f76526b11ec5f5f47287f94e653dd9b7582a2e3f8ce5817d52aa7c9d5689 + languageName: node + linkType: hard + +"react-native-iphone-x-helper@npm:1.3.1": + version: 1.3.1 + resolution: "react-native-iphone-x-helper@npm:1.3.1" + peerDependencies: + react-native: ">=0.42.0" + checksum: 024376646009a966e33e12fc2358751830818b0fb73b1c601a64eb5e490d2dc43eec23668991b985a8c412a84d087f20eb45bb9b593567c08b66e741b7bddda5 + languageName: node + linkType: hard + +"react-native-lightbox-v2@npm:0.9.0": + version: 0.9.0 + resolution: "react-native-lightbox-v2@npm:0.9.0" + peerDependencies: + react: ">=16.8.0" + react-native: ">=0.61.0" + checksum: ffedc7e58348ca28ea3a20baa7fa7d4b6cb493473e5f36a701aca22dc59c47606d64d667bb64ac7a1c2c1af686fcd99cd2883bd713797664517f0ed98ad03929 + languageName: node + linkType: hard + +"react-native-parsed-text@npm:0.0.22": + version: 0.0.22 + resolution: "react-native-parsed-text@npm:0.0.22" + dependencies: + prop-types: ^15.7.x + peerDependencies: + react: "*" + react-native: "*" + checksum: a8672a50bbc60c9d925b8935b484cda2ed976483fce156f31b497d799eb349b24b79934aa683576e606e3fd3d8d97877d0048eba95f702f15ae21263c2658fd5 + languageName: node + linkType: hard + +"react-native-typing-animation@npm:0.1.7": + version: 0.1.7 + resolution: "react-native-typing-animation@npm:0.1.7" + peerDependencies: + prop-types: "*" + react: "*" + react-native: "*" + checksum: e3d0d93313dccbee963d2a123f0901ac10c3c0f91258f419eab3c85969bb7872b0d93f43c5e59b7efb79d917259a1828f45389724c17ba9a50a14c29ed4b30d8 + languageName: node + linkType: hard + "react-native@npm:0.74.2": version: 0.74.2 resolution: "react-native@npm:0.74.2" @@ -13236,6 +13394,15 @@ __metadata: languageName: node linkType: hard +"use-memo-one@npm:1.1.3": + version: 1.1.3 + resolution: "use-memo-one@npm:1.1.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 8f08eba26d69406b61bb4b8dacdd5a92bd6aef5b53d346dfe87954f7330ee10ecabc937cc7854635155d46053828e85c10b5a5aff7a04720e6a97b9f42999bac + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.2.0": version: 1.2.2 resolution: "use-sync-external-store@npm:1.2.2" @@ -13259,6 +13426,24 @@ __metadata: languageName: node linkType: hard +"uuid@npm:3.4.0": + version: 3.4.0 + resolution: "uuid@npm:3.4.0" + bin: + uuid: ./bin/uuid + checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f + languageName: node + linkType: hard + +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 4b81611ade2885d2313ddd8dc865d93d8dccc13ddf901745edca8f86d99bc46d7a330d678e7532e7ebf93ce616679fb19b2e3568873ac0c14c999032acb25869 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1"