fix(knowledge): surface KB description validation errors and raise limit to 10k (#5347)

This commit is contained in:
Theodore Li
2026-07-01 18:29:45 -07:00
committed by GitHub
parent 8d6cec114a
commit 74571840cd
5 changed files with 64 additions and 17 deletions
@@ -17,14 +17,16 @@ import {
type ComboboxOption,
cn,
Loader,
toast,
} from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { type FieldErrors, useForm } from 'react-hook-form'
import { z } from 'zod'
import type { StrategyOptions } from '@/lib/chunkers/types'
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload'
@@ -58,7 +60,13 @@ const FormSchema = z
.min(1, 'Name is required')
.max(100, 'Name must be less than 100 characters')
.refine((value) => value.trim().length > 0, 'Name cannot be empty'),
description: z.string().max(500, 'Description must be less than 500 characters').optional(),
description: z
.string()
.max(
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
)
.optional(),
minChunkSize: z
.number()
.min(1, 'Min chunk size must be at least 1 character')
@@ -223,6 +231,15 @@ export const CreateBaseModal = memo(function CreateBaseModal({
const isSubmitting =
createKnowledgeBaseMutation.isPending || deleteKnowledgeBaseMutation.isPending || isUploading
const onInvalid = (formErrors: FieldErrors<FormInputValues>) => {
const firstMessage = Object.values(formErrors).find(
(fieldError) => typeof fieldError?.message === 'string'
)?.message
toast.error(
typeof firstMessage === 'string' ? firstMessage : 'Please fix the highlighted fields'
)
}
const onSubmit = async (data: FormValues) => {
setSubmitStatus(null)
@@ -292,7 +309,7 @@ export const CreateBaseModal = memo(function CreateBaseModal({
<ChipModal open={open} onOpenChange={handleClose} srTitle='Create Knowledge Base' size='lg'>
<ChipModalHeader onClose={() => handleClose(false)}>Create Knowledge Base</ChipModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
<form onSubmit={handleSubmit(onSubmit, onInvalid)} className='flex min-h-0 flex-1 flex-col'>
<button type='submit' hidden disabled={isSubmitting || !nameValue?.trim()} />
<ChipModalBody>
<input
@@ -317,7 +334,7 @@ export const CreateBaseModal = memo(function CreateBaseModal({
/>
</ChipModalField>
<ChipModalField type='custom' title='Description'>
<ChipModalField type='custom' title='Description' error={errors.description?.message}>
<ChipTextarea
placeholder='Describe this knowledge base (optional)'
rows={4}
@@ -520,7 +537,7 @@ export const CreateBaseModal = memo(function CreateBaseModal({
: 'Creating...'
: 'Creating...'
: 'Create',
onClick: handleSubmit(onSubmit),
onClick: handleSubmit(onSubmit, onInvalid),
disabled: isSubmitting || !nameValue?.trim(),
}}
/>
@@ -8,9 +8,11 @@ import {
ChipModalField,
ChipModalFooter,
ChipModalHeader,
toast,
} from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
import type { ChunkingConfig } from '@/lib/knowledge/types'
const logger = createLogger('EditKnowledgeBaseModal')
@@ -60,31 +62,36 @@ export const EditKnowledgeBaseModal = memo(function EditKnowledgeBaseModal({
}
}
const validate = (): boolean => {
let valid = true
const validate = (): string | null => {
let firstError: string | null = null
if (!name.trim()) {
setNameError('Name is required')
valid = false
firstError ??= 'Name is required'
} else if (name.trim().length > 100) {
setNameError('Name must be less than 100 characters')
valid = false
firstError ??= 'Name must be less than 100 characters'
} else {
setNameError(null)
}
if (description.length > 500) {
setDescriptionError('Description must be less than 500 characters')
valid = false
if (description.length > KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH) {
const message = `Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
setDescriptionError(message)
firstError ??= message
} else {
setDescriptionError(null)
}
return valid
return firstError
}
const handleSubmit = async () => {
if (!validate()) return
const validationError = validate()
if (validationError) {
toast.error(validationError)
return
}
setIsSubmitting(true)
setError(null)
+8 -1
View File
@@ -7,6 +7,7 @@ import {
} from '@/lib/api/contracts/knowledge/shared'
import { defineRouteContract } from '@/lib/api/contracts/types'
import type { StrategyOptions } from '@/lib/chunkers/types'
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
export const knowledgeScopeSchema = z.enum(['active', 'archived', 'all'])
export type KnowledgeScope = z.output<typeof knowledgeScopeSchema>
@@ -51,7 +52,13 @@ export const chunkingConfigSchema = z
export const createKnowledgeBaseBodySchema = z.object({
name: z.string().min(1, 'Name is required'),
description: z.string().optional(),
description: z
.string()
.max(
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
)
.optional(),
workspaceId: z.string().min(1, 'Workspace ID is required'),
embeddingModel: z.literal('text-embedding-3-small').default('text-embedding-3-small'),
embeddingDimension: z.literal(1536).default(1536),
@@ -5,6 +5,7 @@ import {
successResponseSchema,
} from '@/lib/api/contracts/knowledge/shared'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
/**
* Public API v1 schemas (`/api/v1/knowledge/**`)
@@ -36,7 +37,13 @@ export const v1ListKnowledgeBasesQuerySchema = z.object({
export const v1CreateKnowledgeBaseBodySchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
name: z.string().min(1, 'Name is required').max(255, 'Name must be 255 characters or less'),
description: z.string().max(1000, 'Description must be 1000 characters or less').optional(),
description: z
.string()
.max(
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
)
.optional(),
chunkingConfig: v1ChunkingConfigSchema.optional().default({
maxSize: 1024,
minSize: 100,
@@ -54,7 +61,13 @@ export const v1UpdateKnowledgeBaseBodySchema = z
.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
name: z.string().min(1).max(255, 'Name must be 255 characters or less').optional(),
description: z.string().max(1000, 'Description must be 1000 characters or less').optional(),
description: z
.string()
.max(
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
)
.optional(),
chunkingConfig: z
.object({
maxSize: z.number().min(100).max(4000),
+3
View File
@@ -1,3 +1,6 @@
/** Max character length for a knowledge base description, enforced at every layer (UI, internal API, v1 API). */
export const KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH = 10_000
export const TAG_SLOT_CONFIG = {
text: {
slots: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const,