Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions frontend/src/components/agent/FilterBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import { X as LucideX } from 'lucide-react'

interface FilterBadgeProps {
filters: string[]
onRemoveFilter: (index: number) => void
}

export const FilterBadge: React.FC<FilterBadgeProps> = ({
filters,
onRemoveFilter,
}) => {
if (!filters || filters.length === 0) {
return (
<input
type="text"
placeholder='Add filters'
className="flex-1 bg-transparent border-0 outline-none text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 min-w-[100px]"
readOnly
/>
)
}

return (
<>
{filters.map((part, idx) => (
<div
key={idx}
className="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 px-2.5 py-1 rounded-md text-sm"
>
<span>{part}</span>
<button
onClick={(e) => {
e.stopPropagation()
onRemoveFilter(idx)
}}
className="hover:bg-gray-200 dark:hover:bg-gray-600 rounded-sm p-0.5"
>
<LucideX className="h-3 w-3" />
</button>
</div>
))}
</>
)
}
174 changes: 174 additions & 0 deletions frontend/src/components/agent/GmailPeopleFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useState, useEffect } from 'react'
import { Plus, X as LucideX } from 'lucide-react'

interface GmailPeopleFields {
from: string[]
to: string[]
cc: string[]
bcc: string[]
}

interface GmailPeopleFilterProps {
filterValue?: string
onFilterChange: (value: string) => void
}

export const GmailPeopleFilter: React.FC<GmailPeopleFilterProps> = ({
filterValue,
onFilterChange,
}) => {
const [peopleFields, setPeopleFields] = useState<GmailPeopleFields>({
from: [],
to: [],
cc: [],
bcc: [],
})

const [peopleInputs, setPeopleInputs] = useState<{
from: string
to: string
cc: string
bcc: string
}>({
from: '',
to: '',
cc: '',
bcc: '',
})

// Parse existing filter values on mount or when filterValue changes
useEffect(() => {
if (!filterValue) return

const filters = filterValue.split(', ').filter(f => f.trim())
const newFields: GmailPeopleFields = {
from: [],
to: [],
cc: [],
bcc: [],
}

filters.forEach(filter => {
if (filter.startsWith('from:')) {
newFields.from.push(filter.substring(5))
} else if (filter.startsWith('to:')) {
newFields.to.push(filter.substring(3))
} else if (filter.startsWith('cc:')) {
newFields.cc.push(filter.substring(3))
} else if (filter.startsWith('bcc:')) {
newFields.bcc.push(filter.substring(4))
}
})

setPeopleFields(newFields)
}, [filterValue])

const buildFilterString = (fields: GmailPeopleFields) => {
const filterParts: string[] = []
if (fields.from.length > 0) filterParts.push(...fields.from.map(e => `from:${e}`))
if (fields.to.length > 0) filterParts.push(...fields.to.map(e => `to:${e}`))
if (fields.cc.length > 0) filterParts.push(...fields.cc.map(e => `cc:${e}`))
if (fields.bcc.length > 0) filterParts.push(...fields.bcc.map(e => `bcc:${e}`))

// Preserve existing timeline filters from the current filterValue
const currentFilters = filterValue?.split(', ').filter(f => f.trim()) || []
const existingTimelineFilters = currentFilters.filter(f => f.startsWith('~'))
const combinedFilters = [...filterParts, ...existingTimelineFilters]

return combinedFilters.join(', ')
}

const addEmail = (field: keyof GmailPeopleFields) => {
const email = peopleInputs[field].trim()
if (!email) return

const newFields = {
...peopleFields,
[field]: [...peopleFields[field], email],
}
setPeopleFields(newFields)
setPeopleInputs(prev => ({
...prev,
[field]: '',
}))

onFilterChange(buildFilterString(newFields))
}

const removeEmail = (field: keyof GmailPeopleFields, idx: number) => {
const newEmails = peopleFields[field].filter((_, i) => i !== idx)
const newFields = {
...peopleFields,
[field]: newEmails,
}
setPeopleFields(newFields)
onFilterChange(buildFilterString(newFields))
}

const handleKeyDown = (field: keyof GmailPeopleFields, e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation()
if (e.key === 'Enter' && peopleInputs[field].trim()) {
addEmail(field)
}
}

return (
<div className="px-4 py-3 space-y-3">
{(['from', 'to', 'cc', 'bcc'] as const).map((field) => (
<div key={field} className="space-y-2">
<label className="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">
{field}
</label>
<div className="flex items-center gap-2">
<input
type="text"
placeholder={`Enter ${field} address`}
value={peopleInputs[field]}
onChange={(e) => {
setPeopleInputs(prev => ({
...prev,
[field]: e.target.value,
}))
}}
onKeyDown={(e) => handleKeyDown(field, e)}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200"
/>
<button
onClick={(e) => {
e.stopPropagation()
addEmail(field)
}}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Display added emails as pills */}
{peopleFields[field].length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{peopleFields[field].map((email, idx) => (
<div
key={idx}
className="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 px-2.5 py-1 rounded-md text-xs"
>
<span>{email}</span>
<button
onClick={(e) => {
e.stopPropagation()
removeEmail(field, idx)
}}
className="hover:bg-gray-200 dark:hover:bg-gray-600 rounded-sm p-0.5"
>
<LucideX className="h-3 w-3" />
</button>
</div>
))}
</div>
)}
</div>
))}
</div>
)
}
123 changes: 123 additions & 0 deletions frontend/src/components/agent/SlackChannelFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useState, useEffect } from 'react'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { useSlackData } from '@/hooks/useSlackData'
import { SlackEntity } from 'shared/types'

