diff --git a/README.md b/README.md index d2f5e78a..34fa9fef 100644 --- a/README.md +++ b/README.md @@ -16,18 +16,17 @@ across Android, iOS, and macOS platforms. ![](assets/promo.avif) ### What's New 🔥 - +- 🚀 Support streaming rendering of Mermaid charts (From v2.6.0). +
+ + +
- 🚀 Support using Bedrock API Key for Amazon Bedrock models (From v2.5.0). - 🚀 Support virtual try-on, automatically recognize clothes, pants, shoes and try them on (From v2.5.0). - 🚀 Support shortcuts for macOS (From v2.5.0). - Use `Shift + Enter`, `Control + Enter` or `Option + Enter` to add a line break. - Use `⌘ + V` to add images (Screenshot), videos, or documents from your clipboard. - Use `⌘ + N` to opening multiple Mac windows for parallel operations. -- Support adds multiple OpenAI Compatible model providers. You can now - use [Easy Model Deployer](https://github.com/aws-samples/easy-model-deployer), OpenRouter, or any OpenAI-compatible - model provider. (From v2.5.0). -- Supports dark mode on Android, iOS, and Mac (From v2.4.0). -- Support Speech to Speech By Amazon Nova Sonic on Apple Platform. (From v2.3.0). ## 📱 Quick Download @@ -207,6 +206,10 @@ Congratulations 🎉 Your SwiftChat App is ready to use! - `GPT-4.1` - `GPT-4.1 mini` - `GPT-4.1 nano` + - `GPT-5` + - `GPT-5 chat` + - `GPT-5 mini` + - `GPT-5 nano` Additionally, if you have deployed and configured the [SwiftChat Server](#getting-started-with-amazon-bedrock), you can enable the **Use Proxy** option to forward your requests. @@ -232,7 +235,7 @@ can enable the **Use Proxy** option to forward your requests. ## Key Features - Real-time streaming chat with AI -- Rich Markdown Support: Tables, Code Blocks, LaTeX and More +- Rich Markdown Support: Tables, Code Blocks, LaTeX, Mermaid Chart and More - AI image generation with progress - Multimodal support (images, videos & documents) - Conversation history list view and management diff --git a/README_CN.md b/README_CN.md index 2c10e2bd..cecaf0ba 100644 --- a/README_CN.md +++ b/README_CN.md @@ -16,17 +16,17 @@ SwiftChat 是一款快速响应的 AI 聊天应用,采用 [React Native](https ### 新功能 🔥 +- 🚀 支持流式渲染 Mermaid 图表(自 v2.6.0 起)。 +
+ + +
- 🚀 支持使用 Bedrock API Key 连接 Amazon Bedrock 模型(自 v2.5.0 起)。 - 🚀 支持虚拟试衣功能,自动识别衣服、裤子、鞋子并试穿(自 v2.5.0 起)。 - 🚀 支持 macOS 快捷键操作(自 v2.5.0 起)。 - 使用 `Shift + Enter`、`Control + Enter` 或 `Option + Enter` 添加换行。 - 使用 `⌘ + V` 从剪贴板添加图片(截图)、视频或文档。 - 使用 `⌘ + N` 打开多个 Mac 窗口进行并行操作。 -- 支持添加多个 OpenAI Compatible - 模型提供商。您现在可以使用 [Easy Model Deployer](https://github.com/aws-samples/easy-model-deployer)、OpenRouter 或任何 - OpenAI 兼容的模型提供商(自 v2.5.0 起)。 -- 支持 Android、iOS 和 Mac 上的暗黑模式(自 v2.4.0 起)。 -- 在 Apple 平台上支持 Amazon Nova Sonic 语音对语音功能(自 v2.3.0 起)。 ## 📱 快速下载 @@ -195,6 +195,10 @@ SwiftChat 是一款快速响应的 AI 聊天应用,采用 [React Native](https - `GPT-4.1` - `GPT-4.1 mini` - `GPT-4.1 nano` + - `GPT-5` + - `GPT-5 chat` + - `GPT-5 mini` + - `GPT-5 nano` 此外,如果您已部署并配置了 [SwiftChat 服务器](#amazon-bedrock-入门指南),可以启用 **Use Proxy** 选项来转发您的请求。 @@ -218,7 +222,7 @@ SwiftChat 是一款快速响应的 AI 聊天应用,采用 [React Native](https ## 主要功能 - 与 AI 进行实时流式聊天 -- 丰富的 Markdown 支持:表格、代码块、LaTeX 等 +- 丰富的 Markdown 支持:表格、代码块、LaTeX, Mermaid图标等 - 带进度显示的 AI 图像生成 - 多模态支持(图像、视频和文档) - 对话历史列表查看和管理 diff --git a/assets/animations/mermaid.avif b/assets/animations/mermaid.avif new file mode 100644 index 00000000..ed2fcbed Binary files /dev/null and b/assets/animations/mermaid.avif differ diff --git a/assets/animations/mermaid_en.avif b/assets/animations/mermaid_en.avif new file mode 100644 index 00000000..8061b85a Binary files /dev/null and b/assets/animations/mermaid_en.avif differ diff --git a/assets/animations/mermaid_save.avif b/assets/animations/mermaid_save.avif new file mode 100644 index 00000000..31ab008d Binary files /dev/null and b/assets/animations/mermaid_save.avif differ diff --git a/assets/animations/mermaid_save_en.avif b/assets/animations/mermaid_save_en.avif new file mode 100644 index 00000000..47996d8e Binary files /dev/null and b/assets/animations/mermaid_save_en.avif differ diff --git a/react-native/ios/Podfile.lock b/react-native/ios/Podfile.lock index 930f90ed..6c853b3f 100644 --- a/react-native/ios/Podfile.lock +++ b/react-native/ios/Podfile.lock @@ -7,9 +7,9 @@ PODS: - hermes-engine (0.74.1): - hermes-engine/Pre-built (= 0.74.1) - hermes-engine/Pre-built (0.74.1) - - MMKV (1.3.9): - - MMKVCore (~> 1.3.9) - - MMKVCore (1.3.9) + - MMKV (2.2.3): + - MMKVCore (~> 2.2.3) + - MMKVCore (2.2.3) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -1027,6 +1027,27 @@ PODS: - Yoga - react-native-safe-area-context (4.10.8): - React-Core + - react-native-webview (13.16.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - React-nativeconfig (0.74.1) - React-NativeModulesApple (0.74.1): - glog @@ -1374,6 +1395,7 @@ DEPENDENCIES: - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-mmkv (from `../node_modules/react-native-mmkv`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) - 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`) @@ -1488,6 +1510,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-mmkv" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" React-nativeconfig: :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1562,73 +1586,74 @@ SPEC CHECKSUMS: fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 16b8530de1b383cdada1476cf52d1b52f0692cbc - MMKV: 817ba1eea17421547e01e087285606eb270a8dcb - MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 - RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 + MMKV: 941e8774da0e6fdf12c6b3fcc833ca687ae5a42d + MMKVCore: 6d5cc1bacce539f4c974985dfe646fb65a5d27d2 + RCT-Folly: 5dc73daec3476616d19e8a53f0156176f7b55461 RCTDeprecation: efb313d8126259e9294dc4ee0002f44a6f676aba RCTRequired: f49ea29cece52aee20db633ae7edc4b271435562 RCTTypeSafety: a11979ff0570d230d74de9f604f7d19692157bc4 React: 88794fad7f460349dbc9df8a274d95f37a009f5d React-callinvoker: 7a7023e34a55c89ea2aa62486bb3c1164ab0be0c - React-Codegen: af31a9323ce23988c255c9afd0ae9415ff894939 - React-Core: 60075333bc22b5a793d3f62e207368b79bff2e64 - React-CoreModules: 147c314d6b3b1e069c9ad64cbbbeba604854ff86 - React-cxxreact: 5de27fd8bff4764acb2eac3ee66001e0e2b910e7 + React-Codegen: 118828b0731a9ecf9021270b788f958f9ccb2e19 + React-Core: 74cc07109071b230de904d394c2bf15b9f886bff + React-CoreModules: 8beb4863375aafeac52c49a3962b81d137577585 + React-cxxreact: d0b0d575214ba236dff569e14dd4411ac82b3566 React-debug: 6397f0baf751b40511d01e984b01467d7e6d8127 - React-Fabric: 6fa475e16e0a37b38d462cec32b70fd5cf886305 - React-FabricImage: 7e09b3704e3fa084b4d44b5b5ef6e2e3d3334ec0 + React-Fabric: 37f29709a9caefd2a9fece6f695bc88a0af77f40 + React-FabricImage: 9c3f6125b2f5908a2e7d0947cfb74022c1a0b294 React-featureflags: 2eb79dd9df4095bff519379f2a4c915069e330bb - React-graphics: 82a482a3aa5d9659b74cdf2c8b57faf67eaa10fb - React-hermes: d93936b02de2fd7e67c11e92c16d4278a14d0134 - React-ImageManager: ebb3c4812e2c5acba5a89728c2d77729471329ad - React-jserrorhandler: a08e0adcf1612900dde82b8bf8e93e7d2ad953b3 - React-jsi: f46d09ee5079a4f3b637d30d0e59b8ea6470632c - React-jsiexecutor: e73579560957aa3ca9dc02ab90e163454279d48c - React-jsinspector: e8ba20dde269c7c1d45784b858fa1cf4383f0bbb - React-jsitracing: 233d1a798fe0ff33b8e630b8f00f62c4a8115fbc - React-logger: 7e7403a2b14c97f847d90763af76b84b152b6fce - React-Mapbuffer: 11029dcd47c5c9e057a4092ab9c2a8d10a496a33 - react-native-compressor: 2ae9013718fb351264fcfcdf232eccbbf3d280a2 - react-native-document-picker: c4f197741c327270453aa9840932098e0064fd52 - react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 - react-native-image-picker: fb0c2b3adc3eff6caa3cd6a507a34b9dcc9238dd - react-native-mmkv: 8c9a677e64a1ac89b0c6cf240feea528318b3074 - react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371 + React-graphics: d0b9a0a174fb86bfed50bf4fb7c835183a546ab5 + React-hermes: 06e8316213d56ab914afb9a829763123fcfacf22 + React-ImageManager: 821a1182139cc986598868d0e9a00b3a021feddb + React-jserrorhandler: 1dd2a75b24dd9a318ee88fa6792e98524879af24 + React-jsi: e381545475da5ea77777e7b5513031a434ced04b + React-jsiexecutor: ce91dde1a61efd519a5ff7ac0f64b61a14217072 + React-jsinspector: 627ac44b1d090fc6a8039b1df723677bc7d86fe4 + React-jsitracing: dd0e541a34027b3ab668ad94cf268482ad6f82fb + React-logger: 6070f362a1657bb53335eb1fc903d3f49fd79842 + React-Mapbuffer: 2c95cbabc3d75a17747452381e998c35208ea3ee + react-native-compressor: 837b2774cb6e6a026862d90a783586ca317c29c3 + react-native-document-picker: 451699da81cba8b40b596b8076019a4deb86f46e + react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba + react-native-image-picker: f1006d8935a3bc0baf8157faaa7857c76a77c8bb + react-native-mmkv: f8155c2efbe795cb0c7586d00ff484b1c9388af0 + react-native-safe-area-context: b72c4611af2e86d80a59ac76279043d8f75f454c + react-native-webview: b836f1f162b87b5b8351611b5d5299f2b699360a React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec - React-NativeModulesApple: df46ff3e3de5b842b30b4ca8a6caae6d7c8ab09f + React-NativeModulesApple: 61b07ab32af3ea4910ba553932c0a779e853c082 React-perflogger: 3d31e0d1e8ad891e43a09ac70b7b17a79773003a React-RCTActionSheet: c4a3a134f3434c9d7b0c1054f1a8cfed30c7a093 - React-RCTAnimation: 0e5d15320eeece667fcceb6c785acf9a184e9da1 - React-RCTAppDelegate: c4f6c0700b8950a8b18c2e004996eec1807d430a - React-RCTBlob: c46aaaee693d371a1c7cae2a8c8ee2aa7fbc1adb - React-RCTFabric: 0dbf28ce96c7f2843483e32a725a5b5793584ff3 - React-RCTImage: a04dba5fcc823244f5822192c130ecf09623a57f - React-RCTLinking: 533bf13c745fcb2a0c14e0e49fd149586a7f0d14 - React-RCTNetwork: a29e371e0d363d7b4c10ab907bc4d6ae610541e9 - React-RCTSettings: 127813224780861d0d30ecda17a40d1dfebe7d73 - React-RCTText: 8a823f245ecf82edb7569646e3c4d8041deb800a - React-RCTVibration: 46b5fae74e63f240f22f39de16ad6433da3b65d9 - React-rendererdebug: 4653f8da6ab1d7b01af796bdf8ca47a927539e39 + React-RCTAnimation: dab04683056694845eb7a9e283f4c63eec7fa633 + React-RCTAppDelegate: 1785d42459138c45175b2fa18e86cd2aee829a93 + React-RCTBlob: a0a8f6bfd8926bff0e2814ec3f8cd5514f2db243 + React-RCTFabric: f69d856b74b6d385c4cf4bd128c330161ce18306 + React-RCTImage: 51db983bcc5075fa9bf3e094e5c6c1f5b5575472 + React-RCTLinking: 3430cd1023a5ac86a96ed6d4fbf7a8ed7b2e44d5 + React-RCTNetwork: 52198f8a8c823639dcc8f6725ca5b360d66ea1a0 + React-RCTSettings: c127440c2c538128f92fb45524e976e25cb69bd6 + React-RCTText: 640b2d0bfb51d88d8a76c6a1a7ea1f94667bf231 + React-RCTVibration: bd20c8156b649cd745c70db3341c409ae3b42821 + React-rendererdebug: 16394ffe0d852967123b3b76a630233b90ec8e63 React-rncore: 4f1e645acb5107bd4b4cf29eff17b04a7cd422f3 - React-RuntimeApple: 013b606e743efb5ee14ef03c32379b78bfe74354 - React-RuntimeCore: 7205be45a25713b5418bbf2db91ddfcca0761d8b + React-RuntimeApple: 97d0a5c655467c57b88076434427ec32413e7802 + React-RuntimeCore: a55443ddb73e6666b441963d8951a16ba5cfc223 React-runtimeexecutor: a278d4249921853d4a3f24e4d6e0ff30688f3c16 - React-RuntimeHermes: 44c628568ce8feedc3acfbd48fc07b7f0f6d2731 - React-runtimescheduler: e2152ed146b6a35c07386fc2ac4827b27e6aad12 - React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d - ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768 - RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37 - RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 8dbcccada4a7e702e7dec9338c251b1cf393c960 - RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 - RNReanimated: f4ff116e33e0afc3d127f70efe928847c7c66355 - RNScreens: 024c5cb8569dea6667b5c73e6c119beb83f686f0 - RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c - RNSVG: 7ff26379b2d1871b8571e6f9bc9630de6baf9bdf + React-RuntimeHermes: 6273f0755fef304453fc3c356b25abf17e915b83 + React-runtimescheduler: 87b14969bb0b10538014fb8407d472f9904bc8cd + React-utils: 67574b07bff4429fd6c4d43a7fad8254d814ee20 + ReactCommon: 64c64f4ae1f2debe3fab1800e00cb8466a4477b7 + RNCClipboard: 4598dae0fe33e2aa130d9d213e2007be78310266 + RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251 + RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 + RNGestureHandler: 77ca8899a0bd9a9d948e74174ee401bbffa5e524 + RNReactNativeHapticFeedback: a6fb5b7a981683bf58af43e3fb827d4b7ed87f83 + RNReanimated: fe62058b0e1ecb46e252d63d27580f36cd6d9eb2 + RNScreens: df14a2a11e7afb57e6f35f8964d206271f4dae44 + RNShare: 694e19d7f74ac4c04de3a8af0649e9ccc03bd8b1 + RNSVG: 3421710ac15f4f2dc47e5c122f2c2e4282116830 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - Yoga: b9a182ab00cf25926e7f79657d08c5d23c2d03b0 + Yoga: 348f8b538c3ed4423eb58a8e5730feec50bce372 -PODFILE CHECKSUM: ccd372319eaeea775147eb2843bb9cf9d1b4cd7a +PODFILE CHECKSUM: eed3e49f72b6465b4551e72df9de6b83230c91ca -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/react-native/package-lock.json b/react-native/package-lock.json index 4f1f7a6f..bb503377 100644 --- a/react-native/package-lock.json +++ b/react-native/package-lock.json @@ -42,6 +42,7 @@ "react-native-share": "^10.2.1", "react-native-svg": "^15.4.0", "react-native-toast-message": "^2.2.1", + "react-native-webview": "^13.16.0", "react-syntax-highlighter": "^15.5.0" }, "devDependencies": { @@ -13220,6 +13221,32 @@ "react-native": "*" } }, + "node_modules/react-native-webview": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz", + "integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-webview/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-native-windows": { "version": "0.74.21", "resolved": "https://registry.npmjs.org/react-native-windows/-/react-native-windows-0.74.21.tgz", @@ -25839,6 +25866,22 @@ "whatwg-url-without-unicode": "8.0.0-3" } }, + "react-native-webview": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz", + "integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==", + "requires": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + } + }, "react-native-windows": { "version": "0.74.21", "resolved": "https://registry.npmjs.org/react-native-windows/-/react-native-windows-0.74.21.tgz", diff --git a/react-native/package.json b/react-native/package.json index 28c222a1..86c6b07d 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -46,6 +46,7 @@ "react-native-share": "^10.2.1", "react-native-svg": "^15.4.0", "react-native-toast-message": "^2.2.1", + "react-native-webview": "^13.16.0", "react-syntax-highlighter": "^15.5.0" }, "devDependencies": { diff --git a/react-native/src/assets/download.png b/react-native/src/assets/download.png new file mode 100644 index 00000000..608252e0 Binary files /dev/null and b/react-native/src/assets/download.png differ diff --git a/react-native/src/chat/ChatScreen.tsx b/react-native/src/chat/ChatScreen.tsx index 2bb8868e..a5720509 100644 --- a/react-native/src/chat/ChatScreen.tsx +++ b/react-native/src/chat/ChatScreen.tsx @@ -410,6 +410,10 @@ function ChatScreen(): React.JSX.Element { }); }, 100); } + // Notify Mermaid renderers to refresh after streaming completes + setTimeout(() => { + sendEventRef.current('refreshMermaid'); + }, 150); setChatStatus(ChatStatus.Init); } }, [chatStatus]); diff --git a/react-native/src/chat/component/markdown/CustomCodeHighlighter.tsx b/react-native/src/chat/component/markdown/CustomCodeHighlighter.tsx index 085d6527..8247c6be 100644 --- a/react-native/src/chat/component/markdown/CustomCodeHighlighter.tsx +++ b/react-native/src/chat/component/markdown/CustomCodeHighlighter.tsx @@ -159,7 +159,8 @@ export const CustomCodeHighlighter: FunctionComponent = ({ const renderNode = useCallback( (nodes: rendererNode[]): ReactNode => { // Calculate margin bottom value once - const marginBottomValue = -nodes.length * (isMac ? 3 : 2.75); + const scale = rest.language === 'mermaid' ? 1.75 : isMac ? 3 : 2.75; + const marginBottomValue = -nodes.length * scale; // Optimization for streaming content - only process new nodes if (nodes.length >= prevNodesLength.current) { @@ -200,7 +201,7 @@ export const CustomCodeHighlighter: FunctionComponent = ({ ); }, - [processNode] + [processNode, rest.language] ); const renderAndroidNode = useCallback( diff --git a/react-native/src/chat/component/markdown/CustomMarkdownRenderer.tsx b/react-native/src/chat/component/markdown/CustomMarkdownRenderer.tsx index 47b90ab6..75c477ab 100644 --- a/react-native/src/chat/component/markdown/CustomMarkdownRenderer.tsx +++ b/react-native/src/chat/component/markdown/CustomMarkdownRenderer.tsx @@ -40,6 +40,7 @@ import Disc from '@jsamr/counter-style/lib/es/presets/disc'; import MathView from 'react-native-math-view'; import { isAndroid } from '../../../utils/PlatformUtils.ts'; import { ColorScheme } from '../../../theme'; +import MermaidCodeRenderer from './MermaidCodeRenderer'; const CustomCodeHighlighter = lazy(() => import('./CustomCodeHighlighter')); let mathViewIndex = 0; @@ -107,11 +108,25 @@ const MemoizedCodeHighlighter = React.memo( isDark: boolean; }) => { const styles = createCustomStyles(colors); + // Use useRef to always capture the latest text value + const textRef = React.useRef(text); + textRef.current = text; + const handleCopy = useCallback(() => { - Clipboard.setString(text); - }, [text]); + Clipboard.setString(textRef.current); + }, []); const hljsStyle = isDark ? vs2015 : github; + if (language === 'mermaid') { + return ( + + ); + } return ( @@ -144,11 +159,17 @@ const MemoizedCodeHighlighter = React.memo( ); }, - (prevProps, nextProps) => - prevProps.text === nextProps.text && - prevProps.language === nextProps.language && - prevProps.colors === nextProps.colors && - prevProps.isDark === nextProps.isDark + (prevProps, nextProps) => { + if (prevProps.language === 'mermaid' || nextProps.language === 'mermaid') { + return false; + } + return ( + prevProps.text === nextProps.text && + prevProps.language === nextProps.language && + prevProps.colors === nextProps.colors && + prevProps.isDark === nextProps.isDark + ); + } ); export class CustomMarkdownRenderer @@ -281,9 +302,11 @@ export class CustomMarkdownRenderer _textStyle?: TextStyle ): ReactNode { if (text && text !== '') { + const componentKey = + language === 'mermaid' ? 'mermaid-code-block' : this.getKey(); return ( import('./CustomCodeHighlighter') +); + +interface MermaidCodeRendererProps { + text: string; + colors: ColorScheme; + isDark: boolean; + onCopy: () => void; +} + +interface MermaidCodeRendererRef { + updateContent: (newText: string) => void; +} + +interface MermaidRendererRef { + updateContent: (newCode: string) => void; +} + +const MermaidCodeRenderer = forwardRef< + MermaidCodeRendererRef, + MermaidCodeRendererProps +>(({ text, colors, isDark, onCopy }, ref) => { + const [showCode, setShowCode] = useState(false); + const [showNewMermaid, setShowNewMermaid] = useState(false); + const [currentText, setCurrentText] = useState(text); + const { event } = useAppContext(); + const mermaidRendererRef = useRef(null); + const styles = createStyles(colors); + const hljsStyle = isDark ? vs2015 : github; + + const updateContent = useCallback( + (newText: string) => { + setCurrentText(newText); + if (!showCode && mermaidRendererRef.current) { + mermaidRendererRef.current.updateContent(newText); + } + }, + [showCode] + ); + + useImperativeHandle( + ref, + () => ({ + updateContent, + }), + [updateContent] + ); + + useEffect(() => { + setCurrentText(text); + }, [text]); + + const setMermaidMode = () => { + setShowCode(false); + }; + + const setCodeMode = () => { + setShowCode(true); + }; + + // Listen for refresh event from ChatScreen and update key to remount WebView + useEffect(() => { + if (event?.event === 'refreshMermaid') { + setShowNewMermaid(true); + } + }, [event]); + + return ( + + + + + + + mermaid + + + + + code + + + + + + + + {showCode ? ( + Loading...}> + + {currentText} + + + ) : ( + + )} + + ); +}); + +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + borderRadius: 8, + overflow: 'hidden', + backgroundColor: colors.input, + marginVertical: 6, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: colors.borderLight, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + paddingVertical: 2, + paddingHorizontal: 4, + }, + leftSection: { + flexDirection: 'row', + alignItems: 'center', + }, + tabContainer: { + flexDirection: 'row', + backgroundColor: colors.input, + borderRadius: 6, + padding: 2, + }, + tabButton: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + marginHorizontal: 1, + }, + activeTab: { + backgroundColor: colors.text, + }, + tabText: { + fontSize: 14, + color: colors.text, + fontWeight: '500', + opacity: 0.6, + }, + activeTabText: { + color: colors.background, + opacity: 1, + }, + loading: { + padding: 12, + color: colors.text, + }, + codeText: { + fontSize: 14, + paddingVertical: 1.3, + fontFamily: Platform.OS === 'ios' ? 'Menlo-Regular' : 'monospace', + color: colors.text, + }, + mermaidRenderer: { + marginVertical: 0, + minHeight: 100, + }, + }); + +export default MermaidCodeRenderer; diff --git a/react-native/src/chat/component/markdown/MermaidFullScreenViewer.tsx b/react-native/src/chat/component/markdown/MermaidFullScreenViewer.tsx new file mode 100644 index 00000000..37061fcc --- /dev/null +++ b/react-native/src/chat/component/markdown/MermaidFullScreenViewer.tsx @@ -0,0 +1,689 @@ +import React, { + useMemo, + useState, + useRef, + useCallback, + useEffect, +} from 'react'; +import { + Modal, + StyleSheet, + Text, + TouchableOpacity, + View, + Alert, + StatusBar, + Platform, + Image, + Dimensions, +} from 'react-native'; +import { WebView, WebViewMessageEvent } from 'react-native-webview'; +import { + PanGestureHandler, + PinchGestureHandler, + PanGestureHandlerGestureEvent, + PinchGestureHandlerGestureEvent, +} from 'react-native-gesture-handler'; +import Animated, { + useAnimatedGestureHandler, + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated'; +import RNFS from 'react-native-fs'; +import Share from 'react-native-share'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { useTheme } from '../../../theme'; +import { isMac } from '../../../App.tsx'; + +interface MermaidFullScreenViewerProps { + visible: boolean; + onClose: () => void; + code: string; +} + +const MermaidFullScreenViewer: React.FC = ({ + visible, + onClose, + code, +}) => { + const { colors, isDark } = useTheme(); + const webViewRef = useRef(null); + const [hasError, setHasError] = useState(false); + const [copied, setCopied] = useState(false); + const [screenData, setScreenData] = useState(Dimensions.get('window')); + const [isLandscape, setIsLandscape] = useState( + isMac ? false : screenData.width > screenData.height + ); + + // Animation values for pan and zoom + const translateX = useSharedValue(0); + const translateY = useSharedValue(0); + const scale = useSharedValue(1); + const baseScale = useSharedValue(1); + const savedTranslateX = useSharedValue(0); + const savedTranslateY = useSharedValue(0); + + // Listen for orientation changes + useEffect(() => { + const subscription = Dimensions.addEventListener('change', ({ window }) => { + setScreenData(window); + setIsLandscape(isMac ? true : window.width > window.height); + }); + + return () => subscription?.remove(); + }, []); + + // Reset copied state after 2 seconds + useEffect(() => { + if (copied) { + const timer = setTimeout(() => { + setCopied(false); + }, 2000); + + return () => clearTimeout(timer); + } + }, [copied]); + + // Reset transforms when modal opens + useEffect(() => { + if (visible) { + translateX.value = 0; + translateY.value = 0; + scale.value = 1; + baseScale.value = 1; + savedTranslateX.value = 0; + savedTranslateY.value = 0; + setHasError(false); + } + }, [ + visible, + translateX, + translateY, + scale, + baseScale, + savedTranslateX, + savedTranslateY, + ]); + + const pinchHandler = + useAnimatedGestureHandler({ + onStart: () => { + baseScale.value = scale.value; + }, + onActive: event => { + scale.value = Math.max(0.5, Math.min(baseScale.value * event.scale, 5)); + }, + onEnd: () => { + if (scale.value < 1) { + scale.value = withSpring(1); + translateX.value = withSpring(0); + translateY.value = withSpring(0); + } + }, + }); + + const panHandler = useAnimatedGestureHandler({ + onStart: () => { + savedTranslateX.value = translateX.value; + savedTranslateY.value = translateY.value; + }, + onActive: event => { + translateX.value = savedTranslateX.value + event.translationX; + translateY.value = savedTranslateY.value + event.translationY; + }, + onEnd: () => { + // Only spring back to center if scale is 1 or less + if (scale.value <= 1) { + translateX.value = withSpring(0); + translateY.value = withSpring(0); + savedTranslateX.value = 0; + savedTranslateY.value = 0; + } else { + // Save the final position for next pan + savedTranslateX.value = translateX.value; + savedTranslateY.value = translateY.value; + } + }, + }); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { translateX: translateX.value }, + { translateY: translateY.value }, + { scale: scale.value }, + ], + }; + }); + + const captureImageJS = useCallback(() => { + return ` + (function() { + try { + const svg = document.querySelector('#mermaid-display svg'); + if (!svg) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'capture_error', + message: 'No SVG found' + })); + return; + } + + // Clone the SVG to avoid modifying the original + const svgClone = svg.cloneNode(true); + + // Get the actual SVG dimensions from viewBox or computed values + let svgWidth, svgHeight; + const viewBox = svg.getAttribute('viewBox'); + + if (viewBox) { + // Use viewBox dimensions if available + const [x, y, width, height] = viewBox.split(' ').map(Number); + svgWidth = width; + svgHeight = height; + svgClone.setAttribute('width', width); + svgClone.setAttribute('height', height); + } else { + // Fallback to intrinsic dimensions + svgWidth = svg.scrollWidth || svg.clientWidth || parseFloat(svg.getAttribute('width')) || 800; + svgHeight = svg.scrollHeight || svg.clientHeight || parseFloat(svg.getAttribute('height')) || 600; + svgClone.setAttribute('width', svgWidth); + svgClone.setAttribute('height', svgHeight); + } + + // Ensure the SVG has proper styling for export + svgClone.style.background = '${isDark ? '#1a1a1a' : '#ffffff'}'; + svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + svgClone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + + // Serialize the complete SVG + const svgData = new XMLSerializer().serializeToString(svgClone); + + // Create canvas with actual SVG dimensions at higher resolution + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const scale = 2; // Higher resolution multiplier + + canvas.width = svgWidth * scale; + canvas.height = svgHeight * scale; + + const img = new Image(); + img.onload = function() { + try { + // Scale context for higher resolution + ctx.scale(scale, scale); + + // Fill background + ctx.fillStyle = '${isDark ? '#1a1a1a' : '#ffffff'}'; + ctx.fillRect(0, 0, svgWidth, svgHeight); + + // Draw the complete SVG + ctx.drawImage(img, 0, 0, svgWidth, svgHeight); + + const dataURL = canvas.toDataURL('image/png', 0.95); + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'capture_success', + data: dataURL + })); + } catch (error) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'capture_error', + message: 'Canvas operation failed: ' + error.message + })); + } + }; + + img.onerror = function(error) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'capture_error', + message: 'Failed to load image: ' + error + })); + }; + + // Use Data URL instead of Blob URL to avoid security issues + const svgDataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))); + img.src = svgDataUrl; + + } catch (error) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'capture_error', + message: error.message + })); + } + })(); + true; + `; + }, [isDark]); + + const copyImage = useCallback(async () => { + if (!webViewRef.current) { + return; + } + + try { + const copyJS = captureImageJS().replace( + "'capture_success'", + "'copy_success'" + ); + webViewRef.current.injectJavaScript(copyJS); + } catch (error) { + Alert.alert('Error', 'Failed to copy image'); + } + }, [captureImageJS]); + + const saveImage = useCallback(async () => { + if (!webViewRef.current) { + return; + } + + try { + const saveJS = captureImageJS().replace( + "'capture_success'", + "'save_success'" + ); + webViewRef.current.injectJavaScript(saveJS); + } catch (error) { + Alert.alert('Error', 'Failed to capture image'); + } + }, [captureImageJS]); + + // Memoize the copy icon source to prevent flickering + const copyIconSource = useMemo(() => { + return copied + ? isDark + ? require('../../../assets/done_dark.png') + : require('../../../assets/done.png') + : require('../../../assets/copy_grey.png'); + }, [copied, isDark]); + + const handleWebViewMessage = useCallback( + async (event: WebViewMessageEvent) => { + try { + const message = JSON.parse(event.nativeEvent.data); + + if (message.type === 'copy_success') { + const base64Data = message.data.replace( + /^data:image\/png;base64,/, + '' + ); + // Copy image to clipboard (mac only) + try { + Clipboard.setImage(base64Data); + setCopied(true); + } catch (clipboardError) { + console.log( + '[MermaidFullScreenViewer] Clipboard error:', + clipboardError + ); + Alert.alert('Error', 'Failed to copy image to clipboard'); + } + } else if (message.type === 'save_success') { + const base64Data = message.data.replace( + /^data:image\/png;base64,/, + '' + ); + const fileName = `mermaid_diagram_${Date.now()}.png`; + let filePath = `${RNFS.DocumentDirectoryPath}/${fileName}`; + + await RNFS.writeFile(filePath, base64Data, 'base64'); + if (Platform.OS === 'android') { + filePath = 'file://' + filePath; + } + const shareOptions = { + url: filePath, + type: 'image/png', + title: 'Save Mermaid Diagram', + }; + await Share.open(shareOptions); + } else if (message.type === 'capture_error') { + Alert.alert('Error', `Failed to capture image: ${message.message}`); + } else if (message.type === 'rendered') { + setHasError(!message.success); + } + } catch (error) { + console.log('[MermaidFullScreenViewer] Message parse error:', error); + } + }, + [] + ); + + const htmlContent = useMemo(() => { + return ` + + + + + + + + + +
+
Invalid Mermaid syntax
+ + + +`; + }, [code, colors.text, isDark, isLandscape]); + + const styles = StyleSheet.create({ + modal: { + flex: 1, + backgroundColor: isDark ? '#000000' : '#ffffff', + }, + closeButtonTopLeft: { + position: 'absolute', + top: + Platform.OS === 'ios' + ? isLandscape + ? 40 + : 60 + : (StatusBar.currentHeight || 20) + (isLandscape ? 10 : 20), + left: isLandscape ? 40 : 20, + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: 'rgba(50, 50, 50, 0.8)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 1000, + }, + closeButtonX: { + fontSize: 20, + fontWeight: '400', + marginBottom: -2, + color: '#ffffff', + lineHeight: 20, + }, + copyButtonBottomRight: { + position: 'absolute', + bottom: isLandscape ? 80 : 100, + right: isLandscape ? 40 : 20, + width: 48, + height: 48, + borderRadius: 12, + backgroundColor: 'rgba(50, 50, 50, 0.8)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 1000, + }, + saveButtonBottomRight: { + position: 'absolute', + bottom: isLandscape ? 20 : 40, + right: isLandscape ? 40 : 20, + width: 48, + height: 48, + borderRadius: 12, + backgroundColor: 'rgba(50, 50, 50, 0.8)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 1000, + }, + saveIcon: { + width: 24, + height: 24, + tintColor: '#ffffff', + }, + copyIcon: { + width: 18, + height: 18, + tintColor: '#ffffff', + }, + webViewContainer: { + flex: 1, + backgroundColor: isDark ? '#1a1a1a' : '#ffffff', + }, + webView: { + flex: 1, + backgroundColor: 'transparent', + }, + loadingContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: isDark ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,1)', + zIndex: 998, + }, + loadingText: { + marginTop: 10, + fontSize: 16, + color: colors.text, + }, + }); + + if (!visible) { + return null; + } + + return ( + + + + + {/* Close button in top-left */} + + × + + + {/* WebView with gesture handling */} + + + + + + + + + + + {/* Copy button in bottom-right (above save button) */} + {isMac && ( + + + + )} + + {/* Save button in bottom-right */} + + + + + {/* Error overlay */} + {hasError && ( + + {'Invalid Mermaid syntax'} + + )} + + + ); +}; + +export default MermaidFullScreenViewer; diff --git a/react-native/src/chat/component/markdown/MermaidRenderer.tsx b/react-native/src/chat/component/markdown/MermaidRenderer.tsx new file mode 100644 index 00000000..24463f63 --- /dev/null +++ b/react-native/src/chat/component/markdown/MermaidRenderer.tsx @@ -0,0 +1,422 @@ +import React, { + useMemo, + useState, + useRef, + useCallback, + forwardRef, + useImperativeHandle, + useEffect, +} from 'react'; +import { WebView, WebViewMessageEvent } from 'react-native-webview'; +import { + ViewStyle, + TouchableOpacity, + View, + Text, + StyleSheet, +} from 'react-native'; +import { ColorScheme, useTheme } from '../../../theme'; +import MermaidFullScreenViewer from './MermaidFullScreenViewer'; + +interface MermaidRendererProps { + code: string; + style?: ViewStyle; +} + +interface MermaidRendererRef { + updateContent: (newCode: string) => void; +} + +/** + * Validates and filters Gantt chart code lines. + * For Gantt charts, checks the last line: + * - If the last line doesn't start with a digit, removes it + * - If the last line starts with a digit, must contain both colon and comma, and end with a digit + * - Otherwise returns original code + */ +const validateGanttChartCode = (code: string): string => { + const isGantt = code.startsWith('gantt'); + if (!isGantt) { + return code; + } + + const lines = code.split('\n'); + if (lines.length < 2) { + return code; + } + + const lastLine = lines[lines.length - 1]; + const trimmedLast = lastLine.trim(); + + // If last line is empty, keep it + if (trimmedLast === '') { + return code; + } + + // Check if last line starts with a digit + if (/^\d/.test(trimmedLast)) { + // Must contain both colon and comma, and end with a digit + const hasColon = trimmedLast.includes(':'); + const hasComma = trimmedLast.includes(','); + const endsWithDigit = /\d$/.test(trimmedLast); + + if (hasColon && hasComma && endsWithDigit) { + return code; // Valid, return original code + } else { + // Invalid, remove last line + return lines.slice(0, -1).join('\n'); + } + } else { + // Last line doesn't start with a digit, remove it + return lines.slice(0, -1).join('\n'); + } +}; + +const MermaidRenderer = forwardRef( + ({ code, style }, ref) => { + const validatedCode = useMemo(() => validateGanttChartCode(code), [code]); + const [currentCode, setCurrentCode] = useState(validatedCode); + const [showFullScreen, setShowFullScreen] = useState(false); + const [hasError, setHasError] = useState(false); + const webViewRef = useRef(null); + const initialCodeRef = useRef(validatedCode); + const { isDark, colors } = useTheme(); + const styles = createStyles(colors); + + const updateContent = useCallback( + (newCode: string) => { + const validatedNewCode = validateGanttChartCode(newCode); + if (validatedNewCode === currentCode) { + return; + } + if (webViewRef.current) { + const escapedCode = validatedNewCode + .replace(/`/g, '\\`') + .replace(/\$/g, '\\$'); + const jsCode = ` + (function() { + try { + const container = document.getElementById('mermaid-container'); + const displayContainer = document.getElementById('mermaid-display'); + if (!container || !displayContainer) return; + + const newCodeContent = \`${escapedCode}\`; + + // Store the new code in a hidden container + container.textContent = newCodeContent; + container.style.display = 'none'; + + // Try to parse and validate + window.mermaid.parse(newCodeContent, { suppressErrors: true }) + .then((result) => { + if (result) { + // Valid syntax, try to render + return window.mermaid.render('mermaid-graph', newCodeContent); + } else { + // Don't update display, schedule error notification after 1 second + if (window.notifyRN) { + window.notifyRN('update_rendered', { success: false }, 1000); + } + // Terminate the promise chain + return Promise.reject(new Error('Parse failed')); + } + }) + .then((result) => { + // Rendering successful, update display + if (result && result.svg) { + displayContainer.innerHTML = result.svg; + window.lastValidCode = newCodeContent; + if (window.notifyRN) { + // Cancel any pending error notifications and immediately notify success + window.notifyRN('update_rendered', { success: true }, 0); + } + } + }) + .catch((error) => { + // Schedule error notification after 1 second + if (window.notifyRN) { + window.notifyRN('update_rendered', { success: false, error: error.message }, 1000); + } + }); + } catch (error) { + // Don't update display, schedule error notification after 1 second + if (window.notifyRN) { + window.notifyRN('update_rendered', { success: false, error: error.message }, 1000); + } + } + })(); + true; + `; + + webViewRef.current.injectJavaScript(jsCode); + } + + setCurrentCode(validatedNewCode); + }, + [currentCode] + ); + + useEffect(() => { + if (code !== currentCode) { + updateContent(code); + } + }, [code, currentCode, updateContent]); + + useImperativeHandle( + ref, + () => ({ + updateContent, + }), + [updateContent] + ); + + const htmlContent = useMemo(() => { + return ` + + + + + + + + + +
+ + + +`; + }, [isDark]); + + const handleMessage = useCallback((event: WebViewMessageEvent) => { + try { + const message = JSON.parse(event.nativeEvent.data); + + // Handle console logs from WebView + if (message.type === 'console_log') { + console.log('[WebView]', message.message); + return; + } + + if (message.type === 'rendered' || message.type === 'update_rendered') { + setHasError(!message.success); + } + } catch (error) { + console.log('[WebView] Raw message:', event.nativeEvent.data); + } + }, []); + + return ( + <> + setShowFullScreen(true)} + activeOpacity={0.8} + style={styles.container}> + + + {hasError && ( + + {'Invalid Mermaid syntax'} + + )} + + + setShowFullScreen(false)} + code={currentCode} + /> + + ); + } +); + +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + position: 'relative' as const, + }, + webView: { + height: 380, + backgroundColor: 'transparent' as const, + }, + loadingContainer: { + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center' as const, + alignItems: 'center' as const, + backgroundColor: colors.input, + }, + loadingText: { + marginTop: 10, + fontSize: 14, + color: colors.text, + }, + }); + +export default MermaidRenderer; diff --git a/react-native/src/settings/SettingsScreen.tsx b/react-native/src/settings/SettingsScreen.tsx index 5afc0729..c563991e 100644 --- a/react-native/src/settings/SettingsScreen.tsx +++ b/react-native/src/settings/SettingsScreen.tsx @@ -135,6 +135,7 @@ function SettingsScreen(): React.JSX.Element { const { sendEvent } = useAppContext(); const sendEventRef = useRef(sendEvent); const openAICompatConfigsRef = useRef(openAICompatConfigs); + const bedrockConfigModeRef = useRef(bedrockConfigMode); // Handle OpenAI Compatible configs change const handleOpenAICompatConfigsChange = useCallback( @@ -166,7 +167,7 @@ function SettingsScreen(): React.JSX.Element { }; if (shouldFetchBedrock) { bedrockResponse = - bedrockConfigMode === 'bedrock' + bedrockConfigModeRef.current === 'bedrock' ? await requestAllModelsByBedrockAPI() : await requestAllModels(); addBedrockPrefixToDeepseekModels(bedrockResponse.textModel); @@ -250,7 +251,7 @@ function SettingsScreen(): React.JSX.Element { }); } }, - [bedrockConfigMode, textModels, imageModels] + [textModels, imageModels] ); const fetchAndSetModelNamesRef = useRef(fetchAndSetModelNames); @@ -331,6 +332,7 @@ function SettingsScreen(): React.JSX.Element { }, [openAICompatConfigs]); useEffect(() => { + bedrockConfigModeRef.current = bedrockConfigMode; if (bedrockConfigMode === getBedrockConfigMode()) { return; } diff --git a/react-native/src/storage/Constants.ts b/react-native/src/storage/Constants.ts index 85cbaf77..282ede55 100644 --- a/react-native/src/storage/Constants.ts +++ b/react-native/src/storage/Constants.ts @@ -67,7 +67,9 @@ export const DeepSeekModels = [ export const BedrockThinkingModels = [ 'Claude 3.7 Sonnet', 'Claude Sonnet 4', + 'Claude Sonnet 4.5', 'Claude Opus 4', + 'Claude Opus 4.1', ]; export const BedrockVoiceModels = ['Nova Sonic'];