Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
harishmohanraj committed Apr 18, 2024
1 parent 432d677 commit b87c97c
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 65 deletions.
7 changes: 6 additions & 1 deletion app/src/client/app/ModelsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState } from 'react';
import CustomLayout from './layout/CustomLayout';
import CustomBreadcrumb from '../components/CustomBreadcrumb';
import Button from '../components/Button';
Expand Down Expand Up @@ -42,6 +42,10 @@ const ModelsPage = () => {
}
};

const onSuccessCallback = (data: any) => {
console.log('Form submitted with data:', data);
};

return (
<CustomLayout>
<CustomBreadcrumb pageName='Models' />
Expand All @@ -60,6 +64,7 @@ const ModelsPage = () => {
<DynamicFormBuilder
jsonSchema={initialModelSchema}
validationURL={`models/llms/${selectedModel}/validate`}
onSuccessCallback={onSuccessCallback}
/>
)}
</>
Expand Down
92 changes: 55 additions & 37 deletions app/src/client/components/DynamicFormBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
import React from 'react';
import React, { useState } from 'react';
import { useForm } from '../hooks/useForm';
import { JsonSchema } from '../interfaces/models';
import { TextInput } from './form/TextInput';
import { SelectInput } from './form/SelectInput';
import { validateForm } from '../services/commonService';
import NotificationBox from '../components/NotificationBox';
import { object } from 'zod';

interface DynamicFormBuilderProps {
jsonSchema: JsonSchema;
validationURL: string;
onSuccessCallback: (data: any) => void;
}

const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({ jsonSchema, validationURL }) => {
const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({ jsonSchema, validationURL, onSuccessCallback }) => {
const { formData, handleChange } = useForm(jsonSchema);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
console.log('Submitted Data:', formData);
const response = await validateForm(formData, validationURL);
console.log('Validation Response:', response);
setIsLoading(true);
try {
const response = await validateForm(formData, validationURL);
setError(null);
onSuccessCallback(response);
} catch (error: any) {
setError(error.message);
console.log(JSON.parse(error.message));
console.log(typeof JSON.parse(error.message));
}
setIsLoading(false);
};

return (
<form onSubmit={handleSubmit} className='grid grid-cols-1 md:grid-cols-2 gap-9 p-6.5'>
{Object.entries(jsonSchema.properties).map(([key, property]) =>
property?.enum?.length === 1 ? null : (
<div key={key} className='w-full'>
<label htmlFor={key}>{property.title}</label>
{property.enum ? (
<SelectInput
id={key}
value={formData[key]}
options={property.enum}
onChange={(value) => handleChange(key, value)}
/>
) : (
<TextInput
id={key}
value={formData[key]}
placeholder={property.description || ''}
onChange={(value) => handleChange(key, value)}
/>
)}
</div>
)
)}
<div className='col-span-full'>
<button
type='submit'
className='rounded-md px-3.5 py-2.5 text-sm bg-airt-primary text-airt-font-base hover:bg-opacity-85 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
>
Submit
</button>
</div>
</form>
<>
<form onSubmit={handleSubmit} className='grid grid-cols-1 md:grid-cols-2 gap-9 p-6.5'>
{Object.entries(jsonSchema.properties).map(([key, property]) =>
property?.enum?.length === 1 ? null : (
<div key={key} className='w-full'>
<label htmlFor={key}>{property.title}</label>
{property.enum ? (
<SelectInput
id={key}
value={formData[key]}
options={property.enum}
onChange={(value) => handleChange(key, value)}
/>
) : (
<TextInput
id={key}
value={formData[key]}
placeholder={property.description || ''}
onChange={(value) => handleChange(key, value)}
/>
)}
</div>
)
)}
<div className='col-span-full'>
<button
type='submit'
className='rounded-md px-3.5 py-2.5 text-sm bg-airt-primary text-airt-font-base hover:bg-opacity-85 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
disabled={isLoading}
data-testid='form-submit-button'
>
Submit
</button>
</div>
</form>
{error && <NotificationBox type={'error'} onClick={() => setError(null)} message={error} />}
</>
);
};

