Skip to content

Commit 91ec8ef

Browse files
committed
fix(kbtags): added time to date tag, improved ui ux throughout the kb
1 parent cf2f1ab commit 91ec8ef

File tree

18 files changed

+1091
-260
lines changed

18 files changed

+1091
-260
lines changed

apps/sim/app/api/knowledge/search/utils.test.ts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ describe('Knowledge Search Utils', () => {
202202
)
203203
expect(result).toEqual([0.1, 0.2, 0.3])
204204

205-
// Clean up
206205
Object.keys(env).forEach((key) => delete (env as any)[key])
207206
})
208207

@@ -233,7 +232,6 @@ describe('Knowledge Search Utils', () => {
233232
)
234233
expect(result).toEqual([0.1, 0.2, 0.3])
235234

236-
// Clean up
237235
Object.keys(env).forEach((key) => delete (env as any)[key])
238236
})
239237

@@ -262,7 +260,6 @@ describe('Knowledge Search Utils', () => {
262260
expect.any(Object)
263261
)
264262

265-
// Clean up
266263
Object.keys(env).forEach((key) => delete (env as any)[key])
267264
})
268265

@@ -292,7 +289,6 @@ describe('Knowledge Search Utils', () => {
292289
expect.any(Object)
293290
)
294291

295-
// Clean up
296292
Object.keys(env).forEach((key) => delete (env as any)[key])
297293
})
298294

@@ -325,7 +321,6 @@ describe('Knowledge Search Utils', () => {
325321

326322
await expect(generateSearchEmbedding('test query')).rejects.toThrow('Embedding API failed')
327323

328-
// Clean up
329324
Object.keys(env).forEach((key) => delete (env as any)[key])
330325
})
331326

@@ -346,7 +341,6 @@ describe('Knowledge Search Utils', () => {
346341

347342
await expect(generateSearchEmbedding('test query')).rejects.toThrow('Embedding API failed')
348343

349-
// Clean up
350344
Object.keys(env).forEach((key) => delete (env as any)[key])
351345
})
352346

@@ -380,7 +374,6 @@ describe('Knowledge Search Utils', () => {
380374
})
381375
)
382376

383-
// Clean up
384377
Object.keys(env).forEach((key) => delete (env as any)[key])
385378
})
386379

@@ -413,7 +406,6 @@ describe('Knowledge Search Utils', () => {
413406
})
414407
)
415408

416-
// Clean up
417409
Object.keys(env).forEach((key) => delete (env as any)[key])
418410
})
419411
})
@@ -427,4 +419,97 @@ describe('Knowledge Search Utils', () => {
427419
expect(result).toEqual({})
428420
})
429421
})
422+
423+
describe('Date Filter Format Handling', () => {
424+
it('should accept date-only format (YYYY-MM-DD) in structured filters', () => {
425+
const filter = {
426+
tagSlot: 'date1',
427+
fieldType: 'date',
428+
operator: 'eq',
429+
value: '2024-01-15',
430+
}
431+
432+
expect(filter.value).toMatch(/^\d{4}-\d{2}-\d{2}$/)
433+
expect(filter.fieldType).toBe('date')
434+
})
435+
436+
it('should accept ISO 8601 timestamp format in structured filters', () => {
437+
const filter = {
438+
tagSlot: 'date1',
439+
fieldType: 'date',
440+
operator: 'eq',
441+
value: '2024-01-15T14:30:00',
442+
}
443+
444+
expect(filter.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
445+
expect(filter.fieldType).toBe('date')
446+
})
447+
448+
it('should accept ISO 8601 timestamp with UTC timezone in structured filters', () => {
449+
const filter = {
450+
tagSlot: 'date1',
451+
fieldType: 'date',
452+
operator: 'gte',
453+
value: '2024-01-15T14:30:00Z',
454+
}
455+
456+
expect(filter.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)
457+
expect(filter.fieldType).toBe('date')
458+
})
459+
460+
it('should accept ISO 8601 timestamp with timezone offset in structured filters', () => {
461+
const filter = {
462+
tagSlot: 'date1',
463+
fieldType: 'date',
464+
operator: 'lt',
465+
value: '2024-01-15T14:30:00+05:00',
466+
}
467+
468+
expect(filter.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)
469+
expect(filter.fieldType).toBe('date')
470+
})
471+
472+
it('should support all date comparison operators', () => {
473+
const operators = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between']
474+
const validDateValue = '2024-01-15'
475+
476+
for (const operator of operators) {
477+
const filter = {
478+
tagSlot: 'date1',
479+
fieldType: 'date',
480+
operator,
481+
value: validDateValue,
482+
}
483+
expect(filter.operator).toBe(operator)
484+
}
485+
})
486+
487+
it('should support between operator with date range', () => {
488+
const filter = {
489+
tagSlot: 'date1',
490+
fieldType: 'date',
491+
operator: 'between',
492+
value: '2024-01-01',
493+
valueTo: '2024-12-31',
494+
}
495+
496+
expect(filter.operator).toBe('between')
497+
expect(filter.value).toBe('2024-01-01')
498+
expect(filter.valueTo).toBe('2024-12-31')
499+
})
500+
501+
it('should support between operator with timestamp range', () => {
502+
const filter = {
503+
tagSlot: 'date1',
504+
fieldType: 'date',
505+
operator: 'between',
506+
value: '2024-01-01T00:00:00',
507+
valueTo: '2024-12-31T23:59:59',
508+
}
509+
510+
expect(filter.operator).toBe('between')
511+
expect(filter.value).toMatch(/T\d{2}:\d{2}:\d{2}$/)
512+
expect(filter.valueTo).toMatch(/T\d{2}:\d{2}:\d{2}$/)
513+
})
514+
})
430515
})

