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:
Waleed
2026-05-18 17:49:51 -07:00
committed by GitHub
parent f3cf8fc55d
commit 0c1167d76b
25 changed files with 431 additions and 255 deletions
-1
View File
@@ -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,
+1 -1
View File
@@ -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"
/>
+2 -2
View File
@@ -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\) |
+1 -1
View File
@@ -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"
/>
+1 -1
View File
@@ -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"
/>
+1 -1
View File
@@ -6,7 +6,7 @@ description: 複数のファイルを読み込んで解析する
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file_v3"
type="file_v4"
color="#40916C"
/>
+1 -1
View File
@@ -6,7 +6,7 @@ description: 读取并解析多个文件
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file_v3"
type="file_v4"
color="#40916C"
/>
+4 -4
View File
@@ -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() {
+110 -32
View File
@@ -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' },
},
}
+120 -44
View File
@@ -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
)}
+2
View File
@@ -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'
+3 -3
View File
@@ -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)',
},
},
+3 -3
View File
@@ -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)',
},
},
+14 -4
View File
@@ -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 -6
View File
@@ -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[]
}
}