Add unit tests for MessageEditorComponent, update message editing logic, and refactor ChatService model handling.

- Added comprehensive tests for `MessageEditorComponent`.
- Improved message editing functionality and added client store interactions.
- Refactored handling of `getSupportedModels` in `ChatService`.
- Updated PWA configuration and added a Safari-specific instruction.
- Adjusted `.dev.vars` file to reflect local development updates.
This commit is contained in:
geoffsee
2025-05-31 14:47:08 -04:00
committed by Geoff Seemueller
parent 5f913eb2d7
commit ce07b69fbe
5 changed files with 173 additions and 17 deletions

View File

@@ -1,4 +1,3 @@
OPENAI_API_KEY=your-value
EVENTSOURCE_HOST=http://some-event-host:3005
GROQ_API_KEY=your-value
ANTHROPIC_API_KEY=your-value
@@ -7,3 +6,5 @@ XAI_API_KEY=your-value
CEREBRAS_API_KEY=your-value
CLOUDFLARE_API_KEY=your-value
CLOUDFLARE_ACCOUNT_ID=your-value
OPENAI_API_KEY=not-needed
OPENAI_API_ENDPOINT=http://localhost:10240

View File

@@ -2,21 +2,31 @@ import React, { KeyboardEvent, useState } from "react";
import { Box, Flex, IconButton, Textarea } from "@chakra-ui/react";
import { Check, X } from "lucide-react";
import { observer } from "mobx-react-lite";
import store, { type IMessage } from "../../../stores/ClientChatStore";
import { Instance } from "mobx-state-tree";
import Message from "../../../models/Message";
import clientChatStore from "../../../stores/ClientChatStore";
interface MessageEditorProps {
message: IMessage;
message: Instance<typeof Message>;
onCancel: () => void;
}
const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
const [editedContent, setEditedContent] = useState(message.content);
const handleSave = () => {
const messageIndex = store.messages.indexOf(message);
const handleSave = async () => {
message.setContent(editedContent);
// Find the index of the edited message
const messageIndex = clientChatStore.items.indexOf(message);
if (messageIndex !== -1) {
store.editMessage(messageIndex, editedContent);
// Remove all messages after the edited message
clientChatStore.removeAfter(messageIndex);
// Send the message
clientChatStore.sendMessage();
}
onCancel();
};

View File

@@ -0,0 +1,125 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import MessageEditor from '../MessageEditorComponent';
// Import the mocked clientChatStore
import clientChatStore from '../../../../stores/ClientChatStore';
// Mock the Message model
vi.mock('../../../../models/Message', () => {
return {
default: {
// This is needed for the Instance<typeof Message> type
}
};
});
// Mock the ClientChatStore
vi.mock('../../../../stores/ClientChatStore', () => {
const mockStore = {
items: [],
removeAfter: vi.fn(),
sendMessage: vi.fn(),
setIsLoading: vi.fn()
};
// Add the mockUserMessage to the items array
mockStore.items.indexOf = vi.fn().mockReturnValue(0);
return {
default: mockStore
};
});
describe('MessageEditor', () => {
// Create a message object with a setContent method
const mockUserMessage = {
content: 'Test message',
role: 'user',
setContent: vi.fn()
};
const mockOnCancel = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('should render with the message content', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const textarea = screen.getByRole('textbox');
expect(textarea).toBeInTheDocument();
expect(textarea).toHaveValue('Test message');
});
it('should update the content when typing', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'Updated message' } });
expect(textarea).toHaveValue('Updated message');
});
it('should call setContent, removeAfter, sendMessage, and onCancel when save button is clicked', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'Updated message' } });
const saveButton = screen.getByLabelText('Save edit');
fireEvent.click(saveButton);
expect(mockUserMessage.setContent).toHaveBeenCalledWith('Updated message');
expect(clientChatStore.removeAfter).toHaveBeenCalledWith(0);
expect(clientChatStore.sendMessage).toHaveBeenCalled();
expect(mockOnCancel).toHaveBeenCalled();
});
it('should call onCancel when cancel button is clicked', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const cancelButton = screen.getByLabelText('Cancel edit');
fireEvent.click(cancelButton);
expect(mockOnCancel).toHaveBeenCalled();
expect(mockUserMessage.setContent).not.toHaveBeenCalled();
});
it('should save when Ctrl+Enter is pressed', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'Updated message' } });
fireEvent.keyDown(textarea, { key: 'Enter', ctrlKey: true });
expect(mockUserMessage.setContent).toHaveBeenCalledWith('Updated message');
expect(clientChatStore.removeAfter).toHaveBeenCalledWith(0);
expect(clientChatStore.sendMessage).toHaveBeenCalled();
expect(mockOnCancel).toHaveBeenCalled();
});
it('should save when Meta+Enter is pressed', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'Updated message' } });
fireEvent.keyDown(textarea, { key: 'Enter', metaKey: true });
expect(mockUserMessage.setContent).toHaveBeenCalledWith('Updated message');
expect(clientChatStore.removeAfter).toHaveBeenCalledWith(0);
expect(clientChatStore.sendMessage).toHaveBeenCalled();
expect(mockOnCancel).toHaveBeenCalled();
});
it('should cancel when Escape is pressed', () => {
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
const textarea = screen.getByRole('textbox');
fireEvent.keyDown(textarea, { key: 'Escape' });
expect(mockOnCancel).toHaveBeenCalled();
expect(mockUserMessage.setContent).not.toHaveBeenCalled();
});
});

View File

@@ -33,10 +33,28 @@ export default defineConfig(({command}) => {
}),
react(),
// PWA plugin saves money on data transfer by caching assets on the client
/*
For safari, use this script in the console to unregister the service worker.
await navigator.serviceWorker.getRegistrations()
.then(registrations => {
registrations.map(r => {
r.unregister()
})
})
*/
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: false,
},
manifest: {
name: "open-gsio",
short_name: "open-gsio",
description: "Free and open-source platform for conversational AI."
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
navigateFallbackDenylist: [/^\/api\//],
}
})
],

View File

@@ -73,14 +73,6 @@ const ChatService = types
throw new Error('Unsupported message format');
};
const getSupportedModels = async () => {
if(self.env.OPENAI_API_ENDPOINT && self.env.OPENAI_API_ENDPOINT.includes("localhost")) {
const openaiClient = new OpenAI({baseURL: self.env.OPENAI_API_ENDPOINT})
const models = await openaiClient.models.list();
return Response.json(models.data.map(model => model.id));
}
return Response.json(SUPPORTED_MODELS);
};
const createStreamParams = async (
streamConfig: any,
@@ -122,7 +114,17 @@ const ChatService = types
};
return {
getSupportedModels,
async getSupportedModels() {
const isLocal = self.env.OPENAI_API_ENDPOINT && self.env.OPENAI_API_ENDPOINT.includes("localhost");
console.log({isLocal})
if(isLocal) {
console.log("getting local models")
const openaiClient = new OpenAI({baseURL: self.env.OPENAI_API_ENDPOINT})
const models = await openaiClient.models.list();
return Response.json(models.data.map(model => model.id));
}
return Response.json(SUPPORTED_MODELS);
},
setActiveStream(streamId: string, stream: any) {
const validStream = {
name: stream?.name || "Unnamed Stream",