From f3a405364fe4817ae81b8c96d4d10192cada8119 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Tue, 3 Jun 2025 01:09:40 -0700 Subject: [PATCH] improvements(knowledge): ui/ux --- apps/sim/app/api/files/presigned/route.ts | 5 +- .../components/document-loading.tsx | 2 +- apps/sim/app/w/knowledge/[id]/base.tsx | 8 +++ .../components/knowledge-base-loading.tsx | 48 +++++++------- .../knowledge-header/knowledge-header.tsx | 52 ++++++++------- .../components/skeletons/table-skeleton.tsx | 43 +++++++----- apps/sim/app/w/knowledge/loading.tsx | 66 ++++++++++--------- apps/sim/lib/uploads/s3-client.ts | 31 ++++++++- 8 files changed, 158 insertions(+), 97 deletions(-) diff --git a/apps/sim/app/api/files/presigned/route.ts b/apps/sim/app/api/files/presigned/route.ts index f132c4065d..f439fb551b 100644 --- a/apps/sim/app/api/files/presigned/route.ts +++ b/apps/sim/app/api/files/presigned/route.ts @@ -3,7 +3,7 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { type NextRequest, NextResponse } from 'next/server' import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console-logger' -import { getS3Client } from '@/lib/uploads/s3-client' +import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3-client' import { S3_CONFIG, USE_S3_STORAGE } from '@/lib/uploads/setup' import { createErrorResponse, createOptionsResponse } from '../utils' @@ -40,6 +40,9 @@ export async function POST(request: NextRequest) { const safeFileName = fileName.replace(/\s+/g, '-') const uniqueKey = `${Date.now()}-${uuidv4()}-${safeFileName}` + // Sanitize the original filename for S3 metadata to prevent header errors + const sanitizedOriginalName = sanitizeFilenameForMetadata(fileName) + // Create the S3 command const command = new PutObjectCommand({ Bucket: S3_CONFIG.bucket, diff --git a/apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx b/apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx index 265c9dc8c8..ffee9178e5 100644 --- a/apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx +++ b/apps/sim/app/w/knowledge/[id]/[documentId]/components/document-loading.tsx @@ -47,7 +47,7 @@ export function DocumentLoading({
{/* Main Content */} -
+
{/* Search Section */}
diff --git a/apps/sim/app/w/knowledge/[id]/base.tsx b/apps/sim/app/w/knowledge/[id]/base.tsx index 7f4a3bbd60..ea2c345582 100644 --- a/apps/sim/app/w/knowledge/[id]/base.tsx +++ b/apps/sim/app/w/knowledge/[id]/base.tsx @@ -649,6 +649,9 @@ export function KnowledgeBase({ Status + + Status + Actions @@ -718,6 +721,11 @@ export function KnowledgeBase({
—
+ {/* Processing column */} + +
—
+ + {/* Status column */}
—
diff --git a/apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading.tsx b/apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading.tsx index 29ed24ded9..f363b5a31c 100644 --- a/apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading.tsx +++ b/apps/sim/app/w/knowledge/[id]/components/knowledge-base-loading.tsx @@ -1,8 +1,8 @@ 'use client' -import { LibraryBig, Search } from 'lucide-react' -import Link from 'next/link' +import { Search } from 'lucide-react' import { useSidebarStore } from '@/stores/sidebar/store' +import { KnowledgeHeader } from '../../components/knowledge-header/knowledge-header' import { DocumentTableSkeleton } from '../../components/skeletons/table-skeleton' interface KnowledgeBaseLoadingProps { @@ -14,28 +14,29 @@ export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoading const isSidebarCollapsed = mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' + const breadcrumbs = [ + { + id: 'knowledge-root', + label: 'Knowledge', + href: '/w/knowledge', + }, + { + id: 'knowledge-base-loading', + label: knowledgeBaseName, + }, + ] + return (
{/* Fixed Header with Breadcrumbs */} -
- - - Knowledge - - / - {knowledgeBaseName} -
+
{/* Main Content */} -
+
{/* Search and Create Section */}
@@ -51,13 +52,16 @@ export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoading
- {/* */} +
+ {/* Add Documents Button - disabled state */} + +
{/* Table container */} diff --git a/apps/sim/app/w/knowledge/components/knowledge-header/knowledge-header.tsx b/apps/sim/app/w/knowledge/components/knowledge-header/knowledge-header.tsx index 457501b336..aef4edaad1 100644 --- a/apps/sim/app/w/knowledge/components/knowledge-header/knowledge-header.tsx +++ b/apps/sim/app/w/knowledge/components/knowledge-header/knowledge-header.tsx @@ -23,6 +23,8 @@ const HEADER_STYLES = { link: 'group flex items-center gap-2 font-medium text-sm transition-colors hover:text-muted-foreground', label: 'font-medium text-sm', separator: 'text-muted-foreground', + // Always reserve consistent space for actions area + actionsContainer: 'flex h-8 w-8 items-center justify-center', } as const interface KnowledgeHeaderOptions { @@ -60,30 +62,32 @@ export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps) })}
- {/* Actions Menu - only show if onDeleteKnowledgeBase is provided */} - {options?.onDeleteKnowledgeBase && ( - - - - - - - - Delete Knowledge Base - - - - )} + {/* Actions Area - always reserve consistent space */} +
+ {options?.onDeleteKnowledgeBase && ( + + + + + + + + Delete Knowledge Base + + + + )} +
) } diff --git a/apps/sim/app/w/knowledge/components/skeletons/table-skeleton.tsx b/apps/sim/app/w/knowledge/components/skeletons/table-skeleton.tsx index e00e8ebec5..502f31c1c6 100644 --- a/apps/sim/app/w/knowledge/components/skeletons/table-skeleton.tsx +++ b/apps/sim/app/w/knowledge/components/skeletons/table-skeleton.tsx @@ -37,7 +37,12 @@ export function DocumentTableRowSkeleton({ isSidebarCollapsed }: { isSidebarColl
- {/* Status column */} + {/* Processing Status column */} + +
+ + + {/* Active Status column */}
@@ -47,6 +52,7 @@ export function DocumentTableRowSkeleton({ isSidebarCollapsed }: { isSidebarColl
+
@@ -106,17 +112,18 @@ export function DocumentTableSkeleton({ return (
{/* Table header - fixed */} -
- +
+
- - - - + + + + - + - + + @@ -138,6 +145,9 @@ export function DocumentTableSkeleton({ + @@ -151,16 +161,17 @@ export function DocumentTableSkeleton({ {/* Table body - scrollable */}
-
Uploaded + Processing + Status
+
- - - - + + + + - + - + + {Array.from({ length: rowCount }).map((_, i) => ( diff --git a/apps/sim/app/w/knowledge/loading.tsx b/apps/sim/app/w/knowledge/loading.tsx index b94f61739e..6c1053caad 100644 --- a/apps/sim/app/w/knowledge/loading.tsx +++ b/apps/sim/app/w/knowledge/loading.tsx @@ -1,7 +1,8 @@ 'use client' -import { LibraryBig, Plus, Search } from 'lucide-react' +import { Plus, Search } from 'lucide-react' import { useSidebarStore } from '@/stores/sidebar/store' +import { KnowledgeHeader } from './components/knowledge-header/knowledge-header' import { KnowledgeBaseCardSkeletonGrid } from './components/skeletons/knowledge-base-card-skeleton' export default function KnowledgeLoading() { @@ -9,44 +10,47 @@ export default function KnowledgeLoading() { const isSidebarCollapsed = mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' + const breadcrumbs = [{ id: 'knowledge', label: 'Knowledge' }] + return (
- {/* Fixed Header */} -
- -

Knowledge

-
+ {/* Header */} + - {/* Main Content */} -
-
- {/* Search and Create Section */} -
-
-
- - +
+ {/* Main Content */} +
+
+ {/* Search and Create Section */} +
+
+
+ + +
+
+ +
+ + {/* Content Area */} +
- -
- - {/* Content Area */} -
diff --git a/apps/sim/lib/uploads/s3-client.ts b/apps/sim/lib/uploads/s3-client.ts index 8c5acd41b5..ce3a2e544f 100644 --- a/apps/sim/lib/uploads/s3-client.ts +++ b/apps/sim/lib/uploads/s3-client.ts @@ -33,6 +33,27 @@ export function getS3Client(): S3Client { return _s3Client } +/** + * Sanitize a filename for use in S3 metadata headers + * S3 metadata headers must contain only ASCII printable characters (0x20-0x7E) + * and cannot contain certain special characters + */ +export function sanitizeFilenameForMetadata(filename: string): string { + return ( + filename + // Remove non-ASCII characters (keep only printable ASCII 0x20-0x7E) + .replace(/[^\x20-\x7E]/g, '') + // Remove characters that are problematic in HTTP headers + .replace(/["\\]/g, '') + // Replace multiple spaces with single space + .replace(/\s+/g, ' ') + // Trim whitespace + .trim() || + // Provide fallback if completely sanitized + 'file' + ) +} + /** * File information structure */ @@ -110,6 +131,12 @@ export async function uploadToS3( const safeFileName = fileName.replace(/\s+/g, '-') // Replace spaces with hyphens const uniqueKey = `${Date.now()}-${safeFileName}` + // Sanitize filename for S3 metadata (only allow ASCII printable characters) + const sanitizedOriginalName = fileName + .replace(/[^\x20-\x7E]/g, '') // Remove non-ASCII characters + .replace(/["\\]/g, '') // Remove quotes and backslashes + .trim() + const s3Client = getS3Client() // Upload the file to S3 @@ -119,7 +146,7 @@ export async function uploadToS3( Key: uniqueKey, Body: file, ContentType: contentType, - // Add some useful metadata + // Add some useful metadata with sanitized values Metadata: { originalName: encodeURIComponent(fileName), // Encode filename to prevent invalid characters in HTTP headers uploadedAt: new Date().toISOString(), @@ -133,7 +160,7 @@ export async function uploadToS3( return { path: servePath, key: uniqueKey, - name: fileName, + name: fileName, // Return the actual original filename in the response size: fileSize, type: contentType, }