Skip to content

Re-factoring API services #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions src/api/index.ts

This file was deleted.

13 changes: 0 additions & 13 deletions src/components/ChatContainer/ChatContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ jest.mock('../../services/ToolService', () => ({
}))
}));

// Mock the api module
jest.mock('../../api', () => ({
fetchChatHistories: jest.fn(),
fetchLLMProviders: jest.fn(),
fetchTools: jest.fn()
}));

// Mock the auth0 hook
jest.mock('@auth0/auth0-react');

Expand Down Expand Up @@ -95,12 +88,6 @@ beforeAll(() => {
Element.prototype.scrollIntoView = jest.fn();
});

afterAll(() => {
// Clean up
// @ts-ignore
delete window.import;
});

describe('ChatContainer', () => {
const mockModelSettings = {
temperature: 0.7,
Expand Down
33 changes: 14 additions & 19 deletions src/components/ChatContainer/ChatContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useRef, useState} from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useAuth0} from '@auth0/auth0-react';
import {ChatContainerProps, ChatPayload, MessageHandlerConfig} from "../../types/chat.ts";
import {ApiChatMessage, ChatService, ClientChatMessage} from "../../services/ChatService.ts";
Expand All @@ -24,6 +24,16 @@
const [useStreaming, setUseStreaming] = useState(true);
const [availableTools, setAvailableTools] = useState<string[]>([]);

const handleError = useCallback((error: unknown) => {
addNotification(
'error',
error instanceof Error || (typeof error === 'object' && error && 'message' in error)
? (error as { message: string }).message
: 'Failed to send message'
);
}, [addNotification]);


const handleStreamingChange = (value: boolean) => {
setUseStreaming(value);
};
Expand All @@ -50,16 +60,7 @@

if (response.error) {
console.error('Error loading tools:', response.error);
// Creating a local function that uses addNotification
const notifyError = () => {
addNotification(
'error',
typeof response.error === 'object' && true && 'message' in response.error
? response.error
: 'Failed to send message'
);
};
notifyError();
handleError(response.error);
return;
}

Expand All @@ -68,19 +69,13 @@
}
} catch (error) {
console.error('Error fetching tools:', error);
// Creating another local function
const notifyError = () => {
addNotification(
'error',
error instanceof Error ? error.message : 'Failed to send message'
);
};
notifyError();
handleError(error);
return;
}
};

loadTools();
}, [getAccessTokenSilently]);

Check warning on line 78 in src/components/ChatContainer/ChatContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'handleError'. Either include it or remove the dependency array


