mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(workspace): fix resource table column proportions and toast stacking (#4655)
* improvement(workspace): fix resource table column proportions and toast stacking * fix(resource): restore scrollbar-gutter stable on table scroll container * fix(resource): remove scrollbar-gutter stable — single-table layout doesn't need it * fixed files cols * fix(findymail): add required enabled field to wandConfig entries * fix(findymail): remove optional from block outputs — not valid on BlockConfig output type * fixes * fix(files): use folderSizeMap for sort value so size sort matches display * files ref
This commit is contained in:
@@ -263,7 +263,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
extend_v2: ExtendIcon,
|
||||
fathom: FathomIcon,
|
||||
file: DocumentIcon,
|
||||
file_v3: DocumentIcon,
|
||||
file_v4: DocumentIcon,
|
||||
findymail: FindymailIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Mehrere Dateien lesen und parsen
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="file_v3"
|
||||
type="file_v4"
|
||||
color="#40916C"
|
||||
/>
|
||||
|
||||
|
||||
@@ -229,9 +229,9 @@ Free endpoint to retrieve valid location or job title values for use in Search f
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `location_suggestions` | array | Location suggestions when using location_search \(null when searching job titles\) |
|
||||
| `location_suggestions` | array | Location suggestions when using location_search \(empty when searching job titles\) |
|
||||
| ↳ `name` | string | Formatted location name to use in filters |
|
||||
| ↳ `type` | string | Location type \(COUNTRY, STATE, CITY, or ZONE\) |
|
||||
| `job_title_suggestions` | array | Up to 25 job title suggestions ordered by popularity when using job_title_search \(null when searching locations\) |
|
||||
| `job_title_suggestions` | array | Up to 25 job title suggestions ordered by popularity when using job_title_search \(empty when searching locations\) |
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Leer y analizar múltiples archivos
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="file_v3"
|
||||
type="file_v4"
|
||||
color="#40916C"
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Lire et analyser plusieurs fichiers
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="file_v3"
|
||||
type="file_v4"
|
||||
color="#40916C"
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: 複数のファイルを読み込んで解析する
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="file_v3"
|
||||
type="file_v4"
|
||||
color="#40916C"
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: 读取并解析多个文件
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="file_v3"
|
||||
type="file_v4"
|
||||
color="#40916C"
|
||||
/>
|
||||
|
||||
|
||||
@@ -878,22 +878,22 @@ input[type="search"]::-ms-clear {
|
||||
@keyframes toast-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.97);
|
||||
transform: translateX(var(--stack-offset, 0px)) translateY(8px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
transform: translateX(var(--stack-offset, 0px)) translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toast-exit {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
transform: translateX(var(--stack-offset, 0px)) translateY(0) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.97);
|
||||
transform: translateX(var(--stack-offset, 0px)) translateY(8px) scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,4 +25,4 @@ export type {
|
||||
RowDragDropConfig,
|
||||
SelectableConfig,
|
||||
} from './resource/resource'
|
||||
export { Resource, ResourceTable } from './resource/resource'
|
||||
export { EMPTY_CELL_PLACEHOLDER, Resource, ResourceTable } from './resource/resource'
|
||||
|
||||
@@ -23,8 +23,6 @@ export interface ResourceColumn {
|
||||
id: string
|
||||
header: string
|
||||
widthMultiplier?: number
|
||||
/** Fixed pixel width. When set, the column is excluded from proportional sizing. */
|
||||
widthPx?: number
|
||||
}
|
||||
|
||||
export interface ResourceCell {
|
||||
@@ -94,7 +92,7 @@ interface ResourceProps {
|
||||
overlay?: ReactNode
|
||||
}
|
||||
|
||||
const EMPTY_CELL_PLACEHOLDER = '- - -'
|
||||
export const EMPTY_CELL_PLACEHOLDER = '—'
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
|
||||
/**
|
||||
@@ -214,7 +212,6 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
emptyMessage,
|
||||
overlay,
|
||||
}: ResourceTableProps) {
|
||||
const headerRef = useRef<HTMLDivElement>(null)
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null)
|
||||
const sortEnabled = defaultSort != null
|
||||
const [internalSort, setInternalSort] = useState<{ column: string; direction: 'asc' | 'desc' }>({
|
||||
@@ -222,12 +219,6 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
direction: 'desc',
|
||||
})
|
||||
|
||||
const handleBodyScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
if (headerRef.current) {
|
||||
headerRef.current.scrollLeft = e.currentTarget.scrollLeft
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSort = useCallback((column: string, direction: 'asc' | 'desc') => {
|
||||
setInternalSort({ column, direction })
|
||||
}, [])
|
||||
@@ -290,10 +281,10 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
|
||||
return (
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<div ref={headerRef} className='overflow-hidden'>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full table-fixed text-small'>
|
||||
<ResourceColGroup columns={columns} hasCheckbox={hasCheckbox} />
|
||||
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<thead className='sticky top-0 z-10 bg-[var(--bg)] shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<tr>
|
||||
{hasCheckbox && (
|
||||
<th className='h-10 w-[52px] py-1.5 pr-0 pl-5 text-left align-middle'>
|
||||
@@ -341,14 +332,6 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
className='min-h-0 flex-1 overflow-auto [scrollbar-gutter:stable]'
|
||||
onScroll={handleBodyScroll}
|
||||
>
|
||||
<table className='w-full table-fixed text-small'>
|
||||
<ResourceColGroup columns={columns} hasCheckbox={hasCheckbox} />
|
||||
<tbody>
|
||||
{displayRows.map((row) => (
|
||||
<DataRow
|
||||
@@ -644,43 +627,22 @@ interface ResourceColGroupProps {
|
||||
hasCheckbox?: boolean
|
||||
}
|
||||
|
||||
const CHECKBOX_COLUMN_WIDTH_PX = 52
|
||||
const CHECKBOX_COLUMN_WIDTH = `${CHECKBOX_COLUMN_WIDTH_PX}px`
|
||||
const CHECKBOX_COLUMN_WIDTH = '52px'
|
||||
|
||||
const ResourceColGroup = memo(function ResourceColGroup({
|
||||
columns,
|
||||
hasCheckbox,
|
||||
}: ResourceColGroupProps) {
|
||||
const fixedPxTotal = columns.reduce((sum, col) => sum + (col.widthPx ?? 0), 0)
|
||||
const flexibleWeights = columns.map((col, colIdx) =>
|
||||
col.widthPx ? 0 : (colIdx === 0 ? 2.5 : 1.0) * (col.widthMultiplier ?? 1)
|
||||
const weights = columns.map(
|
||||
(col, colIdx) => (colIdx === 0 ? 2.5 : 1.0) * (col.widthMultiplier ?? 1)
|
||||
)
|
||||
const flexibleTotal = flexibleWeights.reduce((s, w) => s + w, 0)
|
||||
const reservedPx = fixedPxTotal + (hasCheckbox ? CHECKBOX_COLUMN_WIDTH_PX : 0)
|
||||
|
||||
const total = weights.reduce((s, w) => s + w, 0)
|
||||
return (
|
||||
<colgroup>
|
||||
{hasCheckbox && <col style={{ width: CHECKBOX_COLUMN_WIDTH }} />}
|
||||
{columns.map((col, colIdx) => {
|
||||
if (col.widthPx) {
|
||||
return <col key={col.id} style={{ width: `${col.widthPx}px` }} />
|
||||
}
|
||||
const columnRatio = flexibleTotal > 0 ? flexibleWeights[colIdx] / flexibleTotal : 0
|
||||
const columnPercent = columnRatio * 100
|
||||
const reservedOffset = reservedPx * columnRatio
|
||||
|
||||
return (
|
||||
<col
|
||||
key={col.id}
|
||||
style={{
|
||||
width:
|
||||
reservedOffset > 0
|
||||
? `calc(${columnPercent}% - ${reservedOffset}px)`
|
||||
: `${columnPercent}%`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{columns.map((col, colIdx) => (
|
||||
<col key={col.id} style={{ width: `${((weights[colIdx] / total) * 100).toFixed(3)}%` }} />
|
||||
))}
|
||||
</colgroup>
|
||||
)
|
||||
})
|
||||
@@ -697,55 +659,48 @@ const DataTableSkeleton = memo(function DataTableSkeleton({
|
||||
hasCheckbox,
|
||||
}: DataTableSkeletonProps) {
|
||||
return (
|
||||
<>
|
||||
<div className='overflow-hidden'>
|
||||
<table className='w-full table-fixed text-small'>
|
||||
<ResourceColGroup columns={columns} hasCheckbox={hasCheckbox} />
|
||||
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<tr>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full table-fixed text-small'>
|
||||
<ResourceColGroup columns={columns} hasCheckbox={hasCheckbox} />
|
||||
<thead className='sticky top-0 z-10 bg-[var(--bg)] shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<tr>
|
||||
{hasCheckbox && (
|
||||
<th className='h-10 w-[52px] py-2.5 pr-0 pl-5 text-left align-middle'>
|
||||
<Skeleton className='size-[14px] rounded-xs' />
|
||||
</th>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.id}
|
||||
className='h-10 px-6 py-2.5 text-left align-middle font-base text-[var(--text-muted)]'
|
||||
>
|
||||
<div className='flex min-h-[20px] items-center'>
|
||||
<Skeleton className='h-[12px] w-[56px]' />
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: rowCount }, (_, i) => (
|
||||
<tr key={i}>
|
||||
{hasCheckbox && (
|
||||
<th className='h-10 w-[52px] py-2.5 pr-0 pl-5 text-left align-middle'>
|
||||
<td className='w-[52px] py-2.5 pr-0 pl-5 align-middle'>
|
||||
<Skeleton className='size-[14px] rounded-xs' />
|
||||
</th>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.id}
|
||||
className='h-10 px-6 py-2.5 text-left align-middle font-base text-[var(--text-muted)]'
|
||||
>
|
||||
<div className='flex min-h-[20px] items-center'>
|
||||
<Skeleton className='h-[12px] w-[56px]' />
|
||||
</div>
|
||||
</th>
|
||||
{columns.map((col, colIdx) => (
|
||||
<td key={col.id} className='px-6 py-2.5 align-middle'>
|
||||
<span className='flex min-h-[21px] items-center gap-3'>
|
||||
{colIdx === 0 && <Skeleton className='size-[14px] rounded-xs' />}
|
||||
<Skeleton className='h-[14px] w-[128px]' />
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full table-fixed text-small'>
|
||||
<ResourceColGroup columns={columns} hasCheckbox={hasCheckbox} />
|
||||
<tbody>
|
||||
{Array.from({ length: rowCount }, (_, i) => (
|
||||
<tr key={i}>
|
||||
{hasCheckbox && (
|
||||
<td className='w-[52px] py-2.5 pr-0 pl-5 align-middle'>
|
||||
<Skeleton className='size-[14px] rounded-xs' />
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col, colIdx) => (
|
||||
<td key={col.id} className='px-6 py-2.5 align-middle'>
|
||||
<span className='flex min-h-[21px] items-center gap-3'>
|
||||
{colIdx === 0 && <Skeleton className='size-[14px] rounded-xs' />}
|
||||
<Skeleton className='h-[14px] w-[128px]' />
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -56,6 +56,7 @@ import type {
|
||||
SortConfig,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import {
|
||||
EMPTY_CELL_PLACEHOLDER,
|
||||
InlineRenameInput,
|
||||
ownerCell,
|
||||
Resource,
|
||||
@@ -112,12 +113,12 @@ const SUPPORTED_EXTENSIONS = [
|
||||
const ACCEPT_ATTR = SUPPORTED_EXTENSIONS.map((ext) => `.${ext}`).join(',')
|
||||
|
||||
const COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'size', header: 'Size', widthPx: 110 },
|
||||
{ id: 'type', header: 'Type', widthPx: 120 },
|
||||
{ id: 'created', header: 'Created', widthPx: 150 },
|
||||
{ id: 'owner', header: 'Owner', widthPx: 160 },
|
||||
{ id: 'updated', header: 'Last Updated', widthPx: 150 },
|
||||
{ id: 'name', header: 'Name', widthMultiplier: 1.15 },
|
||||
{ id: 'size', header: 'Size', widthMultiplier: 0.85 },
|
||||
{ id: 'type', header: 'Type', widthMultiplier: 1.0 },
|
||||
{ id: 'created', header: 'Created' },
|
||||
{ id: 'owner', header: 'Owner' },
|
||||
{ id: 'updated', header: 'Last Updated' },
|
||||
]
|
||||
|
||||
const MIME_TYPE_LABELS: Record<string, string> = {
|
||||
@@ -301,6 +302,26 @@ export function Files() {
|
||||
|
||||
const folderById = useMemo(() => new Map(folders.map((folder) => [folder.id, folder])), [folders])
|
||||
const currentFolder = currentFolderId ? (folderById.get(currentFolderId) ?? null) : null
|
||||
|
||||
const folderSizeMap = useMemo(() => {
|
||||
const directSize = new Map<string, number>()
|
||||
for (const file of files) {
|
||||
if (file.folderId) {
|
||||
directSize.set(file.folderId, (directSize.get(file.folderId) ?? 0) + file.size)
|
||||
}
|
||||
}
|
||||
const totalSize = new Map<string, number>()
|
||||
const getTotal = (folderId: string): number => {
|
||||
if (totalSize.has(folderId)) return totalSize.get(folderId)!
|
||||
const children = folders.filter((f) => f.parentId === folderId)
|
||||
const size =
|
||||
(directSize.get(folderId) ?? 0) + children.reduce((s, c) => s + getTotal(c.id), 0)
|
||||
totalSize.set(folderId, size)
|
||||
return size
|
||||
}
|
||||
for (const folder of folders) getTotal(folder.id)
|
||||
return totalSize
|
||||
}, [files, folders])
|
||||
const currentFolderPath = currentFolder?.path ?? null
|
||||
|
||||
const visibleFolders = useMemo(() => {
|
||||
@@ -406,7 +427,12 @@ export function Files() {
|
||||
icon: <Folder className='size-[14px]' />,
|
||||
label: folder.name,
|
||||
},
|
||||
size: { label: 'Folder' },
|
||||
size: {
|
||||
label:
|
||||
(folderSizeMap.get(folder.id) ?? 0) > 0
|
||||
? formatFileSize(folderSizeMap.get(folder.id)!, { includeBytes: true })
|
||||
: EMPTY_CELL_PLACEHOLDER,
|
||||
},
|
||||
type: {
|
||||
icon: <Folder className='size-[14px]' />,
|
||||
label: 'Folder',
|
||||
@@ -417,7 +443,7 @@ export function Files() {
|
||||
},
|
||||
sortValues: {
|
||||
name: folder.name,
|
||||
size: -1,
|
||||
size: folderSizeMap.get(folder.id) ?? -1,
|
||||
type: 'Folder',
|
||||
created: new Date(folder.createdAt).getTime(),
|
||||
updated: new Date(folder.updatedAt).getTime(),
|
||||
@@ -458,7 +484,7 @@ export function Files() {
|
||||
})
|
||||
|
||||
return [...folderRows, ...fileRows]
|
||||
}, [visibleFolders, filteredFiles, members])
|
||||
}, [visibleFolders, filteredFiles, members, folderSizeMap])
|
||||
|
||||
const rows: ResourceRow[] = useMemo(() => {
|
||||
if (!listRename.editingId) return baseRows
|
||||
@@ -1644,9 +1670,7 @@ export function Files() {
|
||||
? `No files match "${debouncedSearchTerm}"`
|
||||
: hasActiveFilters
|
||||
? 'No files match the active filters'
|
||||
: currentFolderId
|
||||
? 'This folder is empty'
|
||||
: 'No files yet'
|
||||
: undefined
|
||||
|
||||
const filterContent = useMemo(() => {
|
||||
const typeDisplayLabel =
|
||||
|
||||
@@ -30,7 +30,11 @@ import type {
|
||||
SelectableConfig,
|
||||
SortConfig,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import { Resource, ResourceHeader } from '@/app/workspace/[workspaceId]/components'
|
||||
import {
|
||||
EMPTY_CELL_PLACEHOLDER,
|
||||
Resource,
|
||||
ResourceHeader,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import {
|
||||
ChunkContextMenu,
|
||||
ChunkEditor,
|
||||
@@ -127,9 +131,9 @@ function truncateContent(content: string, maxLength = 150, searchQuery = ''): st
|
||||
|
||||
const CHUNK_COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'content', header: 'Content' },
|
||||
{ id: 'index', header: 'Index', widthPx: 100 },
|
||||
{ id: 'tokens', header: 'Tokens', widthPx: 100 },
|
||||
{ id: 'status', header: 'Status', widthPx: 120 },
|
||||
{ id: 'index', header: 'Index', widthMultiplier: 0.6 },
|
||||
{ id: 'tokens', header: 'Tokens', widthMultiplier: 0.6 },
|
||||
{ id: 'status', header: 'Status', widthMultiplier: 0.75 },
|
||||
]
|
||||
|
||||
export function Document({
|
||||
@@ -874,9 +878,9 @@ export function Document({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
index: { label: '—' },
|
||||
tokens: { label: '—' },
|
||||
status: { label: '—' },
|
||||
index: { label: EMPTY_CELL_PLACEHOLDER },
|
||||
tokens: { label: EMPTY_CELL_PLACEHOLDER },
|
||||
status: { label: EMPTY_CELL_PLACEHOLDER },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -85,13 +85,13 @@ const logger = createLogger('KnowledgeBase')
|
||||
const DOCUMENTS_PER_PAGE = 50
|
||||
|
||||
const DOCUMENT_COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'size', header: 'Size', widthPx: 90 },
|
||||
{ id: 'tokens', header: 'Tokens', widthPx: 90 },
|
||||
{ id: 'chunks', header: 'Chunks', widthPx: 90 },
|
||||
{ id: 'uploaded', header: 'Uploaded', widthPx: 140 },
|
||||
{ id: 'status', header: 'Status', widthPx: 110 },
|
||||
{ id: 'tags', header: 'Tags', widthPx: 160 },
|
||||
{ id: 'name', header: 'Name', widthMultiplier: 0.8 },
|
||||
{ id: 'size', header: 'Size', widthMultiplier: 0.75 },
|
||||
{ id: 'tokens', header: 'Tokens', widthMultiplier: 0.75 },
|
||||
{ id: 'chunks', header: 'Chunks', widthMultiplier: 0.75 },
|
||||
{ id: 'uploaded', header: 'Uploaded' },
|
||||
{ id: 'status', header: 'Status', widthMultiplier: 0.75 },
|
||||
{ id: 'tags', header: 'Tags' },
|
||||
]
|
||||
|
||||
interface KnowledgeBaseProps {
|
||||
|
||||
@@ -16,7 +16,12 @@ import type {
|
||||
SearchConfig,
|
||||
SortConfig,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import { ownerCell, Resource, timeCell } from '@/app/workspace/[workspaceId]/components'
|
||||
import {
|
||||
EMPTY_CELL_PLACEHOLDER,
|
||||
ownerCell,
|
||||
Resource,
|
||||
timeCell,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
|
||||
import {
|
||||
CreateBaseModal,
|
||||
@@ -43,19 +48,19 @@ interface KnowledgeBaseWithDocCount extends KnowledgeBaseData {
|
||||
|
||||
const COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'documents', header: 'Documents', widthPx: 110 },
|
||||
{ id: 'tokens', header: 'Tokens', widthPx: 90 },
|
||||
{ id: 'connectors', header: 'Connectors', widthPx: 130 },
|
||||
{ id: 'created', header: 'Created', widthPx: 140 },
|
||||
{ id: 'owner', header: 'Owner', widthPx: 150 },
|
||||
{ id: 'updated', header: 'Last Updated', widthPx: 140 },
|
||||
{ id: 'documents', header: 'Documents', widthMultiplier: 0.6 },
|
||||
{ id: 'tokens', header: 'Tokens', widthMultiplier: 0.6 },
|
||||
{ id: 'connectors', header: 'Connectors', widthMultiplier: 0.7 },
|
||||
{ id: 'created', header: 'Created' },
|
||||
{ id: 'owner', header: 'Owner' },
|
||||
{ id: 'updated', header: 'Last Updated' },
|
||||
]
|
||||
|
||||
const DATABASE_ICON = <Database className='size-[14px]' />
|
||||
|
||||
function connectorCell(connectorTypes?: string[]): ResourceCell {
|
||||
if (!connectorTypes || connectorTypes.length === 0) {
|
||||
return { label: '—' }
|
||||
return { label: EMPTY_CELL_PLACEHOLDER }
|
||||
}
|
||||
|
||||
const entries = connectorTypes
|
||||
@@ -64,7 +69,7 @@ function connectorCell(connectorTypes?: string[]): ResourceCell {
|
||||
Boolean(e.def?.icon)
|
||||
)
|
||||
|
||||
if (entries.length === 0) return { label: '—' }
|
||||
if (entries.length === 0) return { label: EMPTY_CELL_PLACEHOLDER }
|
||||
|
||||
return {
|
||||
content: (
|
||||
|
||||
@@ -106,11 +106,11 @@ const REFRESH_SPINNER_DURATION_MS = 1000 as const
|
||||
|
||||
const LOG_COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'workflow', header: 'Workflow' },
|
||||
{ id: 'date', header: 'Date', widthPx: 160 },
|
||||
{ id: 'status', header: 'Status', widthPx: 120 },
|
||||
{ id: 'cost', header: 'Cost', widthPx: 100 },
|
||||
{ id: 'trigger', header: 'Trigger', widthPx: 140 },
|
||||
{ id: 'duration', header: 'Duration', widthPx: 110 },
|
||||
{ id: 'date', header: 'Date' },
|
||||
{ id: 'status', header: 'Status' },
|
||||
{ id: 'cost', header: 'Cost' },
|
||||
{ id: 'trigger', header: 'Trigger' },
|
||||
{ id: 'duration', header: 'Duration' },
|
||||
]
|
||||
|
||||
interface LogSelectionState {
|
||||
|
||||
@@ -22,7 +22,11 @@ import type {
|
||||
ResourceRow,
|
||||
SortConfig,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import { Resource, timeCell } from '@/app/workspace/[workspaceId]/components'
|
||||
import {
|
||||
EMPTY_CELL_PLACEHOLDER,
|
||||
Resource,
|
||||
timeCell,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import { ScheduleModal } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal'
|
||||
import { ScheduleContextMenu } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-context-menu'
|
||||
import { ScheduleListContextMenu } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-list-context-menu'
|
||||
@@ -44,14 +48,14 @@ function getScheduleDescription(s: WorkspaceScheduleData) {
|
||||
const timing = parseCronToHumanReadable(s.cronExpression, s.timezone)
|
||||
return `Recurring, ${timing.charAt(0).toLowerCase()}${timing.slice(1)}`
|
||||
}
|
||||
return '- - -'
|
||||
return EMPTY_CELL_PLACEHOLDER
|
||||
}
|
||||
|
||||
const COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'task', header: 'Task' },
|
||||
{ id: 'schedule', header: 'Schedule', widthMultiplier: 1.5 },
|
||||
{ id: 'nextRun', header: 'Next Run', widthPx: 160 },
|
||||
{ id: 'lastRun', header: 'Last Run', widthPx: 160 },
|
||||
{ id: 'schedule', header: 'Schedule' },
|
||||
{ id: 'nextRun', header: 'Next Run' },
|
||||
{ id: 'lastRun', header: 'Last Run' },
|
||||
]
|
||||
|
||||
export function ScheduledTasks() {
|
||||
|
||||
@@ -49,11 +49,11 @@ const logger = createLogger('Tables')
|
||||
|
||||
const COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'columns', header: 'Columns', widthPx: 110 },
|
||||
{ id: 'rows', header: 'Rows', widthPx: 100 },
|
||||
{ id: 'created', header: 'Created', widthPx: 150 },
|
||||
{ id: 'owner', header: 'Owner', widthPx: 160 },
|
||||
{ id: 'updated', header: 'Last Updated', widthPx: 150 },
|
||||
{ id: 'columns', header: 'Columns' },
|
||||
{ id: 'rows', header: 'Rows' },
|
||||
{ id: 'created', header: 'Created' },
|
||||
{ id: 'owner', header: 'Owner' },
|
||||
{ id: 'updated', header: 'Last Updated' },
|
||||
]
|
||||
|
||||
export function Tables() {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
// Find Email From Name
|
||||
{
|
||||
id: 'name',
|
||||
id: 'fn_name',
|
||||
title: 'Full Name',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -45,7 +45,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
condition: { field: 'operation', value: 'findymail_find_email_from_name' },
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
id: 'fn_domain',
|
||||
title: 'Company Domain or Name',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -54,7 +54,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
// Find Email From LinkedIn
|
||||
{
|
||||
id: 'linkedin_url',
|
||||
id: 'fefl_linkedin_url',
|
||||
title: 'LinkedIn URL',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -63,7 +63,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
// Find Emails By Domain
|
||||
{
|
||||
id: 'domain',
|
||||
id: 'fed_domain',
|
||||
title: 'Domain',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -77,10 +77,16 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
required: true,
|
||||
placeholder: '["CEO", "Founder"]',
|
||||
condition: { field: 'operation', value: 'findymail_find_emails_by_domain' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a JSON array of job roles/titles to target at the company (max 3). Return ONLY the JSON array - no explanations, no extra text.',
|
||||
placeholder: 'e.g. CEO, Founder, CTO',
|
||||
},
|
||||
},
|
||||
// Verify Email
|
||||
{
|
||||
id: 'email',
|
||||
id: 've_email',
|
||||
title: 'Email Address',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -89,7 +95,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
// Reverse Email Lookup
|
||||
{
|
||||
id: 'email',
|
||||
id: 'rel_email',
|
||||
title: 'Email Address',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -99,27 +105,32 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
{
|
||||
id: 'with_profile',
|
||||
title: 'Return Full Profile',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'findymail_reverse_email_lookup' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Company Info — provide at least one of LinkedIn URL, domain, or name
|
||||
{
|
||||
id: 'linkedin_url',
|
||||
id: 'gc_linkedin_url',
|
||||
title: 'Company LinkedIn URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'LinkedIn URL (at least one of URL/domain/name required)',
|
||||
condition: { field: 'operation', value: 'findymail_get_company' },
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
id: 'gc_domain',
|
||||
title: 'Company Domain',
|
||||
type: 'short-input',
|
||||
placeholder: 'stripe.com (at least one of URL/domain/name required)',
|
||||
condition: { field: 'operation', value: 'findymail_get_company' },
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
id: 'gc_name',
|
||||
title: 'Company Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Stripe (at least one of URL/domain/name required)',
|
||||
@@ -141,6 +152,12 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
required: true,
|
||||
placeholder: '["Software Engineer", "CEO"]',
|
||||
condition: { field: 'operation', value: 'findymail_find_employees' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a JSON array of job titles to search for at the company (max 10). Return ONLY the JSON array - no explanations, no extra text.',
|
||||
placeholder: 'e.g. Software Engineer, CEO, Product Manager',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'count',
|
||||
@@ -152,7 +169,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
// Find Phone
|
||||
{
|
||||
id: 'linkedin_url',
|
||||
id: 'fp_linkedin_url',
|
||||
title: 'LinkedIn URL',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -167,10 +184,16 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
required: true,
|
||||
placeholder: 'React',
|
||||
condition: { field: 'operation', value: 'findymail_search_technologies' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a technology search term to look up in the Findymail technology catalog. Return ONLY the search term - no explanations, no extra text.',
|
||||
placeholder: 'e.g. React, Stripe, Salesforce',
|
||||
},
|
||||
},
|
||||
// Lookup Technologies By Domain
|
||||
{
|
||||
id: 'domain',
|
||||
id: 'lt_domain',
|
||||
title: 'Company Domain',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
@@ -184,6 +207,12 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
placeholder: '["React", "TypeScript"]',
|
||||
condition: { field: 'operation', value: 'findymail_lookup_technologies' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a JSON array of technology names to filter by (case-insensitive). Return ONLY the JSON array - no explanations, no extra text.',
|
||||
placeholder: 'e.g. React, TypeScript, Node.js',
|
||||
},
|
||||
},
|
||||
// API Key
|
||||
{
|
||||
@@ -230,35 +259,58 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation: _operation, ...rest } = params
|
||||
|
||||
// Map unique subBlock IDs back to tool param names
|
||||
const idToParam: Record<string, string> = {
|
||||
fn_name: 'name',
|
||||
fn_domain: 'domain',
|
||||
fefl_linkedin_url: 'linkedin_url',
|
||||
fed_domain: 'domain',
|
||||
ve_email: 'email',
|
||||
rel_email: 'email',
|
||||
gc_linkedin_url: 'linkedin_url',
|
||||
gc_domain: 'domain',
|
||||
gc_name: 'name',
|
||||
fp_linkedin_url: 'linkedin_url',
|
||||
lt_domain: 'domain',
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
if (value === undefined || value === null || value === '') continue
|
||||
if (key === 'count') {
|
||||
const mappedKey = idToParam[key] ?? key
|
||||
if (mappedKey === 'count') {
|
||||
const n = Number(value)
|
||||
if (!Number.isNaN(n)) result[key] = n
|
||||
} else if (key === 'roles' || key === 'job_titles' || key === 'technologies') {
|
||||
if (!Number.isNaN(n)) result[mappedKey] = n
|
||||
} else if (mappedKey === 'with_profile') {
|
||||
result[mappedKey] = value === true || value === 'true'
|
||||
} else if (
|
||||
mappedKey === 'roles' ||
|
||||
mappedKey === 'job_titles' ||
|
||||
mappedKey === 'technologies'
|
||||
) {
|
||||
if (Array.isArray(value)) {
|
||||
result[key] = value
|
||||
result[mappedKey] = value
|
||||
} else if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
if (Array.isArray(parsed)) {
|
||||
result[key] = parsed
|
||||
result[mappedKey] = parsed
|
||||
continue
|
||||
}
|
||||
} catch {
|
||||
// fall through to comma-split
|
||||
}
|
||||
}
|
||||
result[key] = trimmed
|
||||
result[mappedKey] = trimmed
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
} else {
|
||||
result[key] = value
|
||||
result[mappedKey] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -268,23 +320,30 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'Findymail API key' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
name: { type: 'string', description: 'Full name or company name' },
|
||||
domain: { type: 'string', description: 'Company domain' },
|
||||
linkedin_url: { type: 'string', description: 'LinkedIn profile or company URL' },
|
||||
fn_name: { type: 'string', description: 'Full name (find email from name)' },
|
||||
fn_domain: { type: 'string', description: 'Company domain or name (find email from name)' },
|
||||
fefl_linkedin_url: { type: 'string', description: 'LinkedIn URL (find email from LinkedIn)' },
|
||||
fed_domain: { type: 'string', description: 'Company domain (find emails by domain)' },
|
||||
roles: { type: 'array', description: 'Target roles (max 3)' },
|
||||
ve_email: { type: 'string', description: 'Email address to verify' },
|
||||
rel_email: { type: 'string', description: 'Email address for reverse lookup' },
|
||||
with_profile: { type: 'boolean', description: 'Return full profile data on reverse lookup' },
|
||||
gc_linkedin_url: { type: 'string', description: 'Company LinkedIn URL (get company info)' },
|
||||
gc_domain: { type: 'string', description: 'Company domain (get company info)' },
|
||||
gc_name: { type: 'string', description: 'Company name (get company info)' },
|
||||
website: { type: 'string', description: 'Company website for employee search' },
|
||||
job_titles: { type: 'array', description: 'Target job titles (max 10)' },
|
||||
count: { type: 'number', description: 'Number of contacts to return (max 5)' },
|
||||
fp_linkedin_url: { type: 'string', description: 'LinkedIn URL (find phone)' },
|
||||
q: { type: 'string', description: 'Technology search query' },
|
||||
lt_domain: { type: 'string', description: 'Company domain (lookup technologies)' },
|
||||
technologies: { type: 'array', description: 'Technology names to filter by' },
|
||||
},
|
||||
outputs: {
|
||||
// Verify Email
|
||||
verified: { type: 'boolean', description: 'Whether the email is deliverable' },
|
||||
provider: { type: 'string', description: 'Email service provider' },
|
||||
// Find / Reverse / Company
|
||||
// Find Email / LinkedIn
|
||||
contact: {
|
||||
type: 'json',
|
||||
description: 'Contact found (name, email, domain)',
|
||||
@@ -293,6 +352,7 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
type: 'array',
|
||||
description: 'Contacts found at the domain (name, email, domain)',
|
||||
},
|
||||
// Reverse Email Lookup
|
||||
linkedin_url: { type: 'string', description: 'LinkedIn URL' },
|
||||
fullName: { type: 'string', description: 'Full name from LinkedIn profile' },
|
||||
username: { type: 'string', description: 'LinkedIn username' },
|
||||
@@ -302,10 +362,16 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
city: { type: 'string', description: 'City' },
|
||||
region: { type: 'string', description: 'Region/state' },
|
||||
country: { type: 'string', description: 'Country' },
|
||||
companyLinkedinUrl: { type: 'string', description: 'Current company LinkedIn URL' },
|
||||
companyLinkedinUrl: {
|
||||
type: 'string',
|
||||
description: 'Current company LinkedIn URL',
|
||||
},
|
||||
companyName: { type: 'string', description: 'Current company name' },
|
||||
companyWebsite: { type: 'string', description: 'Current company website' },
|
||||
isPremium: { type: 'boolean', description: 'Whether the profile has LinkedIn Premium' },
|
||||
isPremium: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the profile has LinkedIn Premium',
|
||||
},
|
||||
isOpenProfile: { type: 'boolean', description: 'Whether the profile is open' },
|
||||
skills: { type: 'array', description: 'Profile skills' },
|
||||
jobs: { type: 'array', description: 'Job history entries' },
|
||||
@@ -318,9 +384,12 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
description: 'Certifications (name, issuingOrganization, issueDate, expirationDate)',
|
||||
},
|
||||
// Get Company
|
||||
name: { type: 'string', description: 'Company name (get-company)' },
|
||||
name: { type: 'string', description: 'Company name' },
|
||||
domain: { type: 'string', description: 'Company domain' },
|
||||
company_size: { type: 'string', description: 'Headcount range (e.g., 1001-5000)' },
|
||||
company_size: {
|
||||
type: 'string',
|
||||
description: 'Headcount range (e.g., 1001-5000)',
|
||||
},
|
||||
industry: { type: 'string', description: 'Industry classification' },
|
||||
description: { type: 'string', description: 'Company description' },
|
||||
// Find Employees
|
||||
@@ -329,8 +398,14 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
description: 'Employees found (name, linkedinUrl, companyWebsite, companyName, jobTitle)',
|
||||
},
|
||||
// Find Phone
|
||||
phone: { type: 'string', description: 'Phone number in E.164 format (US only)' },
|
||||
line_type: { type: 'string', description: 'Phone line type (Mobile, Landline)' },
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: 'Phone number in E.164 format (US only)',
|
||||
},
|
||||
line_type: {
|
||||
type: 'string',
|
||||
description: 'Phone line type (Mobile, Landline)',
|
||||
},
|
||||
// Technologies
|
||||
technologies: {
|
||||
type: 'array',
|
||||
@@ -338,8 +413,11 @@ export const FindymailBlock: BlockConfig<FindymailResponse> = {
|
||||
},
|
||||
// Get Credits
|
||||
credits: { type: 'number', description: 'Remaining finder credits' },
|
||||
verifier_credits: { type: 'number', description: 'Remaining verifier credits' },
|
||||
// Reverse / Verify shared
|
||||
verifier_credits: {
|
||||
type: 'number',
|
||||
description: 'Remaining verifier credits',
|
||||
},
|
||||
// Verify / Reverse shared
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,21 +70,21 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_person' },
|
||||
},
|
||||
{
|
||||
id: 'company_name',
|
||||
id: 'ep_company_name',
|
||||
title: 'Company Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Intercom',
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_person' },
|
||||
},
|
||||
{
|
||||
id: 'company_website',
|
||||
id: 'ep_company_website',
|
||||
title: 'Company Website',
|
||||
type: 'short-input',
|
||||
placeholder: 'intercom.com',
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_person' },
|
||||
},
|
||||
{
|
||||
id: 'company_linkedin_url',
|
||||
id: 'ep_company_linkedin_url',
|
||||
title: 'Company LinkedIn URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://www.linkedin.com/company/intercom',
|
||||
@@ -100,44 +100,56 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'only_verified_email',
|
||||
id: 'ep_only_verified_email',
|
||||
title: 'Only Verified Email',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_person' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'enrich_mobile',
|
||||
id: 'ep_enrich_mobile',
|
||||
title: 'Enrich Mobile',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_person' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'only_verified_mobile',
|
||||
id: 'ep_only_verified_mobile',
|
||||
title: 'Only Verified Mobile',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_person' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Enrich Company
|
||||
{
|
||||
id: 'company_website',
|
||||
id: 'ec_company_website',
|
||||
title: 'Company Website',
|
||||
type: 'short-input',
|
||||
placeholder: 'intercom.com',
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_company' },
|
||||
},
|
||||
{
|
||||
id: 'company_linkedin_url',
|
||||
id: 'ec_company_linkedin_url',
|
||||
title: 'Company LinkedIn URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://www.linkedin.com/company/intercom',
|
||||
condition: { field: 'operation', value: 'prospeo_enrich_company' },
|
||||
},
|
||||
{
|
||||
id: 'company_name',
|
||||
id: 'ec_company_name',
|
||||
title: 'Company Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Intercom',
|
||||
@@ -155,7 +167,7 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
|
||||
// Bulk Enrich Person
|
||||
{
|
||||
id: 'data',
|
||||
id: 'bep_data',
|
||||
title: 'Records',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
@@ -171,30 +183,42 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'only_verified_email',
|
||||
id: 'bep_only_verified_email',
|
||||
title: 'Only Verified Email',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'prospeo_bulk_enrich_person' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'enrich_mobile',
|
||||
id: 'bep_enrich_mobile',
|
||||
title: 'Enrich Mobile',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'prospeo_bulk_enrich_person' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'only_verified_mobile',
|
||||
id: 'bep_only_verified_mobile',
|
||||
title: 'Only Verified Mobile',
|
||||
type: 'switch',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'prospeo_bulk_enrich_person' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Bulk Enrich Company
|
||||
{
|
||||
id: 'data',
|
||||
id: 'bec_data',
|
||||
title: 'Records',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
@@ -212,7 +236,7 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
|
||||
// Search Person
|
||||
{
|
||||
id: 'filters',
|
||||
id: 'sp_filters',
|
||||
title: 'Filters',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
@@ -228,7 +252,7 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'page',
|
||||
id: 'sp_page',
|
||||
title: 'Page',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
@@ -238,7 +262,7 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
|
||||
// Search Company
|
||||
{
|
||||
id: 'filters',
|
||||
id: 'sc_filters',
|
||||
title: 'Filters',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
@@ -254,7 +278,7 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'page',
|
||||
id: 'sc_page',
|
||||
title: 'Page',
|
||||
type: 'short-input',
|
||||
placeholder: '1',
|
||||
@@ -323,23 +347,45 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const renames: Record<string, string> = {
|
||||
ep_company_name: 'company_name',
|
||||
ep_company_website: 'company_website',
|
||||
ep_company_linkedin_url: 'company_linkedin_url',
|
||||
ep_only_verified_email: 'only_verified_email',
|
||||
ep_enrich_mobile: 'enrich_mobile',
|
||||
ep_only_verified_mobile: 'only_verified_mobile',
|
||||
ec_company_website: 'company_website',
|
||||
ec_company_linkedin_url: 'company_linkedin_url',
|
||||
ec_company_name: 'company_name',
|
||||
bep_data: 'data',
|
||||
bep_only_verified_email: 'only_verified_email',
|
||||
bep_enrich_mobile: 'enrich_mobile',
|
||||
bep_only_verified_mobile: 'only_verified_mobile',
|
||||
bec_data: 'data',
|
||||
sp_filters: 'filters',
|
||||
sp_page: 'page',
|
||||
sc_filters: 'filters',
|
||||
sc_page: 'page',
|
||||
}
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null || value === '') continue
|
||||
if (key === 'page') {
|
||||
if (key === 'operation') continue
|
||||
const targetKey = renames[key] ?? key
|
||||
if (targetKey === 'page') {
|
||||
const n = Number(value)
|
||||
if (Number.isFinite(n)) result[key] = n
|
||||
if (Number.isFinite(n)) result[targetKey] = n
|
||||
continue
|
||||
}
|
||||
if (
|
||||
key === 'only_verified_email' ||
|
||||
key === 'enrich_mobile' ||
|
||||
key === 'only_verified_mobile'
|
||||
targetKey === 'only_verified_email' ||
|
||||
targetKey === 'enrich_mobile' ||
|
||||
targetKey === 'only_verified_mobile'
|
||||
) {
|
||||
result[key] = typeof value === 'string' ? value === 'true' : Boolean(value)
|
||||
result[targetKey] = typeof value === 'string' ? value === 'true' : Boolean(value)
|
||||
continue
|
||||
}
|
||||
result[key] = value
|
||||
result[targetKey] = value
|
||||
}
|
||||
return result
|
||||
},
|
||||
@@ -348,25 +394,55 @@ export const ProspeoBlock: BlockConfig<ProspeoResponse> = {
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'Prospeo API key' },
|
||||
// Enrich Person / Company match keys
|
||||
// Enrich Person match keys
|
||||
first_name: { type: 'string', description: 'First name' },
|
||||
last_name: { type: 'string', description: 'Last name' },
|
||||
full_name: { type: 'string', description: 'Full name' },
|
||||
linkedin_url: { type: 'string', description: 'Person LinkedIn URL' },
|
||||
email: { type: 'string', description: 'Work email' },
|
||||
company_name: { type: 'string', description: 'Company name' },
|
||||
company_website: { type: 'string', description: 'Company website' },
|
||||
company_linkedin_url: { type: 'string', description: 'Company LinkedIn URL' },
|
||||
company_id: { type: 'string', description: 'Prospeo company_id' },
|
||||
ep_company_name: { type: 'string', description: 'Company name (enrich person)' },
|
||||
ep_company_website: { type: 'string', description: 'Company website (enrich person)' },
|
||||
ep_company_linkedin_url: {
|
||||
type: 'string',
|
||||
description: 'Company LinkedIn URL (enrich person)',
|
||||
},
|
||||
person_id: { type: 'string', description: 'Prospeo person_id' },
|
||||
only_verified_email: { type: 'boolean', description: 'Only verified emails' },
|
||||
enrich_mobile: { type: 'boolean', description: 'Reveal mobile numbers' },
|
||||
only_verified_mobile: { type: 'boolean', description: 'Only records with a mobile' },
|
||||
// Bulk
|
||||
data: { type: 'json', description: 'Array of records to enrich' },
|
||||
// Search
|
||||
filters: { type: 'json', description: 'Search filters configuration' },
|
||||
page: { type: 'number', description: 'Page number (defaults to 1)' },
|
||||
ep_only_verified_email: { type: 'string', description: 'Only verified emails (enrich person)' },
|
||||
ep_enrich_mobile: { type: 'string', description: 'Reveal mobile numbers (enrich person)' },
|
||||
ep_only_verified_mobile: {
|
||||
type: 'string',
|
||||
description: 'Only records with a mobile (enrich person)',
|
||||
},
|
||||
// Enrich Company match keys
|
||||
ec_company_website: { type: 'string', description: 'Company website (enrich company)' },
|
||||
ec_company_linkedin_url: {
|
||||
type: 'string',
|
||||
description: 'Company LinkedIn URL (enrich company)',
|
||||
},
|
||||
ec_company_name: { type: 'string', description: 'Company name (enrich company)' },
|
||||
company_id: { type: 'string', description: 'Prospeo company_id' },
|
||||
// Bulk Person
|
||||
bep_data: { type: 'json', description: 'Array of person records to enrich (bulk)' },
|
||||
bep_only_verified_email: {
|
||||
type: 'string',
|
||||
description: 'Only verified emails (bulk enrich person)',
|
||||
},
|
||||
bep_enrich_mobile: {
|
||||
type: 'string',
|
||||
description: 'Reveal mobile numbers (bulk enrich person)',
|
||||
},
|
||||
bep_only_verified_mobile: {
|
||||
type: 'string',
|
||||
description: 'Only records with a mobile (bulk enrich person)',
|
||||
},
|
||||
// Bulk Company
|
||||
bec_data: { type: 'json', description: 'Array of company records to enrich (bulk)' },
|
||||
// Search Person
|
||||
sp_filters: { type: 'json', description: 'Search person filters configuration' },
|
||||
sp_page: { type: 'string', description: 'Search person page number (defaults to 1)' },
|
||||
// Search Company
|
||||
sc_filters: { type: 'json', description: 'Search company filters configuration' },
|
||||
sc_page: { type: 'string', description: 'Search company page number (defaults to 1)' },
|
||||
// Suggestions
|
||||
location_search: { type: 'string', description: 'Location search query' },
|
||||
job_title_search: { type: 'string', description: 'Job title search query' },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type CSSProperties,
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
@@ -17,7 +18,8 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
const AUTO_DISMISS_MS = 5000
|
||||
const EXIT_ANIMATION_MS = 200
|
||||
const MAX_VISIBLE = 20
|
||||
const MAX_VISIBLE = 4
|
||||
const STACK_OFFSET_PX = 3
|
||||
|
||||
const RING_RADIUS = 5.5
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
|
||||
@@ -127,7 +129,15 @@ function CountdownRing({ duration }: { duration: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id: string) => void }) {
|
||||
function ToastItem({
|
||||
toast: t,
|
||||
stackOffset,
|
||||
onDismiss,
|
||||
}: {
|
||||
toast: ToastData
|
||||
stackOffset: number
|
||||
onDismiss: (id: string) => void
|
||||
}) {
|
||||
const [exiting, setExiting] = useState(false)
|
||||
const pausedRef = useRef(false)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
@@ -168,8 +178,9 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:
|
||||
<div
|
||||
onMouseEnter={hasDuration ? handleMouseEnter : undefined}
|
||||
onMouseLeave={hasDuration ? handleMouseLeave : undefined}
|
||||
style={{ '--stack-offset': `${stackOffset}px` } as CSSProperties}
|
||||
className={cn(
|
||||
'pointer-events-auto flex flex-col gap-2 overflow-hidden rounded-lg border px-3 py-2.5 shadow-md transition-[transform,opacity]',
|
||||
'pointer-events-auto flex flex-col gap-2 overflow-hidden rounded-lg border px-3 py-2.5 shadow-md transition-[transform,opacity] [grid-area:1/1]',
|
||||
t.variant === 'error' ? 'w-[min(100vw-2rem,400px)]' : 'w-[min(100vw-2rem,320px)]',
|
||||
VARIANT_STYLES[t.variant],
|
||||
exiting
|
||||
@@ -283,11 +294,20 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
|
||||
<div
|
||||
aria-live='polite'
|
||||
aria-label='Notifications'
|
||||
className='pointer-events-none fixed right-[16px] bottom-4 z-[var(--z-toast)] flex flex-col-reverse items-end gap-2'
|
||||
className='pointer-events-none fixed right-6 bottom-6 z-[var(--z-toast)] grid justify-items-end'
|
||||
>
|
||||
{toasts.map((t) => (
|
||||
<ToastItem key={t.id} toast={t} onDismiss={dismissToast} />
|
||||
))}
|
||||
{toasts.map((t, index) => {
|
||||
const depth = toasts.length - index - 1
|
||||
|
||||
return (
|
||||
<ToastItem
|
||||
key={t.id}
|
||||
toast={t}
|
||||
stackOffset={depth * STACK_OFFSET_PX}
|
||||
onDismiss={dismissToast}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './types'
|
||||
|
||||
import { findEmailFromLinkedInTool } from '@/tools/findymail/find_email_from_linkedin'
|
||||
import { findEmailFromNameTool } from '@/tools/findymail/find_email_from_name'
|
||||
import { findEmailsByDomainTool } from '@/tools/findymail/find_emails_by_domain'
|
||||
|
||||
@@ -32,19 +32,19 @@ export const bulkEnrichPersonTool: ToolConfig<
|
||||
only_verified_email: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return records with a verified email',
|
||||
},
|
||||
enrich_mobile: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Reveal mobile numbers (10 credits per match; email included)',
|
||||
},
|
||||
only_verified_mobile: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return records that have a mobile (implies enrich_mobile)',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,19 +76,19 @@ export const enrichPersonTool: ToolConfig<ProspeoEnrichPersonParams, ProspeoEnri
|
||||
only_verified_email: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return records with a verified email',
|
||||
},
|
||||
enrich_mobile: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Reveal mobile number (10 credits per match; email included)',
|
||||
},
|
||||
only_verified_mobile: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return records that have a mobile (implies enrich_mobile)',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -46,6 +46,16 @@ export const searchSuggestionsTool: ToolConfig<
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.location_search && !params.job_title_search) {
|
||||
throw new Error(
|
||||
'Provide exactly one of location_search or job_title_search (minimum 2 characters).'
|
||||
)
|
||||
}
|
||||
if (params.location_search && params.job_title_search) {
|
||||
throw new Error(
|
||||
'location_search and job_title_search are mutually exclusive — provide only one.'
|
||||
)
|
||||
}
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.location_search) body.location_search = params.location_search
|
||||
if (params.job_title_search) body.job_title_search = params.job_title_search
|
||||
@@ -61,8 +71,8 @@ export const searchSuggestionsTool: ToolConfig<
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
location_suggestions: data.location_suggestions ?? null,
|
||||
job_title_suggestions: data.job_title_suggestions ?? null,
|
||||
location_suggestions: data.location_suggestions ?? [],
|
||||
job_title_suggestions: data.job_title_suggestions ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -71,7 +81,7 @@ export const searchSuggestionsTool: ToolConfig<
|
||||
location_suggestions: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Location suggestions when using location_search (null when searching job titles)',
|
||||
'Location suggestions when using location_search (empty when searching job titles)',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
@@ -87,7 +97,7 @@ export const searchSuggestionsTool: ToolConfig<
|
||||
job_title_suggestions: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Up to 25 job title suggestions ordered by popularity when using job_title_search (null when searching locations)',
|
||||
'Up to 25 job title suggestions ordered by popularity when using job_title_search (empty when searching locations)',
|
||||
optional: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface ProspeoBaseParams {
|
||||
}
|
||||
|
||||
export interface ProspeoPersonData {
|
||||
identifier?: string
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
full_name?: string
|
||||
@@ -18,7 +17,6 @@ export interface ProspeoPersonData {
|
||||
}
|
||||
|
||||
export interface ProspeoCompanyData {
|
||||
identifier?: string
|
||||
company_name?: string
|
||||
company_website?: string
|
||||
company_linkedin_url?: string
|
||||
@@ -35,6 +33,7 @@ export interface ProspeoPaginationOutput {
|
||||
export const PROSPEO_PAGINATION_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Pagination details',
|
||||
optional: true,
|
||||
properties: {
|
||||
current_page: { type: 'number', description: 'Current page number' },
|
||||
per_page: { type: 'number', description: 'Results per page' },
|
||||
@@ -84,7 +83,7 @@ export interface ProspeoEnrichCompanyResponse extends ToolResponse {
|
||||
|
||||
/** Bulk Enrich Person */
|
||||
export interface ProspeoBulkEnrichPersonParams extends ProspeoBaseParams {
|
||||
data: ProspeoPersonData[]
|
||||
data: Array<ProspeoPersonData & { identifier: string }>
|
||||
only_verified_email?: boolean
|
||||
enrich_mobile?: boolean
|
||||
only_verified_mobile?: boolean
|
||||
@@ -105,7 +104,7 @@ export interface ProspeoBulkEnrichPersonResponse extends ToolResponse {
|
||||
|
||||
/** Bulk Enrich Company */
|
||||
export interface ProspeoBulkEnrichCompanyParams extends ProspeoBaseParams {
|
||||
data: ProspeoCompanyData[]
|
||||
data: Array<ProspeoCompanyData & { identifier: string }>
|
||||
}
|
||||
|
||||
export interface ProspeoBulkEnrichCompanyResponse extends ToolResponse {
|
||||
@@ -161,8 +160,8 @@ export interface ProspeoSearchSuggestionsParams extends ProspeoBaseParams {
|
||||
|
||||
export interface ProspeoSearchSuggestionsResponse extends ToolResponse {
|
||||
output: {
|
||||
location_suggestions: Array<{ name: string; type: string }> | null
|
||||
job_title_suggestions: string[] | null
|
||||
location_suggestions: Array<{ name: string; type: string }>
|
||||
job_title_suggestions: string[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user