-
-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feat] add edit message and retry generation (#128)
* feat: add edit/copy/regenerate menu to the chat bubbles * feat: regenerate a response with the same or another model * feat: add edit previous message * chore: add menu, menu item and submenu components * chore: refactor theme colors
- Loading branch information
1 parent
01941c0
commit a45fcb7
Showing
57 changed files
with
2,730 additions
and
953 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const ReactNativeHapticFeedback = { | ||
trigger: jest.fn(), | ||
}; | ||
|
||
export default ReactNativeHapticFeedback; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,91 @@ | ||
import {computed, makeAutoObservable, ObservableMap} from 'mobx'; | ||
|
||
import {modelsList} from '../../jest/fixtures/models'; | ||
|
||
export const mockModelStore = { | ||
models: modelsList, | ||
n_context: 1024, | ||
MIN_CONTEXT_SIZE: 200, | ||
useAutoRelease: true, | ||
useMetal: false, | ||
n_gpu_layers: 50, | ||
activeModelId: undefined as string | undefined, | ||
setNContext: jest.fn(), | ||
updateUseAutoRelease: jest.fn(), | ||
updateUseMetal: jest.fn(), | ||
setNGPULayers: jest.fn(), | ||
refreshDownloadStatuses: jest.fn(), | ||
addLocalModel: jest.fn(), | ||
resetModels: jest.fn(), | ||
initContext: jest.fn().mockResolvedValue(Promise.resolve()), | ||
checkSpaceAndDownload: jest.fn(), | ||
getDownloadProgress: jest.fn(), | ||
manualReleaseContext: jest.fn(), | ||
setActiveModel(modelId: string) { | ||
import {Model} from '../../src/utils/types'; | ||
|
||
class MockModelStore { | ||
models = modelsList; | ||
n_context = 1024; | ||
MIN_CONTEXT_SIZE = 200; | ||
useAutoRelease = true; | ||
useMetal = false; | ||
n_gpu_layers = 50; | ||
activeModelId: string | undefined; | ||
inferencing = false; | ||
isStreaming = false; | ||
downloadJobs = new ObservableMap(); | ||
|
||
refreshDownloadStatuses: jest.Mock; | ||
addLocalModel: jest.Mock; | ||
setNContext: jest.Mock; | ||
updateUseAutoRelease: jest.Mock; | ||
updateUseMetal: jest.Mock; | ||
setNGPULayers: jest.Mock; | ||
resetModels: jest.Mock; | ||
initContext: jest.Mock; | ||
lastUsedModelId: any; | ||
checkSpaceAndDownload: jest.Mock; | ||
getDownloadProgress: jest.Mock; | ||
manualReleaseContext: jest.Mock; | ||
|
||
constructor() { | ||
makeAutoObservable(this, { | ||
refreshDownloadStatuses: false, | ||
addLocalModel: false, | ||
setNContext: false, | ||
updateUseAutoRelease: false, | ||
updateUseMetal: false, | ||
setNGPULayers: false, | ||
resetModels: false, | ||
initContext: false, | ||
checkSpaceAndDownload: false, | ||
getDownloadProgress: false, | ||
manualReleaseContext: false, | ||
lastUsedModel: computed, | ||
activeModel: computed, | ||
isDownloading: computed, | ||
}); | ||
this.refreshDownloadStatuses = jest.fn(); | ||
this.addLocalModel = jest.fn(); | ||
this.setNContext = jest.fn(); | ||
this.updateUseAutoRelease = jest.fn(); | ||
this.updateUseMetal = jest.fn(); | ||
this.setNGPULayers = jest.fn(); | ||
this.resetModels = jest.fn(); | ||
this.initContext = jest.fn().mockResolvedValue(Promise.resolve()); | ||
this.checkSpaceAndDownload = jest.fn(); | ||
this.getDownloadProgress = jest.fn(); | ||
this.manualReleaseContext = jest.fn(); | ||
} | ||
|
||
setActiveModel = (modelId: string) => { | ||
this.activeModelId = modelId; | ||
}, | ||
}; | ||
Object.defineProperty(mockModelStore, 'lastUsedModel', { | ||
get: jest.fn(() => undefined), | ||
configurable: true, | ||
}); | ||
Object.defineProperty(mockModelStore, 'isDownloading', { | ||
get: jest.fn(() => () => false), | ||
configurable: true, | ||
}); | ||
Object.defineProperty(mockModelStore, 'activeModel', { | ||
get: jest.fn(() => | ||
mockModelStore.models.find( | ||
model => model.id === mockModelStore.activeModelId, | ||
), | ||
), | ||
configurable: true, | ||
}); | ||
}; | ||
|
||
setInferencing = (value: boolean) => { | ||
this.inferencing = value; | ||
}; | ||
|
||
setIsStreaming = (value: boolean) => { | ||
this.isStreaming = value; | ||
}; | ||
|
||
get lastUsedModel(): Model | undefined { | ||
return this.lastUsedModelId | ||
? this.models.find(m => m.id === this.lastUsedModelId) | ||
: undefined; | ||
} | ||
|
||
get isDownloading() { | ||
return (modelId: string) => { | ||
return this.downloadJobs.has(modelId); | ||
}; | ||
} | ||
|
||
get activeModel() { | ||
return this.models.find(model => model.id === this.activeModelId); | ||
} | ||
} | ||
|
||
export const mockModelStore = new MockModelStore(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import axios from 'axios'; | ||
import {fetchGGUFSpecs, fetchModelFilesDetails, fetchModels} from '../hf'; | ||
|
||
jest.mock('axios'); | ||
const mockedAxios = axios as jest.Mocked<typeof axios>; | ||
|
||
describe('fetchModels', () => { | ||
it('should fetch models with basic parameters', async () => { | ||
const mockResponse = { | ||
data: [{id: 'model1'}], | ||
headers: {link: 'next-page-link'}, | ||
}; | ||
mockedAxios.get.mockResolvedValueOnce(mockResponse); | ||
|
||
const result = await fetchModels({search: 'test'}); | ||
|
||
expect(mockedAxios.get).toHaveBeenCalledWith( | ||
expect.any(String), | ||
expect.objectContaining({ | ||
params: expect.objectContaining({search: 'test'}), | ||
}), | ||
); | ||
expect(result).toEqual({ | ||
models: [{id: 'model1'}], | ||
nextLink: 'next-page-link', | ||
}); | ||
}); | ||
|
||
it('should handle missing pagination link', async () => { | ||
const mockResponse = { | ||
data: [{id: 'model1'}], | ||
headers: {}, | ||
}; | ||
mockedAxios.get.mockResolvedValueOnce(mockResponse); | ||
|
||
const result = await fetchModels({}); | ||
expect(result.nextLink).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('API error handling', () => { | ||
it('should handle network errors in fetchModels', async () => { | ||
const error = new Error('Network error'); | ||
mockedAxios.get.mockRejectedValueOnce(error); | ||
|
||
await expect(fetchModels({})).rejects.toThrow('Network error'); | ||
}); | ||
|
||
it('should handle non-ok responses in fetchModelFilesDetails', async () => { | ||
global.fetch = jest.fn().mockResolvedValueOnce({ | ||
ok: false, | ||
statusText: 'Not Found', | ||
}); | ||
|
||
await expect(fetchModelFilesDetails('model1')).rejects.toThrow( | ||
'Error fetching model files: Not Found', | ||
); | ||
}); | ||
}); | ||
|
||
describe('fetchGGUFSpecs', () => { | ||
it('should parse GGUF specs correctly', async () => { | ||
const mockSpecs = { | ||
gguf: { | ||
params: 7, | ||
type: 'f16', | ||
}, | ||
}; | ||
global.fetch = jest.fn().mockResolvedValueOnce({ | ||
ok: true, | ||
json: () => Promise.resolve(mockSpecs), | ||
}); | ||
|
||
const result = await fetchGGUFSpecs('model1'); | ||
expect(result).toEqual(mockSpecs); | ||
}); | ||
}); |
74 changes: 74 additions & 0 deletions
74
src/components/BottomSheetSearchbar/__tests__/BottomSheetSearchbar.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import React from 'react'; | ||
import {render, fireEvent} from '@testing-library/react-native'; | ||
import {BottomSheetSearchbar} from '../BottomSheetSearchbar'; | ||
import {useBottomSheetInternal} from '@gorhom/bottom-sheet'; | ||
|
||
jest.mock('@gorhom/bottom-sheet', () => ({ | ||
useBottomSheetInternal: jest.fn(), | ||
})); | ||
|
||
describe('BottomSheetSearchbar', () => { | ||
const mockShouldHandleKeyboardEvents = {value: false}; | ||
|
||
beforeEach(() => { | ||
(useBottomSheetInternal as jest.Mock).mockReturnValue({ | ||
shouldHandleKeyboardEvents: mockShouldHandleKeyboardEvents, | ||
}); | ||
}); | ||
|
||
it('should handle focus event correctly', () => { | ||
const onFocus = jest.fn(); | ||
const {getByTestId} = render( | ||
<BottomSheetSearchbar | ||
testID="searchbar" | ||
onFocus={onFocus} | ||
value="test" | ||
/>, | ||
); | ||
|
||
fireEvent(getByTestId('searchbar'), 'focus'); | ||
|
||
expect(mockShouldHandleKeyboardEvents.value).toBe(true); | ||
expect(onFocus).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should handle blur event correctly', () => { | ||
const onBlur = jest.fn(); | ||
const {getByTestId} = render( | ||
<BottomSheetSearchbar testID="searchbar" onBlur={onBlur} value="test" />, | ||
); | ||
|
||
fireEvent(getByTestId('searchbar'), 'blur'); | ||
|
||
expect(mockShouldHandleKeyboardEvents.value).toBe(false); | ||
expect(onBlur).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should reset keyboard events flag on unmount', () => { | ||
const {unmount} = render(<BottomSheetSearchbar value="test" />); | ||
|
||
unmount(); | ||
|
||
expect(mockShouldHandleKeyboardEvents.value).toBe(false); | ||
}); | ||
|
||
it('should forward props to Searchbar component', () => { | ||
const placeholder = 'Search...'; | ||
const value = 'test'; | ||
const onChangeText = jest.fn(); | ||
|
||
const {getByPlaceholderText} = render( | ||
<BottomSheetSearchbar | ||
placeholder={placeholder} | ||
value={value} | ||
onChangeText={onChangeText} | ||
/>, | ||
); | ||
|
||
const searchbar = getByPlaceholderText(placeholder); | ||
expect(searchbar.props.value).toBe(value); | ||
|
||
fireEvent.changeText(searchbar, 'new value'); | ||
expect(onChangeText).toHaveBeenCalledWith('new value'); | ||
}); | ||
}); |
Oops, something went wrong.