refactor(types): use zod for better type safety, style(Messages): new scroll behavior, style(Buttons): match ChatGPT (#761)

* feat: add zod schemas for better type safety

* refactor(useSetOptions): remove 'as Type' in favor of zod schema

* fix: descendant console error, change <p> tag to <div> tag for content in PluginTooltip component

* style(MessagesView): instant/snappier scroll behavior matching official site

* fix(Messages): add null check for scrollableRef before accessing its properties in handleScroll and useEffect

* fix(messageSchema.js): change type of invocationId from string to number
fix(schemas.ts): make authenticated property in tPluginSchema optional
fix(schemas.ts): make isButton property in tPluginSchema optional
fix(schemas.ts): make messages property in tConversationSchema optional and change its type to array of strings
fix(schemas.ts): make systemMessage property in tConversationSchema nullable and optional
fix(schemas.ts): make modelLabel property in tConversationSchema nullable and optional
fix(schemas.ts): make chatGptLabel property in tConversationSchema nullable and optional
fix(schemas.ts): make promptPrefix property in tConversationSchema nullable and optional
fix(schemas.ts): make context property in tConversationSchema nullable and optional
fix(schemas.ts): make jailbreakConversationId property in tConversationSchema nullable and optional
fix(schemas.ts): make conversationSignature property in tConversationSchema nullable and optional
fix(schemas.ts): make clientId property

* refactor(types): replace main types with zod schemas and inferred types

* refactor(types/schemas): use schemas for better type safety of main types

* style(ModelSelect/Buttons): remove shadow and transition

* style(ModelSelect): button changes to closer match OpenAI

* style(ModelSelect): remove green rings which flicker

* style(scrollToBottom): add two separate scrolling functions

* fix(OptionsBar.tsx): handle onFocus and onBlur events to update opacityClass
fix(Messages/index.jsx): increase debounce time for scrollIntoView function
This commit is contained in:
Danny Avila 2023-08-05 12:10:36 -04:00 committed by GitHub
parent 173b8ce2da
commit 5828200197
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 329 additions and 317 deletions

View file

@ -1,8 +1,9 @@
import type { TConversation, TSubmission, EModelEndpoint } from './types';
import { tConversationSchema } from './schemas';
import type { TSubmission, EModelEndpoint } from './types';
export default function createPayload(submission: TSubmission) {
const { conversation, message, endpointOption } = submission;
const { conversationId } = conversation as TConversation;
const { conversationId } = tConversationSchema.parse(conversation);
const { endpoint } = endpointOption as { endpoint: EModelEndpoint };
const endpointUrlMap = {

View file

@ -1,4 +1,5 @@
import * as t from './types';
import * as s from './schemas';
import request from './request';
import * as endpoints from './api-endpoints';
@ -23,11 +24,11 @@ export function clearAllConversations(): Promise<unknown> {
return request.post(endpoints.deleteConversation(), { arg: {} });
}
export function getMessagesByConvoId(id: string): Promise<t.TMessage[]> {
export function getMessagesByConvoId(id: string): Promise<s.TMessage[]> {
return request.get(endpoints.messages(id));
}
export function getConversationById(id: string): Promise<t.TConversation> {
export function getConversationById(id: string): Promise<s.TConversation> {
return request.get(endpoints.conversationById(id));
}
@ -37,19 +38,19 @@ export function updateConversation(
return request.post(endpoints.updateConversation(), { arg: payload });
}
export function getPresets(): Promise<t.TPreset[]> {
export function getPresets(): Promise<s.TPreset[]> {
return request.get(endpoints.presets());
}
export function createPreset(payload: t.TPreset): Promise<t.TPreset[]> {
export function createPreset(payload: s.TPreset): Promise<s.TPreset[]> {
return request.post(endpoints.presets(), payload);
}
export function updatePreset(payload: t.TPreset): Promise<t.TPreset[]> {
export function updatePreset(payload: s.TPreset): Promise<s.TPreset[]> {
return request.post(endpoints.presets(), payload);
}
export function deletePreset(arg: t.TPreset | object): Promise<t.TPreset[]> {
export function deletePreset(arg: s.TPreset | object): Promise<s.TPreset[]> {
return request.post(endpoints.deletePreset(), arg);
}
@ -106,7 +107,7 @@ export const resetPassword = (payload: t.TResetPassword) => {
return request.post(endpoints.resetPassword(), payload);
};
export const getAvailablePlugins = (): Promise<t.TPlugin[]> => {
export const getAvailablePlugins = (): Promise<s.TPlugin[]> => {
return request.get(endpoints.plugins());
};

View file

@ -7,6 +7,7 @@ import {
QueryObserverResult,
} from '@tanstack/react-query';
import * as t from './types';
import * as s from './schemas';
import * as dataService from './data-service';
export enum QueryKeys {
@ -47,9 +48,9 @@ export const useGetUserQuery = (
export const useGetMessagesByConvoId = (
id: string,
config?: UseQueryOptions<t.TMessage[]>,
): QueryObserverResult<t.TMessage[]> => {
return useQuery<t.TMessage[]>(
config?: UseQueryOptions<s.TMessage[]>,
): QueryObserverResult<s.TMessage[]> => {
return useQuery<s.TMessage[]>(
[QueryKeys.messages, id],
() => dataService.getMessagesByConvoId(id),
{
@ -63,9 +64,9 @@ export const useGetMessagesByConvoId = (
export const useGetConversationByIdQuery = (
id: string,
config?: UseQueryOptions<t.TConversation>,
): QueryObserverResult<t.TConversation> => {
return useQuery<t.TConversation>(
config?: UseQueryOptions<s.TConversation>,
): QueryObserverResult<s.TConversation> => {
return useQuery<s.TConversation>(
[QueryKeys.conversation, id],
() => dataService.getConversationById(id),
{
@ -79,10 +80,10 @@ export const useGetConversationByIdQuery = (
//This isn't ideal because its just a query and we're using mutation, but it was the only way
//to make it work with how the Chat component is structured
export const useGetConversationByIdMutation = (id: string): UseMutationResult<t.TConversation> => {
export const useGetConversationByIdMutation = (id: string): UseMutationResult<s.TConversation> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.getConversationById(id), {
// onSuccess: (res: t.TConversation) => {
// onSuccess: (res: s.TConversation) => {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.conversation, id]);
},
@ -174,13 +175,13 @@ export const useGetEndpointsQuery = (): QueryObserverResult<t.TEndpointsConfig>
};
export const useCreatePresetMutation = (): UseMutationResult<
t.TPreset[],
s.TPreset[],
unknown,
t.TPreset,
s.TPreset,
unknown
> => {
const queryClient = useQueryClient();
return useMutation((payload: t.TPreset) => dataService.createPreset(payload), {
return useMutation((payload: s.TPreset) => dataService.createPreset(payload), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
@ -188,13 +189,13 @@ export const useCreatePresetMutation = (): UseMutationResult<
};
export const useUpdatePresetMutation = (): UseMutationResult<
t.TPreset[],
s.TPreset[],
unknown,
t.TPreset,
s.TPreset,
unknown
> => {
const queryClient = useQueryClient();
return useMutation((payload: t.TPreset) => dataService.updatePreset(payload), {
return useMutation((payload: s.TPreset) => dataService.updatePreset(payload), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
@ -202,9 +203,9 @@ export const useUpdatePresetMutation = (): UseMutationResult<
};
export const useGetPresetsQuery = (
config?: UseQueryOptions<t.TPreset[]>,
): QueryObserverResult<t.TPreset[], unknown> => {
return useQuery<t.TPreset[]>([QueryKeys.presets], () => dataService.getPresets(), {
config?: UseQueryOptions<s.TPreset[]>,
): QueryObserverResult<s.TPreset[], unknown> => {
return useQuery<s.TPreset[]>([QueryKeys.presets], () => dataService.getPresets(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
@ -213,13 +214,13 @@ export const useGetPresetsQuery = (
};
export const useDeletePresetMutation = (): UseMutationResult<
t.TPreset[],
s.TPreset[],
unknown,
t.TPreset | object,
s.TPreset | object,
unknown
> => {
const queryClient = useQueryClient();
return useMutation((payload: t.TPreset | object) => dataService.deletePreset(payload), {
return useMutation((payload: s.TPreset | object) => dataService.deletePreset(payload), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
@ -323,8 +324,8 @@ export const useResetPasswordMutation = (): UseMutationResult<
return useMutation((payload: t.TResetPassword) => dataService.resetPassword(payload));
};
export const useAvailablePluginsQuery = (): QueryObserverResult<t.TPlugin[]> => {
return useQuery<t.TPlugin[]>(
export const useAvailablePluginsQuery = (): QueryObserverResult<s.TPlugin[]> => {
return useQuery<s.TPlugin[]>(
[QueryKeys.availablePlugins],
() => dataService.getAvailablePlugins(),
{

View file

@ -0,0 +1,121 @@
import { z } from 'zod';
export enum EModelEndpoint {
azureOpenAI = 'azureOpenAI',
openAI = 'openAI',
bingAI = 'bingAI',
chatGPT = 'chatGPT',
chatGPTBrowser = 'chatGPTBrowser',
google = 'google',
gptPlugins = 'gptPlugins',
anthropic = 'anthropic',
}
export const eModelEndpointSchema = z.nativeEnum(EModelEndpoint);
export const tMessageSchema = z.object({
messageId: z.string(),
conversationId: z.string(),
clientId: z.string(),
parentMessageId: z.string(),
sender: z.string(),
text: z.string(),
isCreatedByUser: z.boolean(),
error: z.boolean(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type TMessage = z.infer<typeof tMessageSchema>;
export const tPluginAuthConfigSchema = z.object({
authField: z.string(),
label: z.string(),
description: z.string(),
});
export type TPluginAuthConfig = z.infer<typeof tPluginAuthConfigSchema>;
export const tPluginSchema = z.object({
name: z.string(),
pluginKey: z.string(),
description: z.string(),
icon: z.string(),
authConfig: z.array(tPluginAuthConfigSchema),
authenticated: z.boolean().optional(),
isButton: z.boolean().optional(),
});
export type TPlugin = z.infer<typeof tPluginSchema>;
export const tExampleSchema = z.object({
input: z.object({
content: z.string(),
}),
output: z.object({
content: z.string(),
}),
});
export type TExample = z.infer<typeof tExampleSchema>;
export const tAgentOptionsSchema = z.object({
agent: z.string(),
skipCompletion: z.boolean(),
model: z.string(),
temperature: z.number(),
});
export const tConversationSchema = z.object({
conversationId: z.string().nullable(),
title: z.string(),
user: z.string().optional(),
endpoint: eModelEndpointSchema.nullable(),
suggestions: z.array(z.string()).optional(),
messages: z.array(z.string()).optional(),
tools: z.array(tPluginSchema).optional(),
createdAt: z.string(),
updatedAt: z.string(),
systemMessage: z.string().nullable().optional(),
modelLabel: z.string().nullable().optional(),
examples: z.array(tExampleSchema).optional(),
chatGptLabel: z.string().nullable().optional(),
userLabel: z.string().optional(),
model: z.string().optional(),
promptPrefix: z.string().nullable().optional(),
temperature: z.number().optional(),
topP: z.number().optional(),
topK: z.number().optional(),
context: z.string().nullable().optional(),
top_p: z.number().optional(),
frequency_penalty: z.number().optional(),
presence_penalty: z.number().optional(),
jailbreak: z.boolean().optional(),
jailbreakConversationId: z.string().nullable().optional(),
conversationSignature: z.string().nullable().optional(),
parentMessageId: z.string().optional(),
clientId: z.string().nullable().optional(),
invocationId: z.number().nullable().optional(),
toneStyle: z.string().nullable().optional(),
maxOutputTokens: z.number().optional(),
agentOptions: tAgentOptionsSchema.nullable().optional(),
});
export type TConversation = z.infer<typeof tConversationSchema>;
export const tPresetSchema = tConversationSchema
.omit({
conversationId: true,
createdAt: true,
updatedAt: true,
title: true,
})
.merge(
z.object({
conversationId: z.string().optional(),
presetId: z.string().nullable().optional(),
title: z.string().nullable().optional(),
}),
);
export type TPreset = z.infer<typeof tPresetSchema>;

View file

@ -1,42 +1,12 @@
import * as React from 'react';
import { TExample, TMessage, EModelEndpoint, TPlugin, TConversation, TPreset } from './schemas';
export type TMessage = {
messageId: string;
conversationId: string;
clientId: string;
parentMessageId: string;
sender: string;
text: string;
isCreatedByUser: boolean;
error: boolean;
createdAt: string;
updatedAt: string;
};
export * from './schemas';
export type TMessages = TMessage[];
export type TMessagesAtom = TMessages | null;
export type TExample = {
input: {
content: string;
};
output: {
content: string;
};
};
export enum EModelEndpoint {
azureOpenAI = 'azureOpenAI',
openAI = 'openAI',
bingAI = 'bingAI',
chatGPT = 'chatGPT',
chatGPTBrowser = 'chatGPTBrowser',
google = 'google',
gptPlugins = 'gptPlugins',
anthropic = 'anthropic',
}
export type TSubmission = {
clientId?: string;
context?: string;
@ -73,22 +43,6 @@ export type TEndpointOption = {
temperature?: number;
};
export type TPluginAuthConfig = {
authField: string;
label: string;
description: string;
};
export type TPlugin = {
name: string;
pluginKey: string;
description: string;
icon: string;
authConfig: TPluginAuthConfig[];
authenticated: boolean;
isButton?: boolean;
};
export type TPluginAction = {
pluginKey: string;
action: 'install' | 'uninstall';
@ -105,91 +59,6 @@ export type TUpdateUserPlugins = {
auth?: unknown;
};
export type TAgentOptions = {
agent: string;
skipCompletion: boolean;
model: string;
temperature: number;
};
export type TConversation = {
conversationId: string | null;
title: string;
user?: string;
endpoint: EModelEndpoint | null;
suggestions?: string[];
messages?: TMessage[];
tools?: TPlugin[];
createdAt: string;
updatedAt: string;
// google only
systemMessage?: string;
modelLabel?: string;
examples?: TExample[];
// for azureOpenAI, openAI only
chatGptLabel?: string;
userLabel?: string;
model?: string;
promptPrefix?: string;
temperature?: number;
topP?: number;
topK?: number;
// bing and google
context?: string;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
// for bingAI only
jailbreak?: boolean;
jailbreakConversationId?: string;
conversationSignature?: string;
parentMessageId?: string;
clientId?: string;
invocationId?: string;
toneStyle?: string;
maxOutputTokens?: number;
// plugins only
agentOptions?: TAgentOptions;
};
export type TPreset = {
title: string;
conversationId?: string;
endpoint: EModelEndpoint | null;
conversationSignature?: string;
createdAt?: string;
updatedAt?: string;
presetId?: string;
tools?: TPlugin[];
user?: string;
modelLabel?: string;
maxOutputTokens?: number;
topP?: number;
topK?: number;
context?: string;
systemMessage?: string;
// for azureOpenAI, openAI only
chatGptLabel?: string;
frequence_penalty?: number;
model?: string;
presence_penalty?: number;
frequency_penalty?: number;
promptPrefix?: string;
temperature?: number;
top_p?: number;
//for BingAI
clientId?: string;
invocationId?: number;
jailbreak?: boolean;
jailbreakPresetId?: string;
presetSignature?: string;
toneStyle?: string;
// plugins only
agentOptions?: TAgentOptions;
// google only
examples?: TExample[];
};
export type TOptionSettings = {
showExamples?: boolean;
isCodeChat?: boolean;