improvement(kb-connectors): align connector UI surfaces (#4728)

This commit is contained in:
Waleed
2026-05-22 15:39:04 -07:00
committed by GitHub
parent 500f35ac87
commit 5c193442f7
8 changed files with 249 additions and 161 deletions
@@ -14,7 +14,7 @@ interface FooterItem {
}
const PRODUCT_LINKS: FooterItem[] = [
{ label: 'Mothership', href: 'https://docs.sim.ai', external: true },
{ label: 'Mothership', href: 'https://docs.sim.ai/mothership', external: true },
{ label: 'Workflows', href: 'https://docs.sim.ai', external: true },
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },
@@ -28,6 +28,7 @@ import {
} from '@/components/emcn'
import { Database, DatabaseX } from '@/components/emcn/icons'
import { SearchHighlight } from '@/components/ui/search-highlight'
import { cn } from '@/lib/core/utils/cn'
import { ADD_CONNECTOR_SEARCH_PARAM } from '@/lib/credentials/client-state'
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
@@ -920,19 +921,35 @@ export function KnowledgeBase({
const def = CONNECTOR_REGISTRY[connector.connectorType]
const ConnectorIcon = def?.icon
return (
<button
<Button
key={connector.id}
type='button'
variant='ghost'
size='sm'
onClick={() => setShowConnectorsModal(true)}
className='flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption shadow-[inset_0_0_0_1px_var(--border)] transition-colors hover-hover:bg-[var(--surface-3)]'
className='h-7 max-w-[180px] shrink-0 justify-start gap-1.5 rounded-lg border border-[var(--border-muted)] bg-[var(--surface-2)] px-2 text-[var(--text-secondary)] text-caption hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
>
{connector.status === 'syncing' ? (
<Loader className='size-[14px]' animate />
) : (
ConnectorIcon && <ConnectorIcon className='size-[14px]' />
)}
{def?.name || connector.connectorType}
</button>
<span className='relative flex size-4 flex-shrink-0 items-center justify-center'>
{connector.status === 'syncing' ? (
<Loader className='size-[14px]' animate />
) : (
ConnectorIcon && <ConnectorIcon className='size-[14px]' />
)}
{connector.status !== 'active' && connector.status !== 'syncing' && (
<span
className={cn(
'-right-0.5 -top-0.5 absolute size-1.5 rounded-xs border border-[var(--surface-2)]',
connector.status === 'error'
? 'bg-[var(--text-error)]'
: connector.status === 'disabled'
? 'bg-[var(--caution)]'
: 'bg-[var(--text-muted)]'
)}
/>
)}
</span>
<span className='truncate'>{def?.name || connector.connectorType}</span>
</Button>
)
})}
</>
@@ -1317,6 +1334,7 @@ export function KnowledgeBase({
connectors={connectors}
isLoading={isLoadingConnectors}
canEdit={userPermissions.canEdit}
className='mt-0'
/>
</ModalBody>
</ModalContent>
@@ -240,23 +240,23 @@ export function AddConnectorModal({
: `Configure the ${connectorConfig?.name} connector settings`}
</ModalDescription>
<ModalBody>
<ModalBody className='pb-3'>
{step === 'select-type' ? (
<div className='flex flex-col gap-2'>
<div className='flex items-center gap-2 rounded-lg border border-[var(--border)] bg-transparent px-2 py-[5px] transition-colors duration-100 dark:bg-[var(--surface-4)] dark:hover-hover:border-[var(--border-1)] dark:hover-hover:bg-[var(--surface-5)]'>
<div className='flex min-h-0 flex-col gap-2'>
<div className='flex items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-2 py-[5px] transition-colors duration-100 hover-hover:border-[var(--border-1)] hover-hover:bg-[var(--surface-3)]'>
<Search
className='size-[14px] flex-shrink-0 text-[var(--text-tertiary)]'
className='size-[14px] flex-shrink-0 text-[var(--text-icon)]'
strokeWidth={2}
/>
<Input
placeholder='Search sources...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
className='h-auto flex-1 border-0 bg-transparent p-0 leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
</div>
<div className='min-h-[400px] overflow-y-auto'>
<div className='flex flex-col gap-0.5'>
<div className='min-h-[400px] overflow-y-auto [scrollbar-gutter:stable]'>
<div className='flex flex-col gap-0.5 pr-1'>
{filteredEntries.map(([type, config]) => (
<ConnectorTypeCard
key={type}
@@ -265,7 +265,7 @@ export function AddConnectorModal({
/>
))}
{filteredEntries.length === 0 && (
<div className='py-4 text-center text-[var(--text-muted)] text-sm'>
<div className='rounded-lg bg-[var(--surface-3)] px-3 py-8 text-center text-[var(--text-muted)] text-small'>
{CONNECTOR_ENTRIES.length === 0
? 'No connectors available.'
: `No sources found matching "${searchTerm}"`}
@@ -276,7 +276,6 @@ export function AddConnectorModal({
</div>
) : connectorConfig ? (
<div className='flex flex-col gap-3'>
{/* Auth: API key input or OAuth credential selection */}
{isApiKeyMode ? (
<div className='flex flex-col gap-2'>
<Label>
@@ -336,7 +335,6 @@ export function AddConnectorModal({
</div>
)}
{/* Config fields */}
{connectorConfig.configFields.map((field) => {
if (!isFieldVisible(field)) return null
@@ -357,13 +355,14 @@ export function AddConnectorModal({
{field.description && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
<Button
type='button'
className='flex size-[14px] cursor-help items-center justify-center text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-secondary)]'
variant='ghost'
className='flex size-[14px] cursor-help items-center justify-center p-0 text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-secondary)]'
aria-label={`About ${field.title}`}
>
<Info className='size-[12px]' />
</button>
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>{field.description}</Tooltip.Content>
</Tooltip.Root>
@@ -372,13 +371,14 @@ export function AddConnectorModal({
{hasCanonicalPair && canonicalId && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
<Button
type='button'
className='flex size-[18px] items-center justify-center rounded-[3px] text-[var(--text-muted)] transition-colors hover-hover:bg-[var(--surface-3)] hover-hover:text-[var(--text-secondary)]'
variant='ghost'
className='flex size-[18px] items-center justify-center rounded-[3px] p-0 text-[var(--text-muted)] transition-colors hover-hover:bg-[var(--surface-3)] hover-hover:text-[var(--text-secondary)]'
onClick={() => toggleCanonicalMode(canonicalId)}
>
<ArrowLeftRight className='size-[12px]' />
</button>
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{field.mode === 'basic'
@@ -429,48 +429,50 @@ export function AddConnectorModal({
)
})}
{/* Tag definitions (opt-out) */}
{connectorConfig.tagDefinitions && connectorConfig.tagDefinitions.length > 0 && (
<div className='flex flex-col gap-2'>
<Label>Metadata Tags</Label>
{connectorConfig.tagDefinitions.map((tagDef) => (
<div
key={tagDef.id}
role='checkbox'
aria-checked={!disabledTagIds.has(tagDef.id)}
tabIndex={0}
className='flex cursor-pointer items-center gap-2 rounded-sm p-0.5 text-small'
onClick={() => toggleTagDefinition(tagDef.id)}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return
handleKeyboardActivation(event, () => toggleTagDefinition(tagDef.id))
}}
>
<Checkbox
checked={!disabledTagIds.has(tagDef.id)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => {
setDisabledTagIds((prev) => {
const next = new Set(prev)
if (checked) {
next.delete(tagDef.id)
} else {
next.add(tagDef.id)
}
return next
})
<div className='flex flex-col gap-0.5 rounded-lg border border-[var(--border-muted)] bg-[var(--surface-2)] p-1'>
{connectorConfig.tagDefinitions.map((tagDef) => (
<div
key={tagDef.id}
role='checkbox'
aria-checked={!disabledTagIds.has(tagDef.id)}
tabIndex={0}
className='flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-small transition-colors hover-hover:bg-[var(--surface-active)]'
onClick={() => toggleTagDefinition(tagDef.id)}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return
handleKeyboardActivation(event, () => toggleTagDefinition(tagDef.id))
}}
/>
<span className='text-[var(--text-primary)]'>{tagDef.displayName}</span>
<span className='text-[var(--text-muted)] text-xs'>
({tagDef.fieldType})
</span>
</div>
))}
>
<Checkbox
checked={!disabledTagIds.has(tagDef.id)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => {
setDisabledTagIds((prev) => {
const next = new Set(prev)
if (checked) {
next.delete(tagDef.id)
} else {
next.add(tagDef.id)
}
return next
})
}}
/>
<span className='min-w-0 flex-1 truncate text-[var(--text-primary)]'>
{tagDef.displayName}
</span>
<span className='flex-shrink-0 text-[var(--text-muted)] text-xs'>
{tagDef.fieldType}
</span>
</div>
))}
</div>
</div>
)}
{/* Sync interval */}
<div className='flex flex-col gap-2'>
<Label>Sync Frequency</Label>
<ButtonGroup
@@ -550,14 +552,14 @@ function ConnectorTypeCard({ config, onClick }: ConnectorTypeCardProps) {
return (
<button
type='button'
className='flex items-center gap-2.5 rounded-md px-2.5 py-2 text-left transition-colors hover-hover:bg-[var(--surface-3)]'
className='group flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
onClick={onClick}
>
<Icon className='size-[18px] flex-shrink-0' />
<div className='flex min-w-0 flex-col gap-[1px]'>
<span className='truncate font-medium text-[var(--text-primary)] text-small'>
{config.name}
</span>
<div className='flex size-9 flex-shrink-0 items-center justify-center rounded-lg bg-[var(--surface-4)] transition-colors group-hover:bg-[var(--surface-5)]'>
<Icon className='size-[18px] text-[var(--text-secondary)]' />
</div>
<div className='flex min-w-0 flex-1 flex-col gap-[1px]'>
<span className='truncate text-[14px] text-[var(--text-body)]'>{config.name}</span>
<span className='truncate text-[var(--text-muted)] text-xs'>{config.description}</span>
</div>
</button>
@@ -72,7 +72,7 @@ export function ConnectorSelectorField({
if (isLoading && isEnabled) {
return (
<div className='flex items-center gap-2 rounded-sm border border-[var(--border-1)] bg-[var(--surface-5)] px-2 py-1.5 text-[var(--text-muted)] text-sm'>
<div className='flex items-center gap-2 rounded-sm border border-[var(--border-1)] bg-[var(--surface-5)] px-2 py-1.5 text-[var(--text-muted)] text-caption'>
<Loader className='size-3.5' animate />
Loading…
</div>
@@ -84,6 +84,7 @@ export function ConnectorSelectorField({
return (
<Combobox
multiSelect
size='sm'
options={comboboxOptions}
multiSelectValues={multiValues}
onMultiSelectChange={(values) => onChange(values)}
@@ -97,6 +98,7 @@ export function ConnectorSelectorField({
: field.placeholder || `Select ${field.title.toLowerCase()}`
}
disabled={disabled || !credentialId || !depsResolved}
emptyMessage={`No ${field.title.toLowerCase()} found`}
/>
)
}
@@ -104,6 +106,7 @@ export function ConnectorSelectorField({
const singleValue = Array.isArray(value) ? value[0] : value
return (
<Combobox
size='sm'
options={comboboxOptions}
value={singleValue || undefined}
onChange={(next) => onChange(next)}
@@ -117,6 +120,7 @@ export function ConnectorSelectorField({
: field.placeholder || `Select ${field.title.toLowerCase()}`
}
disabled={disabled || !credentialId || !depsResolved}
emptyMessage={`No ${field.title.toLowerCase()} found`}
/>
)
}
@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { format, formatDistanceToNow, isPast } from 'date-fns'
import {
@@ -58,6 +58,7 @@ interface ConnectorsSectionProps {
connectors: ConnectorData[]
isLoading: boolean
canEdit: boolean
className?: string
}
/** 5-minute cooldown after a manual sync trigger */
@@ -68,19 +69,24 @@ const STATUS_CONFIG = {
syncing: { label: 'Syncing', variant: 'amber' as const },
error: { label: 'Error', variant: 'red' as const },
paused: { label: 'Paused', variant: 'gray' as const },
disabled: { label: 'Disabled', variant: 'amber' as const },
disabled: { label: 'Disabled', variant: 'orange' as const },
} as const
const CONNECTOR_ACTION_BUTTON_CLASSES =
'size-7 rounded-lg p-0 text-[var(--text-muted)] hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
export function ConnectorsSection({
workspaceId,
knowledgeBaseId,
connectors,
isLoading,
canEdit,
className,
}: ConnectorsSectionProps) {
const { mutate: triggerSync } = useTriggerSync()
const { mutate: updateConnector } = useUpdateConnector()
const { mutate: deleteConnector, isPending: isDeleting } = useDeleteConnector()
const deleteDocumentsId = useId()
const [deleteTarget, setDeleteTarget] = useState<string | null>(null)
const [deleteDocuments, setDeleteDocuments] = useState(false)
@@ -184,16 +190,16 @@ export function ConnectorsSection({
if (connectors.length === 0 && !canEdit && !isLoading) return null
return (
<div className='mt-4'>
<div className={cn('mt-4', className)}>
{error && <p className='mt-2 text-[var(--text-error)] text-caption leading-tight'>{error}</p>}
{isLoading ? (
<div className='mt-2 flex flex-col gap-2'>
<div className='mt-2 flex flex-col gap-1'>
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className='rounded-lg border border-[var(--border-1)] px-3 py-2.5'>
<div key={i} className='rounded-lg px-2 py-2'>
<div className='flex items-center gap-2.5'>
<Skeleton className='size-5 flex-shrink-0 rounded-sm' />
<div className='flex flex-col gap-1'>
<Skeleton className='size-9 flex-shrink-0 rounded-lg' />
<div className='flex min-w-0 flex-1 flex-col gap-1'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[14px] w-[100px]' />
<Skeleton className='h-[18px] w-[52px] rounded-full' />
@@ -209,7 +215,7 @@ export function ConnectorsSection({
No connected sources yet. Connect an external source to automatically sync documents.
</p>
) : (
<div className='mt-2 flex flex-col gap-2'>
<div className='-mx-2 mt-2 flex flex-col gap-0.5'>
{connectors.map((connector) => (
<ConnectorCard
key={connector.id}
@@ -242,19 +248,19 @@ export function ConnectorsSection({
<ModalContent size='sm'>
<ModalHeader>Remove Connector</ModalHeader>
<ModalBody>
<ModalDescription className='text-[var(--text-secondary)] text-sm'>
<ModalDescription className='text-[var(--text-secondary)] text-small'>
This will disconnect the source and stop future syncs. Documents already synced will
remain in the knowledge base unless you choose to delete them.
</ModalDescription>
<div className='mt-3 flex items-center gap-2'>
<Checkbox
id='delete-docs'
id={deleteDocumentsId}
checked={deleteDocuments}
onCheckedChange={(checked) => setDeleteDocuments(checked === true)}
/>
<label
htmlFor='delete-docs'
className='cursor-pointer text-[var(--text-secondary)] text-sm'
htmlFor={deleteDocumentsId}
className='cursor-pointer text-[var(--text-secondary)] text-small'
>
Also delete all synced documents
</label>
@@ -355,28 +361,35 @@ function ConnectorCard({
const syncLogs = detail?.syncLogs ?? []
return (
<div className='rounded-lg border border-[var(--border-1)]'>
<div className='flex items-center justify-between px-3 py-2.5'>
<div className='flex items-center gap-2.5'>
<div className='relative flex-shrink-0'>
{Icon && <Icon className='size-5' />}
<div
className={cn(
'overflow-hidden rounded-lg border border-transparent transition-colors duration-100',
expanded
? 'border-[var(--border-muted)] bg-[var(--surface-2)]'
: 'hover-hover:bg-[var(--surface-active)]'
)}
>
<div className='flex items-center justify-between gap-2 px-2 py-2'>
<div className='flex min-w-0 items-center gap-2.5'>
<div className='relative flex size-9 flex-shrink-0 items-center justify-center rounded-lg bg-[var(--surface-4)]'>
{Icon && <Icon className='size-5 text-[var(--text-secondary)]' />}
{connector.status === 'disabled' && (
<AlertTriangle className='-right-1 -top-1 absolute size-3 text-amber-500' />
<AlertTriangle className='-right-0.5 -top-0.5 absolute size-3 text-[var(--caution)]' />
)}
</div>
<div className='flex flex-col gap-0.5'>
<div className='flex items-center gap-2'>
<span className='flex items-center gap-1.5 font-medium text-[var(--text-primary)] text-small'>
{connectorDef?.name || connector.connectorType}
<div className='flex min-w-0 flex-col gap-0.5'>
<div className='flex min-w-0 items-center gap-2'>
<span className='flex min-w-0 items-center gap-1.5 font-medium text-[var(--text-primary)] text-small'>
<span className='truncate'>{connectorDef?.name || connector.connectorType}</span>
{(isSyncPending || connector.status === 'syncing') && (
<Loader className='size-3 text-[var(--text-muted)]' animate />
)}
</span>
<Badge variant={statusConfig.variant} className='text-micro'>
<Badge variant={statusConfig.variant} size='sm' dot className='flex-shrink-0'>
{statusConfig.label}
</Badge>
</div>
<div className='flex items-center gap-1.5 text-[var(--text-muted)] text-xs'>
<div className='flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[var(--text-muted)] text-xs'>
{connector.lastSyncAt && (
<span>Last sync: {format(new Date(connector.lastSyncAt), 'MMM d, h:mm a')}</span>
)}
@@ -409,14 +422,14 @@ function ConnectorCard({
</div>
</div>
<div className='flex items-center gap-1'>
<div className='flex flex-shrink-0 items-center gap-0.5'>
{canEdit && (
<>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='size-7 p-0'
className={CONNECTOR_ACTION_BUTTON_CLASSES}
onClick={onSync}
disabled={
connector.status === 'syncing' ||
@@ -426,10 +439,7 @@ function ConnectorCard({
}
>
<RefreshCw
className={cn(
'h-3.5 w-3.5',
connector.status === 'syncing' && 'animate-spin'
)}
className={cn('size-3.5', connector.status === 'syncing' && 'animate-spin')}
/>
</Button>
</Tooltip.Trigger>
@@ -440,7 +450,11 @@ function ConnectorCard({
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button variant='ghost' className='size-7 p-0' onClick={onEdit}>
<Button
variant='ghost'
className={CONNECTOR_ACTION_BUTTON_CLASSES}
onClick={onEdit}
>
<Settings className='size-3.5' />
</Button>
</Tooltip.Trigger>
@@ -451,7 +465,7 @@ function ConnectorCard({
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='size-7 p-0'
className={CONNECTOR_ACTION_BUTTON_CLASSES}
onClick={onTogglePause}
disabled={isUpdating}
>
@@ -473,7 +487,11 @@ function ConnectorCard({
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button variant='ghost' className='size-7 p-0' onClick={onDelete}>
<Button
variant='ghost'
className={CONNECTOR_ACTION_BUTTON_CLASSES}
onClick={onDelete}
>
<Trash className='size-3.5' />
</Button>
</Tooltip.Trigger>
@@ -486,11 +504,11 @@ function ConnectorCard({
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='size-7 p-0'
className={CONNECTOR_ACTION_BUTTON_CLASSES}
onClick={() => setExpanded((prev) => !prev)}
>
<ChevronDown
className={cn('h-3.5 w-3.5 transition-transform', expanded && 'rotate-180')}
className={cn('size-3.5 transition-transform', expanded && 'rotate-180')}
/>
</Button>
</Tooltip.Trigger>
@@ -500,13 +518,13 @@ function ConnectorCard({
</div>
{connector.status === 'disabled' && (
<div className='border-[var(--border-1)] border-t px-3 py-2'>
<div className='flex flex-col gap-1 rounded-sm border border-amber-200 bg-amber-50 px-2 py-1.5 dark:border-amber-900 dark:bg-amber-950'>
<div className='flex items-center gap-1.5 font-medium text-amber-800 text-caption dark:text-amber-200'>
<AlertTriangle className='size-3 flex-shrink-0' />
<div className='border-[var(--border-muted)] border-t px-2 py-2'>
<div className='flex flex-col gap-2 rounded-md border border-[var(--border-muted)] bg-[var(--surface-3)] px-2.5 py-2'>
<div className='flex items-center gap-1.5 font-medium text-[var(--text-primary)] text-caption'>
<AlertTriangle className='size-3 flex-shrink-0 text-[var(--caution)]' />
Connector disabled after repeated sync failures
</div>
<p className='text-amber-700 text-micro dark:text-amber-300'>
<p className='text-[var(--text-muted)] text-caption leading-snug'>
Syncing has been paused due to {connector.consecutiveFailures} consecutive failures.
{serviceId
? ' Reconnect your account to resume syncing.'
@@ -529,7 +547,8 @@ function ConnectorCard({
}
setShowOAuthModal(true)
}}
className='w-full px-2 py-1 font-medium text-caption'
size='sm'
className='w-full'
>
Reconnect
</Button>
@@ -539,10 +558,10 @@ function ConnectorCard({
)}
{missingScopes.length > 0 && connector.status !== 'disabled' && (
<div className='border-[var(--border-1)] border-t px-3 py-2'>
<div className='flex flex-col gap-1 rounded-sm border bg-[var(--surface-2)] px-2 py-1.5'>
<div className='flex items-center font-medium text-caption'>
<span className='mr-1.5 inline-block size-[6px] rounded-xs bg-amber-500' />
<div className='border-[var(--border-muted)] border-t px-2 py-2'>
<div className='flex flex-col gap-2 rounded-md border border-[var(--border-muted)] bg-[var(--surface-3)] px-2.5 py-2'>
<div className='flex items-center font-medium text-[var(--text-primary)] text-caption'>
<span className='mr-1.5 inline-block size-[6px] rounded-xs bg-[var(--caution)]' />
Additional permissions required
</div>
{canEdit && (
@@ -562,7 +581,8 @@ function ConnectorCard({
}
setShowOAuthModal(true)
}}
className='w-full px-2 py-1 font-medium text-caption'
size='sm'
className='w-full'
>
Update access
</Button>
@@ -572,7 +592,7 @@ function ConnectorCard({
)}
{expanded && (
<div className='border-[var(--border-1)] border-t px-3 py-2'>
<div className='border-[var(--border-muted)] border-t px-2 py-2'>
<SyncHistory logs={syncLogs} isLoading={detailLoading} />
</div>
)}
@@ -620,7 +640,7 @@ interface SyncHistoryProps {
function SyncHistory({ logs, isLoading }: SyncHistoryProps) {
if (isLoading) {
return (
<div className='flex items-center gap-2 py-1 text-[var(--text-muted)] text-xs'>
<div className='flex items-center gap-2 rounded-md bg-[var(--surface-3)] px-2 py-2 text-[var(--text-muted)] text-xs'>
<Loader className='size-3' animate />
Loading sync history…
</div>
@@ -628,11 +648,15 @@ function SyncHistory({ logs, isLoading }: SyncHistoryProps) {
}
if (logs.length === 0) {
return <p className='py-1 text-[var(--text-muted)] text-xs'>No sync history yet.</p>
return (
<p className='rounded-md bg-[var(--surface-3)] px-2 py-2 text-[var(--text-muted)] text-xs'>
No sync history yet.
</p>
)
}
return (
<div className='flex flex-col gap-1.5'>
<div className='flex flex-col gap-0.5'>
{logs.map((log) => {
const isError = log.status === 'error' || log.status === 'failed'
const isRunning = log.status === 'running' || log.status === 'syncing'
@@ -640,14 +664,14 @@ function SyncHistory({ logs, isLoading }: SyncHistoryProps) {
log.docsAdded + log.docsUpdated + log.docsDeleted + (log.docsFailed ?? 0)
return (
<div key={log.id} className='flex items-start gap-2 text-xs'>
<div key={log.id} className='flex items-start gap-2 rounded-md px-2 py-1.5 text-xs'>
<div className='mt-[1px] flex-shrink-0'>
{isRunning ? (
<Loader className='size-3 text-[var(--text-muted)]' animate />
) : isError ? (
<XCircle className='size-3 text-[var(--text-error)]' />
) : (
<CheckCircle2 className='size-3 text-[var(--color-green-600)]' />
<CheckCircle2 className='size-3 text-[var(--success)]' />
)}
</div>
@@ -661,14 +685,12 @@ function SyncHistory({ logs, isLoading }: SyncHistoryProps) {
{totalChanges > 0 ? (
<>
{log.docsAdded > 0 && (
<span className='text-[var(--color-green-600)]'>+{log.docsAdded}</span>
<span className='text-[var(--success)]'>+{log.docsAdded}</span>
)}
{log.docsUpdated > 0 && (
<>
{log.docsAdded > 0 && ' '}
<span className='text-[var(--color-amber-600)]'>
~{log.docsUpdated}
</span>
<span className='text-[var(--caution)]'>~{log.docsUpdated}</span>
</>
)}
{log.docsDeleted > 0 && (
@@ -295,7 +295,7 @@ export function EditConnectorModal({
<ModalTabsTrigger value='documents'>Documents</ModalTabsTrigger>
</ModalTabsList>
<ModalBody>
<ModalBody className='pb-3'>
<ModalTabsContent value='settings'>
<SettingsTab
connectorConfig={connectorConfig}
@@ -393,13 +393,14 @@ function SettingsTab({
{field.description && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
<Button
type='button'
className='flex size-[14px] cursor-help items-center justify-center text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-secondary)]'
variant='ghost'
className='flex size-[14px] cursor-help items-center justify-center p-0 text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-secondary)]'
aria-label={`About ${field.title}`}
>
<Info className='size-[12px]' />
</button>
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>{field.description}</Tooltip.Content>
</Tooltip.Root>
@@ -408,13 +409,14 @@ function SettingsTab({
{hasCanonicalPair && canonicalId && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
<Button
type='button'
className='flex size-[18px] items-center justify-center rounded-[3px] text-[var(--text-muted)] transition-colors hover-hover:bg-[var(--surface-3)] hover-hover:text-[var(--text-secondary)]'
variant='ghost'
className='flex size-[18px] items-center justify-center rounded-[3px] p-0 text-[var(--text-muted)] transition-colors hover-hover:bg-[var(--surface-3)] hover-hover:text-[var(--text-secondary)]'
onClick={() => onToggleCanonicalMode(canonicalId)}
>
<ArrowLeftRight className='size-[12px]' />
</button>
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{field.mode === 'basic' ? 'Switch to manual input' : 'Switch to selector'}
@@ -512,46 +514,55 @@ function DocumentsTab({ knowledgeBaseId, connectorId }: DocumentsTabProps) {
if (isLoading) {
return (
<div className='flex flex-col gap-2'>
<Skeleton className='h-6 w-full' />
<Skeleton className='h-6 w-full' />
<Skeleton className='h-6 w-full' />
<Skeleton className='h-7 w-[180px] rounded-md' />
<Skeleton className='h-9 w-full rounded-lg' />
<Skeleton className='h-9 w-full rounded-lg' />
<Skeleton className='h-9 w-full rounded-lg' />
</div>
)
}
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-3'>
<ButtonGroup value={filter} onValueChange={(val) => setFilter(val as 'active' | 'excluded')}>
<ButtonGroupItem value='active'>Active ({counts.active})</ButtonGroupItem>
<ButtonGroupItem value='excluded'>Excluded ({counts.excluded})</ButtonGroupItem>
</ButtonGroup>
<div className='max-h-[320px] min-h-0 overflow-y-auto'>
<div className='max-h-[320px] min-h-0 overflow-y-auto [scrollbar-gutter:stable]'>
{documents.length === 0 ? (
<p className='py-4 text-center text-[var(--text-muted)] text-small'>
<p className='rounded-lg bg-[var(--surface-3)] px-3 py-8 text-center text-[var(--text-muted)] text-small'>
{filter === 'excluded' ? 'No excluded documents' : 'No documents yet'}
</p>
) : (
<div className='flex flex-col gap-2'>
<div className='flex flex-col gap-0.5 pr-1'>
{documents.map((doc) => (
<div key={doc.id} className='flex items-center justify-between'>
<div
key={doc.id}
className='flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<div className='flex min-w-0 items-center gap-1.5'>
<span className='truncate text-[var(--text-primary)] text-small'>
{doc.filename}
</span>
{doc.sourceUrl && (
<a
href={doc.sourceUrl}
target='_blank'
rel='noopener noreferrer'
className='flex-shrink-0 text-[var(--text-muted)] hover-hover:text-[var(--text-secondary)]'
>
<ExternalLink className='size-3' />
</a>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<a
href={doc.sourceUrl}
target='_blank'
rel='noopener noreferrer'
className='flex size-5 flex-shrink-0 items-center justify-center rounded-md text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-primary)]'
>
<ExternalLink className='size-3' />
</a>
</Tooltip.Trigger>
<Tooltip.Content>Open source document</Tooltip.Content>
</Tooltip.Root>
)}
</div>
<Button
variant='ghost'
variant='ghost-secondary'
size='sm'
className='flex-shrink-0'
disabled={doc.userExcluded ? isRestoring : isExcluding}
@@ -1,6 +1,6 @@
'use client'
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { formatAbsoluteDate, formatRelativeTime } from '@sim/utils/formatting'
import { useParams, useRouter } from 'next/navigation'
import { Badge, DocumentAttachment, Tooltip } from '@/components/emcn'
@@ -102,6 +102,19 @@ export function BaseCard({
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const connectorEntries = useMemo(
() =>
connectorTypes
.map((type) => ({ type, config: CONNECTOR_REGISTRY[type] }))
.filter((entry) => Boolean(entry.config?.icon)),
[connectorTypes]
)
const visibleConnectorEntries = useMemo(() => connectorEntries.slice(0, 3), [connectorEntries])
const hiddenConnectorLabels = useMemo(
() => connectorEntries.slice(3).map(({ type, config }) => config?.name ?? type),
[connectorEntries]
)
const hiddenConnectorCount = hiddenConnectorLabels.length
const searchParams = new URLSearchParams({
kbName: title,
@@ -209,19 +222,14 @@ export function BaseCard({
<p className='line-clamp-2 h-[36px] flex-1 text-[var(--text-tertiary)] text-caption leading-[18px]'>
{description}
</p>
{connectorTypes.length > 0 && (
<div className='flex flex-shrink-0 items-center'>
{connectorTypes.map((type, index) => {
const config = CONNECTOR_REGISTRY[type]
if (!config?.icon) return null
{connectorEntries.length > 0 && (
<div className='[&>*:not(:first-child)]:-ml-1 flex flex-shrink-0 items-center'>
{visibleConnectorEntries.map(({ type, config }) => {
const Icon = config.icon
return (
<Tooltip.Root key={type}>
<Tooltip.Trigger asChild>
<div
className='flex size-[20px] flex-shrink-0 items-center justify-center rounded-sm bg-[var(--surface-5)]'
style={{ marginLeft: index > 0 ? '-4px' : '0' }}
>
<div className='flex size-5 flex-shrink-0 items-center justify-center rounded-md border border-[var(--surface-3)] bg-[var(--surface-5)] transition-[border-color] group-hover:border-[var(--surface-4)] dark:border-[var(--surface-4)] dark:group-hover:border-[var(--surface-5)]'>
<Icon className='size-[12px] text-[var(--text-secondary)]' />
</div>
</Tooltip.Trigger>
@@ -229,6 +237,16 @@ export function BaseCard({
</Tooltip.Root>
)
})}
{hiddenConnectorCount > 0 && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div className='flex size-5 flex-shrink-0 items-center justify-center rounded-md border border-[var(--surface-3)] bg-[var(--surface-5)] font-medium text-[var(--text-muted)] text-micro transition-[border-color] group-hover:border-[var(--surface-4)] dark:border-[var(--surface-4)] dark:group-hover:border-[var(--surface-5)]'>
+{hiddenConnectorCount}
</div>
</Tooltip.Trigger>
<Tooltip.Content>{hiddenConnectorLabels.join(', ')}</Tooltip.Content>
</Tooltip.Root>
)}
</div>
)}
</div>
@@ -71,22 +71,35 @@ function connectorCell(connectorTypes?: string[]): ResourceCell {
if (entries.length === 0) return { label: EMPTY_CELL_PLACEHOLDER }
const visibleEntries = entries.slice(0, 3)
const hiddenEntries = entries.slice(3)
return {
content: (
<div className='flex items-center gap-1'>
{entries.map(({ type, def }) => {
{visibleEntries.map(({ type, def }) => {
const Icon = def.icon
return (
<Tooltip.Root key={type}>
<Tooltip.Trigger asChild>
<span className='flex-shrink-0'>
<Icon className='size-3.5' />
<span className='flex size-5 flex-shrink-0 items-center justify-center rounded-md bg-[var(--surface-4)] text-[var(--text-secondary)]'>
<Icon className='size-[13px]' />
</span>
</Tooltip.Trigger>
<Tooltip.Content>{def.name}</Tooltip.Content>
</Tooltip.Root>
)
})}
{hiddenEntries.length > 0 && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='flex size-5 flex-shrink-0 items-center justify-center rounded-md bg-[var(--surface-4)] font-medium text-[var(--text-muted)] text-micro'>
+{hiddenEntries.length}
</span>
</Tooltip.Trigger>
<Tooltip.Content>{hiddenEntries.map(({ def }) => def.name).join(', ')}</Tooltip.Content>
</Tooltip.Root>
)}
</div>
),
}