useEffect(() => {
Expand Down
73 changes: 46 additions & 27 deletions src/components/ChatInputButton/ToolsModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,82 @@
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import {fetchTools} from "../../api";
import {act} from "react";
import {ToolsModal} from "./ToolsModal.tsx";
import {Tool} from "../../types/tools.ts";

// Mock the entire api module
jest.mock('../../api', () => ({
fetchTools: jest.fn()
}));
import { render, screen, act, fireEvent } from '@testing-library/react';
import { ToolsModal } from './ToolsModal';
import { ToolService } from '../../services/ToolService';
import { Tool } from '../../types/tools';

// Create mock data
const mockTools = [
{ name: 'Tool1', description: 'Description 1' },
{ name: 'Tool2', description: 'Description 2' },
{ name: 'Tool3', description: 'Description 3' },
];

// Mock the XMarkIcon component
jest.mock('@heroicons/react/24/outline', () => ({
XMarkIcon: () => <div data-testid="close-icon">X</div>
// Mock the ToolItem component
jest.mock('../ToolItem/ToolItem', () => ({
ToolItem: ({ tool, onToggle }: {
tool: Tool;
isSelected: boolean;
onToggle: (name: string) => void;
}) => (
<div
data-testid={`tool-item-${tool.name}`}
onClick={() => onToggle(tool.name)}
>
{tool.name}
</div>
)
}));

// Mock SearchBar component
jest.mock('../SearchBar.tsx', () => ({
SearchBar: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => (
jest.mock('../SearchBar', () => ({
SearchBar: ({ value, onChange }: {
value: string;
onChange: (value: string) => void;
}) => (
<input
data-testid="search-bar"
placeholder="Search tools"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Search tools"
/>
)
}));

// Mock ToolItem component
// Mock the ToolService
jest.mock('../../services/ToolService', () => ({
ToolService: jest.fn().mockImplementation(() => ({
getTools: jest.fn().mockResolvedValue({
data: mockTools
})
}))
}));

// Mock the ToolItem component
jest.mock('../ToolItem/ToolItem', () => ({
ToolItem: ({ tool, isSelected, onToggle }: {
tool: Tool;
isSelected: boolean;
onToggle: (name: string) => void
onToggle: (name: string) => void;
}) => (
<div
data-testid={`tool-item-${tool.name}`}
onClick={() => onToggle(tool.name)}
>
{tool.name} {isSelected ? '(Selected)' : ''}
{tool.name}
{isSelected && ' (Selected)'}
</div>
)
}));

const mockTools = [
{ name: 'Tool1', description: 'Description 1' },
{ name: 'Tool2', description: 'Description 2' },
{ name: 'Tool3', description: 'Description 3' },
];

describe('ToolsModal', () => {
const mockOnClose = jest.fn();
const mockOnSave = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
(fetchTools as jest.Mock).mockResolvedValue(mockTools);
});


it('should render and fetch tools when opened', async () => {
await act(async () => {
render(
Expand All @@ -71,7 +90,7 @@ describe('ToolsModal', () => {
});

expect(screen.getByText('Available Tools')).toBeInTheDocument();
expect(fetchTools).toHaveBeenCalledTimes(1);
expect(ToolService).toHaveBeenCalledTimes(1);

expect(screen.getByTestId('tool-item-Tool1')).toBeInTheDocument();
expect(screen.getByTestId('tool-item-Tool2')).toBeInTheDocument();
Expand Down
8 changes: 4 additions & 4 deletions src/components/ChatInputButton/ToolsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React, {useEffect, useMemo, useState} from 'react';
import {XMarkIcon} from "@heroicons/react/24/outline";
import {Tool, ToolsModalProps} from "../../types/tools.ts";
import {SearchBar} from "../SearchBar.tsx";
import {fetchTools} from "../../api";
import {ToolItem} from "../ToolItem/ToolItem.tsx";
import {ToolService} from "../../services/ToolService.ts";

export const ToolsModal: React.FC<ToolsModalProps> = ({
isOpen,
Expand All @@ -18,8 +18,9 @@ export const ToolsModal: React.FC<ToolsModalProps> = ({
useEffect(() => {
const loadTools = async () => {
try {
const data = await fetchTools();
setTools(data);
const toolService = new ToolService();
const response = await toolService.getTools();
setTools(response.data || []);
} catch (error) {
console.error('Error fetching tools:', error);
}
Expand All @@ -30,7 +31,6 @@ export const ToolsModal: React.FC<ToolsModalProps> = ({
}
}, [isOpen]);


const handleToolToggle = (toolName: string) => {
setSelectedTools(prev =>
prev.includes(toolName)
Expand Down
7 changes: 4 additions & 3 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// components/Sidebar.tsx
import React, {useCallback, useEffect, useState} from 'react';
import {ChatHistory} from "../types";
import {fetchChatHistories} from "../api";
import {SidebarHeader} from "./sidebar/SidebarHeader";
import {NewChatSection} from "./sidebar/NewChatSection";
import {ChatHistoryList} from "./sidebar/ChatHistoryList";
import {SidebarFooter} from "./sidebar/SidebarFooter";
import {useAuth0} from '@auth0/auth0-react';
import {ChatService} from "../services/ChatService.ts";


interface SidebarProps {
Expand All @@ -33,8 +33,9 @@ export const Sidebar: React.FC<SidebarProps> = ({
setIsLoading(true);
try {
const token = await getAccessTokenSilently();
const response = await fetchChatHistories(token);
setChatHistories(response.chats || []);
const chatService = new ChatService(token);
const response = await chatService.getChatHistories();
setChatHistories(response.data.chats || []);
setError(null);
} catch (err) {
setError('Failed to load chat histories');
Expand Down
11 changes: 7 additions & 4 deletions src/services/ChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ interface StreamChunk {
done?: boolean;
}

interface ChatHistoriesResponse {
chats: ChatHistory[];
}

// Updated ChatService
export class ChatService extends APIClient {
constructor(token: string = '') {
super(import.meta.env.VITE_MCP_BACKEND_API_ENDPOINT, token);
}

async getChatHistories(): Promise<APIResponse<ChatHistory[]>> {
return this.fetchWithError<ChatHistory[]>('/chats');
async getChatHistories(): Promise<APIResponse<ChatHistoriesResponse>> {
return this.fetchWithError<ChatHistoriesResponse>('/api/v1/chats');
}

async sendMessage(payload: ChatPayload): Promise<APIResponse<ChatResponse>> {
Expand All @@ -44,15 +47,15 @@ export class ChatService extends APIClient {
}

async loadChatHistory(chatId: string): Promise<APIResponse<{ messages: ApiChatMessage[] }>> {
return this.fetchWithError<{ messages: ApiChatMessage[] }>(`/chats/${chatId}`);
return this.fetchWithError<{ messages: ApiChatMessage[] }>(`/api/v1/chats/${chatId}`);
}

async sendStreamMessage(
payload: ChatPayload,
onChunk: (chunk: StreamChunk) => void
): Promise<void> {
try {
const response = await this.fetchStream('/ask-stream', {
const response = await this.fetchStream('/api/v1/chats/stream', {
method: 'POST',
body: JSON.stringify(payload),
});
Expand Down
2 changes: 1 addition & 1 deletion src/services/LLMService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export class LLMService extends APIClient {
}

async getLLMProviders(): Promise<APIResponse<LLMProvidersResponse>> {
return this.fetchWithError<LLMProvidersResponse>('/llm-providers');
return this.fetchWithError<LLMProvidersResponse>('/api/v1/llm-providers');
}
}
6 changes: 3 additions & 3 deletions src/services/ToolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {APIClient, APIResponse} from "./APIClient.ts";

export class ToolService extends APIClient {
constructor(token: string = '') {
super('http://localhost:8081/api', token);
super(import.meta.env.VITE_MCP_BACKEND_API_ENDPOINT, token);
}

async getTools(): Promise<APIResponse<Tool[]>> {
return this.fetchWithError<Tool[]>('/tools');
return this.fetchWithError<Tool[]>('/api/v1/tools');
}
}
}
Loading