Expand Down
1 change: 0 additions & 1 deletion app/src/client/services/commonService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const validateForm = async (data: any, validationURL: string) => {
const response = await apiValidateForm({ data, validationURL });
return response;
} catch (error) {
console.error('Failed to fetch models:', error);
throw error;
}
};
77 changes: 56 additions & 21 deletions app/src/client/tests/DynamicFormBuilder.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import { render, fireEvent, screen } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import { test, expect, describe, vi } from 'vitest';
import { act } from 'react-dom/test-utils';
import { renderInContext } from 'wasp/client/test';

import DynamicFormBuilder from '../components/DynamicFormBuilder';
import { JsonSchema } from '../interfaces/models';
import { validateForm } from '../services/commonService';

vi.mock('../hooks/useForm', () => ({
useForm: () => ({
formData: {
model: 'gpt-3.5-turbo',
api_key: '',
base_url: 'https://api.openai.com/v1',
api_type: 'openai',
},
handleChange: vi.fn(),
}),
}));

vi.mock('../services/commonService', () => ({
validateForm: vi.fn(() => Promise.resolve({ success: true })),
}));

const jsonSchema: JsonSchema = {
properties: {
Expand Down Expand Up @@ -43,32 +62,48 @@ const jsonSchema: JsonSchema = {

describe('DynamicFormBuilder', () => {
test('renders form fields correctly', () => {
render(<DynamicFormBuilder jsonSchema={jsonSchema} validationURL='https://some-domain/some-route' />);

renderInContext(
<DynamicFormBuilder
jsonSchema={jsonSchema}
validationURL='https://some-domain/some-route'
onSuccessCallback={vi.fn()}
/>
);
expect(screen.getByLabelText('Model')).toBeInTheDocument();
expect(screen.getByLabelText('API Key')).toBeInTheDocument();
expect(screen.getByLabelText('Base Url')).toBeInTheDocument();
expect(screen.queryByLabelText('API Type')).toBe(null);
});
test('handles form submission successfully', async () => {
const onSuccessCallback = vi.fn();
renderInContext(
<DynamicFormBuilder
jsonSchema={jsonSchema}
validationURL='https://some-domain/some-route'
onSuccessCallback={onSuccessCallback}
/>
);

// test('handles user input and form submission', () => {
// const logSpy = vi.spyOn(console, 'log');
// render(<DynamicFormBuilder jsonSchema={jsonSchema} />);

// fireEvent.change(screen.getByLabelText('Model'), { target: { value: 'gpt-4' } });
// fireEvent.change(screen.getByLabelText('API Key'), { target: { value: 'test-api-key' } });
// fireEvent.change(screen.getByLabelText('Base Url'), { target: { value: 'https://test.com' } });
// fireEvent.click(screen.getByText('Submit'));
const submitButton = screen.getByTestId('form-submit-button');
await act(async () => {
await fireEvent.click(submitButton);
});

// // Check if console.log is called with the correct form data
// expect(logSpy).toHaveBeenCalledWith({
// model: 'gpt-4',
// api_key: 'test-api-key',
// base_url: 'https://test.com',
// api_type: 'openai',
// });
expect(onSuccessCallback).toHaveBeenCalled();
});

// // Restore console.log
// logSpy.mockRestore();
// });
test('shows an error message when submission fails', async () => {
// Mock the validateForm to simulate a failure
vi.mocked(validateForm).mockImplementationOnce(() => Promise.reject(new Error('Failed to validate')));
const onSuccessCallback = vi.fn();
renderInContext(
<DynamicFormBuilder
jsonSchema={jsonSchema}
validationURL='https://some-domain/some-route'
onSuccessCallback={onSuccessCallback}
/>
);
await fireEvent.submit(screen.getByRole('button', { name: /submit/i }));
expect(onSuccessCallback).not.toHaveBeenCalled();
});
});
11 changes: 6 additions & 5 deletions app/src/server/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,17 @@ export const validateForm: ValidateForm<{ data: any; validationURL: string }, an
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json: any = (await response.json()) as { detail?: string }; // Parse JSON once

const json = await response.json();
if (!response.ok) {
const errorMsg = json.detail || `HTTP error with status code ${response.status}`;
console.error('Server Error:', errorMsg);
throw new Error(errorMsg);
throw new HttpError(
response.status,
JSON.stringify(json.detail) || `HTTP error with status code ${response.status}`
);
}

return json;
} catch (error: any) {
throw new HttpError(500, error.message);
throw new HttpError(error.statusCode || 500, error.message || 'Server or network error occurred');
}
};

0 comments on commit b87c97c

Please sign in to comment.