apps/sim/app/api/knowledge/search/utils.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -203,39 +203,74 @@ function buildFilterCondition(filter: StructuredFilter, embeddingTable: any) {
203203
}
204204
}
205205

206-
// Handle date operators - expects YYYY-MM-DD format from frontend
206+
// Handle date operators - accepts YYYY-MM-DD or ISO 8601 timestamp
207207
if (fieldType === 'date') {
208208
const dateStr = String(value)
209-
// Validate YYYY-MM-DD format
210-
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
211-
logger.debug(`[getStructuredTagFilters] Invalid date format: ${dateStr}, expected YYYY-MM-DD`)
209+
const dateOnlyRegex = /^\d{4}-\d{2}-\d{2}$/
210+
const datetimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/
211+
212+
// Validate format - accept date-only or timestamp
213+
const isDateOnly = dateOnlyRegex.test(dateStr)
214+
const isTimestamp = datetimeRegex.test(dateStr)
215+
216+
if (!isDateOnly && !isTimestamp) {
217+
logger.debug(
218+
`[getStructuredTagFilters] Invalid date format: ${dateStr}, expected YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss`
219+
)
212220
return null
213221
}
214222

223+
// Use date comparison for date-only values, timestamp comparison for timestamps
224+
const castType = isDateOnly ? '::date' : '::timestamp'
225+
215226
switch (operator) {
216227
case 'eq':
217-
return sql`${column}::date = ${dateStr}::date`
228+
return isDateOnly
229+
? sql`${column}::date = ${dateStr}::date`
230+
: sql`${column}::timestamp = ${dateStr}::timestamp`
218231
case 'neq':
219-
return sql`${column}::date != ${dateStr}::date`
232+
return isDateOnly
233+
? sql`${column}::date != ${dateStr}::date`
234+
: sql`${column}::timestamp != ${dateStr}::timestamp`
220235
case 'gt':
221-
return sql`${column}::date > ${dateStr}::date`
236+
return isDateOnly
237+
? sql`${column}::date > ${dateStr}::date`
238+
: sql`${column}::timestamp > ${dateStr}::timestamp`
222239
case 'gte':
223-
return sql`${column}::date >= ${dateStr}::date`
240+
return isDateOnly
241+
? sql`${column}::date >= ${dateStr}::date`
242+
: sql`${column}::timestamp >= ${dateStr}::timestamp`
224243
case 'lt':
225-
return sql`${column}::date < ${dateStr}::date`
244+
return isDateOnly
245+
? sql`${column}::date < ${dateStr}::date`
246+
: sql`${column}::timestamp < ${dateStr}::timestamp`
226247
case 'lte':
227-
return sql`${column}::date <= ${dateStr}::date`
248+
return isDateOnly
249+
? sql`${column}::date <= ${dateStr}::date`
250+
: sql`${column}::timestamp <= ${dateStr}::timestamp`
228251
case 'between':
229252
if (valueTo !== undefined) {
230253
const dateStrTo = String(valueTo)
231-
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStrTo)) {
232-
return sql`${column}::date = ${dateStr}::date`
254+
const isToDateOnly = dateOnlyRegex.test(dateStrTo)
255+
const isToTimestamp = datetimeRegex.test(dateStrTo)
256+
if (!isToDateOnly && !isToTimestamp) {
257+
return isDateOnly
258+
? sql`${column}::date = ${dateStr}::date`
259+
: sql`${column}::timestamp = ${dateStr}::timestamp`
260+
}
261+
// Use date comparison if both are date-only, otherwise use timestamp
262+
if (isDateOnly && isToDateOnly) {
263+
return sql`${column}::date >= ${dateStr}::date AND ${column}::date <= ${dateStrTo}::date`
233264
}
234-
return sql`${column}::date >= ${dateStr}::date AND ${column}::date <= ${dateStrTo}::date`
265+
return sql`${column}::timestamp >= ${dateStr}::timestamp AND ${column}::timestamp <= ${dateStrTo}::timestamp`
235266
}
236-
return sql`${column}::date = ${dateStr}::date`
267+
return isDateOnly
268+
? sql`${column}::date = ${dateStr}::date`
269+
: sql`${column}::timestamp = ${dateStr}::timestamp`
237270
default:
238-
return sql`${column}::date = ${dateStr}::date`
271+
return isDateOnly
272+
? sql`${column}::date = ${dateStr}::date`
273+
: sql`${column}::timestamp = ${dateStr}::timestamp`
239274
}
240275
}
241276