interface SlackChannelFilterProps {
filterValue?: string
onFilterChange: (value: string) => void
}

export const SlackChannelFilter: React.FC<SlackChannelFilterProps> = ({
filterValue,
onFilterChange,
}) => {
const [selectedChannels, setSelectedChannels] = useState<Set<string>>(new Set())

const {
items: slackChannels,
searchQuery,
isLoading,
containerRef,
handleSearch,
handleScroll,
fetchItems,
} = useSlackData({ entity: SlackEntity.Channel })

// Load initial channels on mount
useEffect(() => {
fetchItems('', 0, false)
}, [fetchItems])

// Parse existing filter values to set selected channels
useEffect(() => {
if (!filterValue) return

const filters = filterValue.split(', ').filter(f => f.trim())
const channelIds = filters.filter(f => f.startsWith('#')).map(f => f.substring(1))

setSelectedChannels(new Set(channelIds))
}, [filterValue])

const handleChannelSelect = (channel: { id: string; name: string }) => {
const updatedChannels = new Set(selectedChannels)
if (updatedChannels.has(channel.id)) {
updatedChannels.delete(channel.id)
} else {
updatedChannels.add(channel.id)
}
setSelectedChannels(updatedChannels)

// Build filter string from selected channels
const selectedChannelIds = Array.from(updatedChannels).map(id => `#${id}`)

// Preserve existing filters from current filterValue that aren't channel filters
const currentFilters = filterValue?.split(', ').filter(f => f.trim()) || []
const existingNonChannelFilters = currentFilters.filter(f => !f.startsWith('#'))

// Combine new channel filters with existing non-channel filters
const combinedFilters = [...selectedChannelIds, ...existingNonChannelFilters]

onFilterChange(combinedFilters.join(', '))
}

return (
<>
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center">
<input
type="text"
placeholder="Search"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200"
/>
</div>
</div>
<div
ref={containerRef}
onScroll={handleScroll}
className="px-2 max-h-60 overflow-y-auto"
>
{isLoading && slackChannels.length === 0 ? (
<div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
Loading channels...
</div>
) : slackChannels.length === 0 ? (
<div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
No channels found
</div>
) : (
<>
{slackChannels.map((channel: { id: string; name: string }) => (
<DropdownMenuItem
key={channel.id}
onSelect={(e) => {
e.preventDefault()
handleChannelSelect(channel)
}}
className="flex items-center cursor-pointer text-sm py-2 px-2 hover:!bg-gray-100 dark:hover:!bg-gray-700 focus:!bg-gray-100 dark:focus:!bg-gray-700 data-[highlighted]:!bg-gray-100 dark:data-[highlighted]:!bg-gray-700 rounded"
>
<input
type="checkbox"
className="mr-3"
checked={selectedChannels.has(channel.id)}
onChange={() => {}}
/>
<span className="text-gray-700 dark:text-gray-200">{channel.name}</span>
</DropdownMenuItem>
))}
{isLoading && (
<div className="text-center py-2 text-sm text-gray-500 dark:text-gray-400">
Loading more...
</div>
)}
</>
)}
</div>
</>
)
}
Loading
Loading