mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
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:

committed by
Geoff Seemueller

parent
5f913eb2d7
commit
ce07b69fbe
@@ -1,4 +1,3 @@
|
|||||||
OPENAI_API_KEY=your-value
|
|
||||||
EVENTSOURCE_HOST=http://some-event-host:3005
|
EVENTSOURCE_HOST=http://some-event-host:3005
|
||||||
GROQ_API_KEY=your-value
|
GROQ_API_KEY=your-value
|
||||||
ANTHROPIC_API_KEY=your-value
|
ANTHROPIC_API_KEY=your-value
|
||||||
@@ -6,4 +5,6 @@ FIREWORKS_API_KEY=your-value
|
|||||||
XAI_API_KEY=your-value
|
XAI_API_KEY=your-value
|
||||||
CEREBRAS_API_KEY=your-value
|
CEREBRAS_API_KEY=your-value
|
||||||
CLOUDFLARE_API_KEY=your-value
|
CLOUDFLARE_API_KEY=your-value
|
||||||
CLOUDFLARE_ACCOUNT_ID=your-value
|
CLOUDFLARE_ACCOUNT_ID=your-value
|
||||||
|
OPENAI_API_KEY=not-needed
|
||||||
|
OPENAI_API_ENDPOINT=http://localhost:10240
|
||||||
|
@@ -2,21 +2,31 @@ import React, { KeyboardEvent, useState } from "react";
|
|||||||
import { Box, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
import { Box, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
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 {
|
interface MessageEditorProps {
|
||||||
message: IMessage;
|
message: Instance<typeof Message>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
|
const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
|
||||||
const [editedContent, setEditedContent] = useState(message.content);
|
const [editedContent, setEditedContent] = useState(message.content);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
const messageIndex = store.messages.indexOf(message);
|
message.setContent(editedContent);
|
||||||
|
|
||||||
|
// Find the index of the edited message
|
||||||
|
const messageIndex = clientChatStore.items.indexOf(message);
|
||||||
if (messageIndex !== -1) {
|
if (messageIndex !== -1) {
|
||||||
store.editMessage(messageIndex, editedContent);
|
// Remove all messages after the edited message
|
||||||
|
clientChatStore.removeAfter(messageIndex);
|
||||||
|
|
||||||
|
// Send the message
|
||||||
|
clientChatStore.sendMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
@@ -33,10 +33,28 @@ export default defineConfig(({command}) => {
|
|||||||
}),
|
}),
|
||||||
react(),
|
react(),
|
||||||
// PWA plugin saves money on data transfer by caching assets on the client
|
// 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({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
|
devOptions: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: "open-gsio",
|
||||||
|
short_name: "open-gsio",
|
||||||
|
description: "Free and open-source platform for conversational AI."
|
||||||
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
||||||
|
navigateFallbackDenylist: [/^\/api\//],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
@@ -73,14 +73,6 @@ const ChatService = types
|
|||||||
throw new Error('Unsupported message format');
|
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 (
|
const createStreamParams = async (
|
||||||
streamConfig: any,
|
streamConfig: any,
|
||||||
@@ -122,7 +114,17 @@ const ChatService = types
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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) {
|
setActiveStream(streamId: string, stream: any) {
|
||||||
const validStream = {
|
const validStream = {
|
||||||
name: stream?.name || "Unnamed Stream",
|
name: stream?.name || "Unnamed Stream",
|
||||||
|
Reference in New Issue
Block a user