apps/sim/app/playground/page.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ export default function PlaygroundPage() {
146146
const [isDarkMode, setIsDarkMode] = useState(false)
147147
const [buttonGroupValue, setButtonGroupValue] = useState('curl')
148148
const [dateValue, setDateValue] = useState('')
149+
const [dateTimeValue, setDateTimeValue] = useState('')
150+
const [dateTimePreset, setDateTimePreset] = useState('2025-01-30T14:30:00')
149151
const [dateRangeStart, setDateRangeStart] = useState('')
150152
const [dateRangeEnd, setDateRangeEnd] = useState('')
151153
const [tagItems, setTagItems] = useState<TagItem[]>([
@@ -708,6 +710,30 @@ export default function PlaygroundPage() {
708710
</div>
709711
<span className='text-[var(--text-secondary)] text-sm'>{dateValue || 'No date'}</span>
710712
</VariantRow>
713+
<VariantRow label='with time (empty)'>
714+
<div className='w-72'>
715+
<DatePicker
716+
value={dateTimeValue}
717+
onChange={setDateTimeValue}
718+
placeholder='Select date and time'
719+
showTime
720+
/>
721+
</div>
722+
<span className='text-[var(--text-secondary)] text-sm'>
723+
{dateTimeValue || 'No value'}
724+
</span>
725+
</VariantRow>
726+
<VariantRow label='with time (preset)'>
727+
<div className='w-72'>
728+
<DatePicker
729+
value={dateTimePreset}
730+
onChange={setDateTimePreset}
731+
placeholder='Select date and time'
732+
showTime
733+
/>
734+
</div>
735+
<span className='text-[var(--text-secondary)] text-sm'>{dateTimePreset}</span>
736+
</VariantRow>
711737
<VariantRow label='size sm'>
712738
<div className='w-56'>
713739
<DatePicker placeholder='Small size' size='sm' onChange={() => {}} />

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ interface ChunkContextMenuProps {
2525
* Empty space action (shown when right-clicking on empty space)
2626
*/
2727
onAddChunk?: () => void
28+
/**
29+
* View document tags (shown when right-clicking on empty space)
30+
*/
31+
onViewTags?: () => void
2832
/**
2933
* Whether the chunk is currently enabled
3034
*/
@@ -75,6 +79,7 @@ export function ChunkContextMenu({
7579
onToggleEnabled,
7680
onDelete,
7781
onAddChunk,
82+
onViewTags,
7883
isChunkEnabled = true,
7984
hasChunk,
8085
disableToggleEnabled = false,
@@ -181,17 +186,29 @@ export function ChunkContextMenu({
181186
)}
182187
</>
183188
) : (
184-
onAddChunk && (
185-
<PopoverItem
186-
disabled={disableAddChunk}
187-
onClick={() => {
188-
onAddChunk()
189-
onClose()
190-
}}
191-
>
192-
Create chunk
193-
</PopoverItem>
194-
)
189+
<>
190+
{onAddChunk && (
191+
<PopoverItem
192+
disabled={disableAddChunk}
193+
onClick={() => {
194+
onAddChunk()
195+
onClose()
196+
}}
197+
>
198+
Create chunk
199+
</PopoverItem>
200+
)}
201+
{onViewTags && (
202+
<PopoverItem
203+
onClick={() => {
204+
onViewTags()
205+
onClose()
206+
}}
207+
>
208+
View tags
209+
</PopoverItem>
210+
)}
211+
</>
195212
)}
196213
</PopoverContent>
197214
</Popover>

0 commit comments

Comments
 (0)