diff --git a/.dev.vars b/.dev.vars index 21c9559..a38ca6c 100644 --- a/.dev.vars +++ b/.dev.vars @@ -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 @@ -6,4 +5,6 @@ FIREWORKS_API_KEY=your-value XAI_API_KEY=your-value CEREBRAS_API_KEY=your-value CLOUDFLARE_API_KEY=your-value -CLOUDFLARE_ACCOUNT_ID=your-value \ No newline at end of file +CLOUDFLARE_ACCOUNT_ID=your-value +OPENAI_API_KEY=not-needed +OPENAI_API_ENDPOINT=http://localhost:10240 diff --git a/src/components/chat/messages/MessageEditorComponent.tsx b/src/components/chat/messages/MessageEditorComponent.tsx index 46eedef..e3a5d57 100644 --- a/src/components/chat/messages/MessageEditorComponent.tsx +++ b/src/components/chat/messages/MessageEditorComponent.tsx @@ -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; 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(); }; diff --git a/src/components/chat/messages/__tests__/MessageEditorComponent.test.tsx b/src/components/chat/messages/__tests__/MessageEditorComponent.test.tsx new file mode 100644 index 0000000..fd3806b --- /dev/null +++ b/src/components/chat/messages/__tests__/MessageEditorComponent.test.tsx @@ -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 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(); + + const textarea = screen.getByRole('textbox'); + expect(textarea).toBeInTheDocument(); + expect(textarea).toHaveValue('Test message'); + }); + + it('should update the content when typing', () => { + render(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + const textarea = screen.getByRole('textbox'); + fireEvent.keyDown(textarea, { key: 'Escape' }); + + expect(mockOnCancel).toHaveBeenCalled(); + expect(mockUserMessage.setContent).not.toHaveBeenCalled(); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index d52884f..ccb9541 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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\//], } }) ], diff --git a/workers/site/services/ChatService.ts b/workers/site/services/ChatService.ts index 989c42e..6b4cabc 100644 --- a/workers/site/services/ChatService.ts +++ b/workers/site/services/ChatService.ts @@ -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",