From 0d588e05c174ead2329b82d541edc512d0a45a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=8A=E3=83=A0?= Date: Sat, 11 Jul 2026 12:25:23 +0800 Subject: [PATCH] fix(discord): qualify group titles across surfaces (#1422) * fix(discord): qualify group titles across surfaces * feat(ui): add searchable delivery target selectors --------- Co-authored-by: Collective Developer --- ui/web/src/components/ui/combobox.tsx | 14 +++- ui/web/src/i18n/locales/en/channels.json | 1 + ui/web/src/i18n/locales/vi/channels.json | 1 + ui/web/src/i18n/locales/zh/channels.json | 1 + .../heartbeat-delivery-section.tsx | 25 ++++--- .../channel-detail/passive-memory-section.tsx | 68 +++++++------------ .../cron/cron-detail/cron-advanced-dialog.tsx | 27 +++----- .../cron-detail/cron-delivery-section.tsx | 26 ++++--- 8 files changed, 75 insertions(+), 88 deletions(-) diff --git a/ui/web/src/components/ui/combobox.tsx b/ui/web/src/components/ui/combobox.tsx index 70227d33..2b935a35 100644 --- a/ui/web/src/components/ui/combobox.tsx +++ b/ui/web/src/components/ui/combobox.tsx @@ -193,7 +193,18 @@ export function Combobox({ inputDirtyRef.current = true; setInputDirty(true); } - if (!open && options.length > 0) setOpen(true); + if (!open && (options.length > 0 || allowCustom)) setOpen(true); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + setOpen(false); + return; + } + if (e.key === "Enter" && allowCustom && isCustomValue) { + e.preventDefault(); + handleSelect(search.trim()); + } }; const handleFocus = (e: React.FocusEvent) => { @@ -248,6 +259,7 @@ export function Combobox({ ref={inputRef} value={search} onChange={handleInputChange} + onKeyDown={handleKeyDown} onFocus={handleFocus} disabled={disabled} placeholder={placeholder} diff --git a/ui/web/src/i18n/locales/en/channels.json b/ui/web/src/i18n/locales/en/channels.json index 92ad863f..e8c93f3a 100644 --- a/ui/web/src/i18n/locales/en/channels.json +++ b/ui/web/src/i18n/locales/en/channels.json @@ -224,6 +224,7 @@ "excludeGroups": "Excluded Discord channels", "excludeGroupsPlaceholder": "Select a channel to exclude", "manualExcludePlaceholder": "Paste Discord channel ID", + "useDiscordChannelId": "Use Discord channel ID:", "addExcludedGroup": "Add", "noExcludeGroups": "No Discord channel history found yet.", "noExcludedGroups": "No channels excluded.", diff --git a/ui/web/src/i18n/locales/vi/channels.json b/ui/web/src/i18n/locales/vi/channels.json index fad0f4fa..cf00cdec 100644 --- a/ui/web/src/i18n/locales/vi/channels.json +++ b/ui/web/src/i18n/locales/vi/channels.json @@ -223,6 +223,7 @@ "excludeGroups": "Channel Discord loại trừ", "excludeGroupsPlaceholder": "Chọn channel cần loại trừ", "manualExcludePlaceholder": "Dán Discord channel ID", + "useDiscordChannelId": "Dùng Discord channel ID:", "addExcludedGroup": "Thêm", "noExcludeGroups": "Chưa có lịch sử channel Discord.", "noExcludedGroups": "Chưa loại trừ channel nào.", diff --git a/ui/web/src/i18n/locales/zh/channels.json b/ui/web/src/i18n/locales/zh/channels.json index 83e829d5..5d6efece 100644 --- a/ui/web/src/i18n/locales/zh/channels.json +++ b/ui/web/src/i18n/locales/zh/channels.json @@ -223,6 +223,7 @@ "excludeGroups": "排除的 Discord 频道", "excludeGroupsPlaceholder": "选择要排除的频道", "manualExcludePlaceholder": "粘贴 Discord 频道 ID", + "useDiscordChannelId": "使用 Discord 频道 ID:", "addExcludedGroup": "添加", "noExcludeGroups": "尚未发现 Discord 频道历史。", "noExcludedGroups": "尚未排除任何频道。", diff --git a/ui/web/src/pages/agents/agent-detail/heartbeat-delivery-section.tsx b/ui/web/src/pages/agents/agent-detail/heartbeat-delivery-section.tsx index 2de5225d..2a037fd8 100644 --- a/ui/web/src/pages/agents/agent-detail/heartbeat-delivery-section.tsx +++ b/ui/web/src/pages/agents/agent-detail/heartbeat-delivery-section.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { Send } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Combobox } from "@/components/ui/combobox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; @@ -69,19 +70,17 @@ export function HeartbeatDeliverySection({ const filtered = targets.filter((tgt) => tgt.channel === channel); if (filtered.length > 0) { return ( - + ({ + value: target.chatId, + label: target.title ? `${target.title} (${target.chatId})` : target.chatId, + }))} + placeholder={t("heartbeat.chatIdPlaceholder")} + allowCustom + className="w-full text-base md:text-sm" + /> ); } return ( diff --git a/ui/web/src/pages/channels/channel-detail/passive-memory-section.tsx b/ui/web/src/pages/channels/channel-detail/passive-memory-section.tsx index 52a3672b..36998cd9 100644 --- a/ui/web/src/pages/channels/channel-detail/passive-memory-section.tsx +++ b/ui/web/src/pages/channels/channel-detail/passive-memory-section.tsx @@ -4,13 +4,7 @@ import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Pagination } from "@/components/shared/pagination"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Combobox } from "@/components/ui/combobox"; import type { ChannelMemoryConfig } from "@/types/channel"; import { useChannelMemoryExtraction } from "../hooks/use-channel-memory-extraction"; import { @@ -99,13 +93,7 @@ export function PassiveMemorySection({ instanceId, channelType }: PassiveMemoryS }, [status?.config]); useEffect(() => { - if (promptGroupOptions.length === 0) { - setSelectedPromptHistoryKey(""); - return; - } - if (selectedPromptHistoryKey && promptGroupOptions.some((group) => group.history_key === selectedPromptHistoryKey)) { - return; - } + if (selectedPromptHistoryKey) return; setSelectedPromptHistoryKey(promptGroupOptions[0]?.history_key ?? ""); }, [promptGroupOptions, selectedPromptHistoryKey]); @@ -298,22 +286,18 @@ export function PassiveMemorySection({ instanceId, channelType }: PassiveMemoryS
{t("detail.passiveMemory.groupCustomPrompt")}
- + onChange={() => undefined} + onSelect={setSelectedPromptHistoryKey} + options={promptGroupOptions.map((group) => ({ + value: group.history_key, + label: formatPromptGroupLabel(group), + }))} + placeholder={t("detail.passiveMemory.groupCustomPromptPlaceholder")} + allowCustom + customLabel={t("detail.passiveMemory.useDiscordChannelId")} + /> {selectedPromptHistoryKey ? ( {t("detail.passiveMemory.excludeGroups")} - + onChange={() => undefined} + onSelect={addExcludedHistoryKey} + options={availableExcludeGroupOptions.map((group) => ({ + value: group.history_key, + label: formatPromptGroupLabel(group), + }))} + placeholder={t("detail.passiveMemory.excludeGroupsPlaceholder")} + allowCustom + customLabel={t("detail.passiveMemory.useDiscordChannelId")} + /> {excludedGroupOptions.length > 0 ? (
{excludedGroupOptions.map((group) => { diff --git a/ui/web/src/pages/cron/cron-detail/cron-advanced-dialog.tsx b/ui/web/src/pages/cron/cron-detail/cron-advanced-dialog.tsx index 43b525b3..748ec615 100644 --- a/ui/web/src/pages/cron/cron-detail/cron-advanced-dialog.tsx +++ b/ui/web/src/pages/cron/cron-detail/cron-advanced-dialog.tsx @@ -207,22 +207,17 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd const filtered = targets.filter((tgt) => tgt.channel === channel); if (filtered.length > 0) { return ( - + setValue("to", value)} + options={filtered.map((target) => ({ + value: target.chatId, + label: target.title ? `${target.title} (${target.chatId})` : target.chatId, + }))} + placeholder={t("detail.toPlaceholder")} + allowCustom + className="text-base md:text-sm" + /> ); } return ( diff --git a/ui/web/src/pages/cron/cron-detail/cron-delivery-section.tsx b/ui/web/src/pages/cron/cron-detail/cron-delivery-section.tsx index 39192b95..75c6cf91 100644 --- a/ui/web/src/pages/cron/cron-detail/cron-delivery-section.tsx +++ b/ui/web/src/pages/cron/cron-detail/cron-delivery-section.tsx @@ -4,6 +4,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Combobox } from "@/components/ui/combobox"; export interface DeliveryTarget { channel: string; @@ -85,20 +86,17 @@ export function CronDeliverySection({ const filtered = targets.filter((tgt) => tgt.channel === channel); if (filtered.length > 0) { return ( - + ({ + value: target.chatId, + label: target.title ? `${target.title} (${target.chatId})` : target.chatId, + }))} + placeholder={t("detail.toPlaceholder")} + allowCustom + className="text-base md:text-sm" + /> ); } return setTo(e.target.value)}