diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx index bdab7e2a328..74fd800c91f 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { handleKeyboardActivation, Label, Switch } from '@sim/emcn' +import { handleKeyboardActivation, Label, Switch, toast } from '@sim/emcn' import { isApiClientError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import { getKnowledgeChunkContract } from '@/lib/api/contracts/knowledge' @@ -59,6 +59,7 @@ export function ChunkEditor({ const [editedContent, setEditedContent] = useState(isCreateMode ? '' : chunkContent) const [savedContent, setSavedContent] = useState(chunkContent) + const validationToastIdRef = useRef(null) const [tokenizerOn, setTokenizerOn] = useState(false) const [hoveredTokenIndex, setHoveredTokenIndex] = useState(null) const savedContentRef = useRef(chunkContent) @@ -108,8 +109,18 @@ export function ChunkEditor({ const handleSave = useCallback(async () => { const content = editedContentRef.current const trimmed = content.trim() - if (trimmed.length === 0) throw new Error('Content cannot be empty') - if (trimmed.length > 10000) throw new Error('Content exceeds maximum length') + /** Toast every failed attempt, replacing the previous validation toast so retries refresh instead of stack. */ + const failValidation = (message: string): never => { + if (validationToastIdRef.current) toast.dismiss(validationToastIdRef.current) + validationToastIdRef.current = toast.error(message) + throw new Error(message) + } + if (trimmed.length === 0) failValidation('Content cannot be empty') + if (trimmed.length > 10000) failValidation('Content exceeds maximum length (10,000 characters)') + if (validationToastIdRef.current) { + toast.dismiss(validationToastIdRef.current) + validationToastIdRef.current = null + } if (isCreateMode) { const created = await createChunk({ diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/expanded-cell-popover.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/expanded-cell-popover.tsx index 4e51cef0a82..fc1c7daeb07 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/expanded-cell-popover.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/expanded-cell-popover.tsx @@ -197,12 +197,28 @@ function ExpandedCellEditor({ textareaRef, }: ExpandedCellEditorProps) { const [draftValue, setDraftValue] = useState(initialValue) + const [parseError, setParseError] = useState(null) const handleSave = () => { // `displayToStorage` only normalizes dates — it returns null for anything else. // Fall back to the raw draft for non-date columns, matching the inline editor. const raw = displayToStorage(draftValue) ?? draftValue - const cleaned = cleanCellValue(raw, column) + let cleaned: unknown + try { + cleaned = cleanCellValue(raw, column) + } catch { + setParseError('Invalid JSON') + return + } + /** `cleanCellValue` nulls unparseable dates/numbers instead of throwing — reject rather than silently clear. */ + if ( + cleaned === null && + draftValue.trim() !== '' && + (column.type === 'date' || column.type === 'number') + ) { + setParseError(column.type === 'date' ? 'Invalid date' : 'Invalid number') + return + } onSave(rowId, column.key, cleaned, 'blur') onClose() } @@ -219,16 +235,23 @@ function ExpandedCellEditor({