From bf3e88bc52592b1976cfc289f1cb0e4e955e0b66 Mon Sep 17 00:00:00 2001 From: Muhammad Ali Date: Wed, 5 Feb 2025 16:41:56 +0500 Subject: [PATCH] Feat/chat UI improvements (#201) * feat: update menu component (#189) * Feat/chat menu generation settings (#200) * feat: added chat generation settings, rename, remove, duplicate functionalities into chat menu * chore: updated translations * chore: change actions/upload-artifact@v3 => v4 * fix: updated submenu styles & code refactoring --- .github/workflows/ci.yml | 2 +- .svgrrc | 7 + App.tsx | 36 +- __mocks__/external/react-native-svg.js | 1 + __mocks__/stores/chatSessionStore.ts | 4 + ios/PocketPal.xcodeproj/project.pbxproj | 29 ++ jest.config.js | 1 + jest/fixtures/models.ts | 2 + metro.config.js | 8 + package.json | 3 +- src/assets/icons/clock-fast-forward.svg | 3 + src/assets/icons/close.svg | 3 + src/assets/icons/copy.svg | 10 + src/assets/icons/dots-vertical.svg | 5 + src/assets/icons/duplicate.svg | 10 + src/assets/icons/edit-box.svg | 11 + src/assets/icons/edit.svg | 3 + src/assets/icons/grid.svg | 6 + src/assets/icons/index.ts | 15 + src/assets/icons/menu.svg | 3 + src/assets/icons/pencil-line.svg | 3 + src/assets/icons/refresh.svg | 3 + src/assets/icons/reverse-left.svg | 3 + src/assets/icons/settings.svg | 11 + src/assets/icons/share.svg | 3 + src/assets/icons/trash.svg | 3 + .../ChatGenerationSettingsSheet.tsx | 146 ++++++++ .../ChatGenerationSettingsSheet.test.tsx | 238 ++++++++++++++ .../ChatGenerationSettingsSheet/index.ts | 1 + .../ChatGenerationSettingsSheet/styles.ts | 11 + src/components/ChatHeader/ChatHeader.tsx | 47 +++ .../ChatHeader/__tests__/ChatHeader.test.tsx | 75 +++++ src/components/ChatHeader/index.ts | 1 + src/components/ChatHeader/styles.ts | 44 +++ .../ChatHeaderTitle/ChatHeaderTitle.tsx | 28 ++ .../__tests__/ChatHeaderTitle.test.tsx | 69 ++++ src/components/ChatHeaderTitle/index.ts | 1 + src/components/ChatHeaderTitle/styles.ts | 7 + src/components/ChatView/ChatView.tsx | 29 +- src/components/HeaderLeft/HeaderLeft.tsx | 21 ++ src/components/HeaderLeft/index.ts | 1 + src/components/HeaderLeft/styles.ts | 11 + src/components/HeaderRight/HeaderRight.tsx | 199 +++++++++-- .../__tests__/HeaderRight.test.tsx | 154 ++++++++- src/components/HeaderRight/styles.ts | 6 + src/components/Menu/Menu.tsx | 3 + src/components/Menu/MenuItem/MenuItem.tsx | 29 +- src/components/Menu/MenuItem/styles.ts | 19 +- src/components/Menu/SubMenu/SubMenu.tsx | 21 +- .../Menu/SubMenu/__tests__/SubMenu.test.tsx | 15 +- src/components/Menu/SubMenu/styles.ts | 7 +- src/components/Menu/styles.ts | 11 +- .../ModelSettingsSheet/ModelSettingsSheet.tsx | 109 ++++++ .../__tests__/ModelSettingsSheet.test.tsx | 187 +++++++++++ src/components/ModelSettingsSheet/index.ts | 1 + src/components/ModelSettingsSheet/styles.ts | 11 + .../ModelsHeaderRight/ModelsHeaderRight.tsx | 12 +- src/components/ModelsHeaderRight/styles.ts | 3 - src/components/RenameModal/RenameModal.tsx | 78 +++++ src/components/RenameModal/index.ts | 1 + src/components/RenameModal/styles.ts | 83 +++++ src/components/Sheet/Actions.tsx | 28 ++ .../Sheet/BottomSheetAwareScrollview.tsx | 34 ++ src/components/Sheet/CustomBackdrop.tsx | 45 +++ src/components/Sheet/Sheet.tsx | 121 +++++++ src/components/Sheet/index.ts | 1 + src/components/Sheet/styles.ts | 18 + .../SidebarContent/SidebarContent.tsx | 125 +++---- src/components/SidebarContent/styles.ts | 77 ----- src/components/index.ts | 4 + src/global.d.ts | 7 + src/hooks/useChatSession.ts | 36 +- .../CompletionSettings/CompletionSettings.tsx | 49 +-- .../__tests__/CompletionSettings.test.tsx | 35 +- .../ModelsScreen/CompletionSettings/styles.ts | 5 + .../HFModelSearch/HFModelSearch.tsx | 87 ++--- .../ModelsScreen/ModelCard/ModelCard.tsx | 170 +--------- src/screens/ModelsScreen/ModelCard/styles.ts | 3 + .../ModelSettings/ModelSettings.tsx | 212 +++++++----- .../__tests__/ModelSettings.test.tsx | 76 ++++- .../ModelsScreen/ModelSettings/styles.ts | 38 ++- src/screens/ModelsScreen/ModelsScreen.tsx | 24 +- src/store/ChatSessionStore.ts | 50 ++- src/store/ModelStore.ts | 74 ++--- src/store/__tests__/ChatSessionStore.test.ts | 16 +- src/store/__tests__/ModelStore.test.ts | 49 +-- src/store/defaultModels.ts | 32 +- src/utils/index.ts | 2 + src/utils/l10n.ts | 108 ++++++ src/utils/theme.ts | 11 +- src/utils/types.ts | 3 + yarn.lock | 311 +++++++++++++++++- 92 files changed, 2934 insertions(+), 784 deletions(-) create mode 100644 .svgrrc create mode 100644 __mocks__/external/react-native-svg.js create mode 100644 src/assets/icons/clock-fast-forward.svg create mode 100644 src/assets/icons/close.svg create mode 100644 src/assets/icons/copy.svg create mode 100644 src/assets/icons/dots-vertical.svg create mode 100644 src/assets/icons/duplicate.svg create mode 100644 src/assets/icons/edit-box.svg create mode 100644 src/assets/icons/edit.svg create mode 100644 src/assets/icons/grid.svg create mode 100644 src/assets/icons/index.ts create mode 100644 src/assets/icons/menu.svg create mode 100644 src/assets/icons/pencil-line.svg create mode 100644 src/assets/icons/refresh.svg create mode 100644 src/assets/icons/reverse-left.svg create mode 100644 src/assets/icons/settings.svg create mode 100644 src/assets/icons/share.svg create mode 100644 src/assets/icons/trash.svg create mode 100644 src/components/ChatGenerationSettingsSheet/ChatGenerationSettingsSheet.tsx create mode 100644 src/components/ChatGenerationSettingsSheet/__tests__/ChatGenerationSettingsSheet.test.tsx create mode 100644 src/components/ChatGenerationSettingsSheet/index.ts create mode 100644 src/components/ChatGenerationSettingsSheet/styles.ts create mode 100644 src/components/ChatHeader/ChatHeader.tsx create mode 100644 src/components/ChatHeader/__tests__/ChatHeader.test.tsx create mode 100644 src/components/ChatHeader/index.ts create mode 100644 src/components/ChatHeader/styles.ts create mode 100644 src/components/ChatHeaderTitle/ChatHeaderTitle.tsx create mode 100644 src/components/ChatHeaderTitle/__tests__/ChatHeaderTitle.test.tsx create mode 100644 src/components/ChatHeaderTitle/index.ts create mode 100644 src/components/ChatHeaderTitle/styles.ts create mode 100644 src/components/HeaderLeft/HeaderLeft.tsx create mode 100644 src/components/HeaderLeft/index.ts create mode 100644 src/components/HeaderLeft/styles.ts create mode 100644 src/components/ModelSettingsSheet/ModelSettingsSheet.tsx create mode 100644 src/components/ModelSettingsSheet/__tests__/ModelSettingsSheet.test.tsx create mode 100644 src/components/ModelSettingsSheet/index.ts create mode 100644 src/components/ModelSettingsSheet/styles.ts create mode 100644 src/components/RenameModal/RenameModal.tsx create mode 100644 src/components/RenameModal/index.ts create mode 100644 src/components/RenameModal/styles.ts create mode 100644 src/components/Sheet/Actions.tsx create mode 100644 src/components/Sheet/BottomSheetAwareScrollview.tsx create mode 100644 src/components/Sheet/CustomBackdrop.tsx create mode 100644 src/components/Sheet/Sheet.tsx create mode 100644 src/components/Sheet/index.ts create mode 100644 src/components/Sheet/styles.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88bfd70..9683fff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: run: yarn build:android # TODO: change to build:android:release - name: Upload Android APK - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: android-debug-apk # TODO: change to release-apk path: android/app/build/outputs/apk/debug/app-debug.apk diff --git a/.svgrrc b/.svgrrc new file mode 100644 index 0000000..0b3bf61 --- /dev/null +++ b/.svgrrc @@ -0,0 +1,7 @@ +{ + "replaceAttrValues": { + "#333333": "{props.fill}", + "#858585": "{props.fill}", + "#FF653F": "{props.fill}" + } +} diff --git a/App.tsx b/App.tsx index 9470722..94d4370 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import {Dimensions, StyleSheet} from 'react-native'; -import {reaction} from 'mobx'; import {observer} from 'mobx-react'; import {NavigationContainer} from '@react-navigation/native'; import {Provider as PaperProvider} from 'react-native-paper'; @@ -16,8 +15,13 @@ import {KeyboardProvider} from 'react-native-keyboard-controller'; import {useTheme} from './src/hooks'; import {Theme} from './src/utils/types'; -import {modelStore, chatSessionStore} from './src/store'; -import {HeaderRight, SidebarContent, ModelsHeaderRight} from './src/components'; + +import { + SidebarContent, + ModelsHeaderRight, + ChatHeader, + HeaderLeft, +} from './src/components'; import { ChatScreen, ModelsScreen, @@ -30,17 +34,6 @@ const Drawer = createDrawerNavigator(); const screenWidth = Dimensions.get('window').width; const App = observer(() => { - const [chatTitle, setChatTitle] = React.useState('Default Chat Page'); - - React.useEffect(() => { - const dispose = reaction( - () => modelStore.chatTitle, - newTitle => setChatTitle(newTitle), - {fireImmediately: true}, - ); - return () => dispose(); - }, []); - const theme = useTheme(); const styles = createStyles(theme); @@ -48,12 +41,13 @@ const App = observer(() => { - - + + , drawerStyle: { width: screenWidth * 0.8, }, @@ -68,11 +62,7 @@ const App = observer(() => { name="Chat" component={gestureHandlerRootHOC(ChatScreen)} options={{ - title: chatTitle, - headerRight: () => , - headerStyle: chatSessionStore.shouldShowHeaderDivider - ? styles.headerWithDivider - : styles.headerWithoutDivider, + header: () => , }} /> { /> - - + + diff --git a/__mocks__/external/react-native-svg.js b/__mocks__/external/react-native-svg.js new file mode 100644 index 0000000..87d5592 --- /dev/null +++ b/__mocks__/external/react-native-svg.js @@ -0,0 +1 @@ +module.exports = 'SvgMock'; diff --git a/__mocks__/stores/chatSessionStore.ts b/__mocks__/stores/chatSessionStore.ts index cabcd51..2f82770 100644 --- a/__mocks__/stores/chatSessionStore.ts +++ b/__mocks__/stores/chatSessionStore.ts @@ -1,9 +1,11 @@ import {sessionFixtures} from '../../jest/fixtures/chatSessions'; +import {defaultCompletionSettings} from '../../src/store/ChatSessionStore'; export const mockChatSessionStore = { sessions: sessionFixtures, //currentSessionMessages: [], activeSessionId: 'session-1', + newChatCompletionSettings: defaultCompletionSettings, loadSessionList: jest.fn().mockResolvedValue(undefined), deleteSession: jest.fn().mockResolvedValue(undefined), setActiveSession: jest.fn(), @@ -22,6 +24,8 @@ export const mockChatSessionStore = { enterEditMode: jest.fn(), removeMessagesFromId: jest.fn(), setIsGenerating: jest.fn(), + duplicateSession: jest.fn().mockResolvedValue(undefined), + setNewChatCompletionSettings: jest.fn(), }; Object.defineProperty(mockChatSessionStore, 'currentSessionMessages', { diff --git a/ios/PocketPal.xcodeproj/project.pbxproj b/ios/PocketPal.xcodeproj/project.pbxproj index 97918fa..5e7dc8d 100644 --- a/ios/PocketPal.xcodeproj/project.pbxproj +++ b/ios/PocketPal.xcodeproj/project.pbxproj @@ -8,16 +8,23 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* PocketPalTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* PocketPalTests.m */; }; + 0EA415939DF84BA0B5D83752 /* Inter-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 61091C7135514E2DB18E4698 /* Inter-ExtraBold.ttf */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1C5077855E738169D7580281 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 56D1BD968C32BD43D7C02874 /* PrivacyInfo.xcprivacy */; }; + 375E301319AD454A94795C74 /* Inter-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0A2C56A4517648B6980A4706 /* Inter-Regular.ttf */; }; + 41BA04C779A2451C9129DBE3 /* Inter-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 472C516838F74AE493E1D6F1 /* Inter-Medium.ttf */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 92B512A3E25B49FE875B90DD /* Inter-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F3EE19EF80E472DAE9D162E /* Inter-Light.ttf */; }; A8894CE22D0E02AF00FA6CAC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A8894CE12D0E02AF00FA6CAC /* GoogleService-Info.plist */; }; A88DF87F2D0B6F7400239E77 /* DeviceInfoModule.h in Sources */ = {isa = PBXBuildFile; fileRef = A88DF87D2D0B6F7400239E77 /* DeviceInfoModule.h */; }; A88DF8802D0B6F7400239E77 /* DeviceInfoModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A88DF87E2D0B6F7400239E77 /* DeviceInfoModule.m */; }; ACB048A1D4B12175111FED89 /* libPods-PocketPal-PocketPalTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 79A3B9EA89EFE88FA274FD1C /* libPods-PocketPal-PocketPalTests.a */; }; C3B4E5DF753F2A61870BEB67 /* libPods-PocketPal.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B2B1B50A483B5EBA13F9E622 /* libPods-PocketPal.a */; }; + CEFFF7B6DE024CE1B6E1A077 /* Inter-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6C88909D5B5643EBA861C5D0 /* Inter-SemiBold.ttf */; }; + E0EA396ED005462DB3EF953C /* Inter-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2E6C966DE9064B9CAC87A8D4 /* Inter-Bold.ttf */; }; + F436DB309CC848A09D1A78E8 /* Inter-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 99314BF13A4B46BCA459EB37 /* Inter-Thin.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,6 +41,7 @@ 00E356EE1AD99517003FC87E /* PocketPalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PocketPalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* PocketPalTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PocketPalTests.m; sourceTree = ""; }; + 0A2C56A4517648B6980A4706 /* Inter-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Regular.ttf"; path = "../src/assets/fonts/Inter-Regular.ttf"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* PocketPal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PocketPal.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = PocketPal/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = PocketPal/AppDelegate.mm; sourceTree = ""; }; @@ -41,11 +49,17 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = PocketPal/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = PocketPal/main.m; sourceTree = ""; }; 2502009346FB84D4290094D7 /* Pods-PocketPal.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PocketPal.release.xcconfig"; path = "Target Support Files/Pods-PocketPal/Pods-PocketPal.release.xcconfig"; sourceTree = ""; }; + 2E6C966DE9064B9CAC87A8D4 /* Inter-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Bold.ttf"; path = "../src/assets/fonts/Inter-Bold.ttf"; sourceTree = ""; }; 340A2151750B488EE0567E04 /* Pods-PocketPal.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PocketPal.debug.xcconfig"; path = "Target Support Files/Pods-PocketPal/Pods-PocketPal.debug.xcconfig"; sourceTree = ""; }; + 472C516838F74AE493E1D6F1 /* Inter-Medium.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Medium.ttf"; path = "../src/assets/fonts/Inter-Medium.ttf"; sourceTree = ""; }; 56D1BD968C32BD43D7C02874 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = PocketPal/PrivacyInfo.xcprivacy; sourceTree = ""; }; 5E01428B4E51E39CBC5D2180 /* Pods-PocketPal-PocketPalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PocketPal-PocketPalTests.debug.xcconfig"; path = "Target Support Files/Pods-PocketPal-PocketPalTests/Pods-PocketPal-PocketPalTests.debug.xcconfig"; sourceTree = ""; }; + 61091C7135514E2DB18E4698 /* Inter-ExtraBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-ExtraBold.ttf"; path = "../src/assets/fonts/Inter-ExtraBold.ttf"; sourceTree = ""; }; + 6C88909D5B5643EBA861C5D0 /* Inter-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-SemiBold.ttf"; path = "../src/assets/fonts/Inter-SemiBold.ttf"; sourceTree = ""; }; 79A3B9EA89EFE88FA274FD1C /* libPods-PocketPal-PocketPalTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PocketPal-PocketPalTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F3EE19EF80E472DAE9D162E /* Inter-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Light.ttf"; path = "../src/assets/fonts/Inter-Light.ttf"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = PocketPal/LaunchScreen.storyboard; sourceTree = ""; }; + 99314BF13A4B46BCA459EB37 /* Inter-Thin.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-Thin.ttf"; path = "../src/assets/fonts/Inter-Thin.ttf"; sourceTree = ""; }; A8894CE12D0E02AF00FA6CAC /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; A88DF87D2D0B6F7400239E77 /* DeviceInfoModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = DeviceInfoModule.h; path = PocketPal/DeviceInfoModule.h; sourceTree = ""; }; A88DF87E2D0B6F7400239E77 /* DeviceInfoModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = DeviceInfoModule.m; path = PocketPal/DeviceInfoModule.m; sourceTree = ""; }; @@ -121,6 +135,21 @@ name = Frameworks; sourceTree = ""; }; + 56ED328619B54356A6EB292D /* Resources */ = { + isa = PBXGroup; + children = ( + 2E6C966DE9064B9CAC87A8D4 /* Inter-Bold.ttf */, + 7F3EE19EF80E472DAE9D162E /* Inter-Light.ttf */, + 472C516838F74AE493E1D6F1 /* Inter-Medium.ttf */, + 0A2C56A4517648B6980A4706 /* Inter-Regular.ttf */, + 6C88909D5B5643EBA861C5D0 /* Inter-SemiBold.ttf */, + 99314BF13A4B46BCA459EB37 /* Inter-Thin.ttf */, + 61091C7135514E2DB18E4698 /* Inter-ExtraBold.ttf */, + ); + name = Resources; + path = ""; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( diff --git a/jest.config.js b/jest.config.js index 6aedf5a..07cd300 100644 --- a/jest.config.js +++ b/jest.config.js @@ -45,5 +45,6 @@ module.exports = { '/__mocks__/external/@react-native-firebase/app.js', '@react-native-firebase/app-check': '/__mocks__/external/@react-native-firebase/app-check.js', + '\\.svg': '/__mocks__/external/react-native-svg.js', }, }; diff --git a/jest/fixtures/models.ts b/jest/fixtures/models.ts index 8e66480..4ae53d1 100644 --- a/jest/fixtures/models.ts +++ b/jest/fixtures/models.ts @@ -75,6 +75,8 @@ export const mockBasicModel: Model = { chatTemplate: mockChatTemplate, defaultCompletionSettings: mockDefaultCompletionParams, completionSettings: mockCompletionParams, + defaultStopWords: mockDefaultCompletionParams.stop, + stopWords: mockCompletionParams.stop, }; // Factory function for creating custom models diff --git a/metro.config.js b/metro.config.js index 034c922..8b53b67 100644 --- a/metro.config.js +++ b/metro.config.js @@ -2,11 +2,19 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); //const localPackagePaths = ['localpath/code/llama.rn']; +const defaultConfig = getDefaultConfig(__dirname); +const {assetExts, sourceExts} = defaultConfig.resolver; + const config = { resolver: { //nodeModulesPaths: [...localPackagePaths], // update to resolver + assetExts: assetExts.filter(ext => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], }, transformer: { + babelTransformerPath: require.resolve( + 'react-native-svg-transformer/react-native', + ), getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, diff --git a/package.json b/package.json index 5d2382c..bffa44c 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "^4.14.0", "react-native-screens": "^4.4.0", - "react-native-svg": "^15.9.0", + "react-native-svg": "^15.11.1", "react-native-vector-icons": "^10.1.0", "uuid": "^10.0.0" }, @@ -99,6 +99,7 @@ "postinstall-postinstall": "^2.1.0", "prettier": "2.8.8", "react-native-dotenv": "^3.4.11", + "react-native-svg-transformer": "^1.5.0", "react-test-renderer": "18.3.1", "typescript": "5.0.4" }, diff --git a/src/assets/icons/clock-fast-forward.svg b/src/assets/icons/clock-fast-forward.svg new file mode 100644 index 0000000..322e553 --- /dev/null +++ b/src/assets/icons/clock-fast-forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..8871531 --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/copy.svg b/src/assets/icons/copy.svg new file mode 100644 index 0000000..32042e4 --- /dev/null +++ b/src/assets/icons/copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/dots-vertical.svg b/src/assets/icons/dots-vertical.svg new file mode 100644 index 0000000..7abae09 --- /dev/null +++ b/src/assets/icons/dots-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/duplicate.svg b/src/assets/icons/duplicate.svg new file mode 100644 index 0000000..595d2e2 --- /dev/null +++ b/src/assets/icons/duplicate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/edit-box.svg b/src/assets/icons/edit-box.svg new file mode 100644 index 0000000..b93bf64 --- /dev/null +++ b/src/assets/icons/edit-box.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 0000000..7aa9016 --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/grid.svg b/src/assets/icons/grid.svg new file mode 100644 index 0000000..04e4ecc --- /dev/null +++ b/src/assets/icons/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts new file mode 100644 index 0000000..6407154 --- /dev/null +++ b/src/assets/icons/index.ts @@ -0,0 +1,15 @@ +export {default as DotsVerticalIcon} from './dots-vertical.svg'; +export {default as EditIcon} from './edit.svg'; +export {default as GridIcon} from './grid.svg'; +export {default as CopyIcon} from './copy.svg'; +export {default as ShareIcon} from './share.svg'; +export {default as SettingsIcon} from './settings.svg'; +export {default as ClockFastForwardIcon} from './clock-fast-forward.svg'; +export {default as TrashIcon} from './trash.svg'; +export {default as EditBoxIcon} from './edit-box.svg'; +export {default as DuplicateIcon} from './duplicate.svg'; +export {default as PencilLineIcon} from './pencil-line.svg'; +export {default as RefreshIcon} from './refresh.svg'; +export {default as ReverseLeftIcon} from './reverse-left.svg'; +export {default as CloseIcon} from './close.svg'; +export {default as MenuIcon} from './menu.svg'; diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg new file mode 100644 index 0000000..ee57cfb --- /dev/null +++ b/src/assets/icons/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/pencil-line.svg b/src/assets/icons/pencil-line.svg new file mode 100644 index 0000000..66fd206 --- /dev/null +++ b/src/assets/icons/pencil-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/refresh.svg b/src/assets/icons/refresh.svg new file mode 100644 index 0000000..a01ed8c --- /dev/null +++ b/src/assets/icons/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/reverse-left.svg b/src/assets/icons/reverse-left.svg new file mode 100644 index 0000000..91e7e29 --- /dev/null +++ b/src/assets/icons/reverse-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/settings.svg b/src/assets/icons/settings.svg new file mode 100644 index 0000000..26a5744 --- /dev/null +++ b/src/assets/icons/settings.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/share.svg b/src/assets/icons/share.svg new file mode 100644 index 0000000..cfd2efe --- /dev/null +++ b/src/assets/icons/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/trash.svg b/src/assets/icons/trash.svg new file mode 100644 index 0000000..48ca984 --- /dev/null +++ b/src/assets/icons/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/ChatGenerationSettingsSheet/ChatGenerationSettingsSheet.tsx b/src/components/ChatGenerationSettingsSheet/ChatGenerationSettingsSheet.tsx new file mode 100644 index 0000000..1f939cd --- /dev/null +++ b/src/components/ChatGenerationSettingsSheet/ChatGenerationSettingsSheet.tsx @@ -0,0 +1,146 @@ +import React, {useContext, useEffect, useState} from 'react'; +import {Sheet} from '../Sheet/Sheet'; +import {CompletionSettings} from '../../screens/ModelsScreen/CompletionSettings'; +import {CompletionParams} from '@pocketpalai/llama.rn'; +import {chatSessionStore, defaultCompletionSettings} from '../../store'; +import {styles} from './styles'; +import { + COMPLETION_PARAMS_METADATA, + validateCompletionSettings, +} from '../../utils/modelSettings'; +import {Alert, View} from 'react-native'; +import {Button} from 'react-native-paper'; +import {L10nContext} from '../../utils'; + +export const ChatGenerationSettingsSheet = ({ + isVisible, + onClose, +}: { + isVisible: boolean; + onClose: () => void; +}) => { + const session = chatSessionStore.sessions.find( + item => item.id === chatSessionStore.activeSessionId, + ); + + const [settings, setSettings] = useState( + session?.completionSettings ?? chatSessionStore.newChatCompletionSettings, + ); + + useEffect(() => { + setSettings( + session?.completionSettings ?? chatSessionStore.newChatCompletionSettings, + ); + }, [session]); + + const updateSettings = (name: string, value: any) => { + setSettings(prev => ({...prev, [name]: value})); + }; + + const onCloseSheet = () => { + setSettings( + session?.completionSettings ?? chatSessionStore.newChatCompletionSettings, + ); + onClose(); + }; + + const handleSaveSettings = () => { + // Convert string values to numbers where needed + const processedSettings = Object.entries(settings).reduce( + (acc, [key, value]) => { + const metadata = COMPLETION_PARAMS_METADATA[key]; + if (metadata?.validation.type === 'numeric') { + // Handle numeric conversion + let numValue: number; + if (typeof value === 'string') { + numValue = Number(value); + } else if (typeof value === 'number') { + numValue = value; + } else { + // If it's neither string nor number, treat as invalid. Most probably won't happen. + acc.errors[key] = 'Must be a valid number'; + return acc; + } + + if (Number.isNaN(numValue)) { + acc.errors[key] = 'Must be a valid number'; + } else { + acc.settings[key] = numValue; + } + } else { + // For non-numeric values, keep as is + acc.settings[key] = value; + } + return acc; + }, + {settings: {}, errors: {}} as { + settings: typeof settings; + errors: Record; + }, + ); + + // Validate the converted values + const validationResult = validateCompletionSettings( + processedSettings.settings, + ); + const allErrors = { + ...processedSettings.errors, + ...validationResult.errors, + }; + + if (Object.keys(allErrors).length > 0) { + Alert.alert( + 'Invalid Values', + 'Please correct the following:\n' + + Object.entries(allErrors) + .map(([key, msg]) => `• ${key}: ${msg}`) + .join('\n'), + [{text: 'OK'}], + ); + return; + } + + if (session) { + chatSessionStore.updateSessionCompletionSettings(settings); + } else { + chatSessionStore.setNewChatCompletionSettings(settings); + } + onCloseSheet(); + }; + + const handleResetSettings = () => { + setSettings(defaultCompletionSettings); + }; + + const handleCancelSettings = () => { + onCloseSheet(); + }; + + const i10n = useContext(L10nContext); + + return ( + + + + + + + + + + + + + ); +}; diff --git a/src/components/ChatGenerationSettingsSheet/__tests__/ChatGenerationSettingsSheet.test.tsx b/src/components/ChatGenerationSettingsSheet/__tests__/ChatGenerationSettingsSheet.test.tsx new file mode 100644 index 0000000..01553a8 --- /dev/null +++ b/src/components/ChatGenerationSettingsSheet/__tests__/ChatGenerationSettingsSheet.test.tsx @@ -0,0 +1,238 @@ +import React from 'react'; +import {fireEvent, render, act} from '../../../../jest/test-utils'; +import {Alert} from 'react-native'; +import {ChatGenerationSettingsSheet} from '../ChatGenerationSettingsSheet'; +import {chatSessionStore, defaultCompletionSettings} from '../../../store'; +import {validateCompletionSettings} from '../../../utils/modelSettings'; + +// Mock modelSettings validation +jest.mock('../../../utils/modelSettings', () => ({ + COMPLETION_PARAMS_METADATA: { + temperature: { + validation: {type: 'numeric', min: 0, max: 2, required: true}, + }, + }, + validateCompletionSettings: jest.fn().mockReturnValue({errors: {}}), +})); + +// Mock Alert +jest.spyOn(Alert, 'alert'); + +// Mock the CompletionSettings component +jest.mock('../../../screens/ModelsScreen/CompletionSettings', () => { + const {View, TouchableOpacity} = require('react-native'); + return { + CompletionSettings: ({onChange}) => ( + + onChange('temperature', '3.0')} // Value above max of 2 + /> + + ), + }; +}); + +// Mock Sheet component +jest.mock('../../Sheet/Sheet', () => { + const {View, Button} = require('react-native'); + const MockSheet = ({children, isVisible, onClose, title}) => { + if (!isVisible) { + return null; + } + return ( + + {title} + + + + + + + ); + }, +); diff --git a/src/components/ModelSettingsSheet/__tests__/ModelSettingsSheet.test.tsx b/src/components/ModelSettingsSheet/__tests__/ModelSettingsSheet.test.tsx new file mode 100644 index 0000000..6f82236 --- /dev/null +++ b/src/components/ModelSettingsSheet/__tests__/ModelSettingsSheet.test.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import {fireEvent, render, act} from '../../../../jest/test-utils'; +import {ModelSettingsSheet} from '../ModelSettingsSheet'; +import {modelStore} from '../../../store'; +import {Model, ModelOrigin} from '../../../utils/types'; +import {defaultCompletionParams} from '../../../utils/chat'; + +// Mock the ModelSettings component +jest.mock('../../../screens/ModelsScreen/ModelSettings', () => { + const {View} = require('react-native'); + return { + ModelSettings: ({onChange, onStopWordsChange}) => ( + + onChange('chatTemplate', 'new template')} + /> + onStopWordsChange(['stop1', 'stop2'])} + /> + + ), + }; +}); + +// Mock Sheet component +jest.mock('../../../components/Sheet', () => { + const {View, Button} = require('react-native'); + const MockSheet = ({children, isVisible, onClose, title}) => { + if (!isVisible) { + return null; + } + return ( + + {title} + = observer( }}> {memoryWarning} - - {/* Settings Modal */} - - - ); }, diff --git a/src/screens/ModelsScreen/ModelCard/styles.ts b/src/screens/ModelsScreen/ModelCard/styles.ts index 0f8ecc3..e7ef2c9 100644 --- a/src/screens/ModelsScreen/ModelCard/styles.ts +++ b/src/screens/ModelsScreen/ModelCard/styles.ts @@ -158,4 +158,7 @@ export const createStyles = (theme: Theme) => divider: { marginTop: 8, }, + sheetScrollViewContainer: { + padding: 16, + }, }); diff --git a/src/screens/ModelsScreen/ModelSettings/ModelSettings.tsx b/src/screens/ModelsScreen/ModelSettings/ModelSettings.tsx index 806ed1c..cae9262 100644 --- a/src/screens/ModelsScreen/ModelSettings/ModelSettings.tsx +++ b/src/screens/ModelsScreen/ModelSettings/ModelSettings.tsx @@ -1,41 +1,40 @@ import React, {useEffect, useRef, useState} from 'react'; import {TextInput as RNTextInput} from 'react-native'; -import {View, Keyboard, TouchableWithoutFeedback} from 'react-native'; +import {View, Keyboard} from 'react-native'; import {CompletionParams} from '@pocketpalai/llama.rn'; -import {Button, Text, Switch} from 'react-native-paper'; +import {Button, Text, Switch, Chip} from 'react-native-paper'; import LinearGradient from 'react-native-linear-gradient'; import MaskedView from '@react-native-masked-view/masked-view'; -import {Dialog, Divider, TextInput} from '../../../components'; +import {Divider, TextInput} from '../../../components'; import {useTheme} from '../../../hooks'; import {createStyles} from './styles'; import {ChatTemplatePicker} from '../ChatTemplatePicker'; -import {CompletionSettings} from '../CompletionSettings'; import {ChatTemplateConfig} from '../../../utils/types'; +import {Sheet} from '../../../components/Sheet'; interface ModelSettingsProps { chatTemplate: ChatTemplateConfig; - completionSettings: CompletionParams; + stopWords: CompletionParams['stop']; onChange: (name: string, value: any) => void; - onCompletionSettingsChange: (name: string, value: any) => void; - onFocus?: () => void; + onStopWordsChange: (stopWords: CompletionParams['stop']) => void; } export const ModelSettings: React.FC = ({ chatTemplate, - completionSettings, + stopWords, onChange, - onCompletionSettingsChange, - onFocus, + onStopWordsChange, }) => { const [isDialogVisible, setDialogVisible] = useState(false); const [localChatTemplate, setLocalChatTemplate] = useState( chatTemplate.chatTemplate, ); + const [newStopWord, setNewStopWord] = useState(''); const [selectedTemplateName, setSelectedTemplateName] = useState( chatTemplate.name, @@ -143,89 +142,120 @@ export const ModelSettings: React.FC = ({ ); + const renderStopWords = () => ( + + + + STOP WORDS + + + + {/* Display existing stop words as chips */} + + {(stopWords ?? []).map((word, index) => ( + { + const newStops = (stopWords ?? []).filter((_, i) => i !== index); + onStopWordsChange(newStops); + }} + compact + textStyle={styles.stopChipText} + style={styles.stopChip}> + {word} + + ))} + + + {/* Input for new stop words */} + { + if (newStopWord.trim()) { + onStopWordsChange([...(stopWords ?? []), newStopWord.trim()]); + setNewStopWord(''); + } + }} + testID="stop-input" + /> + + ); + + const onCloseSheet = () => { + dismissKeyboard(); + setDialogVisible(false); + }; + return ( - - - {/* Token Settings Section */} - - {renderTokenSetting( - 'BOS', - 'BOS', - chatTemplate.addBosToken ?? false, - chatTemplate.bosToken, - 'addBosToken', - 'bosToken', - )} - - - - {renderTokenSetting( - 'EOS', - 'EOS', - chatTemplate.addEosToken ?? false, - chatTemplate.eosToken, - 'addEosToken', - 'eosToken', - )} - - - - {renderTokenSetting( - 'add-generation-prompt', - 'Add Generation Prompt', - chatTemplate.addGenerationPrompt ?? false, - undefined, - 'addGenerationPrompt', - )} - - - - {/* System Prompt Section */} - - { - onChange('systemPrompt', text); - }} - multiline - numberOfLines={3} - style={styles.textArea} - label={'System Prompt'} - onFocus={() => { - onFocus && onFocus(); - }} - /> - + + {/* Token Settings Section */} + + {renderTokenSetting( + 'BOS', + 'BOS', + chatTemplate.addBosToken ?? false, + chatTemplate.bosToken, + 'addBosToken', + 'bosToken', + )} - + - {renderTemplateSection()} - + {renderTokenSetting( + 'EOS', + 'EOS', + chatTemplate.addEosToken ?? false, + chatTemplate.eosToken, + 'addEosToken', + 'eosToken', + )} + + + + {renderTokenSetting( + 'add-generation-prompt', + 'Add Generation Prompt', + chatTemplate.addGenerationPrompt ?? false, + undefined, + 'addGenerationPrompt', + )} - {/* Completion Settings Section */} - - + { + onChange('systemPrompt', text); + }} + multiline + numberOfLines={3} + style={styles.textArea} + label={'System Prompt'} /> - {/** Chat Template Dialog */} - setDialogVisible(false)} - title="Edit Chat Template" - avoidKeyboard - actions={[ - { - label: 'Close', - onPress: handleSave, - mode: 'contained', - }, - ]}> + + + {renderTemplateSection()} + + + + {renderStopWords()} + {/** Chat Template Dialog */} + + = ({ numberOfLines={10} style={styles.textArea} /> - - - + + + + + + ); }; diff --git a/src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx b/src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx index b6f184e..e9cccee 100644 --- a/src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx +++ b/src/screens/ModelsScreen/ModelSettings/__tests__/ModelSettings.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Keyboard} from 'react-native'; -import {render, fireEvent, waitFor, act} from '../../../../../jest/test-utils'; +import {render, fireEvent, act, waitFor} from '../../../../../jest/test-utils'; import {ModelSettings} from '../ModelSettings'; @@ -14,6 +14,27 @@ jest.mock('../../CompletionSettings', () => { }; }); +// Mock Sheet component +jest.mock('../../../../components/Sheet', () => { + const {View, TextInput, Button} = require('react-native'); + const MockSheet = ({children, isVisible, onClose}) => { + if (!isVisible) { + return null; + } + return ( + +