From 9f77176e20576afb74a650d93c61c9e6ad66675f Mon Sep 17 00:00:00 2001 From: Xiaowei Zhu <33129495+zhu-xiaowei@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:29:32 +0800 Subject: [PATCH] feat: support image, document, and video understanding for Amazon Nova --- README.md | 13 +- README_CN.md | 11 +- react-native/android/app/build.gradle | 4 +- react-native/ios/Podfile.lock | 12 +- .../ios/SwiftChat.xcodeproj/project.pbxproj | 8 +- react-native/ios/SwiftChat/Info.plist | 4 +- .../ios/SwiftChat/PrivacyInfo.xcprivacy | 1 + react-native/package-lock.json | 60 +++-- react-native/package.json | 7 +- react-native/src/assets/play.png | Bin 0 -> 866 bytes react-native/src/chat/ChatScreen.tsx | 31 +-- .../chat/component/CustomAddFileComponent.tsx | 131 ++++++++--- .../src/chat/component/CustomChatFooter.tsx | 9 +- .../component/CustomFileListComponent.tsx | 211 +++++++++++++++--- .../chat/component/CustomMarkdownRenderer.tsx | 4 +- .../chat/component/CustomMessageComponent.tsx | 4 + .../chat/component/CustomSendComponent.tsx | 11 +- .../{ProgressBar.tsx => ImageProgressBar.tsx} | 4 +- .../src/chat/util/BedrockMessageConvertor.ts | 28 ++- react-native/src/chat/util/FileUtils.ts | 82 ++++++- react-native/src/types/Chat.ts | 3 + server/src/main.py | 7 +- server/src/requirements.txt | 4 +- 23 files changed, 512 insertions(+), 137 deletions(-) create mode 100644 react-native/src/assets/play.png rename react-native/src/chat/component/{ProgressBar.tsx => ImageProgressBar.tsx} (98%) diff --git a/README.md b/README.md index a50dd58..e77c89f 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,20 @@ macOS platforms. - Real-time streaming chat with AI - AI image generation with progress -- Multimodal support (camera, photos & documents) +- Multimodal support (images, videos & documents) - Conversation history list view and management - Cross-platform support (Android, iOS, macOS) - Tablet-optimized for iPad and Android tablets - Fast launch and responsive performance - Multiple AI model support and switching +**New Features For Amazon Nova 🎉🎉🎉** + +- Stream conversations with Amazon Nova Micro, Lite and Pro +- Understand images, documents and videos with Nova Lite and Pro +- Record 30-second videos directly on Android and iOS for Nova analysis +- Upload large videos (1080p/4K) beyond 8MB with auto compression + ## Architecture ![](/assets/architecture.png) @@ -85,8 +92,8 @@ can find the **API URL** which looks like: `https://xxx.xxx.awsapprunner.com` or ### Step 3: Download the app and setup with API URL and API Key 1. Download the App - - Android App click to [Download](https://github.com/aws-samples/swift-chat/releases/download/1.5.0/SwiftChat.apk) - - macOS App click to [Download](https://github.com/aws-samples/swift-chat/releases/download/1.5.0/SwiftChat.dmg) + - Android App click to [Download](https://github.com/aws-samples/swift-chat/releases/download/1.6.0/SwiftChat.apk) + - macOS App click to [Download](https://github.com/aws-samples/swift-chat/releases/download/1.6.0/SwiftChat.dmg) - iOS (Currently we do not provide the iOS version, you can build it locally with Xcode) 2. Launch the App, open the drawer menu, and tap **Settings**. diff --git a/README_CN.md b/README_CN.md index d8bfc54..6c921d6 100644 --- a/README_CN.md +++ b/README_CN.md @@ -19,6 +19,13 @@ macOS 等多个平台。 - 快速启动和响应性能 - 支持多种 AI 模型及切换 +**Amazon Nova 新功能 🎉🎉🎉** + +- 支持与 Amazon Nova Micro、Lite 和 Pro 进行流式对话 +- 支持 Nova Lite 和 Pro 对图片、文档及视频内容的理解 +- 支持直接在安卓和 iOS 设备上录制最长 30 秒的视频供 Nova 分析 +- 支持自动压缩上传超过8MB的高清视频(1080p/4K) + ## 架构 ![](/assets/architecture.png) @@ -76,8 +83,8 @@ macOS 等多个平台。 ### 第3步: 下载应用并设置 API URL 和 API Key 1. 下载应用 - - Android 应用点击 [下载](https://github.com/aws-samples/swift-chat/releases/download/1.5.0/SwiftChat.apk) - - macOS 应用点击 [下载](https://github.com/aws-samples/swift-chat/releases/download/1.5.0/SwiftChat.dmg) + - Android 应用点击 [下载](https://github.com/aws-samples/swift-chat/releases/download/1.6.0/SwiftChat.apk) + - macOS 应用点击 [下载](https://github.com/aws-samples/swift-chat/releases/download/1.6.0/SwiftChat.dmg) - iOS (目前不提供 iOS 版本,您可以使用 Xcode 在本地构建) 2. 启动应用,点击左侧菜单按钮,并点击底部的 **Settings**。 diff --git a/react-native/android/app/build.gradle b/react-native/android/app/build.gradle index f621bfa..e82ad7b 100644 --- a/react-native/android/app/build.gradle +++ b/react-native/android/app/build.gradle @@ -79,8 +79,8 @@ android { applicationId "com.aws.swiftchat" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 10 - versionName "1.5.0" + versionCode 11 + versionName "1.6.0" ndk { //noinspection ChromeOsAbiSupport abiFilters 'arm64-v8a' diff --git a/react-native/ios/Podfile.lock b/react-native/ios/Podfile.lock index 09d3bcb..a45225a 100644 --- a/react-native/ios/Podfile.lock +++ b/react-native/ios/Podfile.lock @@ -938,7 +938,7 @@ PODS: - React-Mapbuffer (0.74.1): - glog - React-debug - - react-native-compressor (1.9.0): + - react-native-compressor (1.10.1): - DoubleConversion - glog - hermes-engine @@ -982,7 +982,7 @@ PODS: - Yoga - react-native-get-random-values (1.11.0): - React-Core - - react-native-image-picker (7.1.2): + - react-native-image-picker (7.2.2): - DoubleConversion - glog - hermes-engine @@ -1330,7 +1330,7 @@ PODS: - Yoga - RNShare (10.2.1): - React-Core - - RNSVG (15.4.0): + - RNSVG (15.10.1): - React-Core - SocketRocket (0.7.0) - Yoga (0.0.0) @@ -1588,10 +1588,10 @@ SPEC CHECKSUMS: React-jsitracing: 233d1a798fe0ff33b8e630b8f00f62c4a8115fbc React-logger: 7e7403a2b14c97f847d90763af76b84b152b6fce React-Mapbuffer: 11029dcd47c5c9e057a4092ab9c2a8d10a496a33 - react-native-compressor: 6e5044b1c065421bcc2bfb89579ab30f51803e9a + react-native-compressor: 2ae9013718fb351264fcfcdf232eccbbf3d280a2 react-native-document-picker: c4f197741c327270453aa9840932098e0064fd52 react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 - react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7 + react-native-image-picker: dd85e2530d366acf77745830b053294afed66339 react-native-mmkv: 8c9a677e64a1ac89b0c6cf240feea528318b3074 react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371 React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec @@ -1625,7 +1625,7 @@ SPEC CHECKSUMS: RNReanimated: f4ff116e33e0afc3d127f70efe928847c7c66355 RNScreens: 5aeecbb09aa7285379b6e9f3c8a3c859bb16401c RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c - RNSVG: cb24fb322de8c1ebf59904e7aca0447bb8dbed5a + RNSVG: 7ff26379b2d1871b8571e6f9bc9630de6baf9bdf SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 348f8b538c3ed4423eb58a8e5730feec50bce372 diff --git a/react-native/ios/SwiftChat.xcodeproj/project.pbxproj b/react-native/ios/SwiftChat.xcodeproj/project.pbxproj index 86211a4..f0cfdae 100644 --- a/react-native/ios/SwiftChat.xcodeproj/project.pbxproj +++ b/react-native/ios/SwiftChat.xcodeproj/project.pbxproj @@ -485,7 +485,7 @@ CODE_SIGN_ENTITLEMENTS = SwiftChat/SwiftChat.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = BUA6W9H7T3; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; @@ -497,7 +497,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -526,7 +526,7 @@ CODE_SIGN_ENTITLEMENTS = SwiftChat/SwiftChat.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = BUA6W9H7T3; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; INFOPLIST_FILE = SwiftChat/Info.plist; @@ -537,7 +537,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/react-native/ios/SwiftChat/Info.plist b/react-native/ios/SwiftChat/Info.plist index 1cc84ed..3670a34 100644 --- a/react-native/ios/SwiftChat/Info.plist +++ b/react-native/ios/SwiftChat/Info.plist @@ -34,7 +34,9 @@ NSCameraUsageDescription - Support take camera and summarize it + Support take photos and summarize it + NSMicrophoneUsageDescription + Support record videos and summarize it NSPhotoLibraryUsageDescription Support choose pictures and summarize it UILaunchStoryboardName diff --git a/react-native/ios/SwiftChat/PrivacyInfo.xcprivacy b/react-native/ios/SwiftChat/PrivacyInfo.xcprivacy index 41b8317..d634def 100644 --- a/react-native/ios/SwiftChat/PrivacyInfo.xcprivacy +++ b/react-native/ios/SwiftChat/PrivacyInfo.xcprivacy @@ -9,6 +9,7 @@ NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons + 3B52.1 C617.1 diff --git a/react-native/package-lock.json b/react-native/package-lock.json index 9428589..cca17be 100644 --- a/react-native/package-lock.json +++ b/react-native/package-lock.json @@ -15,7 +15,7 @@ "react": "18.2.0", "react-native": "0.74.1", "react-native-code-highlighter": "^1.2.2", - "react-native-compressor": "^1.9.0", + "react-native-compressor": "^1.10.1", "react-native-dialog": "^9.3.0", "react-native-document-picker": "^9.3.1", "react-native-element-dropdown": "^2.12.1", @@ -26,11 +26,12 @@ "react-native-get-random-values": "^1.11.0", "react-native-gifted-chat": "^2.4.0", "react-native-haptic-feedback": "^2.2.0", - "react-native-image-picker": "^7.1.2", + "react-native-image-picker": "^7.2.2", "react-native-image-viewing": "^0.2.2", "react-native-marked": "^6.0.4", "react-native-mmkv": "^2.12.2", "react-native-polyfill-globals": "^3.1.0", + "react-native-progress": "^5.0.1", "react-native-reanimated": "^3.14.0", "react-native-safe-area-context": "^4.10.8", "react-native-screens": "^3.32.0", @@ -14343,9 +14344,9 @@ "integrity": "sha512-5+C0X9mopI0+qxyQHzOPEi5v5rxNBQjxydPPiKMQSlX1RBIcJ8uTcqUPssQ9Mo8p6c1IKIWJUSqCj4jAmD0qVQ==" }, "node_modules/react-native-compressor": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/react-native-compressor/-/react-native-compressor-1.9.0.tgz", - "integrity": "sha512-kJgRBTrvI8/yOJs8GhXfc1bZui/Wl3FNKOkc3K5+EIgihzU/vS+UTLkojGcYwoYXxj4xP7GFwp0SJLsNOOJZNw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/react-native-compressor/-/react-native-compressor-1.10.1.tgz", + "integrity": "sha512-rbwg4q04VfwnmSxLRv7T5tk2Rq86MpTzDh66Sa6ocN5cImeq7W9jqbFu4TMCq4EkyjoX9hLS5VLNayuLlDPVEQ==", "engines": { "node": ">= 16.0.0" }, @@ -14510,9 +14511,9 @@ } }, "node_modules/react-native-image-picker": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.1.2.tgz", - "integrity": "sha512-b5y5nP60RIPxlAXlptn2QwlIuZWCUDWa/YPUVjgHc0Ih60mRiOg1PSzf0IjHSLeOZShCpirpvSPGnDExIpTRUg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.2.2.tgz", + "integrity": "sha512-hcSaD/ohDac1ooDbZyKltfmOEAWrQOrPbusKEfbJQ7EfGveBEIF5wfmZsCjXRc0HNpGdQ71P1x2JZckkqu7IRw==", "peerDependencies": { "react": "*", "react-native": "*" @@ -15352,6 +15353,17 @@ "web-streams-polyfill": "*" } }, + "node_modules/react-native-progress": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz", + "integrity": "sha512-TYfJ4auAe5vubDma2yfFvt7ktSI+UCfysqJnkdHEcLXqAitRFOozgF/cLgN5VNi/iLdaf3ga1ETi2RF4jVZ/+g==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react-native-svg": "*" + } + }, "node_modules/react-native-reanimated": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.14.0.tgz", @@ -15403,9 +15415,9 @@ } }, "node_modules/react-native-svg": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.4.0.tgz", - "integrity": "sha512-zkBEbme/Dba4yqreg/oI2P6/6LrLywWY7HhaSwpU7Pb5COpTd2fV6/ShsgZz8GRFFdidUPwWmx01FITUsjhkmw==", + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.10.1.tgz", + "integrity": "sha512-Hqz/doQciVFK/Df2v+wsW96oY5jxlta7rZ31KQYo78dlgvAHEaGr6paEOAMvlIruw7EHNQ0Vc1ZmJPJF2kfIPQ==", "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -28858,9 +28870,9 @@ "integrity": "sha512-5+C0X9mopI0+qxyQHzOPEi5v5rxNBQjxydPPiKMQSlX1RBIcJ8uTcqUPssQ9Mo8p6c1IKIWJUSqCj4jAmD0qVQ==" }, "react-native-compressor": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/react-native-compressor/-/react-native-compressor-1.9.0.tgz", - "integrity": "sha512-kJgRBTrvI8/yOJs8GhXfc1bZui/Wl3FNKOkc3K5+EIgihzU/vS+UTLkojGcYwoYXxj4xP7GFwp0SJLsNOOJZNw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/react-native-compressor/-/react-native-compressor-1.10.1.tgz", + "integrity": "sha512-rbwg4q04VfwnmSxLRv7T5tk2Rq86MpTzDh66Sa6ocN5cImeq7W9jqbFu4TMCq4EkyjoX9hLS5VLNayuLlDPVEQ==", "requires": {} }, "react-native-dialog": { @@ -28980,9 +28992,9 @@ "requires": {} }, "react-native-image-picker": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.1.2.tgz", - "integrity": "sha512-b5y5nP60RIPxlAXlptn2QwlIuZWCUDWa/YPUVjgHc0Ih60mRiOg1PSzf0IjHSLeOZShCpirpvSPGnDExIpTRUg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.2.2.tgz", + "integrity": "sha512-hcSaD/ohDac1ooDbZyKltfmOEAWrQOrPbusKEfbJQ7EfGveBEIF5wfmZsCjXRc0HNpGdQ71P1x2JZckkqu7IRw==", "requires": {} }, "react-native-image-viewing": { @@ -29651,6 +29663,14 @@ "integrity": "sha512-6ACmV1SjXvZP2LN6J2yK58yNACKddcvoiKLrSQdISx32IdYStfdmGXrbAfpd+TANrTlIaZ2SLoFXohNwhnqm/w==", "requires": {} }, + "react-native-progress": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz", + "integrity": "sha512-TYfJ4auAe5vubDma2yfFvt7ktSI+UCfysqJnkdHEcLXqAitRFOozgF/cLgN5VNi/iLdaf3ga1ETi2RF4jVZ/+g==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-native-reanimated": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.14.0.tgz", @@ -29687,9 +29707,9 @@ "integrity": "sha512-Z2LWGYWH7raM4H6Oauttv1tEhaB43XSWJAN8iS6oaSG9CnyrUBeYFF4QpU1AH5RgNeylXQdN8CtbizCHHt6coQ==" }, "react-native-svg": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.4.0.tgz", - "integrity": "sha512-zkBEbme/Dba4yqreg/oI2P6/6LrLywWY7HhaSwpU7Pb5COpTd2fV6/ShsgZz8GRFFdidUPwWmx01FITUsjhkmw==", + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.10.1.tgz", + "integrity": "sha512-Hqz/doQciVFK/Df2v+wsW96oY5jxlta7rZ31KQYo78dlgvAHEaGr6paEOAMvlIruw7EHNQ0Vc1ZmJPJF2kfIPQ==", "requires": { "css-select": "^5.1.0", "css-tree": "^1.1.3", diff --git a/react-native/package.json b/react-native/package.json index 18006bf..0938357 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -1,7 +1,7 @@ { "name": "swift-chat", "description": "Sample Bedrock Cross-platform App - SwiftChat", - "version": "1.5.0", + "version": "1.6.0", "private": true, "scripts": { "android": "react-native run-android", @@ -19,7 +19,7 @@ "react": "18.2.0", "react-native": "0.74.1", "react-native-code-highlighter": "^1.2.2", - "react-native-compressor": "^1.9.0", + "react-native-compressor": "^1.10.1", "react-native-dialog": "^9.3.0", "react-native-document-picker": "^9.3.1", "react-native-element-dropdown": "^2.12.1", @@ -30,11 +30,12 @@ "react-native-get-random-values": "^1.11.0", "react-native-gifted-chat": "^2.4.0", "react-native-haptic-feedback": "^2.2.0", - "react-native-image-picker": "^7.1.2", + "react-native-image-picker": "^7.2.2", "react-native-image-viewing": "^0.2.2", "react-native-marked": "^6.0.4", "react-native-mmkv": "^2.12.2", "react-native-polyfill-globals": "^3.1.0", + "react-native-progress": "^5.0.1", "react-native-reanimated": "^3.14.0", "react-native-safe-area-context": "^4.10.8", "react-native-screens": "^3.32.0", diff --git a/react-native/src/assets/play.png b/react-native/src/assets/play.png new file mode 100644 index 0000000000000000000000000000000000000000..2339e5bfee97986a851507ecef7c881e6fc671c7 GIT binary patch literal 866 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy#D0(?STfBg6XWF9$kC^l7 z@4tBQ;*A?Owr$(Cd-v}9_wR4sym`%EnBsac^uda<@Bw z?ed|O3+K(M%LwrIw6@UG(vlKj`Saq#llwou-2OFngZ3W=2BsuW7srr{ds{BC79DmF zaJjf`x=Z1iB%_v+|G!foPw>3fx8;|3MW}IaXW*)i?eDy-nu;Hun7X@{+tlzBLqMat zWM0xG?kfcjt=VnYZdiphHynBTqGPvb{(<-dD_ZY!cgRRQcS%v0te|Qv6p$;?vC%h` zkA=xrs8KPX`o+xf*$hvG_C`;;>!4?#FtMERrA+gz<^7EFlN0XLv;5#`POh__P!!Rz zJlDaled@EHwF^WxOwoAH_*Ljy)U|R3ezDJajSOMj>l(cdWG&joWVYeXGocd_I>$G0 zI~=O|B-3HwbW!p_Ld%@`1IIOf)*oP6yyXY0P>IQH`^Ekz`5!D=oO$`pe24RY*+k44 zX5HmJQCQ04B*Cz2yX1;i^@Lou7t5}Mui4LVw5Ktxe97J>OpWSj@1C z`KjMLHWfhy-!Q)g5w2f?gx2*ug(+dk}FuN(LBD%mg z?2sgziG08MYli^FTPdtR0vyU+vhQ#e{K&a>rck1#Q6tJlv3rq8=cGQazyp8ZxBEBz zv-#5EpKwy{e0(|IiS-?}Urg&$%vOD`@wu5UeZ&2<;JeA|xwd_f`?)Du`*-{GuSV&w zt=MJQW-B+HDXls_VP>=4B0cBEZ3>^h>Imq&$j7t3^5FRJG)!TC%b|MhFLOI4)H^n8 zxyZt#GA$t2q_f4#^}(Jk7gdxlOxw6iVEbB@yF44;%G{l>N!RqQ$d+8++cU1NO}#C% z{cWxFf(^=I)+(El^{iJ|+h^H`P5UPJ{n3G4%TMf$lX|i7+XQj*s&(^zJe~RY>wMk` XJZ*owRdo&n6DfnItDnm{r-UW|5h1** literal 0 HcmV?d00001 diff --git a/react-native/src/chat/ChatScreen.tsx b/react-native/src/chat/ChatScreen.tsx index e859d78..023023a 100644 --- a/react-native/src/chat/ChatScreen.tsx +++ b/react-native/src/chat/ChatScreen.tsx @@ -31,7 +31,6 @@ import { ChatMode, ChatStatus, FileInfo, - FileType, IMessageWithToken, Usage, } from '../types/Chat.ts'; @@ -48,9 +47,14 @@ import { HapticFeedbackTypes } from 'react-native-haptic-feedback/src/types.ts'; import { clearCachedNode } from './component/CustomMarkdownRenderer.tsx'; import { isMac } from '../App.tsx'; import { CustomChatFooter } from './component/CustomChatFooter.tsx'; -import { checkFileNumberLimit } from './util/FileUtils.ts'; +import { + checkFileNumberLimit, + getFileTypeSummary, + isAllFileReady, +} from './util/FileUtils.ts'; import HeaderTitle from './component/HeaderTitle.tsx'; import { HeaderOptions } from '@react-navigation/elements/src/types.tsx'; +import Toast from 'react-native-toast-message'; const BOT_ID = 2; @@ -411,18 +415,16 @@ function ChatScreen(): React.JSX.Element { // handle onSend const onSend = useCallback((message: IMessage[] = []) => { const files = selectedFilesRef.current; + if (!isAllFileReady(files)) { + Toast.show({ + type: 'info', + text1: 'please wait for all videos to be ready', + }); + return; + } if (message[0]?.text || files.length > 0) { if (!message[0]?.text) { - const hasImages = files.some(file => file.type === FileType.image); - const hasDocs = files.some(file => file.type === FileType.document); - message[0].text = - files.length === 1 - ? 'Summarize this' - : hasImages && hasDocs - ? 'Summarize these docs and images' - : hasImages - ? 'Summarize these images' - : 'Summarize these docs'; + message[0].text = getFileTypeSummary(files); } if (selectedFilesRef.current.length > 0) { message[0].image = JSON.stringify(selectedFilesRef.current); @@ -493,10 +495,11 @@ function ChatScreen(): React.JSX.Element { selectedFiles.length > 0 && ( { - if (isDelete) { + onFileUpdated={(files, isUpdate) => { + if (isUpdate) { setSelectedFiles(files); } else { + console.log('handleNewFileSelected'); handleNewFileSelected(files); } }} diff --git a/react-native/src/chat/component/CustomAddFileComponent.tsx b/react-native/src/chat/component/CustomAddFileComponent.tsx index 8e00ce2..eda6f74 100644 --- a/react-native/src/chat/component/CustomAddFileComponent.tsx +++ b/react-native/src/chat/component/CustomAddFileComponent.tsx @@ -1,5 +1,5 @@ import { Actions } from 'react-native-gifted-chat'; -import { Image, StyleSheet, Text } from 'react-native'; +import { Image, Platform, StyleSheet, Text } from 'react-native'; import React from 'react'; import { ImagePickerResponse, @@ -10,8 +10,14 @@ import { FileInfo, FileType } from '../../types/Chat.ts'; import { pick, types } from 'react-native-document-picker'; import Toast from 'react-native-toast-message'; import { saveFile } from '../util/FileUtils.ts'; -import { Image as Img } from 'react-native-compressor'; +import { + createVideoThumbnail, + getImageMetaData, + getVideoMetaData, + Image as Img, +} from 'react-native-compressor'; import { isMac } from '../../App.tsx'; +import { getTextModel } from '../../storage/StorageUtils.ts'; interface CustomRenderActionsProps { onFileSelected: (files: FileInfo[]) => void; @@ -33,19 +39,23 @@ export const CustomAddFileComponent: React.FC = ({ }) => { const handleChooseFiles = async () => { try { + const chooseType = [ + types.images, + types.pdf, + types.csv, + types.doc, + types.docx, + types.xls, + types.xlsx, + types.plainText, + 'public.html', + ]; + if (isVideoSupported()) { + chooseType.push(types.video); + } const pickResults = await pick({ allowMultiSelection: true, - type: [ - types.images, - types.pdf, - types.csv, - types.doc, - types.docx, - types.xls, - types.xlsx, - types.plainText, - 'public.html', - ], + type: chooseType, }); const files: FileInfo[] = []; await Promise.all( @@ -53,7 +63,7 @@ export const CustomAddFileComponent: React.FC = ({ if (pickResult.name && pickResult.uri) { const fileName = getFileNameWithoutExtension(pickResult.name); const fileNameArr = pickResult.name.split('.'); - const format = fileNameArr[fileNameArr.length - 1]; + let format = fileNameArr[fileNameArr.length - 1].toLowerCase(); const fileType = getFileType(format); if (fileType === FileType.unSupported) { const msg = 'Selected UnSupported Files format: .' + format; @@ -70,21 +80,48 @@ export const CustomAddFileComponent: React.FC = ({ } let localFileUrl: string | null; if (fileType === FileType.image) { - const compressUri = await Img.compress(decodeURI(pickResult.uri)); - localFileUrl = await saveFile(compressUri, pickResult.name); + pickResult.uri = decodeURI(pickResult.uri); + if (format === 'png' || format === 'jpg' || format === 'jpeg') { + pickResult.uri = await Img.compress(pickResult.uri); + const metaData = await getImageMetaData(pickResult.uri); + format = metaData.extension; + } + localFileUrl = await saveFile(pickResult.uri, pickResult.name); + } else if (fileType === FileType.video) { + localFileUrl = pickResult.uri; } else { localFileUrl = await saveFile( decodeURI(pickResult.uri), pickResult.name ); } + + let thumbnailUrl; + let width = 0; + let height = 0; + if (fileType === FileType.video) { + if (Platform.OS === 'android') { + localFileUrl = await saveFile(pickResult.uri, fileName); + pickResult.uri = localFileUrl!; + } + const thumbnail = await createVideoThumbnail(pickResult.uri); + thumbnailUrl = + (await saveFile(thumbnail.path, fileName + '.jpeg')) ?? ''; + const metaData = await getVideoMetaData(pickResult.uri); + width = metaData.width; + height = metaData.height; + } + if (localFileUrl) { files.push({ fileName: fileName, url: localFileUrl, + videoThumbnailUrl: thumbnailUrl, fileSize: pickResult.size ?? 0, type: fileType, format: format.toLowerCase() === 'jpg' ? 'jpeg' : format, + width: width, + height: height, }); } } @@ -129,7 +166,9 @@ export const CustomAddFileComponent: React.FC = ({ 'Take Camera': () => { launchCamera({ saveToPhotos: false, - mediaType: 'photo', + mediaType: isVideoSupported() ? 'mixed' : 'photo', + videoQuality: 'high', + durationLimit: 30, includeBase64: false, includeExtra: true, presentationStyle: 'fullScreen', @@ -143,9 +182,10 @@ export const CustomAddFileComponent: React.FC = ({ 'Choose From Photos': () => { launchImageLibrary({ selectionLimit: 0, - mediaType: 'photo', + mediaType: isVideoSupported() ? 'mixed' : 'photo', includeBase64: false, includeExtra: true, + assetRepresentationMode: 'current', }).then(async res => { const files = await getFiles(res); if (files.length > 0) { @@ -170,6 +210,7 @@ const showInfo = (msg: string) => { const MAX_FILE_SIZE = 4.5 * 1024 * 1024; export const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'gif', 'webp']; +export const VIDEO_FORMATS = ['mp4', 'mov', 'mkv', 'webm']; export const DOCUMENT_FORMATS = [ 'pdf', 'csv', @@ -185,6 +226,8 @@ export const DOCUMENT_FORMATS = [ export const getFileType = (format: string) => { if (isImageFormat(format)) { return FileType.image; + } else if (isVideoFormat(format)) { + return FileType.video; } else if (isDocumentFormat(format)) { return FileType.document; } else { @@ -193,42 +236,68 @@ export const getFileType = (format: string) => { }; export const isImageFormat = (format: string) => { - return IMAGE_FORMATS.includes(format.toLowerCase()); + return IMAGE_FORMATS.includes(format); +}; + +export const isVideoFormat = (format: string) => { + return VIDEO_FORMATS.includes(format); }; export const isDocumentFormat = (format: string) => { - return DOCUMENT_FORMATS.includes(format.toLowerCase()); + return DOCUMENT_FORMATS.includes(format); }; const getFileNameWithoutExtension = (fileName: string) => { return fileName.substring(0, fileName.lastIndexOf('.')).trim(); }; +export const isVideoSupported = (): boolean => { + const textModelId = getTextModel().modelId; + return textModelId.includes('nova-pro') || textModelId.includes('nova-lite'); +}; + const getFiles = async (res: ImagePickerResponse) => { const files: FileInfo[] = []; await Promise.all( - res.assets?.map(async image => { - if (image.fileName && image.uri) { - const fileName = getFileNameWithoutExtension(image.fileName); - const fileNameArr = image.fileName.split('.'); - const format = fileNameArr[fileNameArr.length - 1]; + res.assets?.map(async media => { + if (media.fileName && media.uri) { + const fileName = getFileNameWithoutExtension(media.fileName); + const fileNameArr = media.fileName.split('.'); + let format = fileNameArr[fileNameArr.length - 1].toLowerCase(); const fileType = getFileType(format); if (fileType === FileType.unSupported) { const msg = 'Selected UnSupported Files format: .' + format; showInfo(msg); return; } - const compressUri = await Img.compress(image.uri); - const localFileUrl = await saveFile(compressUri, image.fileName); + if (format === 'png' || format === 'jpg' || format === 'jpeg') { + media.uri = await Img.compress(media.uri); + const metaData = await getImageMetaData(media.uri); + format = metaData.extension; + } + let thumbnailUrl; + if (fileType === FileType.video) { + const thumbnail = await createVideoThumbnail(media.uri); + thumbnailUrl = + (await saveFile(thumbnail.path, fileName + '.jpeg')) ?? ''; + } + let localFileUrl: string | null; + if (fileType !== FileType.video) { + localFileUrl = await saveFile(media.uri, media.fileName); + } else { + localFileUrl = media.uri; + } + if (localFileUrl) { files.push({ fileName: fileName, url: localFileUrl, - fileSize: image.fileSize ?? 0, + videoThumbnailUrl: thumbnailUrl, + fileSize: media.fileSize ?? 0, type: fileType, - format: format.toLowerCase() === 'jpg' ? 'jpeg' : format, - width: image.width, - height: image.height, + format: format === 'jpg' ? 'jpeg' : format, + width: media.width, + height: media.height, }); } } diff --git a/react-native/src/chat/component/CustomChatFooter.tsx b/react-native/src/chat/component/CustomChatFooter.tsx index 2ec069f..11a4486 100644 --- a/react-native/src/chat/component/CustomChatFooter.tsx +++ b/react-native/src/chat/component/CustomChatFooter.tsx @@ -5,20 +5,17 @@ import { CustomFileListComponent } from './CustomFileListComponent.tsx'; interface CustomComposerProps { files: FileInfo[]; - onFileSelected: (files: FileInfo[], isDelete?: boolean) => void; + onFileUpdated: (files: FileInfo[], isUpdate?: boolean) => void; } export const CustomChatFooter: React.FC = ({ files, - onFileSelected, + onFileUpdated, }) => { return ( {files.length > 0 && ( - + )} ); diff --git a/react-native/src/chat/component/CustomFileListComponent.tsx b/react-native/src/chat/component/CustomFileListComponent.tsx index f9f5dd4..1af3259 100644 --- a/react-native/src/chat/component/CustomFileListComponent.tsx +++ b/react-native/src/chat/component/CustomFileListComponent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Image, ScrollView, @@ -14,11 +14,14 @@ import { ImageSource } from 'react-native-image-viewing/dist/@types'; import Share from 'react-native-share'; import FileViewer from 'react-native-file-viewer'; import { isMac } from '../../App.tsx'; -import { getFullFileUrl } from '../util/FileUtils.ts'; +import { getFullFileUrl, saveFile } from '../util/FileUtils.ts'; +import { getVideoMetaData, Video } from 'react-native-compressor'; +import * as Progress from 'react-native-progress'; +import Toast from 'react-native-toast-message'; interface CustomFileProps { files: FileInfo[]; - onFileSelected?: (files: FileInfo[], isDelete?: boolean) => void; + onFileUpdated?: (files: FileInfo[], isUpdate?: boolean) => void; mode?: DisplayMode; } @@ -27,6 +30,8 @@ export enum DisplayMode { Display = 'display', } +const MAX_VIDEO_SIZE = 8; + const openInFileViewer = (url: string) => { FileViewer.open(url) .then(() => {}) @@ -35,9 +40,22 @@ const openInFileViewer = (url: string) => { }); }; +const CircularProgress = ({ progress }: { progress: number }) => { + return ( + + + + ); +}; + export const CustomFileListComponent: React.FC = ({ files, - onFileSelected, + onFileUpdated, mode = DisplayMode.Edit, }) => { const [visible, setIsVisible] = useState(false); @@ -45,8 +63,13 @@ export const CustomFileListComponent: React.FC = ({ const [imageUrls, setImageUrls] = useState([]); const scrollViewRef = useRef(null); + const [compressionProgress, setCompressionProgress] = useState(0); + const compressingFiles = useRef(''); + const filesRef = useRef(files); + const isCompressing = useRef(false); useEffect(() => { + filesRef.current = files; if (scrollViewRef.current && mode !== DisplayMode.Display) { setTimeout(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); @@ -54,27 +77,115 @@ export const CustomFileListComponent: React.FC = ({ } }, [files, mode]); + const handleCompression = useCallback(async () => { + for (const file of filesRef.current) { + if ( + !isCompressing.current && + file.type === FileType.video && + !file.videoUrl && + compressingFiles.current !== file.url + ) { + compressingFiles.current = file.url; + try { + isCompressing.current = true; + const uri = await Video.compress( + file.url, + { progressDivider: 1, maxSize: 960 }, + progress => { + setCompressionProgress(progress); + } + ); + const metaData = await getVideoMetaData(uri); + console.log('metaData', metaData); + isCompressing.current = false; + compressingFiles.current = ''; + const currentSize = metaData.size / 1024 / 1024; + if (currentSize < MAX_VIDEO_SIZE) { + // save video to files and update video url + const localFileUrl = await saveFile( + uri, + file.fileName + '.' + metaData.extension + ); + if (localFileUrl) { + const updatedFiles = filesRef.current.map(f => + f.url === file.url + ? { ...f, videoUrl: localFileUrl, format: metaData.extension } + : f + ); + onFileUpdated!(updatedFiles, true); + } + } else { + // remove the video + const newFiles = filesRef.current.filter(f => f.url !== file.url); + onFileUpdated!(newFiles, true); + Toast.show({ + type: 'info', + text1: `Video too large: ${currentSize.toFixed( + 1 + )}MB (max ${MAX_VIDEO_SIZE}MB)`, + }); + } + } catch (error) { + Toast.show({ + type: 'info', + text1: 'Video process failed', + }); + compressingFiles.current = ''; + isCompressing.current = false; + // remove the failed video + const newFiles = filesRef.current.filter(f => f.url !== file.url); + onFileUpdated!(newFiles, true); + } + } + } + }, [onFileUpdated]); + + useEffect(() => { + const checkAndCompressVideos = async () => { + await handleCompression(); + }; + checkAndCompressVideos().then(); + }, [files, handleCompression]); + const renderFileItem = (file: FileInfo, fileIndex: number) => { const isImage = file.type === FileType.image; - const fullFileUrl = getFullFileUrl(file.url); + const isDocument = file.type === FileType.document; + const isVideo = file.type === FileType.video; + const fullFileUrl = + isVideo && !file.videoUrl + ? file.url + : getFullFileUrl(file.videoUrl || file.url); const itemKey = `file-${fileIndex}-${file.url}`; + + const isFileCompressing = compressingFiles.current === file.url; + let ratio = 1; + if (file.width && file.height) { + ratio = file.width / file.height; + ratio = ratio < 1 ? 1 : ratio; + } + const isHideDelete = file.type === FileType.video && !file.videoUrl; return ( - {mode === DisplayMode.Edit && ( + {mode === DisplayMode.Edit && !isHideDelete && ( { const newFiles = files.filter(f => f.url !== file.url); - onFileSelected!(newFiles, true); + onFileUpdated!(newFiles, true); }}> - × + + × + )} @@ -92,7 +203,14 @@ export const CustomFileListComponent: React.FC = ({ } }} onPress={() => { - if (isMac || file.type === FileType.document) { + if (isVideo && isFileCompressing) { + return; + } + if ( + isMac || + file.type === FileType.document || + file.type === FileType.video + ) { openInFileViewer(fullFileUrl); } else { const images = files @@ -106,12 +224,27 @@ export const CustomFileListComponent: React.FC = ({ setIsVisible(true); } }}> - {isImage ? ( - + {isImage || isVideo ? ( + + + {isVideo && !isFileCompressing && ( + + )} + {isVideo && isFileCompressing && ( + + )} + ) : ( @@ -156,10 +289,7 @@ export const CustomFileListComponent: React.FC = ({ {mode === DisplayMode.Edit && ( - + )} ; + return ; } if (uri.endsWith('.svg')) { return ; diff --git a/react-native/src/chat/component/CustomMessageComponent.tsx b/react-native/src/chat/component/CustomMessageComponent.tsx index e35dcfe..84d916a 100644 --- a/react-native/src/chat/component/CustomMessageComponent.tsx +++ b/react-native/src/chat/component/CustomMessageComponent.tsx @@ -255,6 +255,10 @@ const styles = StyleSheet.create({ const customMarkedStyles: MarkedStyles = { table: { marginVertical: 4 }, li: { paddingVertical: 4 }, + h1: { fontSize: 28 }, + h2: { fontSize: 24 }, + h3: { fontSize: 20 }, + h4: { fontSize: 18 }, }; export default CustomMessageComponent; diff --git a/react-native/src/chat/component/CustomSendComponent.tsx b/react-native/src/chat/component/CustomSendComponent.tsx index 5322a11..6d36adb 100644 --- a/react-native/src/chat/component/CustomSendComponent.tsx +++ b/react-native/src/chat/component/CustomSendComponent.tsx @@ -24,7 +24,7 @@ const CustomSendComponent: React.FC = ({ const { text } = props; if ( chatMode !== ChatMode.Text || - !getTextModel().modelId.includes('claude-3') || + !isMultiModalModel() || (text && text!.length > 0) || selectedFiles.length > 0 || chatStatus === ChatStatus.Running @@ -74,6 +74,15 @@ const CustomSendComponent: React.FC = ({ } }; +const isMultiModalModel = (): boolean => { + const textModelId = getTextModel().modelId; + return ( + textModelId.includes('claude-3') || + textModelId.includes('nova-pro') || + textModelId.includes('nova-lite') + ); +}; + const styles = StyleSheet.create({ stopContainer: { marginRight: 15, diff --git a/react-native/src/chat/component/ProgressBar.tsx b/react-native/src/chat/component/ImageProgressBar.tsx similarity index 98% rename from react-native/src/chat/component/ProgressBar.tsx rename to react-native/src/chat/component/ImageProgressBar.tsx index aed404c..4fd86b6 100644 --- a/react-native/src/chat/component/ProgressBar.tsx +++ b/react-native/src/chat/component/ImageProgressBar.tsx @@ -47,7 +47,7 @@ const ProgressCircle = ({ progressAnim }: { progressAnim: Animated.Value }) => { ); }; -const ProgressBar = () => { +const ImageProgressBar = () => { const progressAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current; const [showFinishImage, setShowFinishImage] = useState(false); @@ -127,4 +127,4 @@ const styles = StyleSheet.create({ }, }); -export default ProgressBar; +export default ImageProgressBar; diff --git a/react-native/src/chat/util/BedrockMessageConvertor.ts b/react-native/src/chat/util/BedrockMessageConvertor.ts index dad58eb..49740db 100644 --- a/react-native/src/chat/util/BedrockMessageConvertor.ts +++ b/react-native/src/chat/util/BedrockMessageConvertor.ts @@ -21,7 +21,9 @@ export async function getBedrockMessage( const files = JSON.parse(message.image) as FileInfo[]; for (const file of files) { try { - const fileBytes = await getFileBytes(file.url); + const fileUrl = + file.type === FileType.video ? file.videoUrl! : file.url; + const fileBytes = await getFileBytes(fileUrl); if (file.type === FileType.image) { content.push({ image: { @@ -31,6 +33,15 @@ export async function getBedrockMessage( }, }, }); + } else if (file.type === FileType.video) { + content.push({ + video: { + format: file.format.toLowerCase(), + source: { + bytes: fileBytes, + }, + }, + }); } else if (file.type === FileType.document) { let fileName = file.fileName; if (!isValidFilename(fileName)) { @@ -88,6 +99,15 @@ export interface ImageContent { }; } +export interface VideoContent { + video: { + format: string; + source: { + bytes: string; + }; + }; +} + export interface DocumentContent { document: { format: string; @@ -98,7 +118,11 @@ export interface DocumentContent { }; } -export type MessageContent = TextContent | ImageContent | DocumentContent; +export type MessageContent = + | TextContent + | ImageContent + | VideoContent + | DocumentContent; export type BedrockMessage = { role: string; diff --git a/react-native/src/chat/util/FileUtils.ts b/react-native/src/chat/util/FileUtils.ts index 08c9d2f..159f0b8 100644 --- a/react-native/src/chat/util/FileUtils.ts +++ b/react-native/src/chat/util/FileUtils.ts @@ -2,6 +2,7 @@ import RNFS from 'react-native-fs'; import { Platform } from 'react-native'; import { FileInfo, FileType } from '../../types/Chat.ts'; import Toast from 'react-native-toast-message'; +import { getTextModel } from '../../storage/StorageUtils.ts'; export const saveImageToLocal = async ( base64ImageData: string @@ -82,6 +83,8 @@ export const getFullFileUrl = (url: string) => { const MAX_IMAGES = 20; const MAX_DOCUMENTS = 5; +const MAX_NOVA_FILES = 5; +const MAX_NOVA_VIDEOS = 1; export const checkFileNumberLimit = ( prevFiles: FileInfo[], @@ -101,6 +104,34 @@ export const checkFileNumberLimit = ( let processedNewDocs = newDocs; let showWarning = false; + if (isNova()) { + if (prevFiles.length + newFiles.length > MAX_NOVA_FILES) { + showInfo(`Maximum ${MAX_NOVA_FILES} files allowed`); + } + if (prevFiles.length >= MAX_NOVA_FILES) { + return prevFiles; + } + const existingVideos = prevFiles.filter( + file => file.type === FileType.video + ).length; + const newVideos = newFiles.filter(file => file.type === FileType.video); + + if (existingVideos + newVideos.length > MAX_NOVA_VIDEOS) { + showInfo(`Maximum ${MAX_NOVA_VIDEOS} video allowed`); + } + + const filteredNewFiles = + existingVideos >= MAX_NOVA_VIDEOS + ? newFiles.filter(file => file.type !== FileType.video) + : newFiles.filter( + file => + file.type !== FileType.video || + newVideos.indexOf(file) < MAX_NOVA_VIDEOS - existingVideos + ); + + return [...prevFiles, ...filteredNewFiles].slice(0, MAX_NOVA_FILES); + } + if (totalImages > MAX_IMAGES) { const remainingSlots = Math.max(0, MAX_IMAGES - existingImages.length); processedNewImages = newImages.slice(0, remainingSlots); @@ -115,17 +146,52 @@ export const checkFileNumberLimit = ( if (showWarning) { if (totalImages > MAX_IMAGES) { - Toast.show({ - type: 'info', - text1: `Image limit exceeded, maximum ${MAX_IMAGES} images allowed`, - }); + showInfo(`Image limit exceeded, maximum ${MAX_IMAGES} images allowed`); } if (totalDocs > MAX_DOCUMENTS) { - Toast.show({ - type: 'info', - text1: `Document limit exceeded, maximum ${MAX_DOCUMENTS} files allowed`, - }); + showInfo( + `Document limit exceeded, maximum ${MAX_DOCUMENTS} files allowed` + ); } } return [...prevFiles, ...processedNewImages, ...processedNewDocs]; }; + +const isNova = (): boolean => { + const textModelId = getTextModel().modelId; + return textModelId.includes('nova-pro') || textModelId.includes('nova-lite'); +}; + +export const isAllFileReady = (files: FileInfo[]) => { + const videos = files.filter(file => file.type === FileType.video); + if (videos.length > 0) { + return videos.filter(video => video.videoUrl === undefined).length === 0; + } else { + return true; + } +}; + +const showInfo = (msg: string) => { + Toast.show({ + type: 'info', + text1: msg, + }); +}; + +export const getFileTypeSummary = (files: FileInfo[]) => { + if (files.length === 1) { + return 'Summarize this'; + } + + const imgCount = files.filter(file => file.type === FileType.image).length; + const docCount = files.filter(file => file.type === FileType.document).length; + const videoCount = files.filter(file => file.type === FileType.video).length; + + const types = [ + imgCount && `${imgCount > 1 ? 'images' : 'image'}`, + docCount && `${docCount > 1 ? 'docs' : 'doc'}`, + videoCount && `${videoCount > 1 ? 'videos' : 'video'}`, + ].filter(Boolean); + + return `Summarize these ${types.join(' and ')}`; +}; diff --git a/react-native/src/types/Chat.ts b/react-native/src/types/Chat.ts index ddd7a58..aa48fb6 100644 --- a/react-native/src/types/Chat.ts +++ b/react-native/src/types/Chat.ts @@ -57,12 +57,15 @@ export type UpgradeInfo = { export enum FileType { document = 'document', image = 'image', + video = 'video', unSupported = 'unSupported', } export type FileInfo = { fileName: string; url: string; + videoUrl?: string; + videoThumbnailUrl?: string; fileSize: number; format: string; type: FileType; diff --git a/server/src/main.py b/server/src/main.py index 529b9bd..2af55a1 100644 --- a/server/src/main.py +++ b/server/src/main.py @@ -93,9 +93,12 @@ async def converse(request: ConverseRequest, if 'image' in content: image_bytes = base64.b64decode(content['image']['source']['bytes']) content['image']['source']['bytes'] = image_bytes + if 'video' in content: + video_bytes = base64.b64decode(content['video']['source']['bytes']) + content['video']['source']['bytes'] = video_bytes if 'document' in content: - image_bytes = base64.b64decode(content['document']['source']['bytes']) - content['document']['source']['bytes'] = image_bytes + document_bytes = base64.b64decode(content['document']['source']['bytes']) + content['document']['source']['bytes'] = document_bytes command = { "inferenceConfig": {"maxTokens": max_tokens}, "messages": request.messages, diff --git a/server/src/requirements.txt b/server/src/requirements.txt index 709da89..6e7628d 100644 --- a/server/src/requirements.txt +++ b/server/src/requirements.txt @@ -1,4 +1,4 @@ fastapi~=0.112.1 -boto3==1.34.127 +boto3==1.35.74 pydantic~=2.8.2 -uvicorn~=0.30.6 \ No newline at end of file +uvicorn~=0.30.6