mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-10 11:34:23 +01:00
📈 feat: Chat rating for feedback (#5878)
* feat: working started for feedback implementation. TODO: - needs some refactoring. - needs some UI animations. * feat: working rate functionality * feat: works now as well to reader the already rated responses from the server. * feat: added the option to give feedback in text (optional) * feat: added Dismiss option `x` to the `FeedbackTagOptions` * ✨ feat: Add rating and ratingContent fields to message schema * 🔧 chore: Bump version to 0.0.3 in package.json * ✨ feat: Enhance feedback localization and update UI elements * 🚀 feat: Implement feedback tagging system with thumbs up/down options * 🚀 feat: Add data-provider package to unused i18n keys detection * 🎨 style: update HoverButtons' style * 🎨 style: Update HoverButtons and Fork components for improved styling and visibility * 🔧 feat: Implement feedback system with rating and content options * 🔧 feat: Enhance feedback handling with improved rating toggle and tag options * 🔧 feat: Integrate toast notifications for feedback submission and clean up unused state * 🔧 feat: Remove unused feedback tag options from translation file * ✨ refactor: clean up Feedback component and improve HoverButtons structure * ✨ refactor: remove unused settings switches for auto scroll, hide side panel, and user message markdown * refactor: reorganize import order * ✨ refactor: enhance HoverButtons and Fork components with improved styles and animations * ✨ refactor: update feedback response phrases for improved user engagement * ✨ refactor: add CheckboxOption component and streamline fork options rendering * Refactor feedback components and logic - Consolidated feedback handling into a single Feedback component, removing FeedbackButtons and FeedbackTagOptions. - Introduced new feedback tagging system with detailed tags for both thumbs up and thumbs down ratings. - Updated feedback schema to include new tags and improved type definitions. - Enhanced user interface for feedback collection, including a dialog for additional comments. - Removed obsolete files and adjusted imports accordingly. - Updated translations for new feedback tags and placeholders. * ✨ refactor: update feedback handling by replacing rating fields with feedback in message updates * fix: add missing validateMessageReq middleware to feedback route and refactor feedback system * 🗑️ chore: Remove redundant fork option explanations from translation file * 🔧 refactor: Remove unused dependency from feedback callback * 🔧 refactor: Simplify message update response structure and improve error logging * Chore: removed unused tests. --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
parent
4808c5be48
commit
4cbab86b45
76 changed files with 1592 additions and 835 deletions
|
|
@ -303,7 +303,8 @@ class RequestExecutor {
|
|||
if (this.config.parameterLocations && this.params) {
|
||||
for (const key of Object.keys(this.params)) {
|
||||
// Determine parameter placement; default to "query" for GET and "body" for others.
|
||||
const loc: 'query' | 'path' | 'header' | 'body' = this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
|
||||
const loc: 'query' | 'path' | 'header' | 'body' =
|
||||
this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
|
||||
|
||||
const val = this.params[key];
|
||||
if (loc === 'query') {
|
||||
|
|
@ -351,7 +352,15 @@ export class ActionRequest {
|
|||
contentType: string,
|
||||
parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
|
||||
) {
|
||||
this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType, parameterLocations);
|
||||
this.config = new RequestConfig(
|
||||
domain,
|
||||
path,
|
||||
method,
|
||||
operation,
|
||||
isConsequential,
|
||||
contentType,
|
||||
parameterLocations,
|
||||
);
|
||||
}
|
||||
|
||||
// Add getters to maintain backward compatibility
|
||||
|
|
@ -486,12 +495,12 @@ export function openapiToFunction(
|
|||
}
|
||||
// Record the parameter location from the OpenAPI "in" field.
|
||||
paramLocations[paramName] =
|
||||
(resolvedParam.in === 'query' ||
|
||||
resolvedParam.in === 'path' ||
|
||||
resolvedParam.in === 'header' ||
|
||||
resolvedParam.in === 'body')
|
||||
? resolvedParam.in
|
||||
: 'query';
|
||||
resolvedParam.in === 'query' ||
|
||||
resolvedParam.in === 'path' ||
|
||||
resolvedParam.in === 'header' ||
|
||||
resolvedParam.in === 'body'
|
||||
? resolvedParam.in
|
||||
: 'query';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -272,6 +272,10 @@ export const userTerms = () => '/api/user/terms';
|
|||
export const acceptUserTerms = () => '/api/user/terms/accept';
|
||||
export const banner = () => '/api/banner';
|
||||
|
||||
// Message Feedback
|
||||
export const feedback = (conversationId: string, messageId: string) =>
|
||||
`/api/messages/${conversationId}/${messageId}/feedback`;
|
||||
|
||||
// Two-Factor Endpoints
|
||||
export const enableTwoFactor = () => '/api/auth/2fa/enable';
|
||||
export const verifyTwoFactor = () => '/api/auth/2fa/verify';
|
||||
|
|
|
|||
|
|
@ -765,6 +765,15 @@ export function getBanner(): Promise<t.TBannerResponse> {
|
|||
return request.get(endpoints.banner());
|
||||
}
|
||||
|
||||
export function updateFeedback(
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
payload: t.TUpdateFeedbackRequest,
|
||||
): Promise<t.TUpdateFeedbackResponse> {
|
||||
return request.put(endpoints.feedback(conversationId, messageId), payload);
|
||||
}
|
||||
|
||||
// 2FA
|
||||
export function enableTwoFactor(): Promise<t.TEnable2FAResponse> {
|
||||
return request.get(endpoints.enableTwoFactor());
|
||||
}
|
||||
|
|
|
|||
141
packages/data-provider/src/feedback.ts
Normal file
141
packages/data-provider/src/feedback.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export type TFeedbackRating = 'thumbsUp' | 'thumbsDown';
|
||||
export const FEEDBACK_RATINGS = ['thumbsUp', 'thumbsDown'] as const;
|
||||
|
||||
export const FEEDBACK_REASON_KEYS = [
|
||||
// Down
|
||||
'not_matched',
|
||||
'inaccurate',
|
||||
'bad_style',
|
||||
'missing_image',
|
||||
'unjustified_refusal',
|
||||
'not_helpful',
|
||||
'other',
|
||||
// Up
|
||||
'accurate_reliable',
|
||||
'creative_solution',
|
||||
'clear_well_written',
|
||||
'attention_to_detail',
|
||||
] as const;
|
||||
|
||||
export type TFeedbackTagKey = (typeof FEEDBACK_REASON_KEYS)[number];
|
||||
|
||||
export interface TFeedbackTag {
|
||||
key: TFeedbackTagKey;
|
||||
label: string;
|
||||
direction: TFeedbackRating;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// --- Tag Registry ---
|
||||
export const FEEDBACK_TAGS: TFeedbackTag[] = [
|
||||
// Thumbs Down
|
||||
{
|
||||
key: 'not_matched',
|
||||
label: 'com_ui_feedback_tag_not_matched',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'AlertCircle',
|
||||
},
|
||||
{
|
||||
key: 'inaccurate',
|
||||
label: 'com_ui_feedback_tag_inaccurate',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'AlertCircle',
|
||||
},
|
||||
{
|
||||
key: 'bad_style',
|
||||
label: 'com_ui_feedback_tag_bad_style',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'PenTool',
|
||||
},
|
||||
{
|
||||
key: 'missing_image',
|
||||
label: 'com_ui_feedback_tag_missing_image',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'ImageOff',
|
||||
},
|
||||
{
|
||||
key: 'unjustified_refusal',
|
||||
label: 'com_ui_feedback_tag_unjustified_refusal',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'Ban',
|
||||
},
|
||||
{
|
||||
key: 'not_helpful',
|
||||
label: 'com_ui_feedback_tag_not_helpful',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'ThumbsDown',
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
label: 'com_ui_feedback_tag_other',
|
||||
direction: 'thumbsDown',
|
||||
icon: 'HelpCircle',
|
||||
},
|
||||
// Thumbs Up
|
||||
{
|
||||
key: 'accurate_reliable',
|
||||
label: 'com_ui_feedback_tag_accurate_reliable',
|
||||
direction: 'thumbsUp',
|
||||
icon: 'CheckCircle',
|
||||
},
|
||||
{
|
||||
key: 'creative_solution',
|
||||
label: 'com_ui_feedback_tag_creative_solution',
|
||||
direction: 'thumbsUp',
|
||||
icon: 'Lightbulb',
|
||||
},
|
||||
{
|
||||
key: 'clear_well_written',
|
||||
label: 'com_ui_feedback_tag_clear_well_written',
|
||||
direction: 'thumbsUp',
|
||||
icon: 'PenTool',
|
||||
},
|
||||
{
|
||||
key: 'attention_to_detail',
|
||||
label: 'com_ui_feedback_tag_attention_to_detail',
|
||||
direction: 'thumbsUp',
|
||||
icon: 'Search',
|
||||
},
|
||||
];
|
||||
|
||||
export function getTagsForRating(rating: TFeedbackRating): TFeedbackTag[] {
|
||||
return FEEDBACK_TAGS.filter((tag) => tag.direction === rating);
|
||||
}
|
||||
|
||||
export const feedbackTagKeySchema = z.enum(FEEDBACK_REASON_KEYS);
|
||||
export const feedbackRatingSchema = z.enum(FEEDBACK_RATINGS);
|
||||
|
||||
export const feedbackSchema = z.object({
|
||||
rating: feedbackRatingSchema,
|
||||
tag: feedbackTagKeySchema,
|
||||
text: z.string().max(1024).optional(),
|
||||
});
|
||||
|
||||
export type TMinimalFeedback = z.infer<typeof feedbackSchema>;
|
||||
|
||||
export type TFeedback = {
|
||||
rating: TFeedbackRating;
|
||||
tag: TFeedbackTag | undefined;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export function toMinimalFeedback(feedback: TFeedback | undefined): TMinimalFeedback | undefined {
|
||||
if (!feedback?.rating || !feedback?.tag || !feedback.tag.key) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
rating: feedback.rating,
|
||||
tag: feedback.tag.key,
|
||||
text: feedback.text,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTagByKey(key: TFeedbackTagKey | undefined): TFeedbackTag | undefined {
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
return FEEDBACK_TAGS.find((tag) => tag.key === key);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable max-len */
|
||||
import { z } from 'zod';
|
||||
import { EModelEndpoint } from './schemas';
|
||||
import type { FileConfig, EndpointFileConfig } from './types/files';
|
||||
|
|
|
|||
|
|
@ -39,4 +39,6 @@ import * as dataService from './data-service';
|
|||
export * from './utils';
|
||||
export * from './actions';
|
||||
export { default as createPayload } from './createPayload';
|
||||
/* feedback */
|
||||
export * from './feedback';
|
||||
export * from './parameterSettings';
|
||||
|
|
|
|||
|
|
@ -347,3 +347,19 @@ export const useGetCustomConfigSpeechQuery = (
|
|||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateFeedbackMutation = (
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
): UseMutationResult<t.TUpdateFeedbackResponse, Error, t.TUpdateFeedbackRequest> => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(payload: t.TUpdateFeedbackRequest) =>
|
||||
dataService.updateFeedback(conversationId, messageId, payload),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([QueryKeys.messages, messageId]);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
import { Tools } from './types/assistants';
|
||||
import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants';
|
||||
import { TFeedback, feedbackSchema } from './feedback';
|
||||
import type { SearchResultData } from './types/web';
|
||||
import type { TEphemeralAgent } from './types';
|
||||
import type { TFile } from './types/files';
|
||||
|
|
@ -518,6 +519,7 @@ export const tMessageSchema = z.object({
|
|||
thread_id: z.string().optional(),
|
||||
/* frontend components */
|
||||
iconURL: z.string().nullable().optional(),
|
||||
feedback: feedbackSchema.optional(),
|
||||
});
|
||||
|
||||
export type TAttachmentMetadata = {
|
||||
|
|
@ -543,6 +545,7 @@ export type TMessage = z.input<typeof tMessageSchema> & {
|
|||
siblingIndex?: number;
|
||||
attachments?: TAttachment[];
|
||||
clientTimestamp?: string;
|
||||
feedback?: TFeedback;
|
||||
};
|
||||
|
||||
export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import type {
|
|||
TConversationTag,
|
||||
TBanner,
|
||||
} from './schemas';
|
||||
import { TMinimalFeedback } from './feedback';
|
||||
import { SettingDefinition } from './generate';
|
||||
|
||||
export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam;
|
||||
|
||||
export * from './schemas';
|
||||
|
|
@ -547,6 +549,16 @@ export type TAcceptTermsResponse = {
|
|||
|
||||
export type TBannerResponse = TBanner | null;
|
||||
|
||||
export type TUpdateFeedbackRequest = {
|
||||
feedback?: TMinimalFeedback;
|
||||
};
|
||||
|
||||
export type TUpdateFeedbackResponse = {
|
||||
messageId: string;
|
||||
conversationId: string;
|
||||
feedback?: TMinimalFeedback;
|
||||
}
|
||||
|
||||
export type TBalanceResponse = {
|
||||
tokenCredits: number;
|
||||
// Automatic refill settings
|
||||
|
|
|
|||
|
|
@ -264,19 +264,19 @@ describe('convertJsonSchemaToZod', () => {
|
|||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The user\'s name',
|
||||
description: "The user's name",
|
||||
},
|
||||
age: {
|
||||
type: 'number',
|
||||
description: 'The user\'s age',
|
||||
description: "The user's age",
|
||||
},
|
||||
},
|
||||
};
|
||||
const zodSchema = convertJsonSchemaToZod(schema);
|
||||
|
||||
const shape = (zodSchema as z.ZodObject<any>).shape;
|
||||
expect(shape.name.description).toBe('The user\'s name');
|
||||
expect(shape.age.description).toBe('The user\'s age');
|
||||
expect(shape.name.description).toBe("The user's name");
|
||||
expect(shape.age.description).toBe("The user's age");
|
||||
});
|
||||
|
||||
it('should preserve descriptions in nested objects', () => {
|
||||
|
|
@ -290,7 +290,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The user\'s name',
|
||||
description: "The user's name",
|
||||
},
|
||||
settings: {
|
||||
type: 'object',
|
||||
|
|
@ -318,7 +318,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
|
||||
const userShape = shape.user instanceof z.ZodObject ? shape.user.shape : {};
|
||||
if ('name' in userShape && 'settings' in userShape) {
|
||||
expect(userShape.name.description).toBe('The user\'s name');
|
||||
expect(userShape.name.description).toBe("The user's name");
|
||||
expect(userShape.settings.description).toBe('User preferences');
|
||||
|
||||
const settingsShape =
|
||||
|
|
@ -682,10 +682,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['name'] },
|
||||
{ required: ['age'] },
|
||||
],
|
||||
anyOf: [{ required: ['name'] }, { required: ['age'] }],
|
||||
oneOf: [
|
||||
{ properties: { role: { type: 'string', enum: ['admin'] } } },
|
||||
{ properties: { role: { type: 'string', enum: ['user'] } } },
|
||||
|
|
@ -708,7 +705,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
it('should drop fields from nested schemas', () => {
|
||||
// Create a schema with nested fields that should be dropped
|
||||
const schema: JsonSchemaType & {
|
||||
properties?: Record<string, JsonSchemaType & { anyOf?: any; oneOf?: any }>
|
||||
properties?: Record<string, JsonSchemaType & { anyOf?: any; oneOf?: any }>;
|
||||
} = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
@ -718,10 +715,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
name: { type: 'string' },
|
||||
role: { type: 'string' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['name'] },
|
||||
{ required: ['role'] },
|
||||
],
|
||||
anyOf: [{ required: ['name'] }, { required: ['role'] }],
|
||||
},
|
||||
settings: {
|
||||
type: 'object',
|
||||
|
|
@ -742,20 +736,24 @@ describe('convertJsonSchemaToZod', () => {
|
|||
});
|
||||
|
||||
// The schema should still validate normal properties
|
||||
expect(zodSchema?.parse({
|
||||
user: { name: 'John', role: 'admin' },
|
||||
settings: { theme: 'custom' }, // This would fail if oneOf was still present
|
||||
})).toEqual({
|
||||
expect(
|
||||
zodSchema?.parse({
|
||||
user: { name: 'John', role: 'admin' },
|
||||
settings: { theme: 'custom' }, // This would fail if oneOf was still present
|
||||
}),
|
||||
).toEqual({
|
||||
user: { name: 'John', role: 'admin' },
|
||||
settings: { theme: 'custom' },
|
||||
});
|
||||
|
||||
// But the anyOf constraint should be gone from user
|
||||
// (If it was present, this would fail because neither name nor role is required)
|
||||
expect(zodSchema?.parse({
|
||||
user: {},
|
||||
settings: { theme: 'light' },
|
||||
})).toEqual({
|
||||
expect(
|
||||
zodSchema?.parse({
|
||||
user: {},
|
||||
settings: { theme: 'light' },
|
||||
}),
|
||||
).toEqual({
|
||||
user: {},
|
||||
settings: { theme: 'light' },
|
||||
});
|
||||
|
|
@ -803,10 +801,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
anyOf: [{ minItems: 1 }],
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{ required: ['name', 'permissions'] },
|
||||
{ required: ['name'] },
|
||||
],
|
||||
oneOf: [{ required: ['name', 'permissions'] }, { required: ['name'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -871,10 +866,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
const schema = {
|
||||
type: 'object', // Add a type to satisfy JsonSchemaType
|
||||
properties: {}, // Empty properties
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{ type: 'number' },
|
||||
],
|
||||
oneOf: [{ type: 'string' }, { type: 'number' }],
|
||||
} as JsonSchemaType & { oneOf?: any };
|
||||
|
||||
// Convert with transformOneOfAnyOf option
|
||||
|
|
@ -893,10 +885,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
const schema = {
|
||||
type: 'object', // Add a type to satisfy JsonSchemaType
|
||||
properties: {}, // Empty properties
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{ type: 'number' },
|
||||
],
|
||||
anyOf: [{ type: 'string' }, { type: 'number' }],
|
||||
} as JsonSchemaType & { anyOf?: any };
|
||||
|
||||
// Convert with transformOneOfAnyOf option
|
||||
|
|
@ -956,10 +945,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
properties: {
|
||||
value: { type: 'string' },
|
||||
},
|
||||
oneOf: [
|
||||
{ required: ['value'] },
|
||||
{ properties: { optional: { type: 'boolean' } } },
|
||||
],
|
||||
oneOf: [{ required: ['value'] }, { properties: { optional: { type: 'boolean' } } }],
|
||||
} as JsonSchemaType & { oneOf?: any };
|
||||
|
||||
// Convert with transformOneOfAnyOf option
|
||||
|
|
@ -1013,9 +999,12 @@ describe('convertJsonSchemaToZod', () => {
|
|||
},
|
||||
},
|
||||
} as JsonSchemaType & {
|
||||
properties?: Record<string, JsonSchemaType & {
|
||||
properties?: Record<string, JsonSchemaType & { oneOf?: any }>
|
||||
}>
|
||||
properties?: Record<
|
||||
string,
|
||||
JsonSchemaType & {
|
||||
properties?: Record<string, JsonSchemaType & { oneOf?: any }>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
// Convert with transformOneOfAnyOf option
|
||||
|
|
@ -1024,14 +1013,16 @@ describe('convertJsonSchemaToZod', () => {
|
|||
});
|
||||
|
||||
// The schema should validate nested unions
|
||||
expect(zodSchema?.parse({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'email',
|
||||
email: 'test@example.com',
|
||||
expect(
|
||||
zodSchema?.parse({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'email',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toEqual({
|
||||
}),
|
||||
).toEqual({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'email',
|
||||
|
|
@ -1040,14 +1031,16 @@ describe('convertJsonSchemaToZod', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(zodSchema?.parse({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'phone',
|
||||
phone: '123-456-7890',
|
||||
expect(
|
||||
zodSchema?.parse({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'phone',
|
||||
phone: '123-456-7890',
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toEqual({
|
||||
}),
|
||||
).toEqual({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'phone',
|
||||
|
|
@ -1057,14 +1050,16 @@ describe('convertJsonSchemaToZod', () => {
|
|||
});
|
||||
|
||||
// Should reject invalid contact types
|
||||
expect(() => zodSchema?.parse({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'email',
|
||||
phone: '123-456-7890', // Missing email, has phone instead
|
||||
expect(() =>
|
||||
zodSchema?.parse({
|
||||
user: {
|
||||
contact: {
|
||||
type: 'email',
|
||||
phone: '123-456-7890', // Missing email, has phone instead
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toThrow();
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('should work with dropFields option', () => {
|
||||
|
|
@ -1072,10 +1067,7 @@ describe('convertJsonSchemaToZod', () => {
|
|||
const schema = {
|
||||
type: 'object', // Add a type to satisfy JsonSchemaType
|
||||
properties: {}, // Empty properties
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{ type: 'number' },
|
||||
],
|
||||
oneOf: [{ type: 'string' }, { type: 'number' }],
|
||||
deprecated: true, // Field to drop
|
||||
} as JsonSchemaType & { oneOf?: any; deprecated?: boolean };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue