From ebcbb3c2bd485210bb0579b209db2e3e51a91a33 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 26 Jul 2025 12:24:33 -0700 Subject: [PATCH 01/19] fix lint --- .../[id]/documents/[documentId]/route.ts | 40 +- .../[documentId]/tag-definitions/route.ts | 209 + .../knowledge/[id]/[documentId]/document.tsx | 255 +- .../components/upload-modal/upload-modal.tsx | 31 +- .../components/create-modal/create-modal.tsx | 17 +- .../document-tag-entry/document-tag-entry.tsx | 276 + .../components/tag-input/tag-input.tsx | 27 +- .../knowledge-tag-filter.tsx | 73 + .../components/sub-block/sub-block.tsx | 12 + apps/sim/blocks/blocks/knowledge.ts | 103 +- apps/sim/blocks/types.ts | 1 + .../db/migrations/0063_greedy_sentinel.sql | 13 + .../sim/db/migrations/meta/0063_snapshot.json | 5634 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 26 + 15 files changed, 6633 insertions(+), 91 deletions(-) create mode 100644 apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx create mode 100644 apps/sim/db/migrations/0063_greedy_sentinel.sql create mode 100644 apps/sim/db/migrations/meta/0063_snapshot.json diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts index 8a92badf449..a17e2cc06f6 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts @@ -22,6 +22,14 @@ const UpdateDocumentSchema = z.object({ processingError: z.string().optional(), markFailedDueToTimeout: z.boolean().optional(), retryProcessing: z.boolean().optional(), + // Tag fields + tag1: z.string().optional(), + tag2: z.string().optional(), + tag3: z.string().optional(), + tag4: z.string().optional(), + tag5: z.string().optional(), + tag6: z.string().optional(), + tag7: z.string().optional(), }) export async function GET( @@ -209,9 +217,39 @@ export async function PUT( updateData.processingStatus = validatedData.processingStatus if (validatedData.processingError !== undefined) updateData.processingError = validatedData.processingError + + // Tag field updates + if (validatedData.tag1 !== undefined) updateData.tag1 = validatedData.tag1 + if (validatedData.tag2 !== undefined) updateData.tag2 = validatedData.tag2 + if (validatedData.tag3 !== undefined) updateData.tag3 = validatedData.tag3 + if (validatedData.tag4 !== undefined) updateData.tag4 = validatedData.tag4 + if (validatedData.tag5 !== undefined) updateData.tag5 = validatedData.tag5 + if (validatedData.tag6 !== undefined) updateData.tag6 = validatedData.tag6 + if (validatedData.tag7 !== undefined) updateData.tag7 = validatedData.tag7 } - await db.update(document).set(updateData).where(eq(document.id, documentId)) + await db.transaction(async (tx) => { + // Update the document + await tx.update(document).set(updateData).where(eq(document.id, documentId)) + + // If any tag fields were updated, also update the embeddings + const tagFields = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + const hasTagUpdates = tagFields.some((field) => validatedData[field] !== undefined) + + if (hasTagUpdates) { + const embeddingUpdateData: Record = {} + tagFields.forEach((field) => { + if (validatedData[field] !== undefined) { + embeddingUpdateData[field] = validatedData[field] || null + } + }) + + await tx + .update(embedding) + .set(embeddingUpdateData) + .where(eq(embedding.documentId, documentId)) + } + }) // Fetch the updated document const updatedDocument = await db diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts new file mode 100644 index 00000000000..77ea53fa9d0 --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts @@ -0,0 +1,209 @@ +import { randomUUID } from 'crypto' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' +import { db } from '@/db' +import { document, documentTagDefinitions } from '@/db/schema' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('DocumentTagDefinitionsAPI') + +const TagDefinitionSchema = z.object({ + tagSlot: z.enum(['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7']), + displayName: z.string().min(1, 'Display name is required').max(100, 'Display name too long'), + fieldType: z.string().default('text'), // Currently only 'text', future: 'date', 'number', 'range' +}) + +const BulkTagDefinitionsSchema = z.object({ + definitions: z.array(TagDefinitionSchema).max(7, 'Cannot define more than 7 tags'), +}) + +// GET /api/knowledge/[id]/documents/[documentId]/tag-definitions - Get tag definitions for a document +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string; documentId: string }> } +) { + const requestId = randomUUID().slice(0, 8) + const { id: knowledgeBaseId, documentId } = await params + + try { + logger.info(`[${requestId}] Getting tag definitions for document ${documentId}`) + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user has access to the knowledge base + const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id) + if (!accessCheck.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Verify document exists and belongs to the knowledge base + const documentExists = await db + .select({ id: document.id }) + .from(document) + .where(and(eq(document.id, documentId), eq(document.knowledgeBaseId, knowledgeBaseId))) + .limit(1) + + if (documentExists.length === 0) { + return NextResponse.json({ error: 'Document not found' }, { status: 404 }) + } + + // Get tag definitions for the document + const tagDefinitions = await db + .select({ + id: documentTagDefinitions.id, + tagSlot: documentTagDefinitions.tagSlot, + displayName: documentTagDefinitions.displayName, + fieldType: documentTagDefinitions.fieldType, + createdAt: documentTagDefinitions.createdAt, + updatedAt: documentTagDefinitions.updatedAt, + }) + .from(documentTagDefinitions) + .where(eq(documentTagDefinitions.documentId, documentId)) + + logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`) + + return NextResponse.json({ + success: true, + data: tagDefinitions, + }) + } catch (error) { + logger.error(`[${requestId}] Error getting tag definitions`, error) + return NextResponse.json({ error: 'Failed to get tag definitions' }, { status: 500 }) + } +} + +// POST /api/knowledge/[id]/documents/[documentId]/tag-definitions - Create/update tag definitions +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ id: string; documentId: string }> } +) { + const requestId = randomUUID().slice(0, 8) + const { id: knowledgeBaseId, documentId } = await params + + try { + logger.info(`[${requestId}] Creating/updating tag definitions for document ${documentId}`) + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user has write access to the knowledge base + const accessCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, session.user.id) + if (!accessCheck.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Verify document exists and belongs to the knowledge base + const documentExists = await db + .select({ id: document.id }) + .from(document) + .where(and(eq(document.id, documentId), eq(document.knowledgeBaseId, knowledgeBaseId))) + .limit(1) + + if (documentExists.length === 0) { + return NextResponse.json({ error: 'Document not found' }, { status: 404 }) + } + + const body = await req.json() + const validatedData = BulkTagDefinitionsSchema.parse(body) + + // Validate no duplicate tag slots + const tagSlots = validatedData.definitions.map((def) => def.tagSlot) + const uniqueTagSlots = new Set(tagSlots) + if (tagSlots.length !== uniqueTagSlots.size) { + return NextResponse.json({ error: 'Duplicate tag slots not allowed' }, { status: 400 }) + } + + const now = new Date() + const createdDefinitions = [] + + // Use transaction to ensure consistency + await db.transaction(async (tx) => { + // First, delete existing definitions for this document + await tx + .delete(documentTagDefinitions) + .where(eq(documentTagDefinitions.documentId, documentId)) + + // Then insert new definitions if any + if (validatedData.definitions.length > 0) { + const newDefinitions = validatedData.definitions.map((definition) => ({ + id: randomUUID(), + documentId, + tagSlot: definition.tagSlot, + displayName: definition.displayName, + fieldType: definition.fieldType, + createdAt: now, + updatedAt: now, + })) + + await tx.insert(documentTagDefinitions).values(newDefinitions) + createdDefinitions.push(...newDefinitions) + } + }) + + logger.info(`[${requestId}] Created/updated ${createdDefinitions.length} tag definitions`) + + return NextResponse.json({ + success: true, + data: createdDefinitions, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error creating/updating tag definitions`, error) + return NextResponse.json({ error: 'Failed to create/update tag definitions' }, { status: 500 }) + } +} + +// DELETE /api/knowledge/[id]/documents/[documentId]/tag-definitions - Delete all tag definitions for a document +export async function DELETE( + req: NextRequest, + { params }: { params: Promise<{ id: string; documentId: string }> } +) { + const requestId = randomUUID().slice(0, 8) + const { id: knowledgeBaseId, documentId } = await params + + try { + logger.info(`[${requestId}] Deleting tag definitions for document ${documentId}`) + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user has write access to the knowledge base + const accessCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, session.user.id) + if (!accessCheck.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Delete tag definitions for the document + const result = await db + .delete(documentTagDefinitions) + .where(eq(documentTagDefinitions.documentId, documentId)) + + logger.info(`[${requestId}] Deleted tag definitions for document ${documentId}`) + + return NextResponse.json({ + success: true, + message: 'Tag definitions deleted successfully', + }) + } catch (error) { + logger.error(`[${requestId}] Error deleting tag definitions`, error) + return NextResponse.json({ error: 'Failed to delete tag definitions' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index b31b803bbcf..1f28e24ee63 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -10,8 +10,13 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { createLogger } from '@/lib/logs/console-logger' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/components/providers/workspace-permissions-provider' import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar' +import { + type DocumentTag, + DocumentTagEntry, +} from '@/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry' import { SearchInput } from '@/app/workspace/[workspaceId]/knowledge/components/search-input/search-input' import { useDocumentChunks } from '@/hooks/use-knowledge' +import { useTagDefinitions } from '@/hooks/use-tag-definitions' import { type ChunkData, type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store' import { KnowledgeHeader } from '../../components/knowledge-header/knowledge-header' import { CreateChunkModal } from './components/create-chunk-modal/create-chunk-modal' @@ -45,7 +50,12 @@ export function Document({ knowledgeBaseName, documentName, }: DocumentProps) { - const { getCachedKnowledgeBase, getCachedDocuments } = useKnowledgeStore() + const { + getCachedKnowledgeBase, + getCachedDocuments, + updateDocument: updateDocumentInStore, + refreshDocuments, + } = useKnowledgeStore() const { workspaceId } = useParams() const router = useRouter() const searchParams = useSearchParams() @@ -76,15 +86,100 @@ export function Document({ const [selectedChunks, setSelectedChunks] = useState>(new Set()) const [selectedChunk, setSelectedChunk] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) + const [showTagEntry, setShowTagEntry] = useState(false) + const [documentTags, setDocumentTags] = useState([]) + const [documentData, setDocumentData] = useState(null) + const [isLoadingDocument, setIsLoadingDocument] = useState(true) + const [error, setError] = useState(null) + + // Use tag definitions hook for custom labels + const { getTagLabel, tagDefinitions, fetchTagDefinitions } = useTagDefinitions( + knowledgeBaseId, + documentId + ) + + // Function to build document tags from data and definitions + const buildDocumentTags = useCallback((docData: DocumentData, definitions: any[]) => { + const tags: DocumentTag[] = [] + const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + + tagSlots.forEach((slot) => { + const value = docData[slot] + const definition = definitions.find((def) => def.tagSlot === slot) + + // Include tag if it has a value OR if it has a custom definition + if (value?.trim() || definition?.displayName?.trim()) { + tags.push({ + slot, + displayName: definition?.displayName || '', + fieldType: definition?.fieldType || 'text', + value: value?.trim() || '', + }) + } + }) + + return tags + }, []) + + // Handle tag updates (local state only, no API calls) + const handleTagsChange = useCallback((newTags: DocumentTag[]) => { + // Only update local state, don't save to API + setDocumentTags(newTags) + }, []) + + // Handle saving document tag values to the API + const handleSaveDocumentTags = useCallback( + async (tagsToSave: DocumentTag[]) => { + if (!documentData) return + + try { + // Convert DocumentTag array to tag data for API + const tagData: Record = {} + const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + + // Clear all tags first + tagSlots.forEach((slot) => { + tagData[slot] = '' + }) + + // Set values from tagsToSave + tagsToSave.forEach((tag) => { + if (tag.value.trim()) { + tagData[tag.slot] = tag.value.trim() + } + }) + + // Update document via API + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/documents/${documentId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(tagData), + }) + + if (!response.ok) { + throw new Error('Failed to update document tags') + } + + // Update the document in the store and local state + updateDocumentInStore(knowledgeBaseId, documentId, tagData) + setDocumentData((prev) => (prev ? { ...prev, ...tagData } : null)) + + // Refresh tag definitions to update the display + await fetchTagDefinitions() + } catch (error) { + logger.error('Error updating document tags:', error) + throw error // Re-throw so the component can handle it + } + }, + [documentData, knowledgeBaseId, documentId, updateDocumentInStore, fetchTagDefinitions] + ) const [isCreateChunkModalOpen, setIsCreateChunkModalOpen] = useState(false) const [chunkToDelete, setChunkToDelete] = useState(null) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [isBulkOperating, setIsBulkOperating] = useState(false) - const [document, setDocument] = useState(null) - const [isLoadingDocument, setIsLoadingDocument] = useState(true) - const [error, setError] = useState(null) - const combinedError = error || chunksError // URL synchronization for pagination @@ -116,7 +211,10 @@ export function Document({ const cachedDoc = cachedDocuments?.documents?.find((d) => d.id === documentId) if (cachedDoc) { - setDocument(cachedDoc) + setDocumentData(cachedDoc) + // Initialize tags from cached document + const initialTags = buildDocumentTags(cachedDoc, tagDefinitions) + setDocumentTags(initialTags) setIsLoadingDocument(false) return } @@ -133,7 +231,10 @@ export function Document({ const result = await response.json() if (result.success) { - setDocument(result.data) + setDocumentData(result.data) + // Initialize tags from fetched document + const initialTags = buildDocumentTags(result.data, tagDefinitions) + setDocumentTags(initialTags) } else { throw new Error(result.error || 'Failed to fetch document') } @@ -148,11 +249,11 @@ export function Document({ if (knowledgeBaseId && documentId) { fetchDocument() } - }, [knowledgeBaseId, documentId, getCachedDocuments]) + }, [knowledgeBaseId, documentId, getCachedDocuments, buildDocumentTags, tagDefinitions]) const knowledgeBase = getCachedKnowledgeBase(knowledgeBaseId) const effectiveKnowledgeBaseName = knowledgeBase?.name || knowledgeBaseName || 'Knowledge Base' - const effectiveDocumentName = document?.filename || documentName || 'Document' + const effectiveDocumentName = documentData?.filename || documentName || 'Document' const breadcrumbs = [ { label: 'Knowledge', href: `/workspace/${workspaceId}/knowledge` }, @@ -391,16 +492,16 @@ export function Document({ value={searchQuery} onChange={setSearchQuery} placeholder={ - document?.processingStatus === 'completed' + documentData?.processingStatus === 'completed' ? 'Search chunks...' : 'Document processing...' } - disabled={document?.processingStatus !== 'completed'} + disabled={documentData?.processingStatus !== 'completed'} /> + + +

{showTagEntry ? 'Hide tag editor' : 'Edit document tags'}

+
+ + )} + + + ) : userPermissions.canEdit ? ( +
+
+

No document tags set

+ + + + + +

{showTagEntry ? 'Hide tag editor' : 'Add document tags'}

+
+
) : null })()} + {/* Document Tag Entry */} + {showTagEntry && userPermissions.canEdit && ( +
+ +
+ )} + {/* Error State for chunks */} {combinedError && !isLoadingAllChunks && (
@@ -467,7 +661,8 @@ export function Document({ checked={isAllSelected} onCheckedChange={handleSelectAll} disabled={ - document?.processingStatus !== 'completed' || !userPermissions.canEdit + documentData?.processingStatus !== 'completed' || + !userPermissions.canEdit } aria-label='Select all chunks' className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[#701FFC]/20 data-[state=checked]:border-[#701FFC] data-[state=checked]:bg-[#701FFC] [&>*]:h-3 [&>*]:w-3' @@ -509,7 +704,7 @@ export function Document({ - {document?.processingStatus !== 'completed' ? ( + {documentData?.processingStatus !== 'completed' ? (
@@ -521,13 +716,13 @@ export function Document({
- {document?.processingStatus === 'pending' && + {documentData?.processingStatus === 'pending' && 'Document processing pending...'} - {document?.processingStatus === 'processing' && + {documentData?.processingStatus === 'processing' && 'Document processing in progress...'} - {document?.processingStatus === 'failed' && + {documentData?.processingStatus === 'failed' && 'Document processing failed'} - {!document?.processingStatus && 'Document not ready'} + {!documentData?.processingStatus && 'Document not ready'}
@@ -553,7 +748,7 @@ export function Document({
- {document?.processingStatus === 'completed' + {documentData?.processingStatus === 'completed' ? searchQuery.trim() ? 'No chunks match your search' : 'No chunks found' @@ -703,7 +898,7 @@ export function Document({
{/* Pagination Controls */} - {document?.processingStatus === 'completed' && totalPages > 1 && ( + {documentData?.processingStatus === 'completed' && totalPages > 1 && (
+ )} + {tags.length < 7 && ( + + )} +
+
+ + {tags.length === 0 ? ( +
+

+ No tags added yet. Click "Add Tag" to create your first tag. +

+
+ ) : ( +
+ {tags.map((tag, index) => { + const validation = getTagValidation(tag) + + return ( +
+ {/* Remove button - positioned at top right */} + + +
+ {/* Tag Name */} +
+ + updateTag(index, 'displayName', e.target.value)} + disabled={disabled} + className={`mt-1.5 text-sm ${!validation.isValid ? 'border-red-300 focus:border-red-500' : ''}`} + /> + {validation.errorMessage && ( +

{validation.errorMessage}

+ )} +
+ + {/* Field Type */} +
+ + +
+ + {/* Tag Value */} +
+ + updateTag(index, 'value', e.target.value)} + disabled={disabled} + className='mt-1.5 text-sm' + /> +
+
+
+ ) + })} +
+ )} + + {tags.length > 0 && ( +
{tags.length} of 7 tags used
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx index 3489714e0f9..f81b7c232a2 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { useTagDefinitions } from '@/hooks/use-tag-definitions' export interface TagData { tag1?: string @@ -22,6 +23,8 @@ interface TagInputProps { onTagsChange: (tags: TagData) => void disabled?: boolean className?: string + knowledgeBaseId?: string | null + documentId?: string | null } const TAG_LABELS = [ @@ -34,10 +37,20 @@ const TAG_LABELS = [ { key: 'tag7' as keyof TagData, label: 'Tag 7', placeholder: 'Enter tag value' }, ] -export function TagInput({ tags, onTagsChange, disabled = false, className = '' }: TagInputProps) { +export function TagInput({ + tags, + onTagsChange, + disabled = false, + className = '', + knowledgeBaseId = null, + documentId = null, +}: TagInputProps) { const [isOpen, setIsOpen] = useState(false) const [showAllTags, setShowAllTags] = useState(false) + // Use custom tag definitions if available + const { getTagLabel } = useTagDefinitions(knowledgeBaseId, documentId) + const handleTagChange = (tagKey: keyof TagData, value: string) => { onTagsChange({ ...tags, @@ -53,7 +66,15 @@ export function TagInput({ tags, onTagsChange, disabled = false, className = '' } const hasAnyTags = Object.values(tags).some((tag) => tag?.trim()) - const visibleTags = showAllTags ? TAG_LABELS : TAG_LABELS.slice(0, 2) + + // Create tag labels using custom definitions or fallback to defaults + const tagLabels = TAG_LABELS.map(({ key, placeholder }) => ({ + key, + label: getTagLabel(key), + placeholder, + })) + + const visibleTags = showAllTags ? tagLabels : tagLabels.slice(0, 2) return (
@@ -153,7 +174,7 @@ export function TagInput({ tags, onTagsChange, disabled = false, className = ''
{Object.entries(tags).map(([key, value]) => { if (!value?.trim()) return null - const tagLabel = TAG_LABELS.find((t) => t.key === key)?.label || key + const tagLabel = getTagLabel(key) return ( { + if (isPreview) return + setStoreValue(value.trim() || null) + } + + // Get placeholder text + const placeholder = subBlock.placeholder || `Filter by ${customLabel.toLowerCase()}` + + return ( +
+ + handleChange(e.target.value)} + placeholder={placeholder} + disabled={disabled || isPreview} + className='text-sm' + /> +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index ef64bc70533..ca75adcd000 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -19,6 +19,7 @@ import { FileSelectorInput } from './components/file-selector/file-selector-inpu import { FileUpload } from './components/file-upload' import { FolderSelectorInput } from './components/folder-selector/components/folder-selector-input' import { KnowledgeBaseSelector } from './components/knowledge-base-selector/knowledge-base-selector' +import { KnowledgeTagFilter } from './components/knowledge-tag-filter/knowledge-tag-filter' import { LongInput } from './components/long-input' import { ProjectSelectorInput } from './components/project-selector/project-selector-input' import { ResponseFormat } from './components/response/response-format' @@ -352,6 +353,17 @@ export function SubBlock({ previewValue={previewValue} /> ) + case 'knowledge-tag-filter': + return ( + + ) case 'document-selector': return ( statement-breakpoint +ALTER TABLE "document_tag_definitions" ADD CONSTRAINT "document_tag_definitions_document_id_document_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."document"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "doc_tag_definitions_doc_slot_idx" ON "document_tag_definitions" USING btree ("document_id","tag_slot");--> statement-breakpoint +CREATE INDEX "doc_tag_definitions_doc_id_idx" ON "document_tag_definitions" USING btree ("document_id"); \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0063_snapshot.json b/apps/sim/db/migrations/meta/0063_snapshot.json new file mode 100644 index 00000000000..0a602c699ef --- /dev/null +++ b/apps/sim/db/migrations/meta/0063_snapshot.json @@ -0,0 +1,5634 @@ +{ + "id": "23d1b664-e4b0-4886-89f0-59e465f7a545", + "prevId": "5fe645b1-2d33-4fd7-8144-49dc2f3a3fd6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_checkpoints": { + "name": "copilot_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "yaml": { + "name": "yaml", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_checkpoints_user_id_idx": { + "name": "copilot_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_checkpoints_workflow_id_idx": { + "name": "copilot_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_checkpoints_chat_id_idx": { + "name": "copilot_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_checkpoints_user_workflow_idx": { + "name": "copilot_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_checkpoints_workflow_chat_idx": { + "name": "copilot_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_checkpoints_created_at_idx": { + "name": "copilot_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_checkpoints_chat_created_at_idx": { + "name": "copilot_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_checkpoints_user_id_user_id_fk": { + "name": "copilot_checkpoints_user_id_user_id_fk", + "tableFrom": "copilot_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_checkpoints_workflow_id_workflow_id_fk": { + "name": "copilot_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "copilot_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "copilot_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_tag_definitions": { + "name": "document_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_tag_definitions_doc_slot_idx": { + "name": "doc_tag_definitions_doc_slot_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag_definitions_doc_id_idx": { + "name": "doc_tag_definitions_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_tag_definitions_document_id_document_id_fk": { + "name": "document_tag_definitions_document_id_document_id_fk", + "tableFrom": "document_tag_definitions", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR (metadata IS NOT NULL AND (metadata->>'perSeatAllowance' IS NOT NULL OR metadata->>'totalAllowance' IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_rate_limits_user_id_user_id_fk": { + "name": "user_rate_limits_user_id_user_id_fk", + "tableFrom": "user_rate_limits", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'5'" + }, + "usage_limit_set_by": { + "name": "usage_limit_set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "block_count": { + "name": "block_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_input_cost": { + "name": "total_input_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_output_cost": { + "name": "total_output_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_cost_idx": { + "name": "workflow_execution_logs_cost_idx", + "columns": [ + { + "expression": "total_cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_duration_idx": { + "name": "workflow_execution_logs_duration_idx", + "columns": [ + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index 427c5fae963..de807e0e2a0 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -435,6 +435,13 @@ "when": 1753383446084, "tag": "0062_previous_phantom_reporter", "breakpoints": true + }, + { + "idx": 63, + "version": "7", + "when": 1753488920902, + "tag": "0063_greedy_sentinel", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 992f6557e8a..1a2b5e51f35 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -794,6 +794,32 @@ export const document = pgTable( }) ) +export const documentTagDefinitions = pgTable( + 'document_tag_definitions', + { + id: text('id').primaryKey(), + documentId: text('document_id') + .notNull() + .references(() => document.id, { onDelete: 'cascade' }), + tagSlot: text('tag_slot', { + enum: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'], + }).notNull(), + displayName: text('display_name').notNull(), + fieldType: text('field_type').notNull().default('text'), // 'text', future: 'date', 'number', 'range' + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + // Ensure unique tag slot per document + documentTagSlotIdx: uniqueIndex('doc_tag_definitions_doc_slot_idx').on( + table.documentId, + table.tagSlot + ), + // Index for querying by document + documentIdIdx: index('doc_tag_definitions_doc_id_idx').on(table.documentId), + }) +) + export const embedding = pgTable( 'embedding', { From 61562f6cfdae727f60a97407ed923ae1b8be3e4e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 26 Jul 2025 14:28:49 -0700 Subject: [PATCH 02/19] checkpoint --- .../[documentId]/tag-definitions/route.ts | 220 ++++++- .../knowledge/[id]/tag-definitions/route.ts | 57 ++ .../knowledge/[id]/[documentId]/document.tsx | 130 +--- .../document-tag-entry/document-tag-entry.tsx | 580 ++++++++++++------ .../components/tag-input/tag-input.tsx | 4 +- .../knowledge-tag-filter.tsx | 103 +++- .../db/migrations/0063_greedy_sentinel.sql | 13 - apps/sim/db/migrations/0063_lame_sandman.sql | 13 + .../sim/db/migrations/meta/0063_snapshot.json | 212 +++---- apps/sim/db/migrations/meta/_journal.json | 4 +- apps/sim/db/schema.ts | 18 +- .../use-knowledge-base-tag-definitions.ts | 87 +++ apps/sim/hooks/use-tag-definitions.ts | 166 +++++ 13 files changed, 1109 insertions(+), 498 deletions(-) create mode 100644 apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts delete mode 100644 apps/sim/db/migrations/0063_greedy_sentinel.sql create mode 100644 apps/sim/db/migrations/0063_lame_sandman.sql create mode 100644 apps/sim/hooks/use-knowledge-base-tag-definitions.ts create mode 100644 apps/sim/hooks/use-tag-definitions.ts diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts index 77ea53fa9d0..190e264e1f7 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts @@ -1,12 +1,12 @@ import { randomUUID } from 'crypto' -import { and, eq } from 'drizzle-orm' +import { and, eq, isNotNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' import { db } from '@/db' -import { document, documentTagDefinitions } from '@/db/schema' +import { document, knowledgeBaseTagDefinitions } from '@/db/schema' export const dynamic = 'force-dynamic' @@ -22,6 +22,92 @@ const BulkTagDefinitionsSchema = z.object({ definitions: z.array(TagDefinitionSchema).max(7, 'Cannot define more than 7 tags'), }) +// Helper function to clean up unused tag definitions +async function cleanupUnusedTagDefinitions(knowledgeBaseId: string, requestId: string) { + try { + // Get all current tag definitions for this KB + const currentDefinitions = await db + .select({ + id: knowledgeBaseTagDefinitions.id, + displayName: knowledgeBaseTagDefinitions.displayName, + tagSlot: knowledgeBaseTagDefinitions.tagSlot, + }) + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) + + if (currentDefinitions.length === 0) { + return 0 // No definitions to clean up + } + + // Check which tag names are actually in use by documents + const documentsWithTags = await db + .select({ + tag1: document.tag1, + tag2: document.tag2, + tag3: document.tag3, + tag4: document.tag4, + tag5: document.tag5, + tag6: document.tag6, + tag7: document.tag7, + }) + .from(document) + .where( + and( + eq(document.knowledgeBaseId, knowledgeBaseId), + or( + isNotNull(document.tag1), + isNotNull(document.tag2), + isNotNull(document.tag3), + isNotNull(document.tag4), + isNotNull(document.tag5), + isNotNull(document.tag6), + isNotNull(document.tag7) + ) + ) + ) + + // Collect all tag names that are actually in use + const usedTagNames = new Set() + for (const doc of documentsWithTags) { + const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + for (const slot of tagSlots) { + const tagValue = doc[slot] + if (tagValue?.trim()) { + // Find the tag definition for this slot to get the display name + const definition = currentDefinitions.find((def) => def.tagSlot === slot) + if (definition) { + usedTagNames.add(definition.displayName) + } + } + } + } + + // Find definitions that are not in use + const unusedDefinitions = currentDefinitions.filter((def) => !usedTagNames.has(def.displayName)) + + if (unusedDefinitions.length === 0) { + return 0 // No unused definitions + } + + // Remove unused definitions + const unusedIds = unusedDefinitions.map((def) => def.id) + await db + .delete(knowledgeBaseTagDefinitions) + .where( + and( + eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId), + or(...unusedIds.map((id) => eq(knowledgeBaseTagDefinitions.id, id))) + ) + ) + + logger.info(`[${requestId}] Cleaned up ${unusedDefinitions.length} unused tag definitions`) + return unusedDefinitions.length + } catch (error) { + logger.warn(`[${requestId}] Failed to cleanup unused tag definitions:`, error) + return 0 // Don't fail the main operation if cleanup fails + } +} + // GET /api/knowledge/[id]/documents/[documentId]/tag-definitions - Get tag definitions for a document export async function GET( req: NextRequest, @@ -55,18 +141,18 @@ export async function GET( return NextResponse.json({ error: 'Document not found' }, { status: 404 }) } - // Get tag definitions for the document + // Get tag definitions for the knowledge base const tagDefinitions = await db .select({ - id: documentTagDefinitions.id, - tagSlot: documentTagDefinitions.tagSlot, - displayName: documentTagDefinitions.displayName, - fieldType: documentTagDefinitions.fieldType, - createdAt: documentTagDefinitions.createdAt, - updatedAt: documentTagDefinitions.updatedAt, + id: knowledgeBaseTagDefinitions.id, + tagSlot: knowledgeBaseTagDefinitions.tagSlot, + displayName: knowledgeBaseTagDefinitions.displayName, + fieldType: knowledgeBaseTagDefinitions.fieldType, + createdAt: knowledgeBaseTagDefinitions.createdAt, + updatedAt: knowledgeBaseTagDefinitions.updatedAt, }) - .from(documentTagDefinitions) - .where(eq(documentTagDefinitions.documentId, documentId)) + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`) @@ -126,31 +212,97 @@ export async function POST( const now = new Date() const createdDefinitions = [] + // Get existing definitions count before transaction for cleanup check + const existingDefinitions = await db + .select() + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) + // Use transaction to ensure consistency await db.transaction(async (tx) => { - // First, delete existing definitions for this document - await tx - .delete(documentTagDefinitions) - .where(eq(documentTagDefinitions.documentId, documentId)) - - // Then insert new definitions if any - if (validatedData.definitions.length > 0) { - const newDefinitions = validatedData.definitions.map((definition) => ({ - id: randomUUID(), - documentId, - tagSlot: definition.tagSlot, - displayName: definition.displayName, - fieldType: definition.fieldType, - createdAt: now, - updatedAt: now, - })) - - await tx.insert(documentTagDefinitions).values(newDefinitions) - createdDefinitions.push(...newDefinitions) + // Create maps for lookups + const existingByName = new Map(existingDefinitions.map((def) => [def.displayName, def])) + const existingBySlot = new Map(existingDefinitions.map((def) => [def.tagSlot, def])) + + // Process each new definition + for (const definition of validatedData.definitions) { + const existingByDisplayName = existingByName.get(definition.displayName) + const existingByTagSlot = existingBySlot.get(definition.tagSlot) + + if (existingByDisplayName) { + // Update existing definition (same display name) + if (existingByDisplayName.tagSlot !== definition.tagSlot) { + // Slot is changing - check if target slot is available + if (existingByTagSlot && existingByTagSlot.id !== existingByDisplayName.id) { + // Target slot is occupied by a different definition - this is a conflict + // For now, keep the existing slot to avoid constraint violation + logger.warn( + `[${requestId}] Slot conflict for ${definition.displayName}: keeping existing slot ${existingByDisplayName.tagSlot}` + ) + createdDefinitions.push(existingByDisplayName) + continue + } + } + + await tx + .update(knowledgeBaseTagDefinitions) + .set({ + tagSlot: definition.tagSlot, + fieldType: definition.fieldType, + updatedAt: now, + }) + .where(eq(knowledgeBaseTagDefinitions.id, existingByDisplayName.id)) + + createdDefinitions.push({ + ...existingByDisplayName, + tagSlot: definition.tagSlot, + fieldType: definition.fieldType, + updatedAt: now, + }) + } else if (existingByTagSlot) { + // Slot is occupied by a different display name - update it + await tx + .update(knowledgeBaseTagDefinitions) + .set({ + displayName: definition.displayName, + fieldType: definition.fieldType, + updatedAt: now, + }) + .where(eq(knowledgeBaseTagDefinitions.id, existingByTagSlot.id)) + + createdDefinitions.push({ + ...existingByTagSlot, + displayName: definition.displayName, + fieldType: definition.fieldType, + updatedAt: now, + }) + } else { + // Create new definition + const newDefinition = { + id: randomUUID(), + knowledgeBaseId, + tagSlot: definition.tagSlot, + displayName: definition.displayName, + fieldType: definition.fieldType, + createdAt: now, + updatedAt: now, + } + + await tx.insert(knowledgeBaseTagDefinitions).values(newDefinition) + createdDefinitions.push(newDefinition) + } } }) - logger.info(`[${requestId}] Created/updated ${createdDefinitions.length} tag definitions`) + // Run cleanup immediately - document values should be saved before tag definitions + const cleanedUpCount = await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId) + if (cleanedUpCount > 0) { + logger.info( + `[${requestId}] Created/updated ${createdDefinitions.length} tag definitions, cleaned up ${cleanedUpCount} unused definitions` + ) + } else { + logger.info(`[${requestId}] Created/updated ${createdDefinitions.length} tag definitions`) + } return NextResponse.json({ success: true, @@ -191,10 +343,10 @@ export async function DELETE( return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - // Delete tag definitions for the document + // Delete tag definitions for the knowledge base const result = await db - .delete(documentTagDefinitions) - .where(eq(documentTagDefinitions.documentId, documentId)) + .delete(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) logger.info(`[${requestId}] Deleted tag definitions for document ${documentId}`) diff --git a/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts new file mode 100644 index 00000000000..38f87be8fa8 --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts @@ -0,0 +1,57 @@ +import { randomUUID } from 'crypto' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils' +import { db } from '@/db' +import { knowledgeBaseTagDefinitions } from '@/db/schema' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('KnowledgeBaseTagDefinitionsAPI') + +// GET /api/knowledge/[id]/tag-definitions - Get all tag definitions for a knowledge base +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = randomUUID().slice(0, 8) + const { id: knowledgeBaseId } = await params + + try { + logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`) + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user has access to the knowledge base + const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id) + if (!accessCheck.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Get tag definitions for the knowledge base + const tagDefinitions = await db + .select({ + id: knowledgeBaseTagDefinitions.id, + tagSlot: knowledgeBaseTagDefinitions.tagSlot, + displayName: knowledgeBaseTagDefinitions.displayName, + fieldType: knowledgeBaseTagDefinitions.fieldType, + createdAt: knowledgeBaseTagDefinitions.createdAt, + updatedAt: knowledgeBaseTagDefinitions.updatedAt, + }) + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) + .orderBy(knowledgeBaseTagDefinitions.tagSlot) + + logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`) + + return NextResponse.json({ + success: true, + data: tagDefinitions, + }) + } catch (error) { + logger.error(`[${requestId}] Error getting tag definitions`, error) + return NextResponse.json({ error: 'Failed to get tag definitions' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 1f28e24ee63..0fa144740d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -86,7 +86,7 @@ export function Document({ const [selectedChunks, setSelectedChunks] = useState>(new Set()) const [selectedChunk, setSelectedChunk] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) - const [showTagEntry, setShowTagEntry] = useState(false) + const [documentTags, setDocumentTags] = useState([]) const [documentData, setDocumentData] = useState(null) const [isLoadingDocument, setIsLoadingDocument] = useState(true) @@ -107,13 +107,13 @@ export function Document({ const value = docData[slot] const definition = definitions.find((def) => def.tagSlot === slot) - // Include tag if it has a value OR if it has a custom definition - if (value?.trim() || definition?.displayName?.trim()) { + // Only include tag if the document actually has a value for it + if (value?.trim()) { tags.push({ slot, displayName: definition?.displayName || '', fieldType: definition?.fieldType || 'text', - value: value?.trim() || '', + value: value.trim(), }) } }) @@ -249,7 +249,15 @@ export function Document({ if (knowledgeBaseId && documentId) { fetchDocument() } - }, [knowledgeBaseId, documentId, getCachedDocuments, buildDocumentTags, tagDefinitions]) + }, [knowledgeBaseId, documentId, getCachedDocuments, buildDocumentTags]) + + // Separate effect to rebuild tags when tag definitions change (without re-fetching document) + useEffect(() => { + if (documentData) { + const rebuiltTags = buildDocumentTags(documentData, tagDefinitions) + setDocumentTags(rebuiltTags) + } + }, [documentData, tagDefinitions, buildDocumentTags]) const knowledgeBase = getCachedKnowledgeBase(knowledgeBaseId) const effectiveKnowledgeBaseName = knowledgeBase?.name || knowledgeBaseName || 'Knowledge Base' @@ -510,118 +518,8 @@ export function Document({
- {/* Document Tags Display */} - {documentData && - (() => { - const tags = [ - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag1')?.displayName || - 'Tag 1', - value: documentData.tag1, - slot: 'tag1', - }, - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag2')?.displayName || - 'Tag 2', - value: documentData.tag2, - slot: 'tag2', - }, - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag3')?.displayName || - 'Tag 3', - value: documentData.tag3, - slot: 'tag3', - }, - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag4')?.displayName || - 'Tag 4', - value: documentData.tag4, - slot: 'tag4', - }, - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag5')?.displayName || - 'Tag 5', - value: documentData.tag5, - slot: 'tag5', - }, - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag6')?.displayName || - 'Tag 6', - value: documentData.tag6, - slot: 'tag6', - }, - { - label: - tagDefinitions.find((def) => def.tagSlot === 'tag7')?.displayName || - 'Tag 7', - value: documentData.tag7, - slot: 'tag7', - }, - ].filter((tag) => tag.value?.trim()) - - return tags.length > 0 ? ( -
-
- {tags.map((tag, index) => ( - - {tag.label}: - {tag.value} - - ))} - {userPermissions.canEdit && ( - - - - - -

{showTagEntry ? 'Hide tag editor' : 'Edit document tags'}

-
-
- )} -
-
- ) : userPermissions.canEdit ? ( -
-
-

No document tags set

- - - - - -

{showTagEntry ? 'Hide tag editor' : 'Add document tags'}

-
-
-
-
- ) : null - })()} - {/* Document Tag Entry */} - {showTagEntry && userPermissions.canEdit && ( + {userPermissions.canEdit && (
([]) - - // Track initial tags to detect real changes - if (initialTags.length === 0 && tags.length > 0) { - setInitialTags([...tags]) - } - - const addTag = () => { - if (tags.length >= 7) return + const { saveTagDefinitions } = useTagDefinitions(knowledgeBaseId, documentId) + const { tagDefinitions: kbTagDefinitions } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) + const [inputValue, setInputValue] = useState('') + const [showSuggestions, setShowSuggestions] = useState(false) + const [showInput, setShowInput] = useState(false) + const [editingTag, setEditingTag] = useState<{ + index: number + value: string + tagName: string + isNew: boolean + } | null>(null) + const inputRef = useRef(null) + const suggestionsRef = useRef(null) - // Find the next available slot + const getNextAvailableSlot = (): DocumentTag['slot'] => { const usedSlots = new Set(tags.map((tag) => tag.slot)) - const availableSlot = TAG_SLOTS.find((slot) => !usedSlots.has(slot)) + for (const slot of TAG_SLOTS) { + if (!usedSlots.has(slot)) { + return slot + } + } + return 'tag1' // fallback + } - if (!availableSlot) return + const handleSaveDefinitions = async (tagsToSave?: DocumentTag[]) => { + if (!knowledgeBaseId || !documentId) return - const newTag: DocumentTag = { - slot: availableSlot, - displayName: '', - fieldType: 'text', - value: '', - } + const currentTags = tagsToSave || tags + + // Create definitions for tags that have display names + const definitions: TagDefinitionInput[] = currentTags + .filter((tag) => tag.displayName.trim()) + .map((tag) => ({ + tagSlot: tag.slot, + displayName: tag.displayName.trim(), + fieldType: tag.fieldType, + })) - onTagsChange([...tags, newTag]) - setHasUnsavedChanges(true) + // Save the definitions + await saveTagDefinitions(definitions) } - const removeTag = (index: number) => { - const newTags = tags.filter((_, i) => i !== index) - onTagsChange(newTags) - setHasUnsavedChanges(true) + // Filter suggestions based on input + const filteredSuggestions = kbTagDefinitions.filter((tag) => + tag.displayName.toLowerCase().includes(inputValue.toLowerCase()) + ) + + // Check if current input would create a duplicate + const wouldCreateDuplicate = + inputValue.trim() && + tags.some((tag) => tag.displayName.toLowerCase() === inputValue.trim().toLowerCase()) + + const handleInputChange = (value: string) => { + setInputValue(value) + setShowSuggestions(value.length > 0) + // Keep input visible if there's a value, but don't hide it if we're editing a tag + if (!value && !editingTag) { + setShowInput(false) + setShowSuggestions(false) + } } - const updateTag = (index: number, field: keyof DocumentTag, value: string) => { - const newTags = tags.map((tag, i) => (i === index ? { ...tag, [field]: value } : tag)) - onTagsChange(newTags) - setHasUnsavedChanges(true) + const handleSuggestionClick = (tagName: string) => { + setInputValue('') + setShowSuggestions(false) + setEditingTag({ index: -1, value: '', tagName, isNew: false }) // Existing tag + // Focus will be handled by the edit input } - // Validation helper - const getTagValidation = (tag: DocumentTag) => { - const hasValue = tag.value.trim().length > 0 - const hasDisplayName = tag.displayName.trim().length > 0 + const handleCreateNewTag = async (tagName: string, value: string, fieldType = 'text') => { + if (!tagName.trim() || !value.trim()) return - return { - isValid: !hasValue || hasDisplayName, // If has value, must have display name - errorMessage: - hasValue && !hasDisplayName ? 'Tag name is required when value is provided' : null, + // Check if tag name already exists in current document + const tagNameLower = tagName.trim().toLowerCase() + const existingTag = tags.find((tag) => tag.displayName.toLowerCase() === tagNameLower) + if (existingTag) { + alert(`Tag "${tagName}" already exists. Please choose a different name.`) + return } - } - const handleSaveDefinitions = useCallback( - async (tagsToSave?: DocumentTag[]) => { - if (!knowledgeBaseId || !documentId) return - - const tagsData = tagsToSave || tags - - // Save tag definitions (only for tags with display names) - const definitions: TagDefinitionInput[] = tagsData - .filter((tag) => tag.displayName.trim()) - .map((tag) => ({ - tagSlot: tag.slot, - displayName: tag.displayName.trim(), - fieldType: tag.fieldType, - })) - - try { - await saveTagDefinitions(definitions) - } catch (error) { - console.error('Failed to save tag definitions:', error) - } - }, - [knowledgeBaseId, documentId, tags, saveTagDefinitions] - ) + const newTag: DocumentTag = { + slot: getNextAvailableSlot(), + displayName: tagName.trim(), + fieldType: fieldType, + value: value.trim(), + } - const handleSave = async () => { - if (!knowledgeBaseId || !documentId) return + const updatedTags = [...tags, newTag] + onTagsChange(updatedTags) - setIsSaving(true) + // Save immediately - document values first, then definitions try { - // Save tag definitions first - await handleSaveDefinitions() + // First save the document tag values + if (onSave) { + await onSave(updatedTags) + } + // Then save the tag definitions (cleanup will run and see the updated document values) + await handleSaveDefinitions(updatedTags) + } catch (error) { + console.error('Failed to save tag:', error) + } + } - // Then save document values if onSave is provided + const handleUpdateTag = async (index: number, newValue: string) => { + if (!newValue.trim()) return + + const updatedTags = tags.map((tag, i) => + i === index ? { ...tag, value: newValue.trim() } : tag + ) + onTagsChange(updatedTags) + + // Save immediately - document values first, then definitions + try { + // First save the document tag values if (onSave) { - await onSave(tags) + await onSave(updatedTags) } + // Then save the tag definitions (cleanup will run and see the updated document values) + await handleSaveDefinitions(updatedTags) + } catch (error) { + console.error('Failed to update tag:', error) + } + } - // Refresh tag definitions to get the latest data - await fetchTagDefinitions() + const handleRemoveTag = async (index: number) => { + const updatedTags = tags.filter((_, i) => i !== index) + onTagsChange(updatedTags) - setHasUnsavedChanges(false) + // Save immediately + try { + // First save the document tag values + if (onSave) { + await onSave(updatedTags) + } + // Then save the tag definitions (cleanup will run and see the updated document values) + await handleSaveDefinitions(updatedTags) + + // The document page will automatically rebuild tags when definitions change } catch (error) { - console.error('Failed to save tags:', error) - } finally { - setIsSaving(false) + console.error('Failed to remove tag:', error) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && inputValue.trim()) { + e.preventDefault() + // If there's an exact match, use it; otherwise create new + const exactMatch = kbTagDefinitions.find( + (tag) => tag.displayName.toLowerCase() === inputValue.toLowerCase() + ) + if (exactMatch) { + handleSuggestionClick(exactMatch.displayName) + } else if (!wouldCreateDuplicate) { + // Create new tag only if it's not a duplicate + const tagName = inputValue.trim() + setEditingTag({ index: -1, value: '', tagName, isNew: true }) + setShowSuggestions(false) + // Don't clear input here - let the modal handle it + } + // If it would create duplicate, do nothing (user sees the disabled state) + } else if (e.key === 'Escape') { + setInputValue('') + setShowSuggestions(false) + setShowInput(false) } } + // Close suggestions when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + suggestionsRef.current && + !suggestionsRef.current.contains(event.target as Node) && + inputRef.current && + !inputRef.current.contains(event.target as Node) + ) { + setShowSuggestions(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + return ( -
-
- -
- {hasUnsavedChanges && ( +
+ {/* Existing Tags as Chips */} +
+ {tags.map((tag, index) => ( +
+ setEditingTag({ index, value: tag.value, tagName: tag.displayName, isNew: false }) + } + > + {tag.displayName}: + {tag.value} - )} - {tags.length < 7 && ( - - )} -
+
+ ))}
- {tags.length === 0 ? ( -
-

- No tags added yet. Click "Add Tag" to create your first tag. -

-
- ) : ( -
- {tags.map((tag, index) => { - const validation = getTagValidation(tag) - - return ( -
- {/* Remove button - positioned at top right */} - - -
- {/* Tag Name */} -
- - updateTag(index, 'displayName', e.target.value)} - disabled={disabled} - className={`mt-1.5 text-sm ${!validation.isValid ? 'border-red-300 focus:border-red-500' : ''}`} - /> - {validation.errorMessage && ( -

{validation.errorMessage}

- )} -
- - {/* Field Type */} -
- - handleInputChange(e.target.value)} + onKeyDown={handleKeyDown} + onFocus={() => inputValue && setShowSuggestions(true)} + onBlur={() => { + // Hide input if empty and no suggestions showing + setTimeout(() => { + if (!inputValue && !showSuggestions) { + setShowInput(false) + } + }, 150) // Small delay to allow clicking suggestions + }} + disabled={disabled || tags.length >= 7} + className='text-sm' + autoFocus + /> + + {/* Suggestions Dropdown */} + {showSuggestions && ( +
+ {filteredSuggestions.length > 0 && ( + <> + {filteredSuggestions.map((tag) => ( +
- - {/* Tag Value */} -
- - updateTag(index, 'value', e.target.value)} - disabled={disabled} - className='mt-1.5 text-sm' - /> -
-
-
- ) - })} + {tag.displayName} + {tag.fieldType} + + ))} +
+ + )} + +
+ )}
+ ) : ( + + )} + + {/* Edit Tag Value Modal */} + {editingTag !== null && ( + t.displayName === editingTag.tagName)?.fieldType + } + onSave={(value, type) => { + if (editingTag.index === -1) { + // Creating new tag + handleCreateNewTag(editingTag.tagName, value, type) + } else { + // Updating existing tag + handleUpdateTag(editingTag.index, value) + } + setEditingTag(null) + setInputValue('') + setShowInput(false) + }} + onCancel={() => { + setEditingTag(null) + setInputValue('') + setShowInput(false) + }} + /> )} {tags.length > 0 && ( @@ -274,3 +376,99 @@ export function DocumentTagEntry({
) } + +// Simple modal for editing tag values +interface EditTagModalProps { + tagName: string + initialValue: string + isNew: boolean + existingType?: string + onSave: (value: string, type?: string) => void + onCancel: () => void +} + +function EditTagModal({ + tagName, + initialValue, + isNew, + existingType, + onSave, + onCancel, +}: EditTagModalProps) { + const [value, setValue] = useState(initialValue) + const [fieldType, setFieldType] = useState(existingType || 'text') + const inputRef = useRef(null) + + useEffect(() => { + inputRef.current?.focus() + }, []) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (value.trim()) { + onSave(value.trim(), fieldType) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onCancel() + } + } + + return ( +
+
+
+

+ {isNew ? `Create "${tagName}" tag` : `Edit "${tagName}" value`} +

+ {/* Type Badge in Top Right */} + {!isNew && existingType && ( + + {existingType.toUpperCase()} + + )} +
+
+ {/* Type Selection for New Tags */} + {isNew && ( +
+ + +
+ )} + + {/* Value Input */} +
+ + setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder='Enter tag value' + className='mt-1 text-sm' + /> +
+ +
+ + +
+
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx index f81b7c232a2..ffcc916b0bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useTagDefinitions } from '@/hooks/use-tag-definitions' +import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' export interface TagData { tag1?: string @@ -49,7 +49,7 @@ export function TagInput({ const [showAllTags, setShowAllTags] = useState(false) // Use custom tag definitions if available - const { getTagLabel } = useTagDefinitions(knowledgeBaseId, documentId) + const { getTagLabel } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) const handleTagChange = (tagKey: keyof TagData, value: string) => { onTagsChange({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx index 9765cf8bd31..8f773a455de 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx @@ -2,8 +2,15 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import type { SubBlockConfig } from '@/blocks/types' -import { useTagDefinitions } from '@/hooks/use-tag-definitions' +import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' import { useSubBlockValue } from '../../hooks/use-sub-block-value' interface KnowledgeTagFilterProps { @@ -35,39 +42,85 @@ export function KnowledgeTagFilter({ knowledgeBaseIdSingleValue || (typeof knowledgeBaseIdValue === 'string' ? knowledgeBaseIdValue.split(',')[0] : null) - // Use tag definitions hook to get custom label - const { getTagLabel } = useTagDefinitions(knowledgeBaseId, documentIdValue) + // Use KB tag definitions hook to get available tags + const { tagDefinitions, isLoading, getTagLabel } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) - // Extract tag slot from subBlock id (e.g., 'tag1', 'tag2', 'createTag1', etc.) - const tagSlot = subBlock.id.startsWith('createTag') - ? subBlock.id.replace('createTag', 'tag').toLowerCase() - : subBlock.id - - // Get the custom label or fallback to default - const customLabel = getTagLabel(tagSlot) + // Parse the current value to extract tag name and value + const parseTagFilter = (filterValue: string) => { + if (!filterValue) return { tagName: '', tagValue: '' } + const [tagName, ...valueParts] = filterValue.split(':') + return { tagName: tagName?.trim() || '', tagValue: valueParts.join(':').trim() || '' } + } - // Use preview value if in preview mode, otherwise use store value const currentValue = isPreview ? previewValue : storeValue + const { tagName, tagValue } = parseTagFilter(currentValue || '') + + const handleTagNameChange = (newTagName: string) => { + if (isPreview) return + const newValue = + newTagName && tagValue ? `${newTagName}:${tagValue}` : newTagName || tagValue || '' + setStoreValue(newValue.trim() || null) + } - const handleChange = (value: string) => { + const handleTagValueChange = (newTagValue: string) => { if (isPreview) return - setStoreValue(value.trim() || null) + const newValue = + tagName && newTagValue ? `${tagName}:${newTagValue}` : tagName || newTagValue || '' + setStoreValue(newValue.trim() || null) } - // Get placeholder text - const placeholder = subBlock.placeholder || `Filter by ${customLabel.toLowerCase()}` + if (isPreview) { + return ( +
+ + +
+ ) + } return ( -
- - handleChange(e.target.value)} - placeholder={placeholder} - disabled={disabled || isPreview} - className='text-sm' - /> +
+ + +
+ {/* Tag Name Selector */} +
+ + +
+ + {/* Tag Value Input */} +
+ + handleTagValueChange(e.target.value)} + placeholder='Enter value' + disabled={disabled || isConnecting} + className='text-sm' + /> +
+
) } diff --git a/apps/sim/db/migrations/0063_greedy_sentinel.sql b/apps/sim/db/migrations/0063_greedy_sentinel.sql deleted file mode 100644 index af7d32e963b..00000000000 --- a/apps/sim/db/migrations/0063_greedy_sentinel.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE "document_tag_definitions" ( - "id" text PRIMARY KEY NOT NULL, - "document_id" text NOT NULL, - "tag_slot" text NOT NULL, - "display_name" text NOT NULL, - "field_type" text DEFAULT 'text' NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "document_tag_definitions" ADD CONSTRAINT "document_tag_definitions_document_id_document_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."document"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE UNIQUE INDEX "doc_tag_definitions_doc_slot_idx" ON "document_tag_definitions" USING btree ("document_id","tag_slot");--> statement-breakpoint -CREATE INDEX "doc_tag_definitions_doc_id_idx" ON "document_tag_definitions" USING btree ("document_id"); \ No newline at end of file diff --git a/apps/sim/db/migrations/0063_lame_sandman.sql b/apps/sim/db/migrations/0063_lame_sandman.sql new file mode 100644 index 00000000000..3b5da7c9285 --- /dev/null +++ b/apps/sim/db/migrations/0063_lame_sandman.sql @@ -0,0 +1,13 @@ +CREATE TABLE "knowledge_base_tag_definitions" ( + "id" text PRIMARY KEY NOT NULL, + "knowledge_base_id" text NOT NULL, + "tag_slot" text NOT NULL, + "display_name" text NOT NULL, + "field_type" text DEFAULT 'text' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "knowledge_base_tag_definitions" ADD CONSTRAINT "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk" FOREIGN KEY ("knowledge_base_id") REFERENCES "public"."knowledge_base"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "kb_tag_definitions_kb_slot_idx" ON "knowledge_base_tag_definitions" USING btree ("knowledge_base_id","tag_slot");--> statement-breakpoint +CREATE INDEX "kb_tag_definitions_kb_id_idx" ON "knowledge_base_tag_definitions" USING btree ("knowledge_base_id"); \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0063_snapshot.json b/apps/sim/db/migrations/meta/0063_snapshot.json index 0a602c699ef..3a483105c50 100644 --- a/apps/sim/db/migrations/meta/0063_snapshot.json +++ b/apps/sim/db/migrations/meta/0063_snapshot.json @@ -1,5 +1,5 @@ { - "id": "23d1b664-e4b0-4886-89f0-59e465f7a545", + "id": "d6721471-f616-4e02-bfba-e5271c59857f", "prevId": "5fe645b1-2d33-4fd7-8144-49dc2f3a3fd6", "version": "7", "dialect": "postgresql", @@ -1352,111 +1352,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.document_tag_definitions": { - "name": "document_tag_definitions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "document_id": { - "name": "document_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tag_slot": { - "name": "tag_slot", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "field_type": { - "name": "field_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'text'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "doc_tag_definitions_doc_slot_idx": { - "name": "doc_tag_definitions_doc_slot_idx", - "columns": [ - { - "expression": "document_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "tag_slot", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag_definitions_doc_id_idx": { - "name": "doc_tag_definitions_doc_id_idx", - "columns": [ - { - "expression": "document_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "document_tag_definitions_document_id_document_id_fk": { - "name": "document_tag_definitions_document_id_document_id_fk", - "tableFrom": "document_tag_definitions", - "tableTo": "document", - "columnsFrom": ["document_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.embedding": { "name": "embedding", "schema": "", @@ -2206,6 +2101,111 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.marketplace": { "name": "marketplace", "schema": "", diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index de807e0e2a0..b9075261038 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -439,8 +439,8 @@ { "idx": 63, "version": "7", - "when": 1753488920902, - "tag": "0063_greedy_sentinel", + "when": 1753558819517, + "tag": "0063_lame_sandman", "breakpoints": true } ] diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 1a2b5e51f35..787cacd1c0a 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -794,13 +794,13 @@ export const document = pgTable( }) ) -export const documentTagDefinitions = pgTable( - 'document_tag_definitions', +export const knowledgeBaseTagDefinitions = pgTable( + 'knowledge_base_tag_definitions', { id: text('id').primaryKey(), - documentId: text('document_id') + knowledgeBaseId: text('knowledge_base_id') .notNull() - .references(() => document.id, { onDelete: 'cascade' }), + .references(() => knowledgeBase.id, { onDelete: 'cascade' }), tagSlot: text('tag_slot', { enum: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'], }).notNull(), @@ -810,13 +810,13 @@ export const documentTagDefinitions = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - // Ensure unique tag slot per document - documentTagSlotIdx: uniqueIndex('doc_tag_definitions_doc_slot_idx').on( - table.documentId, + // Ensure unique tag slot per knowledge base + kbTagSlotIdx: uniqueIndex('kb_tag_definitions_kb_slot_idx').on( + table.knowledgeBaseId, table.tagSlot ), - // Index for querying by document - documentIdIdx: index('doc_tag_definitions_doc_id_idx').on(table.documentId), + // Index for querying by knowledge base + kbIdIdx: index('kb_tag_definitions_kb_id_idx').on(table.knowledgeBaseId), }) ) diff --git a/apps/sim/hooks/use-knowledge-base-tag-definitions.ts b/apps/sim/hooks/use-knowledge-base-tag-definitions.ts new file mode 100644 index 00000000000..716e72f21e8 --- /dev/null +++ b/apps/sim/hooks/use-knowledge-base-tag-definitions.ts @@ -0,0 +1,87 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { createLogger } from '@/lib/logs/console-logger' + +const logger = createLogger('useKnowledgeBaseTagDefinitions') + +export interface TagDefinition { + id: string + tagSlot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + displayName: string + fieldType: string + createdAt: string + updatedAt: string +} + +/** + * Hook for fetching KB-scoped tag definitions (for filtering/selection) + * @param knowledgeBaseId - The knowledge base ID + */ +export function useKnowledgeBaseTagDefinitions(knowledgeBaseId: string | null) { + const [tagDefinitions, setTagDefinitions] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const fetchTagDefinitions = useCallback(async () => { + if (!knowledgeBaseId) { + setTagDefinitions([]) + return + } + + setIsLoading(true) + setError(null) + + try { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/tag-definitions`) + + if (!response.ok) { + throw new Error(`Failed to fetch tag definitions: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success && Array.isArray(data.data)) { + setTagDefinitions(data.data) + } else { + throw new Error('Invalid response format') + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + logger.error('Error fetching tag definitions:', err) + setError(errorMessage) + setTagDefinitions([]) + } finally { + setIsLoading(false) + } + }, [knowledgeBaseId]) + + const getTagLabel = useCallback( + (tagSlot: string): string => { + const definition = tagDefinitions.find((def) => def.tagSlot === tagSlot) + return definition?.displayName || tagSlot + }, + [tagDefinitions] + ) + + const getTagDefinition = useCallback( + (tagSlot: string): TagDefinition | undefined => { + return tagDefinitions.find((def) => def.tagSlot === tagSlot) + }, + [tagDefinitions] + ) + + // Auto-fetch on mount and when dependencies change + useEffect(() => { + fetchTagDefinitions() + }, [fetchTagDefinitions]) + + return { + tagDefinitions, + isLoading, + error, + fetchTagDefinitions, + getTagLabel, + getTagDefinition, + } +} diff --git a/apps/sim/hooks/use-tag-definitions.ts b/apps/sim/hooks/use-tag-definitions.ts new file mode 100644 index 00000000000..28d15b19509 --- /dev/null +++ b/apps/sim/hooks/use-tag-definitions.ts @@ -0,0 +1,166 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { createLogger } from '@/lib/logs/console-logger' + +const logger = createLogger('useTagDefinitions') + +export interface TagDefinition { + id: string + tagSlot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + displayName: string + fieldType: string + createdAt: string + updatedAt: string +} + +export interface TagDefinitionInput { + tagSlot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + displayName: string + fieldType: string +} + +/** + * Hook for managing KB-scoped tag definitions + * @param knowledgeBaseId - The knowledge base ID + * @param documentId - The document ID (kept for API compatibility but not used for fetching) + */ +export function useTagDefinitions( + knowledgeBaseId: string | null, + documentId: string | null = null +) { + const [tagDefinitions, setTagDefinitions] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const fetchTagDefinitions = useCallback(async () => { + if (!knowledgeBaseId || !documentId) { + setTagDefinitions([]) + return + } + + setIsLoading(true) + setError(null) + + try { + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/tag-definitions` + ) + + if (!response.ok) { + throw new Error(`Failed to fetch tag definitions: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success && Array.isArray(data.data)) { + setTagDefinitions(data.data) + } else { + throw new Error('Invalid response format') + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + logger.error('Error fetching tag definitions:', err) + setError(errorMessage) + setTagDefinitions([]) + } finally { + setIsLoading(false) + } + }, [knowledgeBaseId, documentId]) + + const saveTagDefinitions = useCallback( + async (definitions: TagDefinitionInput[]) => { + if (!knowledgeBaseId || !documentId) { + throw new Error('Knowledge base ID and document ID are required') + } + + try { + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/tag-definitions`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ definitions }), + } + ) + + if (!response.ok) { + throw new Error(`Failed to save tag definitions: ${response.statusText}`) + } + + const data = await response.json() + + if (!data.success) { + throw new Error(data.error || 'Failed to save tag definitions') + } + + // Refresh the definitions after saving + await fetchTagDefinitions() + + return data.data + } catch (err) { + logger.error('Error saving tag definitions:', err) + throw err + } + }, + [knowledgeBaseId, documentId, fetchTagDefinitions] + ) + + const deleteTagDefinitions = useCallback(async () => { + if (!knowledgeBaseId || !documentId) { + throw new Error('Knowledge base ID and document ID are required') + } + + try { + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/tag-definitions`, + { + method: 'DELETE', + } + ) + + if (!response.ok) { + throw new Error(`Failed to delete tag definitions: ${response.statusText}`) + } + + // Refresh the definitions after deleting + await fetchTagDefinitions() + } catch (err) { + logger.error('Error deleting tag definitions:', err) + throw err + } + }, [knowledgeBaseId, documentId, fetchTagDefinitions]) + + const getTagLabel = useCallback( + (tagSlot: string): string => { + const definition = tagDefinitions.find((def) => def.tagSlot === tagSlot) + return definition?.displayName || tagSlot + }, + [tagDefinitions] + ) + + const getTagDefinition = useCallback( + (tagSlot: string): TagDefinition | undefined => { + return tagDefinitions.find((def) => def.tagSlot === tagSlot) + }, + [tagDefinitions] + ) + + // Auto-fetch on mount and when dependencies change + useEffect(() => { + fetchTagDefinitions() + }, [fetchTagDefinitions]) + + return { + tagDefinitions, + isLoading, + error, + fetchTagDefinitions, + saveTagDefinitions, + deleteTagDefinitions, + getTagLabel, + getTagDefinition, + } +} From 7feea4a9c183c7d6f6235c0801cfba9620803830 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 26 Jul 2025 15:34:05 -0700 Subject: [PATCH 03/19] works --- .../[documentId]/tag-definitions/route.ts | 146 ++++---- .../knowledge/[id]/[documentId]/document.tsx | 49 +-- .../document-tag-entry/document-tag-entry.tsx | 333 ++++++++---------- apps/sim/hooks/use-tag-definitions.ts | 7 +- 4 files changed, 260 insertions(+), 275 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts index 190e264e1f7..4a71e9a7621 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'crypto' -import { and, eq, isNotNull, or } from 'drizzle-orm' +import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -25,83 +25,54 @@ const BulkTagDefinitionsSchema = z.object({ // Helper function to clean up unused tag definitions async function cleanupUnusedTagDefinitions(knowledgeBaseId: string, requestId: string) { try { - // Get all current tag definitions for this KB - const currentDefinitions = await db - .select({ - id: knowledgeBaseTagDefinitions.id, - displayName: knowledgeBaseTagDefinitions.displayName, - tagSlot: knowledgeBaseTagDefinitions.tagSlot, - }) + logger.info(`[${requestId}] Starting cleanup for KB ${knowledgeBaseId}`) + + // Get all tag definitions for this KB + const allDefinitions = await db + .select() .from(knowledgeBaseTagDefinitions) .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) - if (currentDefinitions.length === 0) { - return 0 // No definitions to clean up + logger.info(`[${requestId}] Found ${allDefinitions.length} tag definitions to check`) + + if (allDefinitions.length === 0) { + return 0 } - // Check which tag names are actually in use by documents - const documentsWithTags = await db - .select({ - tag1: document.tag1, - tag2: document.tag2, - tag3: document.tag3, - tag4: document.tag4, - tag5: document.tag5, - tag6: document.tag6, - tag7: document.tag7, - }) - .from(document) - .where( - and( - eq(document.knowledgeBaseId, knowledgeBaseId), - or( - isNotNull(document.tag1), - isNotNull(document.tag2), - isNotNull(document.tag3), - isNotNull(document.tag4), - isNotNull(document.tag5), - isNotNull(document.tag6), - isNotNull(document.tag7) - ) - ) - ) + let cleanedCount = 0 - // Collect all tag names that are actually in use - const usedTagNames = new Set() - for (const doc of documentsWithTags) { - const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const - for (const slot of tagSlots) { - const tagValue = doc[slot] - if (tagValue?.trim()) { - // Find the tag definition for this slot to get the display name - const definition = currentDefinitions.find((def) => def.tagSlot === slot) - if (definition) { - usedTagNames.add(definition.displayName) - } - } - } - } + // For each tag definition, check if any documents use that tag slot + for (const definition of allDefinitions) { + const slot = definition.tagSlot - // Find definitions that are not in use - const unusedDefinitions = currentDefinitions.filter((def) => !usedTagNames.has(def.displayName)) + // Use raw SQL with proper column name injection + const countResult = await db.execute(sql` + SELECT count(*) as count + FROM document + WHERE knowledge_base_id = ${knowledgeBaseId} + AND ${sql.raw(slot)} IS NOT NULL + AND trim(${sql.raw(slot)}) != '' + `) + const count = Number(countResult[0]?.count) || 0 - if (unusedDefinitions.length === 0) { - return 0 // No unused definitions - } + logger.info( + `[${requestId}] Tag ${definition.displayName} (${slot}): ${count} documents using it` + ) - // Remove unused definitions - const unusedIds = unusedDefinitions.map((def) => def.id) - await db - .delete(knowledgeBaseTagDefinitions) - .where( - and( - eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId), - or(...unusedIds.map((id) => eq(knowledgeBaseTagDefinitions.id, id))) + // If count is 0, remove this tag definition + if (count === 0) { + await db + .delete(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.id, definition.id)) + + cleanedCount++ + logger.info( + `[${requestId}] Removed unused tag definition: ${definition.displayName} (${definition.tagSlot})` ) - ) + } + } - logger.info(`[${requestId}] Cleaned up ${unusedDefinitions.length} unused tag definitions`) - return unusedDefinitions.length + return cleanedCount } catch (error) { logger.warn(`[${requestId}] Failed to cleanup unused tag definitions:`, error) return 0 // Don't fail the main operation if cleanup fails @@ -199,7 +170,22 @@ export async function POST( return NextResponse.json({ error: 'Document not found' }, { status: 404 }) } - const body = await req.json() + let body + try { + body = await req.json() + } catch (error) { + logger.error(`[${requestId}] Failed to parse JSON body:`, error) + return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + } + + if (!body || typeof body !== 'object') { + logger.error(`[${requestId}] Invalid request body:`, body) + return NextResponse.json( + { error: 'Request body must be a valid JSON object' }, + { status: 400 } + ) + } + const validatedData = BulkTagDefinitionsSchema.parse(body) // Validate no duplicate tag slots @@ -328,10 +314,10 @@ export async function DELETE( ) { const requestId = randomUUID().slice(0, 8) const { id: knowledgeBaseId, documentId } = await params + const { searchParams } = new URL(req.url) + const action = searchParams.get('action') // 'cleanup' or 'all' try { - logger.info(`[${requestId}] Deleting tag definitions for document ${documentId}`) - const session = await getSession() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -343,19 +329,29 @@ export async function DELETE( return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - // Delete tag definitions for the knowledge base + if (action === 'cleanup') { + // Just run cleanup + logger.info(`[${requestId}] Running cleanup for KB ${knowledgeBaseId}`) + const cleanedUpCount = await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId) + + return NextResponse.json({ + success: true, + data: { cleanedUp: cleanedUpCount }, + }) + } + // Delete all tag definitions (original behavior) + logger.info(`[${requestId}] Deleting all tag definitions for KB ${knowledgeBaseId}`) + const result = await db .delete(knowledgeBaseTagDefinitions) .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) - logger.info(`[${requestId}] Deleted tag definitions for document ${documentId}`) - return NextResponse.json({ success: true, message: 'Tag definitions deleted successfully', }) } catch (error) { - logger.error(`[${requestId}] Error deleting tag definitions`, error) - return NextResponse.json({ error: 'Failed to delete tag definitions' }, { status: 500 }) + logger.error(`[${requestId}] Error with tag definitions operation`, error) + return NextResponse.json({ error: 'Failed to process tag definitions' }, { status: 500 }) } } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 0fa144740d2..1b278c84ec7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -99,27 +99,32 @@ export function Document({ ) // Function to build document tags from data and definitions - const buildDocumentTags = useCallback((docData: DocumentData, definitions: any[]) => { - const tags: DocumentTag[] = [] - const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const - - tagSlots.forEach((slot) => { - const value = docData[slot] - const definition = definitions.find((def) => def.tagSlot === slot) - - // Only include tag if the document actually has a value for it - if (value?.trim()) { - tags.push({ - slot, - displayName: definition?.displayName || '', - fieldType: definition?.fieldType || 'text', - value: value.trim(), - }) - } - }) + const buildDocumentTags = useCallback( + (docData: DocumentData, definitions: any[], currentTags?: DocumentTag[]) => { + const tags: DocumentTag[] = [] + const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + + tagSlots.forEach((slot) => { + const value = docData[slot] + const definition = definitions.find((def) => def.tagSlot === slot) + const currentTag = currentTags?.find((tag) => tag.slot === slot) + + // Only include tag if the document actually has a value for it + if (value?.trim()) { + tags.push({ + slot, + // Preserve existing displayName if definition is not found yet + displayName: definition?.displayName || currentTag?.displayName || '', + fieldType: definition?.fieldType || currentTag?.fieldType || 'text', + value: value.trim(), + }) + } + }) - return tags - }, []) + return tags + }, + [] + ) // Handle tag updates (local state only, no API calls) const handleTagsChange = useCallback((newTags: DocumentTag[]) => { @@ -233,7 +238,7 @@ export function Document({ if (result.success) { setDocumentData(result.data) // Initialize tags from fetched document - const initialTags = buildDocumentTags(result.data, tagDefinitions) + const initialTags = buildDocumentTags(result.data, tagDefinitions, []) setDocumentTags(initialTags) } else { throw new Error(result.error || 'Failed to fetch document') @@ -254,7 +259,7 @@ export function Document({ // Separate effect to rebuild tags when tag definitions change (without re-fetching document) useEffect(() => { if (documentData) { - const rebuiltTags = buildDocumentTags(documentData, tagDefinitions) + const rebuiltTags = buildDocumentTags(documentData, tagDefinitions, documentTags) setDocumentTags(rebuiltTags) } }, [documentData, tagDefinitions, buildDocumentTags]) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx index dd9f4cb264a..5e9deda6488 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx @@ -1,8 +1,14 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { Plus, X } from 'lucide-react' +import { ChevronDown, Plus, X } from 'lucide-react' import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -42,18 +48,15 @@ export function DocumentTagEntry({ onSave, }: DocumentTagEntryProps) { const { saveTagDefinitions } = useTagDefinitions(knowledgeBaseId, documentId) - const { tagDefinitions: kbTagDefinitions } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) - const [inputValue, setInputValue] = useState('') - const [showSuggestions, setShowSuggestions] = useState(false) - const [showInput, setShowInput] = useState(false) + const { tagDefinitions: kbTagDefinitions, fetchTagDefinitions: refreshTagDefinitions } = + useKnowledgeBaseTagDefinitions(knowledgeBaseId) + const [editingTag, setEditingTag] = useState<{ index: number value: string tagName: string isNew: boolean } | null>(null) - const inputRef = useRef(null) - const suggestionsRef = useRef(null) const getNextAvailableSlot = (): DocumentTag['slot'] => { const usedSlots = new Set(tags.map((tag) => tag.slot)) @@ -72,42 +75,48 @@ export function DocumentTagEntry({ // Create definitions for tags that have display names const definitions: TagDefinitionInput[] = currentTags - .filter((tag) => tag.displayName.trim()) + .filter((tag) => tag && tag.displayName && tag.displayName.trim()) .map((tag) => ({ tagSlot: tag.slot, displayName: tag.displayName.trim(), - fieldType: tag.fieldType, + fieldType: tag.fieldType || 'text', })) - // Save the definitions - await saveTagDefinitions(definitions) + // Only save if we have valid definitions + if (definitions.length > 0) { + await saveTagDefinitions(definitions) + } } - // Filter suggestions based on input - const filteredSuggestions = kbTagDefinitions.filter((tag) => - tag.displayName.toLowerCase().includes(inputValue.toLowerCase()) - ) + const handleCleanupUnusedTags = async () => { + if (!knowledgeBaseId || !documentId) return + + try { + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/tag-definitions?action=cleanup`, + { + method: 'DELETE', + } + ) + + if (!response.ok) { + throw new Error(`Cleanup failed: ${response.statusText}`) + } - // Check if current input would create a duplicate - const wouldCreateDuplicate = - inputValue.trim() && - tags.some((tag) => tag.displayName.toLowerCase() === inputValue.trim().toLowerCase()) - - const handleInputChange = (value: string) => { - setInputValue(value) - setShowSuggestions(value.length > 0) - // Keep input visible if there's a value, but don't hide it if we're editing a tag - if (!value && !editingTag) { - setShowInput(false) - setShowSuggestions(false) + const result = await response.json() + console.log('Cleanup result:', result) + } catch (error) { + console.error('Failed to cleanup unused tags:', error) } } + // Get available tag names that aren't already used in this document + const availableTagNames = kbTagDefinitions + .map((tag) => tag.displayName) + .filter((tagName) => !tags.some((tag) => tag.displayName === tagName)) + const handleSuggestionClick = (tagName: string) => { - setInputValue('') - setShowSuggestions(false) - setEditingTag({ index: -1, value: '', tagName, isNew: false }) // Existing tag - // Focus will be handled by the edit input + setEditingTag({ index: -1, value: '', tagName, isNew: false }) } const handleCreateNewTag = async (tagName: string, value: string, fieldType = 'text') => { @@ -129,18 +138,30 @@ export function DocumentTagEntry({ } const updatedTags = [...tags, newTag] - onTagsChange(updatedTags) - // Save immediately - document values first, then definitions + // Save both operations - if either fails, revert the local state try { - // First save the document tag values + console.log('Creating new tag:', { tagName, value, fieldType, slot: newTag.slot }) + + // First save the tag definitions to ensure they exist + console.log('Saving tag definitions...') + await handleSaveDefinitions(updatedTags) + + // Then save the document tag values + console.log('Saving document values...') if (onSave) { await onSave(updatedTags) } - // Then save the tag definitions (cleanup will run and see the updated document values) - await handleSaveDefinitions(updatedTags) + + // Only update local state after both operations succeed + console.log('Both operations succeeded, updating local state') + onTagsChange(updatedTags) + + // Refresh tag definitions for dropdown + await refreshTagDefinitions() } catch (error) { console.error('Failed to save tag:', error) + alert(`Failed to save tag "${tagName}". Please try again.`) } } @@ -150,16 +171,20 @@ export function DocumentTagEntry({ const updatedTags = tags.map((tag, i) => i === index ? { ...tag, value: newValue.trim() } : tag ) - onTagsChange(updatedTags) - // Save immediately - document values first, then definitions + // FULLY SYNCHRONOUS - DO NOT UPDATE UI UNTIL BOTH OPERATIONS COMPLETE try { // First save the document tag values if (onSave) { await onSave(updatedTags) } - // Then save the tag definitions (cleanup will run and see the updated document values) + // Then save the tag definitions await handleSaveDefinitions(updatedTags) + // ONLY NOW update the UI + onTagsChange(updatedTags) + + // Refresh tag definitions for dropdown (in case cleanup removed unused ones) + await refreshTagDefinitions() } catch (error) { console.error('Failed to update tag:', error) } @@ -167,64 +192,35 @@ export function DocumentTagEntry({ const handleRemoveTag = async (index: number) => { const updatedTags = tags.filter((_, i) => i !== index) - onTagsChange(updatedTags) - // Save immediately + console.log('Removing tag, updated tags:', updatedTags) + + // FULLY SYNCHRONOUS - DO NOT UPDATE UI UNTIL ALL OPERATIONS COMPLETE try { - // First save the document tag values + // 1. Save the document tag values + console.log('Saving document values after tag removal...') if (onSave) { await onSave(updatedTags) } - // Then save the tag definitions (cleanup will run and see the updated document values) + + // 2. Save the tag definitions + console.log('Saving tag definitions after tag removal...') await handleSaveDefinitions(updatedTags) - // The document page will automatically rebuild tags when definitions change + // 3. Run cleanup to remove unused tag definitions + console.log('Running cleanup to remove unused tag definitions...') + await handleCleanupUnusedTags() + + // 4. ONLY NOW update the UI + onTagsChange(updatedTags) + + // 5. Refresh tag definitions for dropdown + await refreshTagDefinitions() } catch (error) { console.error('Failed to remove tag:', error) } } - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && inputValue.trim()) { - e.preventDefault() - // If there's an exact match, use it; otherwise create new - const exactMatch = kbTagDefinitions.find( - (tag) => tag.displayName.toLowerCase() === inputValue.toLowerCase() - ) - if (exactMatch) { - handleSuggestionClick(exactMatch.displayName) - } else if (!wouldCreateDuplicate) { - // Create new tag only if it's not a duplicate - const tagName = inputValue.trim() - setEditingTag({ index: -1, value: '', tagName, isNew: true }) - setShowSuggestions(false) - // Don't clear input here - let the modal handle it - } - // If it would create duplicate, do nothing (user sees the disabled state) - } else if (e.key === 'Escape') { - setInputValue('') - setShowSuggestions(false) - setShowInput(false) - } - } - - // Close suggestions when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - suggestionsRef.current && - !suggestionsRef.current.contains(event.target as Node) && - inputRef.current && - !inputRef.current.contains(event.target as Node) - ) { - setShowSuggestions(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) - return (
{/* Existing Tags as Chips */} @@ -256,88 +252,56 @@ export function DocumentTagEntry({ ))}
- {/* Add Tag Input or Plus Button */} - {showInput ? ( -
- handleInputChange(e.target.value)} - onKeyDown={handleKeyDown} - onFocus={() => inputValue && setShowSuggestions(true)} - onBlur={() => { - // Hide input if empty and no suggestions showing - setTimeout(() => { - if (!inputValue && !showSuggestions) { - setShowInput(false) - } - }, 150) // Small delay to allow clicking suggestions - }} + {/* Add Tag Dropdown Selector */} + + + - ))} -
- - )} - -
+ className='gap-1 text-muted-foreground hover:text-foreground' + > + + Add Tag + + +
+ + {/* Existing tag names */} + {availableTagNames.length > 0 && ( + <> + {availableTagNames.map((tagName) => { + const tagDefinition = kbTagDefinitions.find((def) => def.displayName === tagName) + return ( + handleSuggestionClick(tagName)} + className='flex items-center justify-between' + > + {tagName} + + {tagDefinition?.fieldType || 'text'} + + + ) + })} +
+ )} -
- ) : ( - - )} + + {/* Create new tag option */} + { + setEditingTag({ index: -1, value: '', tagName: '', isNew: true }) + }} + className='flex items-center gap-2 text-blue-600' + > + + Create new tag + +
+
{/* Edit Tag Value Modal */} {editingTag !== null && ( @@ -350,22 +314,19 @@ export function DocumentTagEntry({ ? undefined : kbTagDefinitions.find((t) => t.displayName === editingTag.tagName)?.fieldType } - onSave={(value, type) => { + onSave={(value, type, newTagName) => { if (editingTag.index === -1) { - // Creating new tag - handleCreateNewTag(editingTag.tagName, value, type) + // Creating new tag - use newTagName if provided, otherwise fall back to editingTag.tagName + const tagName = newTagName || editingTag.tagName + handleCreateNewTag(tagName, value, type) } else { // Updating existing tag handleUpdateTag(editingTag.index, value) } setEditingTag(null) - setInputValue('') - setShowInput(false) }} onCancel={() => { setEditingTag(null) - setInputValue('') - setShowInput(false) }} /> )} @@ -383,7 +344,7 @@ interface EditTagModalProps { initialValue: string isNew: boolean existingType?: string - onSave: (value: string, type?: string) => void + onSave: (value: string, type?: string, newTagName?: string) => void onCancel: () => void } @@ -397,6 +358,7 @@ function EditTagModal({ }: EditTagModalProps) { const [value, setValue] = useState(initialValue) const [fieldType, setFieldType] = useState(existingType || 'text') + const [newTagName, setNewTagName] = useState(tagName) const inputRef = useRef(null) useEffect(() => { @@ -405,8 +367,8 @@ function EditTagModal({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (value.trim()) { - onSave(value.trim(), fieldType) + if (value.trim() && (isNew ? newTagName.trim() : true)) { + onSave(value.trim(), fieldType, isNew ? newTagName.trim() : undefined) } } @@ -421,7 +383,7 @@ function EditTagModal({

- {isNew ? `Create "${tagName}" tag` : `Edit "${tagName}" value`} + {isNew ? 'Create new tag' : `Edit "${tagName}" value`}

{/* Type Badge in Top Right */} {!isNew && existingType && ( @@ -431,6 +393,19 @@ function EditTagModal({ )}
+ {/* Tag Name Input for New Tags */} + {isNew && ( +
+ + setNewTagName(e.target.value)} + placeholder='Enter tag name' + className='mt-1 text-sm' + /> +
+ )} + {/* Type Selection for New Tags */} {isNew && (
@@ -463,7 +438,11 @@ function EditTagModal({ -
diff --git a/apps/sim/hooks/use-tag-definitions.ts b/apps/sim/hooks/use-tag-definitions.ts index 28d15b19509..824c91ba2bc 100644 --- a/apps/sim/hooks/use-tag-definitions.ts +++ b/apps/sim/hooks/use-tag-definitions.ts @@ -74,6 +74,11 @@ export function useTagDefinitions( throw new Error('Knowledge base ID and document ID are required') } + // Simple validation + const validDefinitions = (definitions || []).filter( + (def) => def && def.tagSlot && def.displayName && def.displayName.trim() + ) + try { const response = await fetch( `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/tag-definitions`, @@ -82,7 +87,7 @@ export function useTagDefinitions( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ definitions }), + body: JSON.stringify({ definitions: validDefinitions }), } ) From dac0e23d7eff38e68126cf719daef52a3523338b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 26 Jul 2025 15:42:23 -0700 Subject: [PATCH 04/19] simplify --- .../[documentId]/tag-definitions/route.ts | 10 +------ .../document-tag-entry/document-tag-entry.tsx | 27 ++++++------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts index 4a71e9a7621..5c0dbea8737 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts @@ -280,15 +280,7 @@ export async function POST( } }) - // Run cleanup immediately - document values should be saved before tag definitions - const cleanedUpCount = await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId) - if (cleanedUpCount > 0) { - logger.info( - `[${requestId}] Created/updated ${createdDefinitions.length} tag definitions, cleaned up ${cleanedUpCount} unused definitions` - ) - } else { - logger.info(`[${requestId}] Created/updated ${createdDefinitions.length} tag definitions`) - } + logger.info(`[${requestId}] Created/updated ${createdDefinitions.length} tag definitions`) return NextResponse.json({ success: true, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx index 5e9deda6488..f3eb8bb4434 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx @@ -139,26 +139,18 @@ export function DocumentTagEntry({ const updatedTags = [...tags, newTag] - // Save both operations - if either fails, revert the local state + // SIMPLE ATOMIC OPERATION - NO CLEANUP try { - console.log('Creating new tag:', { tagName, value, fieldType, slot: newTag.slot }) - - // First save the tag definitions to ensure they exist - console.log('Saving tag definitions...') + // 1. Save tag definition first await handleSaveDefinitions(updatedTags) - // Then save the document tag values - console.log('Saving document values...') + // 2. Save document values if (onSave) { await onSave(updatedTags) } - // Only update local state after both operations succeed - console.log('Both operations succeeded, updating local state') + // 3. Update UI onTagsChange(updatedTags) - - // Refresh tag definitions for dropdown - await refreshTagDefinitions() } catch (error) { console.error('Failed to save tag:', error) alert(`Failed to save tag "${tagName}". Please try again.`) @@ -172,19 +164,16 @@ export function DocumentTagEntry({ i === index ? { ...tag, value: newValue.trim() } : tag ) - // FULLY SYNCHRONOUS - DO NOT UPDATE UI UNTIL BOTH OPERATIONS COMPLETE + // SIMPLE ATOMIC OPERATION - NO CLEANUP try { - // First save the document tag values + // 1. Save document values if (onSave) { await onSave(updatedTags) } - // Then save the tag definitions + // 2. Save tag definitions await handleSaveDefinitions(updatedTags) - // ONLY NOW update the UI + // 3. Update UI onTagsChange(updatedTags) - - // Refresh tag definitions for dropdown (in case cleanup removed unused ones) - await refreshTagDefinitions() } catch (error) { console.error('Failed to update tag:', error) } From 7af895299b5188312fc2a038c6e974468549df78 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 26 Jul 2025 17:09:15 -0700 Subject: [PATCH 05/19] checkpoint --- apps/sim/app/api/knowledge/search/route.ts | 219 ++++++++++++------ .../knowledge-tag-filter.tsx | 64 +++-- .../knowledge-tag-filters.tsx | 170 ++++++++++++++ .../components/sub-block/sub-block.tsx | 12 + apps/sim/blocks/blocks/knowledge.ts | 90 +------ apps/sim/tools/knowledge/search.ts | 91 +++----- 6 files changed, 410 insertions(+), 236 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 38be635b626..84ee647026a 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -8,31 +8,50 @@ import { estimateTokenCount } from '@/lib/tokenization/estimators' import { getUserId } from '@/app/api/auth/oauth/utils' import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils' import { db } from '@/db' -import { embedding } from '@/db/schema' +import { embedding, knowledgeBaseTagDefinitions } from '@/db/schema' import { calculateCost } from '@/providers/utils' const logger = createLogger('VectorSearchAPI') function getTagFilters(filters: Record, embedding: any) { return Object.entries(filters).map(([key, value]) => { - switch (key) { - case 'tag1': - return sql`LOWER(${embedding.tag1}) = LOWER(${value})` - case 'tag2': - return sql`LOWER(${embedding.tag2}) = LOWER(${value})` - case 'tag3': - return sql`LOWER(${embedding.tag3}) = LOWER(${value})` - case 'tag4': - return sql`LOWER(${embedding.tag4}) = LOWER(${value})` - case 'tag5': - return sql`LOWER(${embedding.tag5}) = LOWER(${value})` - case 'tag6': - return sql`LOWER(${embedding.tag6}) = LOWER(${value})` - case 'tag7': - return sql`LOWER(${embedding.tag7}) = LOWER(${value})` - default: - return sql`1=1` // No-op for unknown keys + // Handle OR logic within same tag + const values = value.includes('|OR|') ? value.split('|OR|') : [value] + console.log(`[getTagFilters] Processing ${key}="${value}" -> values:`, values) + + const getColumnForKey = (key: string) => { + switch (key) { + case 'tag1': + return embedding.tag1 + case 'tag2': + return embedding.tag2 + case 'tag3': + return embedding.tag3 + case 'tag4': + return embedding.tag4 + case 'tag5': + return embedding.tag5 + case 'tag6': + return embedding.tag6 + case 'tag7': + return embedding.tag7 + default: + return null + } + } + + const column = getColumnForKey(key) + if (!column) return sql`1=1` // No-op for unknown keys + + if (values.length === 1) { + // Single value - simple equality + console.log(`[getTagFilters] Single value filter: ${key} = ${values[0]}`) + return sql`LOWER(${column}) = LOWER(${values[0]})` } + // Multiple values - OR logic + console.log(`[getTagFilters] OR filter: ${key} IN (${values.join(', ')})`) + const orConditions = values.map((v) => sql`LOWER(${column}) = LOWER(${v})`) + return sql`(${sql.join(orConditions, sql` OR `)})` }) } @@ -53,17 +72,7 @@ const VectorSearchSchema = z.object({ ]), query: z.string().min(1, 'Search query is required'), topK: z.number().min(1).max(100).default(10), - filters: z - .object({ - tag1: z.string().optional(), - tag2: z.string().optional(), - tag3: z.string().optional(), - tag4: z.string().optional(), - tag5: z.string().optional(), - tag6: z.string().optional(), - tag7: z.string().optional(), - }) - .optional(), + filters: z.record(z.string()).optional(), // Allow dynamic filter keys (display names) }) async function generateSearchEmbedding(query: string): Promise { @@ -187,6 +196,7 @@ async function executeSingleQuery( distanceThreshold: number, filters?: Record ) { + console.log(`[executeSingleQuery] Called with filters:`, filters) return await db .select({ id: embedding.id, @@ -201,6 +211,7 @@ async function executeSingleQuery( tag6: embedding.tag6, tag7: embedding.tag7, distance: sql`${embedding.embedding} <=> ${queryVector}::vector`.as('distance'), + knowledgeBaseId: embedding.knowledgeBaseId, }) .from(embedding) .where( @@ -208,28 +219,7 @@ async function executeSingleQuery( inArray(embedding.knowledgeBaseId, knowledgeBaseIds), eq(embedding.enabled, true), sql`${embedding.embedding} <=> ${queryVector}::vector < ${distanceThreshold}`, - ...(filters - ? Object.entries(filters).map(([key, value]) => { - switch (key) { - case 'tag1': - return sql`LOWER(${embedding.tag1}) = LOWER(${value})` - case 'tag2': - return sql`LOWER(${embedding.tag2}) = LOWER(${value})` - case 'tag3': - return sql`LOWER(${embedding.tag3}) = LOWER(${value})` - case 'tag4': - return sql`LOWER(${embedding.tag4}) = LOWER(${value})` - case 'tag5': - return sql`LOWER(${embedding.tag5}) = LOWER(${value})` - case 'tag6': - return sql`LOWER(${embedding.tag6}) = LOWER(${value})` - case 'tag7': - return sql`LOWER(${embedding.tag7}) = LOWER(${value})` - default: - return sql`1=1` // No-op for unknown keys - } - }) - : []) + ...(filters ? getTagFilters(filters, embedding) : []) ) ) .orderBy(sql`${embedding.embedding} <=> ${queryVector}::vector`) @@ -271,6 +261,54 @@ export async function POST(request: NextRequest) { } } + // Map display names to tag slots for filtering + let mappedFilters: Record = {} + if (validatedData.filters && accessibleKbIds.length > 0) { + try { + // Fetch tag definitions for the first accessible KB (since we're using single KB now) + const kbId = accessibleKbIds[0] + const tagDefs = await db + .select({ + tagSlot: knowledgeBaseTagDefinitions.tagSlot, + displayName: knowledgeBaseTagDefinitions.displayName, + }) + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, kbId)) + + console.log(`[${requestId}] Found tag definitions:`, tagDefs) + console.log(`[${requestId}] Original filters:`, validatedData.filters) + + // Create mapping from display name to tag slot + const displayNameToSlot: Record = {} + tagDefs.forEach((def) => { + displayNameToSlot[def.displayName] = def.tagSlot + }) + + // Map the filters and handle OR logic + Object.entries(validatedData.filters).forEach(([key, value]) => { + if (value) { + const tagSlot = displayNameToSlot[key] || key // Fallback to key if no mapping found + + // Check if this is an OR filter (contains |OR| separator) + if (value.includes('|OR|')) { + console.log( + `[${requestId}] OR filter detected: "${key}" -> "${tagSlot}" = "${value}"` + ) + } + + mappedFilters[tagSlot] = value + console.log(`[${requestId}] Mapped filter: "${key}" -> "${tagSlot}" = "${value}"`) + } + }) + + console.log(`[${requestId}] Final mapped filters:`, mappedFilters) + } catch (error) { + console.error(`[${requestId}] Filter mapping error:`, error) + // If mapping fails, use original filters + mappedFilters = validatedData.filters + } + } + if (accessibleKbIds.length === 0) { return NextResponse.json( { error: 'Knowledge base not found or access denied' }, @@ -299,22 +337,24 @@ export async function POST(request: NextRequest) { if (strategy.useParallel) { // Execute parallel queries for better performance with many KBs + console.log(`[${requestId}] Executing parallel queries with filters:`, mappedFilters) const parallelResults = await executeParallelQueries( accessibleKbIds, queryVector, validatedData.topK, strategy.distanceThreshold, - validatedData.filters + mappedFilters ) results = mergeAndRankResults(parallelResults, validatedData.topK) } else { // Execute single optimized query for fewer KBs + console.log(`[${requestId}] Executing single query with filters:`, mappedFilters) results = await executeSingleQuery( accessibleKbIds, queryVector, validatedData.topK, strategy.distanceThreshold, - validatedData.filters + mappedFilters ) } @@ -331,23 +371,68 @@ export async function POST(request: NextRequest) { // Continue without cost information rather than failing the search } + // Fetch tag definitions for display name mapping (reuse the same fetch from filtering) + const tagDefinitionsMap: Record> = {} + for (const kbId of accessibleKbIds) { + try { + const tagDefs = await db + .select({ + tagSlot: knowledgeBaseTagDefinitions.tagSlot, + displayName: knowledgeBaseTagDefinitions.displayName, + }) + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, kbId)) + + tagDefinitionsMap[kbId] = {} + tagDefs.forEach((def) => { + tagDefinitionsMap[kbId][def.tagSlot] = def.displayName + }) + console.log( + `[${requestId}] Display mapping - KB ${kbId} tag definitions:`, + tagDefinitionsMap[kbId] + ) + } catch (error) { + console.error( + `[${requestId}] Failed to fetch tag definitions for display mapping:`, + error + ) + tagDefinitionsMap[kbId] = {} + } + } + return NextResponse.json({ success: true, data: { - results: results.map((result) => ({ - id: result.id, - content: result.content, - documentId: result.documentId, - chunkIndex: result.chunkIndex, - tag1: result.tag1, - tag2: result.tag2, - tag3: result.tag3, - tag4: result.tag4, - tag5: result.tag5, - tag6: result.tag6, - tag7: result.tag7, - similarity: 1 - result.distance, - })), + results: results.map((result) => { + const kbTagMap = tagDefinitionsMap[result.knowledgeBaseId] || {} + console.log( + `[${requestId}] Result KB: ${result.knowledgeBaseId}, available mappings:`, + kbTagMap + ) + + // Create tags object with display names + const tags: Record = {} + const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + + tagSlots.forEach((slot) => { + if (result[slot]) { + const displayName = kbTagMap[slot] || slot + console.log( + `[${requestId}] Mapping ${slot}="${result[slot]}" -> "${displayName}"="${result[slot]}"` + ) + tags[displayName] = result[slot] + } + }) + + return { + id: result.id, + content: result.content, + documentId: result.documentId, + chunkIndex: result.chunkIndex, + tags, // Clean display name mapped tags + similarity: 1 - result.distance, + } + }), query: validatedData.query, knowledgeBaseIds: accessibleKbIds, knowledgeBaseId: accessibleKbIds[0], diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx index 8f773a455de..86a45ef2b7b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filter/knowledge-tag-filter.tsx @@ -84,43 +84,35 @@ export function KnowledgeTagFilter({ } return ( -
- +
+ {/* Tag Name Selector */} + -
- {/* Tag Name Selector */} -
- - -
- - {/* Tag Value Input */} -
- - handleTagValueChange(e.target.value)} - placeholder='Enter value' - disabled={disabled || isConnecting} - className='text-sm' - /> -
-
+ {/* Tag Value Input - only show if tag is selected */} + {tagName && ( + handleTagValueChange(e.target.value)} + placeholder={`Enter ${tagName} value`} + disabled={disabled || isConnecting} + className='text-sm' + /> + )}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx new file mode 100644 index 00000000000..17db72d1447 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -0,0 +1,170 @@ +'use client' + +import { Plus, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import type { SubBlockConfig } from '@/blocks/types' +import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' +import { useSubBlockValue } from '../../hooks/use-sub-block-value' + +interface TagFilter { + id: string + tagName: string + tagValue: string +} + +interface KnowledgeTagFiltersProps { + blockId: string + subBlock: SubBlockConfig + disabled?: boolean + isPreview?: boolean + previewValue?: string | null + isConnecting?: boolean +} + +export function KnowledgeTagFilters({ + blockId, + subBlock, + disabled = false, + isPreview = false, + previewValue, + isConnecting = false, +}: KnowledgeTagFiltersProps) { + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + + // Get the knowledge base ID from other sub-blocks + const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId') + const knowledgeBaseId = knowledgeBaseIdValue || null + + // Use KB tag definitions hook to get available tags + const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) + + // Parse the current value to extract filters + const parseFilters = (filterValue: string): TagFilter[] => { + if (!filterValue) return [] + try { + return JSON.parse(filterValue) + } catch { + return [] + } + } + + const currentValue = isPreview ? previewValue : storeValue + const filters = parseFilters(currentValue || '') + + const updateFilters = (newFilters: TagFilter[]) => { + if (isPreview) return + const value = newFilters.length > 0 ? JSON.stringify(newFilters) : null + setStoreValue(value) + } + + const addFilter = () => { + const newFilter: TagFilter = { + id: Date.now().toString(), + tagName: '', + tagValue: '', + } + updateFilters([...filters, newFilter]) + } + + const removeFilter = (filterId: string) => { + updateFilters(filters.filter((f) => f.id !== filterId)) + } + + const updateFilter = (filterId: string, field: keyof TagFilter, value: string) => { + updateFilters(filters.map((f) => (f.id === filterId ? { ...f, [field]: value } : f))) + } + + if (isPreview) { + return ( +
+ +
+ {filters.length > 0 ? `${filters.length} filter(s)` : 'No filters'} +
+
+ ) + } + + return ( +
+
+ + +
+ + {filters.length === 0 && ( +
+ No tag filters. Click "Add Filter" to add one. +
+ )} + +
+ {filters.map((filter) => ( +
+ {/* Tag Name Selector */} +
+ +
+ + {/* Tag Value Input */} +
+ updateFilter(filter.id, 'tagValue', e.target.value)} + placeholder={filter.tagName ? `Enter ${filter.tagName} value` : 'Enter value'} + disabled={disabled || isConnecting} + className='text-sm h-8' + /> +
+ + {/* Remove Button */} + +
+ ))} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index ca75adcd000..97ceedd79c0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -20,6 +20,7 @@ import { FileUpload } from './components/file-upload' import { FolderSelectorInput } from './components/folder-selector/components/folder-selector-input' import { KnowledgeBaseSelector } from './components/knowledge-base-selector/knowledge-base-selector' import { KnowledgeTagFilter } from './components/knowledge-tag-filter/knowledge-tag-filter' +import { KnowledgeTagFilters } from './components/knowledge-tag-filters/knowledge-tag-filters' import { LongInput } from './components/long-input' import { ProjectSelectorInput } from './components/project-selector/project-selector-input' import { ResponseFormat } from './components/response/response-format' @@ -364,6 +365,17 @@ export function SubBlock({ isConnecting={isConnecting} /> ) + case 'knowledge-tag-filters': + return ( + + ) case 'document-selector': return ( { // Validate required fields for each operation - if (params.operation === 'search' && !params.knowledgeBaseIds) { - throw new Error('Knowledge base IDs are required for search operation') + if (params.operation === 'search' && !params.knowledgeBaseId) { + throw new Error('Knowledge base ID is required for search operation') } if ( (params.operation === 'upload_chunk' || params.operation === 'create_document') && @@ -74,21 +74,14 @@ export const KnowledgeBlock: BlockConfig = { }, inputs: { operation: { type: 'string', required: true }, - knowledgeBaseIds: { type: 'string', required: false }, knowledgeBaseId: { type: 'string', required: false }, query: { type: 'string', required: false }, topK: { type: 'number', required: false }, documentId: { type: 'string', required: false }, content: { type: 'string', required: false }, name: { type: 'string', required: false }, - // Tag filters for search - tag1: { type: 'string', required: false }, - tag2: { type: 'string', required: false }, - tag3: { type: 'string', required: false }, - tag4: { type: 'string', required: false }, - tag5: { type: 'string', required: false }, - tag6: { type: 'string', required: false }, - tag7: { type: 'string', required: false }, + // Dynamic tag filters for search + tagFilters: { type: 'string', required: false }, // Tag values for create document createTag1: { type: 'string', required: false }, createTag2: { type: 'string', required: false }, @@ -116,15 +109,6 @@ export const KnowledgeBlock: BlockConfig = { ], value: () => 'search', }, - { - id: 'knowledgeBaseIds', - title: 'Knowledge Bases', - type: 'knowledge-base-selector', - layout: 'full', - placeholder: 'Select knowledge bases', - multiSelect: true, - condition: { field: 'operation', value: 'search' }, - }, { id: 'knowledgeBaseId', title: 'Knowledge Base', @@ -132,7 +116,7 @@ export const KnowledgeBlock: BlockConfig = { layout: 'full', placeholder: 'Select knowledge base', multiSelect: false, - condition: { field: 'operation', value: ['upload_chunk', 'create_document'] }, + condition: { field: 'operation', value: ['search', 'upload_chunk', 'create_document'] }, }, { id: 'query', @@ -151,65 +135,11 @@ export const KnowledgeBlock: BlockConfig = { condition: { field: 'operation', value: 'search' }, }, { - id: 'tag1', - title: 'Tag 1 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 1', - condition: { field: 'operation', value: 'search' }, - mode: 'advanced', - }, - { - id: 'tag2', - title: 'Tag 2 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 2', - condition: { field: 'operation', value: 'search' }, - mode: 'advanced', - }, - { - id: 'tag3', - title: 'Tag 3 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 3', - condition: { field: 'operation', value: 'search' }, - mode: 'advanced', - }, - { - id: 'tag4', - title: 'Tag 4 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 4', - condition: { field: 'operation', value: 'search' }, - mode: 'advanced', - }, - { - id: 'tag5', - title: 'Tag 5 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 5', - condition: { field: 'operation', value: 'search' }, - mode: 'advanced', - }, - { - id: 'tag6', - title: 'Tag 6 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 6', - condition: { field: 'operation', value: 'search' }, - mode: 'advanced', - }, - { - id: 'tag7', - title: 'Tag 7 Filter', // This will be dynamically updated by the component - type: 'knowledge-tag-filter', - layout: 'half', - placeholder: 'Filter by tag 7', + id: 'tagFilters', + title: 'Tag Filters', + type: 'knowledge-tag-filters', + layout: 'full', + placeholder: 'Add tag filters', condition: { field: 'operation', value: 'search' }, mode: 'advanced', }, diff --git a/apps/sim/tools/knowledge/search.ts b/apps/sim/tools/knowledge/search.ts index 73b0ca786e2..6b12397a6df 100644 --- a/apps/sim/tools/knowledge/search.ts +++ b/apps/sim/tools/knowledge/search.ts @@ -7,11 +7,10 @@ export const knowledgeSearchTool: ToolConfig = { description: 'Search for similar content in one or more knowledge bases using vector similarity', version: '1.0.0', params: { - knowledgeBaseIds: { + knowledgeBaseId: { type: 'string', required: true, - description: - 'ID of the knowledge base to search in, or comma-separated IDs for multiple knowledge bases', + description: 'ID of the knowledge base to search in', }, query: { type: 'string', @@ -23,40 +22,10 @@ export const knowledgeSearchTool: ToolConfig = { required: false, description: 'Number of most similar results to return (1-100)', }, - tag1: { - type: 'string', - required: false, - description: 'Filter by tag 1 value', - }, - tag2: { - type: 'string', - required: false, - description: 'Filter by tag 2 value', - }, - tag3: { - type: 'string', - required: false, - description: 'Filter by tag 3 value', - }, - tag4: { - type: 'string', - required: false, - description: 'Filter by tag 4 value', - }, - tag5: { - type: 'string', - required: false, - description: 'Filter by tag 5 value', - }, - tag6: { - type: 'string', - required: false, - description: 'Filter by tag 6 value', - }, - tag7: { - type: 'string', + tagFilters: { + type: 'any', required: false, - description: 'Filter by tag 7 value', + description: 'Array of tag filters with tagName and tagValue properties', }, }, request: { @@ -68,25 +37,41 @@ export const knowledgeSearchTool: ToolConfig = { body: (params) => { const workflowId = params._context?.workflowId - // Handle multiple knowledge base IDs - let knowledgeBaseIds = params.knowledgeBaseIds - if (typeof knowledgeBaseIds === 'string' && knowledgeBaseIds.includes(',')) { - // Split comma-separated string into array - knowledgeBaseIds = knowledgeBaseIds - .split(',') - .map((id) => id.trim()) - .filter((id) => id.length > 0) - } + // Use single knowledge base ID + const knowledgeBaseIds = [params.knowledgeBaseId] - // Build filters object from tag parameters + // Parse dynamic tag filters and send display names to API const filters: Record = {} - if (params.tag1) filters.tag1 = params.tag1.toString() - if (params.tag2) filters.tag2 = params.tag2.toString() - if (params.tag3) filters.tag3 = params.tag3.toString() - if (params.tag4) filters.tag4 = params.tag4.toString() - if (params.tag5) filters.tag5 = params.tag5.toString() - if (params.tag6) filters.tag6 = params.tag6.toString() - if (params.tag7) filters.tag7 = params.tag7.toString() + if (params.tagFilters) { + let tagFilters = params.tagFilters + + // Handle both string (JSON) and array formats + if (typeof tagFilters === 'string') { + try { + tagFilters = JSON.parse(tagFilters) + } catch (error) { + tagFilters = [] + } + } + + if (Array.isArray(tagFilters)) { + // Group filters by tag name for OR logic within same tag + const groupedFilters: Record = {} + tagFilters.forEach((filter: any) => { + if (filter.tagName && filter.tagValue) { + if (!groupedFilters[filter.tagName]) { + groupedFilters[filter.tagName] = [] + } + groupedFilters[filter.tagName].push(filter.tagValue) + } + }) + + // Convert to filters format - for now, join multiple values with OR separator + Object.entries(groupedFilters).forEach(([tagName, values]) => { + filters[tagName] = values.join('|OR|') // Use special separator for OR logic + }) + } + } const requestBody = { knowledgeBaseIds, From 92be7ef375d21c56b6cedb2ec0fae7134ce38136 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 26 Jul 2025 17:11:45 -0700 Subject: [PATCH 06/19] works --- .../components/knowledge-tag-filters/knowledge-tag-filters.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index 17db72d1447..0e1483e889c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -96,8 +96,7 @@ export function KnowledgeTagFilters({ return (
-
- +
{filters.length === 0 && ( -
+
No tag filters. Click "Add Filter" to add one.
)}
{filters.map((filter) => ( -
+
{/* Tag Name Selector */}
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
+
+
+ = 7} + 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 = '' + } + }} + /> +
+
+ {usedTagNames.size > 0 ? 'Press Enter (no duplicates)' : 'Press Enter to add'} +
+
+
+ + {/* Empty State */} + {tags.length === 0 && availableTagNames.length === 0 && ( +
+
No tags available
+
Create a new tag above to get started
+
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index 97ceedd79c0..f78b17b7e4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -13,6 +13,7 @@ import { ConditionInput } from './components/condition-input' import { CredentialSelector } from './components/credential-selector/credential-selector' import { DateInput } from './components/date-input' import { DocumentSelector } from './components/document-selector/document-selector' +import { DocumentTagEntry } from './components/document-tag-entry/document-tag-entry' import { Dropdown } from './components/dropdown' import { EvalInput } from './components/eval-input' import { FileSelectorInput } from './components/file-selector/file-selector-input' @@ -376,6 +377,18 @@ export function SubBlock({ isConnecting={isConnecting} /> ) + + case 'document-tag-entry': + return ( + + ) case 'document-selector': return ( `/api/knowledge/${params.knowledgeBaseId}/documents`, @@ -95,20 +100,31 @@ export const knowledgeCreateDocumentTool: ToolConfig = {} + + if (params.documentTagsData && Array.isArray(params.documentTagsData)) { + // Use structured tag data - pass it to the API for proper handling + // The API will create tag definitions and map to slots + tagData.documentTagsData = JSON.stringify(params.documentTagsData) + } else { + // Fallback to individual tag parameters + if (params.tag1) tagData.tag1 = params.tag1 + if (params.tag2) tagData.tag2 = params.tag2 + if (params.tag3) tagData.tag3 = params.tag3 + if (params.tag4) tagData.tag4 = params.tag4 + if (params.tag5) tagData.tag5 = params.tag5 + if (params.tag6) tagData.tag6 = params.tag6 + if (params.tag7) tagData.tag7 = params.tag7 + } + const documents = [ { filename: documentName.endsWith('.txt') ? documentName : `${documentName}.txt`, fileUrl: dataUri, fileSize: contentBytes, mimeType: 'text/plain', - // Include tags if provided - tag1: params.tag1 || undefined, - tag2: params.tag2 || undefined, - tag3: params.tag3 || undefined, - tag4: params.tag4 || undefined, - tag5: params.tag5 || undefined, - tag6: params.tag6 || undefined, - tag7: params.tag7 || undefined, + ...tagData, }, ] From f61b486758d9d1ead55fdb483878025ecbe9d4bd Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 15:21:38 -0700 Subject: [PATCH 09/19] working block --- .../app/api/knowledge/[id]/documents/route.ts | 43 +++++++++++++++---- .../document-tag-entry/document-tag-entry.tsx | 31 +------------ apps/sim/blocks/blocks/knowledge.ts | 24 ----------- apps/sim/blocks/types.ts | 2 + apps/sim/tools/knowledge/create_document.ts | 30 +++++++------ 5 files changed, 54 insertions(+), 76 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 290b4d5bb5c..a55fc55408d 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -89,7 +89,7 @@ async function processDocumentTags( } await db.insert(knowledgeBaseTagDefinitions).values(newDefinition) - existingBySlot.set(targetSlot, newDefinition) + existingBySlot.set(targetSlot as any, newDefinition) logger.info(`[${requestId}] Created tag definition: ${tagName} -> ${targetSlot}`) } @@ -438,6 +438,31 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const documentId = crypto.randomUUID() const now = new Date() + // Process documentTagsData if provided (for knowledge base block) + let processedTags: Record = { + tag1: null, + tag2: null, + tag3: null, + tag4: null, + tag5: null, + tag6: null, + tag7: null, + } + + if (docData.documentTagsData) { + try { + const tagData = JSON.parse(docData.documentTagsData) + if (Array.isArray(tagData)) { + processedTags = await processDocumentTags(knowledgeBaseId, tagData, requestId) + } + } catch (error) { + logger.warn( + `[${requestId}] Failed to parse documentTagsData for bulk document:`, + error + ) + } + } + const newDocument = { id: documentId, knowledgeBaseId, @@ -451,14 +476,14 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: processingStatus: 'pending' as const, enabled: true, uploadedAt: now, - // Include tags from upload - tag1: docData.tag1 || null, - tag2: docData.tag2 || null, - tag3: docData.tag3 || null, - tag4: docData.tag4 || null, - tag5: docData.tag5 || null, - tag6: docData.tag6 || null, - tag7: docData.tag7 || null, + // Use processed tags if available, otherwise fall back to individual tag fields + tag1: processedTags.tag1 || docData.tag1 || null, + tag2: processedTags.tag2 || docData.tag2 || null, + tag3: processedTags.tag3 || docData.tag3 || null, + tag4: processedTags.tag4 || docData.tag4 || null, + tag5: processedTags.tag5 || docData.tag5 || null, + tag6: processedTags.tag6 || docData.tag6 || null, + tag7: processedTags.tag7 || docData.tag7 || null, } await tx.insert(document).values(newDocument) 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 dd4954afdad..93a85241ecf 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 @@ -3,13 +3,13 @@ import { Plus, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' -import type { SubBlockConfig } from '@/types/block' import { useSubBlockValue } from '../../hooks/use-sub-block-value' interface DocumentTag { id: string - tagName: string + tagName: string // This will be mapped to displayName for API fieldType: string value: string } @@ -59,16 +59,6 @@ export function DocumentTagEntry({ setStoreValue(value) } - const addTag = () => { - const newTag: DocumentTag = { - id: Date.now().toString(), - tagName: '', - fieldType: 'text', - value: '', - } - updateTags([...tags, newTag]) - } - const removeTag = (tagId: string) => { updateTags(tags.filter((t) => t.id !== tagId)) } @@ -77,29 +67,12 @@ export function DocumentTagEntry({ updateTags(tags.map((tag) => (tag.id === tagId ? { ...tag, ...updates } : tag))) } - const handleTagNameChange = (tagId: string, value: string) => { - if (value === '__create_new__') { - // Switch to input mode by setting a placeholder name - updateTag(tagId, { tagName: '' }) - return - } - updateTag(tagId, { tagName: value }) - } - // 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)) - // Field type options - const fieldTypes = [ - { value: 'text', label: 'Text' }, - { value: 'number', label: 'Number' }, - { value: 'date', label: 'Date' }, - { value: 'boolean', label: 'Boolean' }, - ] - if (isLoading) { return
Loading tag definitions...
} diff --git a/apps/sim/blocks/blocks/knowledge.ts b/apps/sim/blocks/blocks/knowledge.ts index 2e53dac5b69..75914c09756 100644 --- a/apps/sim/blocks/blocks/knowledge.ts +++ b/apps/sim/blocks/blocks/knowledge.ts @@ -43,30 +43,6 @@ export const KnowledgeBlock: BlockConfig = { throw new Error('Document ID is required for upload_chunk operation') } - // For create_document operation, map documentTags to individual tag fields - if (params.operation === 'create_document') { - const mappedParams = { ...params } - - // Parse documentTags and pass to API for proper handling - if (params.documentTags) { - try { - const tags = JSON.parse(params.documentTags) - if (Array.isArray(tags)) { - // Pass the structured tag data to the API - // The API will handle tag definition creation and slot mapping - mappedParams.documentTagsData = tags - } - } catch (error) { - console.warn('Failed to parse documentTags:', error) - } - } - - // Remove documentTags field from the final params - mappedParams.documentTags = undefined - - return mappedParams - } - return params }, }, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 359807945be..14f2e8eae9e 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -34,7 +34,9 @@ export type SubBlockType = | 'folder-selector' // Folder selector for Gmail, etc. | 'knowledge-base-selector' // Knowledge base selector | 'knowledge-tag-filter' // Dynamic tag filter for knowledge bases + | 'knowledge-tag-filters' // Multiple tag filters for knowledge bases | 'document-selector' // Document selector for knowledge bases + | 'document-tag-entry' // Document tag entry for creating documents | 'input-format' // Input structure format | 'response-format' // Response structure format | 'file-upload' // File uploader diff --git a/apps/sim/tools/knowledge/create_document.ts b/apps/sim/tools/knowledge/create_document.ts index 1e8143f693d..950f75cbd29 100644 --- a/apps/sim/tools/knowledge/create_document.ts +++ b/apps/sim/tools/knowledge/create_document.ts @@ -100,22 +100,24 @@ export const knowledgeCreateDocumentTool: ToolConfig = {} - if (params.documentTagsData && Array.isArray(params.documentTagsData)) { - // Use structured tag data - pass it to the API for proper handling - // The API will create tag definitions and map to slots - tagData.documentTagsData = JSON.stringify(params.documentTagsData) - } else { - // Fallback to individual tag parameters - if (params.tag1) tagData.tag1 = params.tag1 - if (params.tag2) tagData.tag2 = params.tag2 - if (params.tag3) tagData.tag3 = params.tag3 - if (params.tag4) tagData.tag4 = params.tag4 - if (params.tag5) tagData.tag5 = params.tag5 - if (params.tag6) tagData.tag6 = params.tag6 - if (params.tag7) tagData.tag7 = params.tag7 + if (params.documentTags) { + let parsedTags = params.documentTags + + // Handle both string (JSON) and array formats + if (typeof params.documentTags === 'string') { + try { + parsedTags = JSON.parse(params.documentTags) + } catch (error) { + console.warn('Failed to parse documentTags string:', error) + parsedTags = [] + } + } + + if (Array.isArray(parsedTags)) { + tagData.documentTagsData = JSON.stringify(parsedTags) + } } const documents = [ From 7a34315a71f35d8ef14d9da624a454edf1b862e7 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 16:48:21 -0700 Subject: [PATCH 10/19] fix import conflicts --- .../[documentId]/tag-definitions/route.ts | 4 +-- .../app/api/knowledge/[id]/documents/route.ts | 4 +-- .../knowledge/[id]/tag-definitions/route.ts | 2 +- .../knowledge/[id]/[documentId]/document.tsx | 22 ++++++---------- .../components/upload-modal/upload-modal.tsx | 7 +++-- .../components/create-modal/create-modal.tsx | 8 +----- .../components/sub-block/sub-block.tsx | 26 ------------------- .../use-knowledge-base-tag-definitions.ts | 2 +- apps/sim/hooks/use-tag-definitions.ts | 2 +- 9 files changed, 18 insertions(+), 59 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts index 5c0dbea8737..d8c287f82ac 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts @@ -3,7 +3,7 @@ import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { createLogger } from '@/lib/logs/console-logger' +import { createLogger } from '@/lib/logs/console/logger' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' import { db } from '@/db' import { document, knowledgeBaseTagDefinitions } from '@/db/schema' @@ -196,7 +196,7 @@ export async function POST( } const now = new Date() - const createdDefinitions = [] + const createdDefinitions: (typeof knowledgeBaseTagDefinitions.$inferSelect)[] = [] // Get existing definitions count before transaction for cleanup check const existingDefinitions = await db diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index c1bb0087062..85debbfd416 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -5,15 +5,13 @@ import { z } from 'zod' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' import { getUserId } from '@/app/api/auth/oauth/utils' -import { db } from '@/db' -import { document, knowledgeBaseTagDefinitions } from '@/db/schema' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess, processDocumentAsync, } from '@/app/api/knowledge/utils' import { db } from '@/db' -import { document } from '@/db/schema' +import { document, knowledgeBaseTagDefinitions } from '@/db/schema' const logger = createLogger('DocumentsAPI') diff --git a/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts index 38f87be8fa8..6d43155f03f 100644 --- a/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { createLogger } from '@/lib/logs/console-logger' +import { createLogger } from '@/lib/logs/console/logger' import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils' import { db } from '@/db' import { knowledgeBaseTagDefinitions } from '@/db/schema' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 40eb5d87c25..37c33dc9ac7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -13,11 +13,6 @@ import { } from '@/components/ui' import { createLogger } from '@/lib/logs/console/logger' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/components/providers/workspace-permissions-provider' -import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar' -import { - type DocumentTag, - DocumentTagEntry, -} from '@/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry' import { CreateChunkModal, DeleteChunkModal, @@ -26,6 +21,10 @@ import { } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components' import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components' import { KnowledgeHeader, SearchInput } from '@/app/workspace/[workspaceId]/knowledge/components' +import { + type DocumentTag, + DocumentTagEntry, +} from '@/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry' import { useDocumentChunks } from '@/hooks/use-knowledge' import { useTagDefinitions } from '@/hooks/use-tag-definitions' import { type ChunkData, type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store' @@ -60,7 +59,6 @@ export function Document({ getCachedKnowledgeBase, getCachedDocuments, updateDocument: updateDocumentInStore, - refreshDocuments, } = useKnowledgeStore() const { workspaceId } = useParams() const router = useRouter() @@ -71,7 +69,6 @@ export function Document({ const { chunks: paginatedChunks, allChunks, - filteredChunks, searchQuery, setSearchQuery, currentPage, @@ -99,10 +96,7 @@ export function Document({ const [error, setError] = useState(null) // Use tag definitions hook for custom labels - const { getTagLabel, tagDefinitions, fetchTagDefinitions } = useTagDefinitions( - knowledgeBaseId, - documentId - ) + const { tagDefinitions, fetchTagDefinitions } = useTagDefinitions(knowledgeBaseId, documentId) // Function to build document tags from data and definitions const buildDocumentTags = useCallback( @@ -369,7 +363,7 @@ export function Document({ } } - const handleChunkCreated = async (newChunk: ChunkData) => { + const handleChunkCreated = async () => { // Refresh the chunks list to include the new chunk await refreshChunks() } @@ -872,7 +866,7 @@ export function Document({ {/* Edit Chunk Modal */} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx index 8f4af309732..8bfae543208 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx @@ -5,12 +5,11 @@ import { X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' +import { createLogger } from '@/lib/logs/console/logger' import { type DocumentTag, DocumentTagEntry, -} from '../../../components/document-tag-entry/document-tag-entry' -import { createLogger } from '@/lib/logs/console/logger' -import { type TagData, TagInput } from '@/app/workspace/[workspaceId]/knowledge/components' +} from '@/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry' import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload' const logger = createLogger('UploadModal') @@ -157,7 +156,7 @@ export function UploadModal({ // Create files with tags for upload const filesWithTags = files.map((file) => { // Add tags as custom properties to the file object - const fileWithTags = file as File & Record + const fileWithTags = file as unknown as File & Record Object.assign(fileWithTags, tagData) return fileWithTags }) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx index 587a37a9cab..5eece7b5af9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx @@ -13,14 +13,9 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { createLogger } from '@/lib/logs/console/logger' -import { - getDocumentIcon, - type TagData, - TagInput, -} from '@/app/workspace/[workspaceId]/knowledge/components' +import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload' import type { KnowledgeBaseData } from '@/stores/knowledge/store' -import { useKnowledgeUpload } from '../../hooks/use-knowledge-upload' const logger = createLogger('CreateModal') @@ -308,7 +303,6 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea maxChunkSize: 1024, overlapSize: 200, }) - setTags({}) // Clean up file previews files.forEach((file) => URL.revokeObjectURL(file.preview)) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index 34f335963c6..ef58591c803 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -32,35 +32,9 @@ import { import { getBlock } from '@/blocks/index' import type { SubBlockConfig } from '@/blocks/types' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import { ChannelSelectorInput } from './components/channel-selector/channel-selector-input' -import { CheckboxList } from './components/checkbox-list' -import { Code } from './components/code' -import { ComboBox } from './components/combobox' -import { ConditionInput } from './components/condition-input' -import { CredentialSelector } from './components/credential-selector/credential-selector' -import { DateInput } from './components/date-input' -import { DocumentSelector } from './components/document-selector/document-selector' import { DocumentTagEntry } from './components/document-tag-entry/document-tag-entry' -import { Dropdown } from './components/dropdown' -import { EvalInput } from './components/eval-input' -import { FileSelectorInput } from './components/file-selector/file-selector-input' -import { FileUpload } from './components/file-upload' -import { FolderSelectorInput } from './components/folder-selector/components/folder-selector-input' -import { KnowledgeBaseSelector } from './components/knowledge-base-selector/knowledge-base-selector' import { KnowledgeTagFilter } from './components/knowledge-tag-filter/knowledge-tag-filter' import { KnowledgeTagFilters } from './components/knowledge-tag-filters/knowledge-tag-filters' -import { LongInput } from './components/long-input' -import { ProjectSelectorInput } from './components/project-selector/project-selector-input' -import { ResponseFormat } from './components/response/response-format' -import { ScheduleConfig } from './components/schedule/schedule-config' -import { ShortInput } from './components/short-input' -import { SliderInput } from './components/slider-input' -import { InputFormat } from './components/starter/input-format' -import { Switch } from './components/switch' -import { Table } from './components/table' -import { TimeInput } from './components/time-input' -import { ToolInput } from './components/tool-input/tool-input' -import { WebhookConfig } from './components/webhook/webhook' interface SubBlockProps { blockId: string diff --git a/apps/sim/hooks/use-knowledge-base-tag-definitions.ts b/apps/sim/hooks/use-knowledge-base-tag-definitions.ts index 716e72f21e8..d00f8d38960 100644 --- a/apps/sim/hooks/use-knowledge-base-tag-definitions.ts +++ b/apps/sim/hooks/use-knowledge-base-tag-definitions.ts @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useState } from 'react' -import { createLogger } from '@/lib/logs/console-logger' +import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('useKnowledgeBaseTagDefinitions') diff --git a/apps/sim/hooks/use-tag-definitions.ts b/apps/sim/hooks/use-tag-definitions.ts index bf342eb0072..6e884fb2643 100644 --- a/apps/sim/hooks/use-tag-definitions.ts +++ b/apps/sim/hooks/use-tag-definitions.ts @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useState } from 'react' -import { createLogger } from '@/lib/logs/console-logger' +import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('useTagDefinitions') From 7467b4147d25217a17a421762b35c31d9b57cb83 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 16:55:53 -0700 Subject: [PATCH 11/19] fix tests --- .../[id]/documents/[documentId]/route.test.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts index 148b793e5aa..302d5f0b1b3 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts @@ -218,8 +218,15 @@ describe('Document By ID API Route', () => { }), } + // Mock transaction + mockDbChain.transaction.mockImplementation(async (callback) => { + const mockTx = { + update: vi.fn().mockReturnValue(updateChain), + } + await callback(mockTx) + }) + // Mock db operations in sequence - mockDbChain.update.mockReturnValue(updateChain) mockDbChain.select.mockReturnValue(selectChain) const req = createMockRequest('PUT', validUpdateData) @@ -231,7 +238,7 @@ describe('Document By ID API Route', () => { expect(data.success).toBe(true) expect(data.data.filename).toBe('updated-document.pdf') expect(data.data.enabled).toBe(false) - expect(mockDbChain.update).toHaveBeenCalled() + expect(mockDbChain.transaction).toHaveBeenCalled() expect(mockDbChain.select).toHaveBeenCalled() }) @@ -298,8 +305,15 @@ describe('Document By ID API Route', () => { }), } + // Mock transaction + mockDbChain.transaction.mockImplementation(async (callback) => { + const mockTx = { + update: vi.fn().mockReturnValue(updateChain), + } + await callback(mockTx) + }) + // Mock db operations in sequence - mockDbChain.update.mockReturnValue(updateChain) mockDbChain.select.mockReturnValue(selectChain) const req = createMockRequest('PUT', { markFailedDueToTimeout: true }) @@ -309,7 +323,7 @@ describe('Document By ID API Route', () => { expect(response.status).toBe(200) expect(data.success).toBe(true) - expect(mockDbChain.update).toHaveBeenCalled() + expect(mockDbChain.transaction).toHaveBeenCalled() expect(updateChain.set).toHaveBeenCalledWith( expect.objectContaining({ processingStatus: 'failed', @@ -479,7 +493,9 @@ describe('Document By ID API Route', () => { document: mockDocument, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) - mockDbChain.set.mockRejectedValue(new Error('Database error')) + + // Mock transaction to throw an error + mockDbChain.transaction.mockRejectedValue(new Error('Database error')) const req = createMockRequest('PUT', validUpdateData) const { PUT } = await import('@/app/api/knowledge/[id]/documents/[documentId]/route') From 2bff74d2e4f563d7931fc07b0a2800d71144d7e1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 17:56:19 -0700 Subject: [PATCH 12/19] add blockers to going past max tag slots --- .../[documentId]/tag-definitions/route.ts | 22 ++++++++- .../app/api/knowledge/[id]/documents/route.ts | 19 ++++---- .../knowledge/[id]/[documentId]/document.tsx | 7 +-- .../document-tag-entry/document-tag-entry.tsx | 45 ++++++++++++------- .../components/tag-input/tag-input.tsx | 25 ++++------- .../knowledge/hooks/use-knowledge-upload.ts | 9 +--- .../document-tag-entry/document-tag-entry.tsx | 31 +++++++------ apps/sim/db/schema.ts | 3 +- .../use-knowledge-base-tag-definitions.ts | 3 +- apps/sim/hooks/use-tag-definitions.ts | 5 ++- apps/sim/lib/constants/knowledge.ts | 15 +++++++ 11 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 apps/sim/lib/constants/knowledge.ts diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts index d8c287f82ac..135c74b8b7a 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts @@ -3,6 +3,7 @@ import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { MAX_TAG_SLOTS, TAG_SLOTS } from '@/lib/constants/knowledge' import { createLogger } from '@/lib/logs/console/logger' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' import { db } from '@/db' @@ -13,13 +14,15 @@ export const dynamic = 'force-dynamic' const logger = createLogger('DocumentTagDefinitionsAPI') const TagDefinitionSchema = z.object({ - tagSlot: z.enum(['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7']), + tagSlot: z.enum(TAG_SLOTS as [string, ...string[]]), displayName: z.string().min(1, 'Display name is required').max(100, 'Display name too long'), fieldType: z.string().default('text'), // Currently only 'text', future: 'date', 'number', 'range' }) const BulkTagDefinitionsSchema = z.object({ - definitions: z.array(TagDefinitionSchema).max(7, 'Cannot define more than 7 tags'), + definitions: z + .array(TagDefinitionSchema) + .max(MAX_TAG_SLOTS, `Cannot define more than ${MAX_TAG_SLOTS} tags`), }) // Helper function to clean up unused tag definitions @@ -204,6 +207,21 @@ export async function POST( .from(knowledgeBaseTagDefinitions) .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId)) + // Check if we're trying to create more tag definitions than available slots + const existingTagNames = new Set(existingDefinitions.map((def) => def.displayName)) + const trulyNewTags = validatedData.definitions.filter( + (def) => !existingTagNames.has(def.displayName) + ) + + if (existingDefinitions.length + trulyNewTags.length > MAX_TAG_SLOTS) { + return NextResponse.json( + { + error: `Cannot create ${trulyNewTags.length} new tags. Knowledge base already has ${existingDefinitions.length} tag definitions. Maximum is ${MAX_TAG_SLOTS} total.`, + }, + { status: 400 } + ) + } + // Use transaction to ensure consistency await db.transaction(async (tx) => { // Create maps for lookups diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 85debbfd416..7619a330f7b 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -3,6 +3,7 @@ import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { TAG_SLOTS } from '@/lib/constants/knowledge' import { createLogger } from '@/lib/logs/console/logger' import { getUserId } from '@/app/api/auth/oauth/utils' import { @@ -28,15 +29,12 @@ async function processDocumentTags( tagData: Array<{ tagName: string; fieldType: string; value: string }>, requestId: string ): Promise> { - const result: Record = { - tag1: null, - tag2: null, - tag3: null, - tag4: null, - tag5: null, - tag6: null, - tag7: null, - } + const result: Record = {} + + // Initialize all tag slots to null + TAG_SLOTS.forEach((slot) => { + result[slot] = null + }) if (!Array.isArray(tagData) || tagData.length === 0) { return result @@ -68,8 +66,7 @@ async function processDocumentTags( targetSlot = existingDef.tagSlot } else { // Find next available slot - const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const - for (const slot of tagSlots) { + for (const slot of TAG_SLOTS) { if (!existingBySlot.has(slot)) { targetSlot = slot break diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 37c33dc9ac7..901b0a74bb2 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -11,6 +11,7 @@ import { TooltipContent, TooltipTrigger, } from '@/components/ui' +import { TAG_SLOTS } from '@/lib/constants/knowledge' import { createLogger } from '@/lib/logs/console/logger' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/components/providers/workspace-permissions-provider' import { @@ -102,10 +103,10 @@ export function Document({ const buildDocumentTags = useCallback( (docData: DocumentData, definitions: any[], currentTags?: DocumentTag[]) => { const tags: DocumentTag[] = [] - const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + const tagSlots = TAG_SLOTS tagSlots.forEach((slot) => { - const value = docData[slot] + const value = (docData as any)[slot] as string | null | undefined const definition = definitions.find((def) => def.tagSlot === slot) const currentTag = currentTags?.find((tag) => tag.slot === slot) @@ -140,7 +141,7 @@ export function Document({ try { // Convert DocumentTag array to tag data for API const tagData: Record = {} - const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const + const tagSlots = TAG_SLOTS // Clear all tags first tagSlots.forEach((slot) => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx index 8b06a8d9556..ab3aa91d1a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx @@ -18,11 +18,12 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { MAX_TAG_SLOTS, TAG_SLOTS, type TagSlot } from '@/lib/constants/knowledge' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' import { type TagDefinitionInput, useTagDefinitions } from '@/hooks/use-tag-definitions' export interface DocumentTag { - slot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + slot: TagSlot displayName: string fieldType: string value: string @@ -37,7 +38,7 @@ interface DocumentTagEntryProps { onSave?: (tags: DocumentTag[]) => Promise } -const TAG_SLOTS = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const +// TAG_SLOTS is now imported from constants export function DocumentTagEntry({ tags, @@ -77,7 +78,7 @@ export function DocumentTagEntry({ const definitions: TagDefinitionInput[] = currentTags .filter((tag) => tag?.displayName?.trim()) .map((tag) => ({ - tagSlot: tag.slot, + tagSlot: tag.slot as TagSlot, displayName: tag.displayName.trim(), fieldType: tag.fieldType || 'text', })) @@ -115,6 +116,9 @@ export function DocumentTagEntry({ .map((tag) => tag.displayName) .filter((tagName) => !tags.some((tag) => tag.displayName === tagName)) + // Check if we can add more tags (KB has less than MAX_TAG_SLOTS tag definitions) + const canAddMoreTags = kbTagDefinitions.length < MAX_TAG_SLOTS + const handleSuggestionClick = (tagName: string) => { setEditingTag({ index: -1, value: '', tagName, isNew: false }) } @@ -248,7 +252,7 @@ export function DocumentTagEntry({ type='button' variant='outline' size='sm' - disabled={disabled || tags.length >= 7} + disabled={disabled || (!canAddMoreTags && availableTagNames.length === 0)} className='gap-1 text-muted-foreground hover:text-foreground' > @@ -279,16 +283,22 @@ export function DocumentTagEntry({ )} - {/* Create new tag option */} - { - setEditingTag({ index: -1, value: '', tagName: '', isNew: true }) - }} - className='flex items-center gap-2 text-blue-600' - > - - Create new tag - + {/* Create new tag option or disabled message */} + {canAddMoreTags ? ( + { + setEditingTag({ index: -1, value: '', tagName: '', isNew: true }) + }} + className='flex items-center gap-2 text-blue-600' + > + + Create new tag + + ) : ( +
+ All {MAX_TAG_SLOTS} tag slots used in this knowledge base +
+ )} @@ -320,8 +330,11 @@ export function DocumentTagEntry({ /> )} - {tags.length > 0 && ( -
{tags.length} of 7 tags used
+ {/* Tag count display */} + {kbTagDefinitions.length > 0 && ( +
+ {kbTagDefinitions.length} of {MAX_TAG_SLOTS} tag slots used in this knowledge base +
)}
) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx index ffcc916b0bf..1657967dbc9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx @@ -6,16 +6,11 @@ import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { TAG_SLOTS, type TagSlot } from '@/lib/constants/knowledge' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' -export interface TagData { - tag1?: string - tag2?: string - tag3?: string - tag4?: string - tag5?: string - tag6?: string - tag7?: string +export type TagData = { + [K in TagSlot]?: string } interface TagInputProps { @@ -27,15 +22,11 @@ interface TagInputProps { documentId?: string | null } -const TAG_LABELS = [ - { key: 'tag1' as keyof TagData, label: 'Tag 1', placeholder: 'Enter tag value' }, - { key: 'tag2' as keyof TagData, label: 'Tag 2', placeholder: 'Enter tag value' }, - { key: 'tag3' as keyof TagData, label: 'Tag 3', placeholder: 'Enter tag value' }, - { key: 'tag4' as keyof TagData, label: 'Tag 4', placeholder: 'Enter tag value' }, - { key: 'tag5' as keyof TagData, label: 'Tag 5', placeholder: 'Enter tag value' }, - { key: 'tag6' as keyof TagData, label: 'Tag 6', placeholder: 'Enter tag value' }, - { key: 'tag7' as keyof TagData, label: 'Tag 7', placeholder: 'Enter tag value' }, -] +const TAG_LABELS = TAG_SLOTS.map((slot, index) => ({ + key: slot as keyof TagData, + label: `Tag ${index + 1}`, + placeholder: 'Enter tag value', +})) export function TagInput({ tags, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts index 77b8d541695..99dae058688 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts @@ -274,14 +274,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) { const processPayload = { documents: uploadedFiles.map((file) => ({ ...file, - // Extract tags from file if they exist (added by upload modal) - tag1: (file as any).tag1, - tag2: (file as any).tag2, - tag3: (file as any).tag3, - tag4: (file as any).tag4, - tag5: (file as any).tag5, - tag6: (file as any).tag6, - tag7: (file as any).tag7, + // Tags are already included in the file object from createUploadedFile })), processingOptions: { chunkSize: processingOptions.chunkSize || 1024, 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 93a85241ecf..5fb85fa91b0 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 @@ -3,6 +3,7 @@ import { Plus, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { MAX_TAG_SLOTS } from '@/lib/constants/knowledge' import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' import { useSubBlockValue } from '../../hooks/use-sub-block-value' @@ -74,7 +75,7 @@ export function DocumentTagEntry({ .filter((name) => !usedTagNames.has(name)) if (isLoading) { - return
Loading tag definitions...
+ return
Loading tag definitions...
} return ( @@ -82,7 +83,7 @@ export function DocumentTagEntry({ {/* Available Tags Section */} {availableTagNames.length > 0 && (
-
+
Available Tags (click to add)
@@ -104,11 +105,11 @@ export function DocumentTagEntry({ } }} disabled={disabled || isConnecting} - className='inline-flex items-center gap-1 rounded-full border border-dashed border-gray-300 bg-gray-50 px-3 py-1 text-sm text-gray-600 transition-colors hover:border-blue-300 hover:bg-blue-50 hover:text-blue-700 disabled:opacity-50' + className='inline-flex items-center gap-1 rounded-full border border-gray-300 border-dashed bg-gray-50 px-3 py-1 text-gray-600 text-sm transition-colors hover:border-blue-300 hover:bg-blue-50 hover:text-blue-700 disabled:opacity-50' > {tagName} - + ({tagDef?.fieldType || 'text'}) @@ -121,16 +122,16 @@ export function DocumentTagEntry({ {/* Selected Tags Section */} {tags.length > 0 && (
-
Document Tags
+
Document Tags
{tags.map((tag) => (
{/* Tag Name */}
-
+
{tag.tagName || 'Unnamed Tag'}
-
{tag.fieldType}
+
{tag.fieldType}
{/* Value Input */} @@ -163,12 +164,12 @@ export function DocumentTagEntry({ {/* Create New Tag Section */}
-
Create New Tag
-
+
Create New Tag
+
= 7} + placeholder={tagDefinitions.length >= 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()) { @@ -196,8 +197,12 @@ export function DocumentTagEntry({ }} />
-
- {usedTagNames.size > 0 ? 'Press Enter (no duplicates)' : 'Press Enter to add'} +
+ {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'}
diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 787cacd1c0a..d5a106570fe 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -16,6 +16,7 @@ import { uuid, vector, } from 'drizzle-orm/pg-core' +import { TAG_SLOTS } from '@/lib/constants/knowledge' // Custom tsvector type for full-text search export const tsvector = customType<{ @@ -802,7 +803,7 @@ export const knowledgeBaseTagDefinitions = pgTable( .notNull() .references(() => knowledgeBase.id, { onDelete: 'cascade' }), tagSlot: text('tag_slot', { - enum: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'], + enum: TAG_SLOTS, }).notNull(), displayName: text('display_name').notNull(), fieldType: text('field_type').notNull().default('text'), // 'text', future: 'date', 'number', 'range' diff --git a/apps/sim/hooks/use-knowledge-base-tag-definitions.ts b/apps/sim/hooks/use-knowledge-base-tag-definitions.ts index d00f8d38960..4d2bd6c33ad 100644 --- a/apps/sim/hooks/use-knowledge-base-tag-definitions.ts +++ b/apps/sim/hooks/use-knowledge-base-tag-definitions.ts @@ -1,13 +1,14 @@ 'use client' import { useCallback, useEffect, useState } from 'react' +import type { TagSlot } from '@/lib/constants/knowledge' import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('useKnowledgeBaseTagDefinitions') export interface TagDefinition { id: string - tagSlot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + tagSlot: TagSlot displayName: string fieldType: string createdAt: string diff --git a/apps/sim/hooks/use-tag-definitions.ts b/apps/sim/hooks/use-tag-definitions.ts index 6e884fb2643..b92f7f5eb74 100644 --- a/apps/sim/hooks/use-tag-definitions.ts +++ b/apps/sim/hooks/use-tag-definitions.ts @@ -1,13 +1,14 @@ 'use client' import { useCallback, useEffect, useState } from 'react' +import type { TagSlot } from '@/lib/constants/knowledge' import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('useTagDefinitions') export interface TagDefinition { id: string - tagSlot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + tagSlot: TagSlot displayName: string fieldType: string createdAt: string @@ -15,7 +16,7 @@ export interface TagDefinition { } export interface TagDefinitionInput { - tagSlot: 'tag1' | 'tag2' | 'tag3' | 'tag4' | 'tag5' | 'tag6' | 'tag7' + tagSlot: TagSlot displayName: string fieldType: string } diff --git a/apps/sim/lib/constants/knowledge.ts b/apps/sim/lib/constants/knowledge.ts new file mode 100644 index 00000000000..5436b75e176 --- /dev/null +++ b/apps/sim/lib/constants/knowledge.ts @@ -0,0 +1,15 @@ +/** + * Knowledge base and document constants + */ + +// Maximum number of tag slots allowed per knowledge base +export const MAX_TAG_SLOTS = 7 + +// Tag slot names (derived from MAX_TAG_SLOTS) +export const TAG_SLOTS = Array.from({ length: MAX_TAG_SLOTS }, (_, i) => `tag${i + 1}`) as [ + string, + ...string[], +] + +// Type for tag slot names +export type TagSlot = (typeof TAG_SLOTS)[number] From a639df2870846b342c2ea2451cbeb1b840bc0e35 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 18:08:18 -0700 Subject: [PATCH 13/19] remove console logs --- apps/sim/app/api/knowledge/search/route.ts | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 1934ae8df52..150f0efeda9 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -17,7 +17,7 @@ function getTagFilters(filters: Record, embedding: any) { return Object.entries(filters).map(([key, value]) => { // Handle OR logic within same tag const values = value.includes('|OR|') ? value.split('|OR|') : [value] - console.log(`[getTagFilters] Processing ${key}="${value}" -> values:`, values) + logger.debug(`[getTagFilters] Processing ${key}="${value}" -> values:`, values) const getColumnForKey = (key: string) => { switch (key) { @@ -45,11 +45,11 @@ function getTagFilters(filters: Record, embedding: any) { if (values.length === 1) { // Single value - simple equality - console.log(`[getTagFilters] Single value filter: ${key} = ${values[0]}`) + logger.debug(`[getTagFilters] Single value filter: ${key} = ${values[0]}`) return sql`LOWER(${column}) = LOWER(${values[0]})` } // Multiple values - OR logic - console.log(`[getTagFilters] OR filter: ${key} IN (${values.join(', ')})`) + logger.debug(`[getTagFilters] OR filter: ${key} IN (${values.join(', ')})`) const orConditions = values.map((v) => sql`LOWER(${column}) = LOWER(${v})`) return sql`(${sql.join(orConditions, sql` OR `)})` }) @@ -196,7 +196,7 @@ async function executeSingleQuery( distanceThreshold: number, filters?: Record ) { - console.log(`[executeSingleQuery] Called with filters:`, filters) + logger.debug(`[executeSingleQuery] Called with filters:`, filters) return await db .select({ id: embedding.id, @@ -275,8 +275,8 @@ export async function POST(request: NextRequest) { .from(knowledgeBaseTagDefinitions) .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, kbId)) - console.log(`[${requestId}] Found tag definitions:`, tagDefs) - console.log(`[${requestId}] Original filters:`, validatedData.filters) + logger.debug(`[${requestId}] Found tag definitions:`, tagDefs) + logger.debug(`[${requestId}] Original filters:`, validatedData.filters) // Create mapping from display name to tag slot const displayNameToSlot: Record = {} @@ -291,19 +291,19 @@ export async function POST(request: NextRequest) { // Check if this is an OR filter (contains |OR| separator) if (value.includes('|OR|')) { - console.log( + logger.debug( `[${requestId}] OR filter detected: "${key}" -> "${tagSlot}" = "${value}"` ) } mappedFilters[tagSlot] = value - console.log(`[${requestId}] Mapped filter: "${key}" -> "${tagSlot}" = "${value}"`) + logger.debug(`[${requestId}] Mapped filter: "${key}" -> "${tagSlot}" = "${value}"`) } }) - console.log(`[${requestId}] Final mapped filters:`, mappedFilters) + logger.debug(`[${requestId}] Final mapped filters:`, mappedFilters) } catch (error) { - console.error(`[${requestId}] Filter mapping error:`, error) + logger.error(`[${requestId}] Filter mapping error:`, error) // If mapping fails, use original filters mappedFilters = validatedData.filters } @@ -337,7 +337,7 @@ export async function POST(request: NextRequest) { if (strategy.useParallel) { // Execute parallel queries for better performance with many KBs - console.log(`[${requestId}] Executing parallel queries with filters:`, mappedFilters) + logger.debug(`[${requestId}] Executing parallel queries with filters:`, mappedFilters) const parallelResults = await executeParallelQueries( accessibleKbIds, queryVector, @@ -348,7 +348,7 @@ export async function POST(request: NextRequest) { results = mergeAndRankResults(parallelResults, validatedData.topK) } else { // Execute single optimized query for fewer KBs - console.log(`[${requestId}] Executing single query with filters:`, mappedFilters) + logger.debug(`[${requestId}] Executing single query with filters:`, mappedFilters) results = await executeSingleQuery( accessibleKbIds, queryVector, @@ -387,7 +387,7 @@ export async function POST(request: NextRequest) { tagDefs.forEach((def) => { tagDefinitionsMap[kbId][def.tagSlot] = def.displayName }) - console.log( + logger.debug( `[${requestId}] Display mapping - KB ${kbId} tag definitions:`, tagDefinitionsMap[kbId] ) From 22e4dab470545fec38fffa992540eeeb2f8644f2 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 18:09:18 -0700 Subject: [PATCH 14/19] forgot a few --- apps/sim/app/api/knowledge/search/route.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 150f0efeda9..90469fb8c25 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -392,10 +392,7 @@ export async function POST(request: NextRequest) { tagDefinitionsMap[kbId] ) } catch (error) { - console.error( - `[${requestId}] Failed to fetch tag definitions for display mapping:`, - error - ) + logger.warn(`[${requestId}] Failed to fetch tag definitions for display mapping:`, error) tagDefinitionsMap[kbId] = {} } } @@ -405,7 +402,7 @@ export async function POST(request: NextRequest) { data: { results: results.map((result) => { const kbTagMap = tagDefinitionsMap[result.knowledgeBaseId] || {} - console.log( + logger.debug( `[${requestId}] Result KB: ${result.knowledgeBaseId}, available mappings:`, kbTagMap ) @@ -417,7 +414,7 @@ export async function POST(request: NextRequest) { tagSlots.forEach((slot) => { if (result[slot]) { const displayName = kbTagMap[slot] || slot - console.log( + logger.debug( `[${requestId}] Mapping ${slot}="${result[slot]}" -> "${displayName}"="${result[slot]}"` ) tags[displayName] = result[slot] From 7df161527a7ea58d6d699c9041e59559b8be9138 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 18:10:14 -0700 Subject: [PATCH 15/19] Update apps/sim/tools/knowledge/search.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/tools/knowledge/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/tools/knowledge/search.ts b/apps/sim/tools/knowledge/search.ts index 6b12397a6df..b6a3d0b4e1b 100644 --- a/apps/sim/tools/knowledge/search.ts +++ b/apps/sim/tools/knowledge/search.ts @@ -4,7 +4,7 @@ import type { ToolConfig } from '@/tools/types' export const knowledgeSearchTool: ToolConfig = { id: 'knowledge_search', name: 'Knowledge Search', - description: 'Search for similar content in one or more knowledge bases using vector similarity', + description: 'Search for similar content in a knowledge base using vector similarity', version: '1.0.0', params: { knowledgeBaseId: { From fd8b143389994f4141ce0510236a328b18bd8824 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 18:11:16 -0700 Subject: [PATCH 16/19] remove console.warn --- apps/sim/tools/knowledge/create_document.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sim/tools/knowledge/create_document.ts b/apps/sim/tools/knowledge/create_document.ts index 950f75cbd29..6a6bbef588f 100644 --- a/apps/sim/tools/knowledge/create_document.ts +++ b/apps/sim/tools/knowledge/create_document.ts @@ -110,7 +110,6 @@ export const knowledgeCreateDocumentTool: ToolConfig Date: Mon, 28 Jul 2025 18:13:22 -0700 Subject: [PATCH 17/19] Update apps/sim/hooks/use-tag-definitions.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/hooks/use-tag-definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/hooks/use-tag-definitions.ts b/apps/sim/hooks/use-tag-definitions.ts index b92f7f5eb74..603cbd6fd6c 100644 --- a/apps/sim/hooks/use-tag-definitions.ts +++ b/apps/sim/hooks/use-tag-definitions.ts @@ -24,7 +24,7 @@ export interface TagDefinitionInput { /** * Hook for managing KB-scoped tag definitions * @param knowledgeBaseId - The knowledge base ID - * @param documentId - The document ID (kept for API compatibility but not used for fetching) + * @param documentId - The document ID (required for API calls) */ export function useTagDefinitions( knowledgeBaseId: string | null, From f0b9657bec6f3e8f2c4314f1153e02e9ff235df9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 18:18:18 -0700 Subject: [PATCH 18/19] use tag slots consts in more places --- .../[id]/documents/[documentId]/route.ts | 22 +++++++++---------- apps/sim/app/api/knowledge/search/route.ts | 4 ++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts index 1a1d476a851..c3912f47e80 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts @@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { TAG_SLOTS } from '@/lib/constants/knowledge' import { createLogger } from '@/lib/logs/console/logger' export const dynamic = 'force-dynamic' @@ -223,13 +224,11 @@ export async function PUT( updateData.processingError = validatedData.processingError // Tag field updates - if (validatedData.tag1 !== undefined) updateData.tag1 = validatedData.tag1 - if (validatedData.tag2 !== undefined) updateData.tag2 = validatedData.tag2 - if (validatedData.tag3 !== undefined) updateData.tag3 = validatedData.tag3 - if (validatedData.tag4 !== undefined) updateData.tag4 = validatedData.tag4 - if (validatedData.tag5 !== undefined) updateData.tag5 = validatedData.tag5 - if (validatedData.tag6 !== undefined) updateData.tag6 = validatedData.tag6 - if (validatedData.tag7 !== undefined) updateData.tag7 = validatedData.tag7 + TAG_SLOTS.forEach((slot) => { + if ((validatedData as any)[slot] !== undefined) { + ;(updateData as any)[slot] = (validatedData as any)[slot] + } + }) } await db.transaction(async (tx) => { @@ -237,14 +236,13 @@ export async function PUT( await tx.update(document).set(updateData).where(eq(document.id, documentId)) // If any tag fields were updated, also update the embeddings - const tagFields = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const - const hasTagUpdates = tagFields.some((field) => validatedData[field] !== undefined) + const hasTagUpdates = TAG_SLOTS.some((field) => (validatedData as any)[field] !== undefined) if (hasTagUpdates) { const embeddingUpdateData: Record = {} - tagFields.forEach((field) => { - if (validatedData[field] !== undefined) { - embeddingUpdateData[field] = validatedData[field] || null + TAG_SLOTS.forEach((field) => { + if ((validatedData as any)[field] !== undefined) { + embeddingUpdateData[field] = (validatedData as any)[field] || null } }) diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 90469fb8c25..d3bc6ae1e59 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -1,6 +1,7 @@ import { and, eq, inArray, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { TAG_SLOTS } from '@/lib/constants/knowledge' import { retryWithExponentialBackoff } from '@/lib/documents/utils' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' @@ -409,9 +410,8 @@ export async function POST(request: NextRequest) { // Create tags object with display names const tags: Record = {} - const tagSlots = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const - tagSlots.forEach((slot) => { + TAG_SLOTS.forEach((slot) => { if (result[slot]) { const displayName = kbTagMap[slot] || slot logger.debug( From 035c53c789c566dd3c6954070bc95607525b1a13 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Jul 2025 18:42:04 -0700 Subject: [PATCH 19/19] remove duplicate title --- .../components/document-tag-entry/document-tag-entry.tsx | 1 - 1 file changed, 1 deletion(-) 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 5fb85fa91b0..916b461bd68 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 @@ -122,7 +122,6 @@ export function DocumentTagEntry({ {/* Selected Tags Section */} {tags.length > 0 && (
-
Document Tags
{tags.map((tag) => (