From e53335e66bcf0f4e64b3eb756cec2b47500a864e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 30 Jul 2025 12:37:59 -0700 Subject: [PATCH 1/3] improvement(doc-tags-subblock): use table for doc tags create doc tool in KB block --- .../document-tag-entry/document-tag-entry.tsx | 450 ++++++++++++------ 1 file changed, 295 insertions(+), 155 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx index 916b461bd68..0526128ed49 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx @@ -1,18 +1,28 @@ 'use client' -import { Plus, X } from 'lucide-react' +import { useMemo, useState } from 'react' +import { Plus, Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { MAX_TAG_SLOTS } from '@/lib/constants/knowledge' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { cn } from '@/lib/utils' import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' import { useSubBlockValue } from '../../hooks/use-sub-block-value' -interface DocumentTag { +interface DocumentTagRow { id: string - tagName: string // This will be mapped to displayName for API - fieldType: string - value: string + cells: { + tagName: string + type: string + value: string + } } interface DocumentTagEntryProps { @@ -32,7 +42,7 @@ export function DocumentTagEntry({ previewValue, isConnecting = false, }: DocumentTagEntryProps) { - const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) // Get the knowledge base ID from other sub-blocks const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId') @@ -41,176 +51,306 @@ export function DocumentTagEntry({ // Use KB tag definitions hook to get available tags const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) - // Parse the current value to extract tags - const parseTags = (tagValue: string): DocumentTag[] => { - if (!tagValue) return [] - try { - return JSON.parse(tagValue) - } catch { - return [] + // State for dropdown visibility - one for each row + const [dropdownStates, setDropdownStates] = useState>({}) + + // Use preview value when in preview mode, otherwise use store value + const currentValue = isPreview ? previewValue : storeValue + + // Transform stored JSON string to table format for display + const rows = useMemo(() => { + // If we have stored data, use it + if (currentValue) { + try { + const tagData = JSON.parse(currentValue) + if (Array.isArray(tagData) && tagData.length > 0) { + return tagData.map((tag: any, index: number) => ({ + id: `tag-${index}`, + cells: { + tagName: tag.tagName || '', + type: tag.fieldType || 'text', + value: tag.value || '', + }, + })) + } + } catch { + // If parsing fails, fall through to default + } } + + // Default: just one empty row + return [ + { + id: 'empty-row', + cells: { tagName: '', type: 'text', value: '' }, + }, + ] + }, [currentValue]) + + // Get available tag names and check for case-insensitive duplicates + const usedTagNames = new Set( + rows.map((row) => row.cells.tagName?.toLowerCase()).filter((name) => name && name.trim()) + ) + + const availableTagDefinitions = tagDefinitions.filter( + (def) => !usedTagNames.has(def.displayName.toLowerCase()) + ) + + // Function to pre-fill existing tags + const handlePreFillTags = () => { + if (isPreview || disabled) return + + const existingTagRows = tagDefinitions.map((tagDef) => ({ + tagName: tagDef.displayName, + fieldType: tagDef.fieldType, + value: '', + })) + + const jsonString = existingTagRows.length > 0 ? JSON.stringify(existingTagRows) : '' + setStoreValue(jsonString) } - const currentValue = isPreview ? previewValue : storeValue - const tags = parseTags(currentValue || '') + const handleCellChange = (rowIndex: number, column: string, value: string) => { + if (isPreview || disabled) return + + const updatedRows = [...rows].map((row, idx) => { + if (idx === rowIndex) { + const newCells = { ...row.cells, [column]: value } + + // Auto-select type when existing tag is selected + if (column === 'tagName' && value) { + const tagDef = tagDefinitions.find( + (def) => def.displayName.toLowerCase() === value.toLowerCase() + ) + if (tagDef) { + newCells.type = tagDef.fieldType + } + } + + return { + ...row, + cells: newCells, + } + } + return row + }) + + // No auto-add rows - user will manually add them with plus button + + // Store all rows including empty ones - don't auto-remove + const dataToStore = updatedRows.map((row) => ({ + tagName: row.cells.tagName || '', + fieldType: row.cells.type || 'text', + value: row.cells.value || '', + })) - const updateTags = (newTags: DocumentTag[]) => { - if (isPreview) return - const value = newTags.length > 0 ? JSON.stringify(newTags) : null - setStoreValue(value) + const jsonString = dataToStore.length > 0 ? JSON.stringify(dataToStore) : '' + setStoreValue(jsonString) } - const removeTag = (tagId: string) => { - updateTags(tags.filter((t) => t.id !== tagId)) + const handleAddRow = () => { + if (isPreview || disabled) return + + // Get current data and add a new empty row + const currentData = currentValue ? JSON.parse(currentValue) : [] + const newData = [...currentData, { tagName: '', fieldType: 'text', value: '' }] + setStoreValue(JSON.stringify(newData)) } - const updateTag = (tagId: string, updates: Partial) => { - updateTags(tags.map((tag) => (tag.id === tagId ? { ...tag, ...updates } : tag))) + const handleDeleteRow = (rowIndex: number) => { + if (isPreview || disabled || rows.length <= 1) return + const updatedRows = rows.filter((_, idx) => idx !== rowIndex) + + // Store all remaining rows including empty ones - don't auto-remove + const tableDataForStorage = updatedRows.map((row) => ({ + tagName: row.cells.tagName || '', + fieldType: row.cells.type || 'text', + value: row.cells.value || '', + })) + + const jsonString = tableDataForStorage.length > 0 ? JSON.stringify(tableDataForStorage) : '' + setStoreValue(jsonString) } - // Get available tag names that aren't already used - const usedTagNames = new Set(tags.map((tag) => tag.tagName).filter(Boolean)) - const availableTagNames = tagDefinitions - .map((def) => def.displayName) - .filter((name) => !usedTagNames.has(name)) + // Check for duplicate tag names (case-insensitive) + const getDuplicateStatus = (rowIndex: number, tagName: string) => { + if (!tagName.trim()) return false + const lowerTagName = tagName.toLowerCase() + return rows.some( + (row, idx) => + idx !== rowIndex && + row.cells.tagName?.toLowerCase() === lowerTagName && + row.cells.tagName.trim() + ) + } if (isLoading) { return
Loading tag definitions...
} - return ( -
- {/* Available Tags Section */} - {availableTagNames.length > 0 && ( -
-
- Available Tags (click to add) -
-
- {availableTagNames.map((tagName) => { - const tagDef = tagDefinitions.find((def) => def.displayName === tagName) - return ( - - ) - })} -
-
- )} + const renderHeader = () => ( + + + Tag Name + Type + Value + + + ) + + const renderTagNameCell = (row: DocumentTagRow, rowIndex: number) => { + const cellValue = row.cells.tagName || '' + const isDuplicate = getDuplicateStatus(rowIndex, cellValue) + const showDropdown = dropdownStates[rowIndex] || false + + const setShowDropdown = (show: boolean) => { + setDropdownStates((prev) => ({ ...prev, [rowIndex]: show })) + } - {/* Selected Tags Section */} - {tags.length > 0 && ( -
-
- {tags.map((tag) => ( -
- {/* Tag Name */} -
-
- {tag.tagName || 'Unnamed Tag'} + return ( + +
+ handleCellChange(rowIndex, 'tagName', e.target.value)} + onFocus={() => setShowDropdown(true)} + onBlur={() => setTimeout(() => setShowDropdown(false), 200)} + disabled={disabled || isConnecting} + className={cn(isDuplicate && 'border-red-500 bg-red-50')} + /> + {showDropdown && availableTagDefinitions.length > 0 && ( +
+ {availableTagDefinitions + .filter((tagDef) => + tagDef.displayName.toLowerCase().includes(cellValue.toLowerCase()) + ) + .map((tagDef) => ( +
{ + handleCellChange(rowIndex, 'tagName', tagDef.displayName) + setShowDropdown(false) + }} + > + {tagDef.displayName}
-
{tag.fieldType}
-
- - {/* Value Input */} -
- updateTag(tag.id, { value: e.target.value })} - placeholder='Value' - disabled={disabled || isConnecting} - className='h-9 placeholder:text-xs' - type={tag.fieldType === 'number' ? 'number' : 'text'} - /> -
- - {/* Remove Button */} - -
- ))} -
+ ))} +
+ )}
- )} + + ) + } - {/* Create New Tag Section */} -
-
Create New Tag
-
-
- = MAX_TAG_SLOTS ? '' : 'Tag name'} - disabled={disabled || isConnecting || tagDefinitions.length >= MAX_TAG_SLOTS} - className='h-9 border-0 bg-transparent p-0 placeholder:text-xs focus-visible:ring-0' - onKeyDown={(e) => { - if (e.key === 'Enter' && e.currentTarget.value.trim()) { - const tagName = e.currentTarget.value.trim() - - // Check for duplicates - if (usedTagNames.has(tagName)) { - // Visual feedback for duplicate - could add toast notification here - e.currentTarget.style.borderColor = '#ef4444' - setTimeout(() => { - e.currentTarget.style.borderColor = '' - }, 1000) - return - } - - const newTag: DocumentTag = { - id: Date.now().toString(), - tagName, - fieldType: 'text', - value: '', - } - updateTags([...tags, newTag]) - e.currentTarget.value = '' - } - }} - /> -
-
- {tagDefinitions.length >= MAX_TAG_SLOTS - ? `All ${MAX_TAG_SLOTS} tag slots used in this knowledge base` - : usedTagNames.size > 0 - ? 'Press Enter (no duplicates)' - : 'Press Enter to add'} -
+ const renderTypeCell = (row: DocumentTagRow, rowIndex: number) => { + const cellValue = row.cells.type || 'text' + const tagName = row.cells.tagName || '' + + // Check if this is an existing tag (should be read-only) + const existingTag = tagDefinitions.find( + (def) => def.displayName.toLowerCase() === tagName.toLowerCase() + ) + const isReadOnly = !!existingTag + + return ( + + + + ) + } + + const renderValueCell = (row: DocumentTagRow, rowIndex: number) => { + const cellValue = row.cells.value || '' + + return ( + + handleCellChange(rowIndex, 'value', e.target.value)} + disabled={disabled || isConnecting} + /> + + ) + } + + const renderDeleteButton = (rowIndex: number) => { + // Allow deletion of any row + const canDelete = !isPreview && !disabled + + return canDelete ? ( + + + + ) : null + } + + // Show pre-fill button if there are available tags and only empty rows + const showPreFillButton = + tagDefinitions.length > 0 && + rows.length === 1 && + !rows[0].cells.tagName && + !rows[0].cells.value && + !isPreview && + !disabled + + return ( +
+ {showPreFillButton && ( +
+
+ )} +
+ + {renderHeader()} + + {rows.map((row, rowIndex) => ( + + {renderTagNameCell(row, rowIndex)} + {renderTypeCell(row, rowIndex)} + {renderValueCell(row, rowIndex)} + {renderDeleteButton(rowIndex)} + + ))} + +
- {/* Empty State */} - {tags.length === 0 && availableTagNames.length === 0 && ( -
-
No tags available
-
Create a new tag above to get started
+ {/* Add Row Button */} + {!isPreview && !disabled && ( +
+
)}
From 0c2b32606d5e4ede4b2e4fae1e7f1ad8f2f8d72f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 30 Jul 2025 12:52:56 -0700 Subject: [PATCH 2/3] enforce max tags --- .../document-tag-entry/document-tag-entry.tsx | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx index 0526128ed49..f5f8a2adc39 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx @@ -11,6 +11,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { MAX_TAG_SLOTS } from '@/lib/constants/knowledge' import { cn } from '@/lib/utils' import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' @@ -96,6 +97,16 @@ export function DocumentTagEntry({ (def) => !usedTagNames.has(def.displayName.toLowerCase()) ) + // Check if we can add more tags based on MAX_TAG_SLOTS + const newTagsBeingCreated = rows.filter( + (row) => + row.cells.tagName?.trim() && + !tagDefinitions.some( + (def) => def.displayName.toLowerCase() === row.cells.tagName.toLowerCase() + ) + ).length + const canAddMoreTags = tagDefinitions.length + newTagsBeingCreated < MAX_TAG_SLOTS + // Function to pre-fill existing tags const handlePreFillTags = () => { if (isPreview || disabled) return @@ -113,6 +124,30 @@ export function DocumentTagEntry({ const handleCellChange = (rowIndex: number, column: string, value: string) => { if (isPreview || disabled) return + // Check if this is a new tag name that would exceed the limit + if (column === 'tagName' && value.trim()) { + const isExistingTag = tagDefinitions.some( + (def) => def.displayName.toLowerCase() === value.toLowerCase() + ) + + if (!isExistingTag) { + // Count current new tags being created (excluding the current row) + const currentNewTags = rows.filter( + (row, idx) => + idx !== rowIndex && + row.cells.tagName?.trim() && + !tagDefinitions.some( + (def) => def.displayName.toLowerCase() === row.cells.tagName.toLowerCase() + ) + ).length + + if (tagDefinitions.length + currentNewTags >= MAX_TAG_SLOTS) { + // Don't allow creating new tags if we've reached the limit + return + } + } + } + const updatedRows = [...rows].map((row, idx) => { if (idx === rowIndex) { const newCells = { ...row.cells, [column]: value } @@ -346,11 +381,21 @@ export function DocumentTagEntry({ {/* Add Row Button */} {!isPreview && !disabled && ( -
- + + {/* Tag slots usage indicator */} +
+ {tagDefinitions.length + newTagsBeingCreated} of {MAX_TAG_SLOTS} tag slots used + {!canAddMoreTags && ( +
+ Maximum tag slots reached for this knowledge base +
+ )} +
)}
From 7f8d04b1fcbe0b332eb7cc4504c2833afbe4d0aa Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 30 Jul 2025 12:56:13 -0700 Subject: [PATCH 3/3] remove red warning text --- .../components/document-tag-entry/document-tag-entry.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx index f5f8a2adc39..95a253c0353 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-tag-entry/document-tag-entry.tsx @@ -390,11 +390,6 @@ export function DocumentTagEntry({ {/* Tag slots usage indicator */}
{tagDefinitions.length + newTagsBeingCreated} of {MAX_TAG_SLOTS} tag slots used - {!canAddMoreTags && ( -
- Maximum tag slots reached for this knowledge base -
- )}
)}