diff --git a/internal/api/messages_handler.go b/internal/api/messages_handler.go index 4418311..2b559f4 100644 --- a/internal/api/messages_handler.go +++ b/internal/api/messages_handler.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/http" "strconv" + "strings" "time" "github.com/go-chi/chi/v5" @@ -399,9 +400,60 @@ func (h *MessagesHandler) SearchMessages(w http.ResponseWriter, r *http.Request) limit = 20 } + // Parse advanced filter parameters + channelFilter := r.URL.Query().Get("channel") + agentFilter := r.URL.Query().Get("agent") + afterFilter := r.URL.Query().Get("after") + beforeFilter := r.URL.Query().Get("before") + var allMessages []*messaging.Message for _, agent := range ownedAgents { - opts := messaging.SearchOptions{Limit: limit} + opts := messaging.SearchOptions{ + Limit: limit, + After: afterFilter, + Before: beforeFilter, + } + + // Apply channel filters (comma-separated, prefix with - to exclude) + if channelFilter != "" { + channels := strings.Split(channelFilter, ",") + var includeChannels []string + var excludeChannels []string + for _, ch := range channels { + ch = strings.TrimSpace(ch) + if ch == "" { + continue + } + if strings.HasPrefix(ch, "-") { + excludeChannels = append(excludeChannels, strings.TrimPrefix(ch, "-")) + } else { + includeChannels = append(includeChannels, ch) + } + } + opts.Channels = includeChannels + opts.ExcludeChannels = excludeChannels + } + + // Apply agent filters (comma-separated, prefix with - to exclude) + if agentFilter != "" { + agentNames := strings.Split(agentFilter, ",") + var includeAgents []string + var excludeAgents []string + for _, a := range agentNames { + a = strings.TrimSpace(a) + if a == "" { + continue + } + if strings.HasPrefix(a, "-") { + excludeAgents = append(excludeAgents, strings.TrimPrefix(a, "-")) + } else { + includeAgents = append(includeAgents, a) + } + } + opts.Agents = includeAgents + opts.ExcludeAgents = excludeAgents + } + result, err := h.msgService.SearchMessages(r.Context(), agent.Name, query, opts) if err != nil { continue diff --git a/internal/messaging/options.go b/internal/messaging/options.go index ebfc626..2ec494b 100644 --- a/internal/messaging/options.go +++ b/internal/messaging/options.go @@ -25,14 +25,18 @@ type ReadOptions struct { // SearchOptions configures message search behavior. type SearchOptions struct { - FromAgent string `json:"from_agent,omitempty"` - ToAgent string `json:"to_agent,omitempty"` - ChannelID *int64 `json:"channel_id,omitempty"` - MinPriority int `json:"min_priority,omitempty"` - Status string `json:"status,omitempty"` - Limit int `json:"limit,omitempty"` - Offset int `json:"offset,omitempty"` - After string `json:"after,omitempty"` - Before string `json:"before,omitempty"` - Channel string `json:"channel,omitempty"` + FromAgent string `json:"from_agent,omitempty"` + ToAgent string `json:"to_agent,omitempty"` + ChannelID *int64 `json:"channel_id,omitempty"` + MinPriority int `json:"min_priority,omitempty"` + Status string `json:"status,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` + After string `json:"after,omitempty"` + Before string `json:"before,omitempty"` + Channel string `json:"channel,omitempty"` + Channels []string `json:"channels,omitempty"` // include messages in these channels + ExcludeChannels []string `json:"exclude_channels,omitempty"` // exclude messages in these channels + Agents []string `json:"agents,omitempty"` // include messages from/to these agents + ExcludeAgents []string `json:"exclude_agents,omitempty"` // exclude messages from/to these agents } diff --git a/internal/messaging/store.go b/internal/messaging/store.go index b575a1b..3bdb0ed 100644 --- a/internal/messaging/store.go +++ b/internal/messaging/store.go @@ -492,6 +492,57 @@ func (s *SQLiteMessageStore) buildSearchConditions(agentName, query string, opts args = append(args, opts.Channel) } + if len(opts.Channels) > 0 { + placeholders := make([]string, len(opts.Channels)) + for i, ch := range opts.Channels { + placeholders[i] = "?" + args = append(args, ch) + } + conditions = append(conditions, fmt.Sprintf("m.channel_id IN (SELECT id FROM channels WHERE LOWER(name) IN (%s))", strings.Join(placeholders, ","))) + } + + if len(opts.ExcludeChannels) > 0 { + placeholders := make([]string, len(opts.ExcludeChannels)) + for i, ch := range opts.ExcludeChannels { + placeholders[i] = "?" + args = append(args, ch) + } + conditions = append(conditions, fmt.Sprintf("(m.channel_id IS NULL OR m.channel_id NOT IN (SELECT id FROM channels WHERE LOWER(name) IN (%s)))", strings.Join(placeholders, ","))) + } + + if len(opts.Agents) > 0 { + placeholders := make([]string, len(opts.Agents)) + for i, a := range opts.Agents { + placeholders[i] = "?" + args = append(args, a) + } + inClause := strings.Join(placeholders, ",") + // Clone placeholders for the second IN clause + placeholders2 := make([]string, len(opts.Agents)) + for i, a := range opts.Agents { + placeholders2[i] = "?" + args = append(args, a) + } + inClause2 := strings.Join(placeholders2, ",") + conditions = append(conditions, fmt.Sprintf("(m.from_agent IN (%s) OR m.to_agent IN (%s))", inClause, inClause2)) + } + + if len(opts.ExcludeAgents) > 0 { + placeholders := make([]string, len(opts.ExcludeAgents)) + for i, a := range opts.ExcludeAgents { + placeholders[i] = "?" + args = append(args, a) + } + inClause := strings.Join(placeholders, ",") + placeholders2 := make([]string, len(opts.ExcludeAgents)) + for i, a := range opts.ExcludeAgents { + placeholders2[i] = "?" + args = append(args, a) + } + inClause2 := strings.Join(placeholders2, ",") + conditions = append(conditions, fmt.Sprintf("m.from_agent NOT IN (%s) AND (m.to_agent = '' OR m.to_agent NOT IN (%s))", inClause, inClause2)) + } + if opts.After != "" { if t, err := time.Parse(time.RFC3339, opts.After); err == nil { conditions = append(conditions, "m.created_at >= ?") diff --git a/internal/web/dist/index.html b/internal/web/dist/index.html index 2bccb06..6b78884 100644 --- a/internal/web/dist/index.html +++ b/internal/web/dist/index.html @@ -8,29 +8,29 @@ - - - - - - - - + + + + + + + +
-
-

Conversations

+
+ +
+
+ + + + + {#if searchQuery || hasSearched} + + {/if} + +
+
- {#if searchResults !== null} -
-
-

- Search Results ({searchResults.length}) -

-
- {#if searchResults.length === 0} -
- No messages found for '{$page.url.searchParams.get('q')}' -
- {:else} -
- {#each searchResults as msg} - -
- {msg.from_agent} - {new Date(msg.created_at).toLocaleString()} -
-

{msg.body.slice(0, 200)}

-
+ +
+ + + {#if activeFilterCount > 0} + + {/if} +
+ + + {#if showFilters} +
+ +
+ +
+ {#each [ + { value: 'any', label: 'Any time' }, + { value: '24h', label: 'Past 24h' }, + { value: 'week', label: 'Past week' }, + { value: 'month', label: 'Past month' }, + { value: '3months', label: 'Past 3 months' }, + { value: 'custom', label: 'Custom range' } + ] as opt} + {/each}
+ {#if timeRange === 'custom'} +
+
+ + +
+
+ + +
+
+ {/if} +
+ + +
+ + + {#if channelList.length > 0} +
+ {#each channelList.slice(0, 8) as ch} + + {/each} +
+ {/if} +
+ + +
+ + +
+ + + {#if hasSearched} + {/if}
{/if} -
-
-

All Conversations

-
- {#if loadingData} -
+ + {#if loading} +
+
{#each Array(5) as _}
-
-
+
+
+
+
+
+
{/each}
- {:else if convList.length === 0} -
- No conversations yet. +
+ {:else if hasSearched && searchResults !== null} + +
+

+ {searchTotal} result{searchTotal !== 1 ? 's' : ''} for "{$page.url.searchParams.get('q')}" +

+
+ + {#if searchResults.length === 0} +
+
+ + + +

No messages found

+

Try different keywords or adjust your filters

+
{:else} -
- {#each convList as conv} - -
- + {:else} + +
+
+ + + +
+

Search messages

+

+ Search across all your conversations, channels, and direct messages. + Use filters to narrow results by time, channel, or agent. +

+
+ + + +
+
+ {/if}