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 <man@collective.dev>
This commit is contained in:
ナムandCollective Developer authored and GitHub committed 2026-07-11 11:25:23 +07:00
1 parent 0c1ededc92
commit 0d588e05c1
8 files changed
+75 -88

No files matched your search

+13 -1
View File
@@ -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<HTMLInputElement>) => {
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}
+1
View File
@@ -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.",
+1
View File
@@ -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.",
+1
View File
@@ -223,6 +223,7 @@
"excludeGroups": "排除的 Discord 频道",
"excludeGroupsPlaceholder": "选择要排除的频道",
"manualExcludePlaceholder": "粘贴 Discord 频道 ID",
"useDiscordChannelId": "使用 Discord 频道 ID:",
"addExcludedGroup": "添加",
"noExcludeGroups": "尚未发现 Discord 频道历史。",
"noExcludedGroups": "尚未排除任何频道。",
@@ -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 (
<Select value={chatId || "__none__"} onValueChange={(v) => setChatId(v === "__none__" ? "" : v)}>
<SelectTrigger className="w-full text-base md:text-sm">
<SelectValue placeholder={t("heartbeat.chatIdPlaceholder")} />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="__none__">{t("heartbeat.channelNone")}</SelectItem>
{filtered.map((tgt) => (
<SelectItem key={tgt.chatId} value={tgt.chatId}>
{tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}
</SelectItem>
))}
</SelectContent>
</Select>
<Combobox
value={chatId}
onChange={setChatId}
options={filtered.map((target) => ({
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 (
@@ -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
<div className="text-xs font-medium text-muted-foreground">
{t("detail.passiveMemory.groupCustomPrompt")}
</div>
<Select
<Combobox
value={selectedPromptHistoryKey}
onValueChange={setSelectedPromptHistoryKey}
disabled={promptGroupOptions.length === 0}
>
<SelectTrigger size="sm">
<SelectValue placeholder={t("detail.passiveMemory.groupCustomPromptPlaceholder")} />
</SelectTrigger>
<SelectContent>
{promptGroupOptions.map((group) => (
<SelectItem key={group.history_key} value={group.history_key}>
{formatPromptGroupLabel(group)}
</SelectItem>
))}
</SelectContent>
</Select>
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 ? (
<TextareaBlock
label={selectedPromptGroup ? formatPromptGroupLabel(selectedPromptGroup) : selectedPromptHistoryKey}
@@ -330,22 +314,18 @@ export function PassiveMemorySection({ instanceId, channelType }: PassiveMemoryS
<div className="text-xs font-medium text-muted-foreground">
{t("detail.passiveMemory.excludeGroups")}
</div>
<Select
<Combobox
value=""
onValueChange={addExcludedHistoryKey}
disabled={availableExcludeGroupOptions.length === 0}
>
<SelectTrigger size="sm">
<SelectValue placeholder={t("detail.passiveMemory.excludeGroupsPlaceholder")} />
</SelectTrigger>
<SelectContent>
{availableExcludeGroupOptions.map((group) => (
<SelectItem key={group.history_key} value={group.history_key}>
{formatPromptGroupLabel(group)}
</SelectItem>
))}
</SelectContent>
</Select>
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 ? (
<div className="flex flex-wrap gap-2">
{excludedGroupOptions.map((group) => {
@@ -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 (
<Select
value={to || "__none__"}
onValueChange={(v) => setValue("to", v === "__none__" ? "" : v)}
>
<SelectTrigger className="text-base md:text-sm">
<SelectValue placeholder={t("detail.toPlaceholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">{t("detail.toPlaceholder")}</SelectItem>
{filtered.map((tgt) => (
<SelectItem key={tgt.chatId} value={tgt.chatId} title={tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}>
<span className="truncate">{tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Combobox
value={to}
onChange={(value) => 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 (
@@ -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 (
<Select value={to || "__none__"} onValueChange={(v) => setTo(v === "__none__" ? "" : v)}>
<SelectTrigger className="text-base md:text-sm">
<SelectValue placeholder={t("detail.toPlaceholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">{t("detail.toPlaceholder")}</SelectItem>
{filtered.map((tgt) => (
<SelectItem key={tgt.chatId} value={tgt.chatId}
title={tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}>
<span className="truncate">{tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Combobox
value={to}
onChange={setTo}
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 <Input value={to} onChange={(e) => setTo(e.target.value)}