Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down Expand Up @@ -520,7 +537,7 @@ export const CreateBaseModal = memo(function CreateBaseModal({
: 'Creating...'
: 'Creating...'
: 'Create',
onClick: handleSubmit(onSubmit),
onClick: handleSubmit(onSubmit, onInvalid),
disabled: isSubmitting || !nameValue?.trim(),
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion apps/sim/lib/api/contracts/knowledge/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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),
Expand Down
17 changes: 15 additions & 2 deletions apps/sim/lib/api/contracts/v1/knowledge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/**`)
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/knowledge/constants.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading