fix(ui): add behavior setting tooltips

Add localized field-level tooltips for Agent and Channel delivery behavior settings.
This commit is contained in:
Duy /zuey/ authored and GitHub committed 2026-06-11 23:57:32 +07:00
1 parent 50c8519f01
commit f077d56ea7
10 files changed
+460 -55

No files matched your search

+11 -1
View File
@@ -1124,16 +1124,26 @@
"delivery": {
"title": "Delivery Behavior",
"description": "Agent-level override for Quick Acknowledgement and Intermediate Replies. Channel settings still win over this.",
"modeTip": "Choose whether this agent inherits Workspace behavior or uses its own delivery settings. Channel settings still have the highest priority.",
"inherit": "Inherit",
"custom": "Custom",
"quickAck": "Quick acknowledgement",
"quickAckTip": "Sends a short natural receipt before long work starts. It is generated outside the session history and is separate from Show Reasoning.",
"intermediate": "Intermediate replies",
"intermediateTip": "Sends natural progress bubbles during long or tool-heavy runs. These updates are not added to session history.",
"provider": "Provider",
"quickAckProviderTip": "Optional provider for quick acknowledgements. Leave empty to use the agent provider; choose a fast, cheap provider here.",
"intermediateProviderTip": "Optional provider for intermediate progress. Leave empty to use the agent provider; choose a fast, cheap provider here.",
"providerPlaceholder": "Use agent provider",
"model": "Model",
"quickAckModelTip": "Optional model for quick acknowledgements. Leave empty to use the agent model; custom model names are allowed.",
"intermediateModelTip": "Optional model for progress bubbles. Leave empty to use the agent model; custom model names are allowed.",
"modelPlaceholder": "Use agent model",
"quickAckDelay": "Ack delay (ms)",
"timeoutMs": "Timeout (ms)"
"quickAckDelayTip": "How long to wait before sending a quick acknowledgement. Increase this if fast answers should avoid an extra bubble.",
"timeoutMs": "Timeout (ms)",
"quickAckTimeoutTip": "Maximum time to wait for the acknowledgement model before skipping generated acknowledgement.",
"intermediateTimeoutTip": "Maximum time to wait for the progress model before skipping an intermediate update."
},
"toast": {
"created": "Agent created",
+112
View File
@@ -394,6 +394,78 @@
"help": "Restrict which tools the agent can use in this group"
},
"system_prompt": { "label": "System Prompt" },
"chat_behavior": {
"enabled": {
"label": "Human-like Delivery",
"help": "Channel-level override for delivery behavior. Channel settings win over Agent and Workspace settings."
},
"intermediate_replies": {
"enabled": {
"label": "Intermediate Replies",
"help": "Send natural progress bubbles during long or tool-heavy runs. These messages are not added to session history."
},
"mode": {
"label": "Intermediate Reply Mode",
"help": "Choose whether progress updates are generated by the delivery model or disabled for this channel."
},
"provider": {
"label": "Intermediate Provider",
"help": "Optional provider for progress bubbles. Leave empty to inherit the agent provider; pick a fast, cheap provider here."
},
"model": {
"label": "Intermediate Model",
"help": "Optional model for progress bubbles. Leave empty to inherit the agent model; custom model names are allowed."
},
"timeout_ms": {
"label": "Intermediate Timeout (ms)",
"help": "Maximum time to wait for the delivery model before skipping an intermediate update."
},
"max_tokens": {
"label": "Intermediate Max Tokens",
"help": "Token cap for each generated progress bubble. Keep this small so updates stay short."
},
"max_chars": {
"label": "Intermediate Max Chars",
"help": "Character cap after generation so progress bubbles do not become full answers."
}
},
"quick_ack": {
"enabled": {
"label": "Quick Acknowledgement",
"help": "Send a short natural acknowledgement before long work starts. This message is not added to session history."
},
"mode": {
"label": "Quick Ack Mode",
"help": "Generated modes use the configured delivery provider/model. Fixed template mode is the only mode that sends template text."
},
"provider": {
"label": "Quick Ack Provider",
"help": "Optional provider for quick acknowledgements. Leave empty to inherit the agent provider; pick a fast, cheap provider here."
},
"model": {
"label": "Quick Ack Model",
"help": "Optional model for quick acknowledgements. Leave empty to inherit the agent model; custom model names are allowed."
},
"timeout_ms": {
"label": "Quick Ack Timeout (ms)",
"help": "Maximum time to wait for the acknowledgement model before skipping generated acknowledgement."
},
"max_tokens": {
"label": "Quick Ack Max Tokens",
"help": "Token cap for the acknowledgement. Keep this small so it reads like a receipt, not an answer."
},
"max_chars": {
"label": "Quick Ack Max Chars",
"help": "Character cap after generation so acknowledgement bubbles stay compact."
}
},
"final_split": {
"enabled": {
"label": "Final Reply Splitting",
"help": "Split long final answers into multiple channel messages. This is separate from reasoning and progress bubbles."
}
}
},
"platform": { "label": "Platform", "help": "Select the platform this Pancake page serves." },
"tiktok_type": { "label": "TikTok Type", "help": "Select the TikTok account type for this page" },
"features.private_reply": {
@@ -432,6 +504,46 @@
"always_bubbles": "Always as bubbles",
"off": "Off"
},
"chat_behavior": {
"enabled": {
"inherit": "Inherit from gateway",
"true": "Enabled",
"false": "Disabled"
},
"intermediate_replies": {
"enabled": {
"inherit": "Inherit from gateway",
"true": "Enabled",
"false": "Disabled"
},
"mode": {
"inherit": "Inherit from gateway",
"sidecar_generated": "Sidecar-generated",
"off": "Off"
}
},
"quick_ack": {
"enabled": {
"inherit": "Inherit from gateway",
"true": "Enabled",
"false": "Disabled"
},
"mode": {
"inherit": "Inherit from gateway",
"sidecar_generated": "Sidecar-generated",
"llm_generated": "LLM-generated",
"fixed_template": "Fixed template",
"off": "Off"
}
},
"final_split": {
"enabled": {
"inherit": "Inherit from gateway",
"true": "Enabled",
"false": "Disabled"
}
}
},
"domain": {
"lark": "Lark (Global)",
"feishu": "Feishu (China)"
+11 -1
View File
@@ -1109,16 +1109,26 @@
"delivery": {
"title": "Hành vi gửi tin",
"description": "Ghi đè cấp Agent cho Xác nhận nhanh và Phản hồi trung gian. Cấu hình Channel vẫn có ưu tiên cao hơn.",
"modeTip": "Chọn agent này kế thừa cấu hình Workspace hay dùng cấu hình gửi tin riêng. Cấu hình Channel vẫn có ưu tiên cao nhất.",
"inherit": "Kế thừa",
"custom": "Tuỳ chỉnh",
"quickAck": "Xác nhận nhanh",
"quickAckTip": "Gửi một câu xác nhận ngắn, tự nhiên trước khi tác vụ dài bắt đầu. Tin này được tạo ngoài lịch sử phiên và tách biệt với Hiện suy luận.",
"intermediate": "Phản hồi trung gian",
"intermediateTip": "Gửi các bubble cập nhật tiến độ tự nhiên trong tác vụ dài hoặc dùng nhiều tool. Các cập nhật này không được thêm vào lịch sử phiên.",
"provider": "Provider",
"quickAckProviderTip": "Provider tuỳ chọn cho xác nhận nhanh. Để trống để dùng provider của agent; nên chọn provider nhanh và rẻ.",
"intermediateProviderTip": "Provider tuỳ chọn cho phản hồi tiến độ. Để trống để dùng provider của agent; nên chọn provider nhanh và rẻ.",
"providerPlaceholder": "Dùng provider của agent",
"model": "Model",
"quickAckModelTip": "Model tuỳ chọn cho xác nhận nhanh. Để trống để dùng model của agent; có thể nhập custom model name.",
"intermediateModelTip": "Model tuỳ chọn cho bubble tiến độ. Để trống để dùng model của agent; có thể nhập custom model name.",
"modelPlaceholder": "Dùng model của agent",
"quickAckDelay": "Độ trễ ack (ms)",
"timeoutMs": "Timeout (ms)"
"quickAckDelayTip": "Thời gian chờ trước khi gửi xác nhận nhanh. Tăng giá trị này nếu câu trả lời nhanh không cần thêm bubble.",
"timeoutMs": "Timeout (ms)",
"quickAckTimeoutTip": "Thời gian chờ tối đa cho model xác nhận trước khi bỏ qua xác nhận được generate.",
"intermediateTimeoutTip": "Thời gian chờ tối đa cho model tiến độ trước khi bỏ qua cập nhật trung gian."
},
"toast": {
"created": "Đã tạo agent",
+112
View File
@@ -324,6 +324,78 @@
"skills": { "label": "Bộ lọc skill", "help": "Giới hạn skill khả dụng cho nhóm này" },
"tools": { "label": "Danh sách công cụ được phép", "help": "Giới hạn công cụ agent có thể dùng trong nhóm này" },
"system_prompt": { "label": "Prompt hệ thống" },
"chat_behavior": {
"enabled": {
"label": "Hành vi gửi tin tự nhiên",
"help": "Ghi đè hành vi gửi tin ở cấp Channel. Cấu hình Channel ưu tiên cao hơn Agent và Workspace."
},
"intermediate_replies": {
"enabled": {
"label": "Phản hồi trung gian",
"help": "Gửi bubble cập nhật tiến độ tự nhiên trong tác vụ dài hoặc dùng nhiều tool. Tin này không được thêm vào lịch sử phiên."
},
"mode": {
"label": "Chế độ phản hồi trung gian",
"help": "Chọn cập nhật tiến độ được generate bằng model gửi tin hay tắt ở channel này."
},
"provider": {
"label": "Intermediate Provider",
"help": "Provider tuỳ chọn cho bubble tiến độ. Để trống để kế thừa provider của agent; nên chọn provider nhanh và rẻ."
},
"model": {
"label": "Intermediate Model",
"help": "Model tuỳ chọn cho bubble tiến độ. Để trống để kế thừa model của agent; có thể nhập custom model name."
},
"timeout_ms": {
"label": "Intermediate Timeout (ms)",
"help": "Thời gian chờ tối đa cho model gửi tin trước khi bỏ qua cập nhật trung gian."
},
"max_tokens": {
"label": "Intermediate Max Tokens",
"help": "Giới hạn token cho mỗi bubble tiến độ. Nên để nhỏ để cập nhật ngắn gọn."
},
"max_chars": {
"label": "Intermediate Max Chars",
"help": "Giới hạn ký tự sau khi generate để bubble tiến độ không biến thành câu trả lời đầy đủ."
}
},
"quick_ack": {
"enabled": {
"label": "Xác nhận nhanh",
"help": "Gửi một câu xác nhận ngắn, tự nhiên trước khi tác vụ dài bắt đầu. Tin này không được thêm vào lịch sử phiên."
},
"mode": {
"label": "Chế độ xác nhận nhanh",
"help": "Các chế độ generate dùng provider/model gửi tin đã cấu hình. Chỉ chế độ fixed template mới gửi text cố định."
},
"provider": {
"label": "Quick Ack Provider",
"help": "Provider tuỳ chọn cho xác nhận nhanh. Để trống để kế thừa provider của agent; nên chọn provider nhanh và rẻ."
},
"model": {
"label": "Quick Ack Model",
"help": "Model tuỳ chọn cho xác nhận nhanh. Để trống để kế thừa model của agent; có thể nhập custom model name."
},
"timeout_ms": {
"label": "Quick Ack Timeout (ms)",
"help": "Thời gian chờ tối đa cho model xác nhận trước khi bỏ qua xác nhận được generate."
},
"max_tokens": {
"label": "Quick Ack Max Tokens",
"help": "Giới hạn token cho câu xác nhận. Nên để nhỏ để nó giống xác nhận đã nhận việc, không phải câu trả lời."
},
"max_chars": {
"label": "Quick Ack Max Chars",
"help": "Giới hạn ký tự sau khi generate để bubble xác nhận luôn ngắn gọn."
}
},
"final_split": {
"enabled": {
"label": "Tách câu trả lời cuối",
"help": "Tách câu trả lời dài thành nhiều tin nhắn channel. Tính năng này tách biệt với suy luận và bubble tiến độ."
}
}
},
"platform": { "label": "Nền tảng", "help": "Chọn nền tảng mà trang Pancake này phục vụ." },
"tiktok_type": { "label": "Loại tài khoản TikTok", "help": "Chọn loại tài khoản TikTok cho trang này" },
"features.private_reply": {
@@ -362,6 +434,46 @@
"always_bubbles": "Luôn thành bubble",
"off": "Tắt"
},
"chat_behavior": {
"enabled": {
"inherit": "Kế thừa từ gateway",
"true": "Bật",
"false": "Tắt"
},
"intermediate_replies": {
"enabled": {
"inherit": "Kế thừa từ gateway",
"true": "Bật",
"false": "Tắt"
},
"mode": {
"inherit": "Kế thừa từ gateway",
"sidecar_generated": "Sidecar generate",
"off": "Tắt"
}
},
"quick_ack": {
"enabled": {
"inherit": "Kế thừa từ gateway",
"true": "Bật",
"false": "Tắt"
},
"mode": {
"inherit": "Kế thừa từ gateway",
"sidecar_generated": "Sidecar generate",
"llm_generated": "LLM generate",
"fixed_template": "Template cố định",
"off": "Tắt"
}
},
"final_split": {
"enabled": {
"inherit": "Kế thừa từ gateway",
"true": "Bật",
"false": "Tắt"
}
}
},
"domain": {
"lark": "Lark (Quốc tế)",
"feishu": "Feishu (Trung Quốc)"
+11 -1
View File
@@ -1109,16 +1109,26 @@
"delivery": {
"title": "投递行为",
"description": "Agent 级 Quick Acknowledgement 和 Intermediate Replies 覆盖。Channel 设置仍然优先。",
"modeTip": "选择此 Agent 继承 Workspace 行为,还是使用自己的投递设置。Channel 设置仍然具有最高优先级。",
"inherit": "继承",
"custom": "自定义",
"quickAck": "快速确认",
"quickAckTip": "在长任务开始前发送一条自然的简短确认。此消息不会写入会话历史,并且独立于显示推理。",
"intermediate": "中间回复",
"intermediateTip": "在长任务或大量工具调用期间发送自然的进度气泡。这些更新不会写入会话历史。",
"provider": "Provider",
"quickAckProviderTip": "快速确认使用的可选 Provider。留空则使用 Agent Provider;适合选择快速、低成本的 Provider。",
"intermediateProviderTip": "中间进度使用的可选 Provider。留空则使用 Agent Provider;适合选择快速、低成本的 Provider。",
"providerPlaceholder": "使用 agent provider",
"model": "Model",
"quickAckModelTip": "快速确认使用的可选模型。留空则使用 Agent 模型;允许输入自定义模型名。",
"intermediateModelTip": "进度气泡使用的可选模型。留空则使用 Agent 模型;允许输入自定义模型名。",
"modelPlaceholder": "使用 agent model",
"quickAckDelay": "确认延迟(毫秒)",
"timeoutMs": "超时(毫秒)"
"quickAckDelayTip": "发送快速确认前等待的时间。如果快速回答不需要额外气泡,可以调高此值。",
"timeoutMs": "超时(毫秒)",
"quickAckTimeoutTip": "等待确认模型的最长时间,超时后跳过生成的确认消息。",
"intermediateTimeoutTip": "等待进度模型的最长时间,超时后跳过中间更新。"
},
"toast": {
"created": "代理已创建",
+112
View File
@@ -324,6 +324,78 @@
"skills": { "label": "Skill过滤", "help": "限制此群组可用的Skill" },
"tools": { "label": "工具白名单", "help": "限制Agent在此群组中可使用的工具" },
"system_prompt": { "label": "系统提示词" },
"chat_behavior": {
"enabled": {
"label": "自然投递行为",
"help": "Channel 级投递行为覆盖。Channel 设置优先于 Agent 和 Workspace 设置。"
},
"intermediate_replies": {
"enabled": {
"label": "中间回复",
"help": "在长任务或大量工具调用期间发送自然的进度气泡。此消息不会写入会话历史。"
},
"mode": {
"label": "中间回复模式",
"help": "选择由投递模型生成进度更新,或在此 Channel 中禁用。"
},
"provider": {
"label": "Intermediate Provider",
"help": "进度气泡使用的可选 Provider。留空则继承 Agent Provider;适合选择快速、低成本的 Provider。"
},
"model": {
"label": "Intermediate Model",
"help": "进度气泡使用的可选模型。留空则继承 Agent 模型;允许输入自定义模型名。"
},
"timeout_ms": {
"label": "Intermediate Timeout (ms)",
"help": "等待投递模型的最长时间,超时后跳过中间更新。"
},
"max_tokens": {
"label": "Intermediate Max Tokens",
"help": "每条进度气泡的 token 上限。保持较小可让更新更简短。"
},
"max_chars": {
"label": "Intermediate Max Chars",
"help": "生成后的字符上限,防止进度气泡变成完整回答。"
}
},
"quick_ack": {
"enabled": {
"label": "快速确认",
"help": "在长任务开始前发送一条自然的简短确认。此消息不会写入会话历史。"
},
"mode": {
"label": "快速确认模式",
"help": "生成模式使用配置的投递 Provider/模型。只有固定模板模式会发送模板文本。"
},
"provider": {
"label": "Quick Ack Provider",
"help": "快速确认使用的可选 Provider。留空则继承 Agent Provider;适合选择快速、低成本的 Provider。"
},
"model": {
"label": "Quick Ack Model",
"help": "快速确认使用的可选模型。留空则继承 Agent 模型;允许输入自定义模型名。"
},
"timeout_ms": {
"label": "Quick Ack Timeout (ms)",
"help": "等待确认模型的最长时间,超时后跳过生成的确认消息。"
},
"max_tokens": {
"label": "Quick Ack Max Tokens",
"help": "确认消息的 token 上限。保持较小可让它像确认收到,而不是完整回答。"
},
"max_chars": {
"label": "Quick Ack Max Chars",
"help": "生成后的字符上限,确保确认气泡保持简短。"
}
},
"final_split": {
"enabled": {
"label": "最终回复拆分",
"help": "将较长的最终回答拆分为多条 Channel 消息。此功能独立于推理和进度气泡。"
}
}
},
"platform": { "label": "平台", "help": "选择此 Pancake 页面所服务的平台。" },
"tiktok_type": { "label": "TikTok 类型", "help": "选择此页面的 TikTok 账户类型" },
"features.private_reply": {
@@ -362,6 +434,46 @@
"always_bubbles": "始终作为气泡",
"off": "关闭"
},
"chat_behavior": {
"enabled": {
"inherit": "继承自 gateway",
"true": "启用",
"false": "禁用"
},
"intermediate_replies": {
"enabled": {
"inherit": "继承自 gateway",
"true": "启用",
"false": "禁用"
},
"mode": {
"inherit": "继承自 gateway",
"sidecar_generated": "Sidecar 生成",
"off": "关闭"
}
},
"quick_ack": {
"enabled": {
"inherit": "继承自 gateway",
"true": "启用",
"false": "禁用"
},
"mode": {
"inherit": "继承自 gateway",
"sidecar_generated": "Sidecar 生成",
"llm_generated": "LLM 生成",
"fixed_template": "固定模板",
"off": "关闭"
}
},
"final_split": {
"enabled": {
"inherit": "继承自 gateway",
"true": "启用",
"false": "禁用"
}
}
},
"domain": {
"lark": "Lark(国际版)",
"feishu": "飞书(中国版)"
@@ -3,9 +3,9 @@ import { useTranslation } from "react-i18next";
import { Save, Settings, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { InfoLabel } from "@/components/shared/info-label";
import { ProviderModelSelect } from "@/components/shared/provider-model-select";
import {
Dialog, DialogContent, DialogHeader, DialogTitle,
@@ -312,7 +312,9 @@ function DeliveryBehaviorSection({
<div className="space-y-3 rounded-md border p-4">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-sm font-medium">{t("delivery.title")}</h3>
<InfoLabel tip={t("delivery.modeTip")} labelClassName="text-sm font-medium">
{t("delivery.title")}
</InfoLabel>
<p className="text-xs text-muted-foreground">{t("delivery.description")}</p>
</div>
<Select value={mode} onValueChange={(next) => onModeChange(next as "inherit" | "custom")}>
@@ -328,39 +330,59 @@ function DeliveryBehaviorSection({
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<Label>{t("delivery.quickAck")}</Label>
<InfoLabel tip={t("delivery.quickAckTip")}>{t("delivery.quickAck")}</InfoLabel>
<Switch checked={value.quick_ack?.enabled ?? true} disabled={disabled} onCheckedChange={(enabled) => patchQuick({ enabled })} />
</div>
<DeliveryProviderFields
disabled={disabled}
provider={value.quick_ack?.provider ?? ""}
model={value.quick_ack?.model ?? ""}
providerTip={t("delivery.quickAckProviderTip")}
modelTip={t("delivery.quickAckModelTip")}
onProvider={(provider) => patchQuick({ provider, model: "" })}
onModel={(model) => patchQuick({ model })}
/>
<NumberField label={t("delivery.quickAckDelay")} value={value.quick_ack?.min_delay_ms ?? 1000} disabled={disabled} onChange={(min_delay_ms) => patchQuick({ min_delay_ms })} />
<NumberField label={t("delivery.timeoutMs")} value={value.quick_ack?.timeout_ms ?? 2500} disabled={disabled} onChange={(timeout_ms) => patchQuick({ timeout_ms })} />
<NumberField label={t("delivery.quickAckDelay")} tip={t("delivery.quickAckDelayTip")} value={value.quick_ack?.min_delay_ms ?? 1000} disabled={disabled} onChange={(min_delay_ms) => patchQuick({ min_delay_ms })} />
<NumberField label={t("delivery.timeoutMs")} tip={t("delivery.quickAckTimeoutTip")} value={value.quick_ack?.timeout_ms ?? 2500} disabled={disabled} onChange={(timeout_ms) => patchQuick({ timeout_ms })} />
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<Label>{t("delivery.intermediate")}</Label>
<InfoLabel tip={t("delivery.intermediateTip")}>{t("delivery.intermediate")}</InfoLabel>
<Switch checked={value.intermediate_replies?.enabled ?? false} disabled={disabled} onCheckedChange={(enabled) => patchIntermediate({ enabled })} />
</div>
<DeliveryProviderFields
disabled={disabled}
provider={value.intermediate_replies?.provider ?? ""}
model={value.intermediate_replies?.model ?? ""}
providerTip={t("delivery.intermediateProviderTip")}
modelTip={t("delivery.intermediateModelTip")}
onProvider={(provider) => patchIntermediate({ provider, model: "" })}
onModel={(model) => patchIntermediate({ model })}
/>
<NumberField label={t("delivery.timeoutMs")} value={value.intermediate_replies?.timeout_ms ?? 2500} disabled={disabled} onChange={(timeout_ms) => patchIntermediate({ timeout_ms })} />
<NumberField label={t("delivery.timeoutMs")} tip={t("delivery.intermediateTimeoutTip")} value={value.intermediate_replies?.timeout_ms ?? 2500} disabled={disabled} onChange={(timeout_ms) => patchIntermediate({ timeout_ms })} />
</div>
</div>
</div>
);
}
function DeliveryProviderFields({ disabled, provider, model, onProvider, onModel }: { disabled: boolean; provider: string; model: string; onProvider: (value: string) => void; onModel: (value: string) => void }) {
function DeliveryProviderFields({
disabled,
provider,
model,
providerTip,
modelTip,
onProvider,
onModel,
}: {
disabled: boolean;
provider: string;
model: string;
providerTip: string;
modelTip: string;
onProvider: (value: string) => void;
onModel: (value: string) => void;
}) {
const { t } = useTranslation("agents");
return (
<ProviderModelSelect
@@ -372,16 +394,18 @@ function DeliveryProviderFields({ disabled, provider, model, onProvider, onModel
allowEmpty
providerLabel={t("delivery.provider")}
modelLabel={t("delivery.model")}
providerTip={providerTip}
modelTip={modelTip}
providerPlaceholder={t("delivery.providerPlaceholder")}
modelPlaceholder={t("delivery.modelPlaceholder")}
/>
);
}
function NumberField({ label, value, disabled, onChange }: { label: string; value: number; disabled: boolean; onChange: (value: number) => void }) {
function NumberField({ label, tip, value, disabled, onChange }: { label: string; tip: string; value: number; disabled: boolean; onChange: (value: number) => void }) {
return (
<div className="grid gap-1.5">
<Label>{label}</Label>
<InfoLabel tip={tip}>{label}</InfoLabel>
<Input className="text-base md:text-sm" type="number" min={0} value={value} disabled={disabled} onChange={(e) => onChange(Number(e.target.value))} />
</div>
);
+33 -26
View File
@@ -14,6 +14,7 @@ import {
import { ToolNameSelect } from "@/components/shared/tool-name-select";
import { SkillNameSelect } from "@/components/shared/skill-name-select";
import { ProviderModelSelect } from "@/components/shared/provider-model-select";
import { InfoLabel } from "@/components/shared/info-label";
import { BitrixPortalSelect } from "./bitrix24/bitrix-portal-select";
import { deliveryModelKey, isDeliveryModelKey, isDeliveryProviderKey } from "./channel-delivery-provider-fields";
import type { FieldDef } from "./channel-schemas";
@@ -102,6 +103,14 @@ export function ChannelFields({ fields, values, onChange, idPrefix, isEdit, cont
);
}
function isChatBehaviorField(key: string) {
return key.startsWith("chat_behavior.");
}
function FieldLabel({ htmlFor, text, tip }: { htmlFor?: string; text: string; tip?: string }) {
return tip ? <InfoLabel tip={tip}>{text}</InfoLabel> : <Label htmlFor={htmlFor}>{text}</Label>;
}
function DeliveryProviderModelField({
providerField,
modelField,
@@ -170,6 +179,8 @@ function FieldRenderer({
// i18n: try "fieldConfig.<key>.label" / "fieldConfig.<key>.help", fall back to hardcoded schema string
const label = t(`fieldConfig.${field.key}.label`, { defaultValue: field.label });
const help = field.help ? t(`fieldConfig.${field.key}.help`, { defaultValue: field.help }) : "";
const tooltipHelp = isChatBehaviorField(field.key) && help ? help : undefined;
const inlineHelp = tooltipHelp ? "" : help;
const resolvedHint = disabledHint ? t(disabledHint, { defaultValue: disabledHint }) : undefined;
const labelSuffix = field.required && !isEdit ? " *" : "";
const editHint = isEdit && field.type === "password" ? ` ${t("form.credentialsHint")}` : "";
@@ -184,24 +195,20 @@ function FieldRenderer({
if (channelType === "bitrix24" && field.key === "portal" && field.type === "text") {
return (
<div className="grid gap-1.5">
<Label htmlFor={id}>
{label}{labelSuffix}
</Label>
<FieldLabel htmlFor={id} text={`${label}${labelSuffix}`} tip={tooltipHelp} />
<BitrixPortalSelect
value={(value as string) ?? ""}
onChange={onChange}
onCreateRequest={onPortalCreateRequest ?? (() => {})}
onResumeAuthorize={onPortalResumeAuthorize}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
}
return (
<div className="grid gap-1.5">
<Label htmlFor={id}>
{label}{labelSuffix}{editHint}
</Label>
<FieldLabel htmlFor={id} text={`${label}${labelSuffix}${editHint}`} tip={tooltipHelp} />
<Input
id={id}
type={field.type}
@@ -209,14 +216,14 @@ function FieldRenderer({
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
case "number":
return (
<div className="grid gap-1.5">
<Label htmlFor={id}>{label}{labelSuffix}</Label>
<FieldLabel htmlFor={id} text={`${label}${labelSuffix}`} tip={tooltipHelp} />
<Input
id={id}
type="number"
@@ -224,12 +231,12 @@ function FieldRenderer({
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
placeholder={field.defaultValue !== undefined ? String(field.defaultValue) : undefined}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
case "boolean": {
const boolHint = resolvedHint || help;
const boolHint = resolvedHint || inlineHelp;
return (
<div className={`grid gap-1${disabled ? " opacity-50" : ""}`}>
<div className="flex items-center gap-2">
@@ -239,7 +246,7 @@ function FieldRenderer({
onCheckedChange={(v) => onChange(v)}
disabled={disabled}
/>
<Label htmlFor={id}>{label}</Label>
<FieldLabel htmlFor={id} text={label} tip={tooltipHelp} />
</div>
{boolHint && <p className="text-xs text-muted-foreground ml-9">{boolHint}</p>}
</div>
@@ -249,7 +256,7 @@ function FieldRenderer({
case "select":
return (
<div className="grid gap-1.5">
<Label>{label}{labelSuffix}</Label>
<FieldLabel text={`${label}${labelSuffix}`} tip={tooltipHelp} />
<Select
value={(value as string) ?? (field.defaultValue as string) ?? ""}
onValueChange={(v) => onChange(v)}
@@ -265,7 +272,7 @@ function FieldRenderer({
))}
</SelectContent>
</Select>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
@@ -281,7 +288,7 @@ function FieldRenderer({
const selectValue = (value as string) || INHERIT;
return (
<div className="grid gap-1.5">
<Label>{label}</Label>
<FieldLabel text={label} tip={tooltipHelp} />
<Select
value={selectValue}
onValueChange={(v) => onChange(v === INHERIT ? undefined : v)}
@@ -297,7 +304,7 @@ function FieldRenderer({
))}
</SelectContent>
</Select>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
}
@@ -321,7 +328,7 @@ function FieldRenderer({
return (
<div className={`grid gap-1.5${disabled ? " opacity-50" : ""}`}>
<Label>{label}</Label>
<FieldLabel text={label} tip={tooltipHelp} />
<Select value={boolToStr(value)} onValueChange={(v) => onChange(strToBool(v))} disabled={disabled}>
<SelectTrigger>
<SelectValue />
@@ -333,7 +340,7 @@ function FieldRenderer({
</SelectContent>
</Select>
{resolvedHint && <p className="text-xs text-muted-foreground">{resolvedHint}</p>}
{!resolvedHint && help && <p className="text-xs text-muted-foreground">{help}</p>}
{!resolvedHint && inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
}
@@ -341,7 +348,7 @@ function FieldRenderer({
case "textarea":
return (
<div className="grid gap-1.5">
<Label htmlFor={id}>{label}</Label>
<FieldLabel htmlFor={id} text={label} tip={tooltipHelp} />
<Textarea
id={id}
value={(value as string) ?? ""}
@@ -349,46 +356,46 @@ function FieldRenderer({
placeholder={field.placeholder}
rows={3}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
case "tool-select":
return (
<div className="grid gap-1.5">
<Label>{label}</Label>
<FieldLabel text={label} tip={tooltipHelp} />
<ToolNameSelect
value={(value as string[]) ?? []}
onChange={(v) => onChange(v.length > 0 ? v : undefined)}
placeholder={field.placeholder}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
case "skill-select":
return (
<div className="grid gap-1.5">
<Label>{label}</Label>
<FieldLabel text={label} tip={tooltipHelp} />
<SkillNameSelect
value={(value as string[]) ?? []}
onChange={(v) => onChange(v.length > 0 ? v : undefined)}
placeholder={field.placeholder}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
case "tags":
return (
<div className="grid gap-1.5">
<Label htmlFor={id}>{label}</Label>
<FieldLabel htmlFor={id} text={label} tip={tooltipHelp} />
<MultiUserPicker
value={(value as string[]) ?? []}
onChange={(v) => onChange(v.length > 0 ? v : undefined)}
placeholder={field.placeholder ?? t("groupOverrides.fields.allowedUsersPlaceholder")}
/>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{inlineHelp && <p className="text-xs text-muted-foreground">{inlineHelp}</p>}
</div>
);
@@ -45,6 +45,14 @@ describe("telegram configSchema", () => {
expect(isDeliveryModelKey("chat_behavior.intermediate_replies.model")).toBe(true);
expect(deliveryModelKey("chat_behavior.intermediate_replies.provider")).toBe("chat_behavior.intermediate_replies.model");
});
it("documents every channel behavior field for tooltip rendering", () => {
const behaviorFields = telegramConfig.filter((field) => field.key.startsWith("chat_behavior."));
expect(behaviorFields.length).toBeGreaterThan(0);
for (const field of behaviorFields) {
expect(field.help?.trim(), `${field.key} should have tooltip help`).toBeTruthy();
}
});
});
describe("pancake configSchema", () => {
+16 -16
View File
@@ -36,22 +36,22 @@ const quickAckModeOptions = [
];
const chatBehaviorOverrideFields: FieldDef[] = [
{ key: "chat_behavior.enabled", label: "Human-like Delivery", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Override gateway acknowledgement, intermediate progress, and final reply splitting." },
{ key: "chat_behavior.intermediate_replies.enabled", label: "Intermediate Replies", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Send natural sidecar progress updates during tool phases without adding them to session history." },
{ key: "chat_behavior.intermediate_replies.mode", label: "Intermediate Reply Mode", type: "select", options: [{ value: "inherit", label: "Inherit from gateway" }, { value: "sidecar_generated", label: "Sidecar-generated" }, { value: "off", label: "Off" }], defaultValue: "inherit" },
{ key: "chat_behavior.intermediate_replies.provider", label: "Intermediate Provider", type: "text", placeholder: "Use agent provider", help: "Optional cheap provider for sidecar progress updates." },
{ key: "chat_behavior.intermediate_replies.model", label: "Intermediate Model", type: "text", placeholder: "Use agent model" },
{ key: "chat_behavior.intermediate_replies.timeout_ms", label: "Intermediate Timeout (ms)", type: "number", defaultValue: 2500 },
{ key: "chat_behavior.intermediate_replies.max_tokens", label: "Intermediate Max Tokens", type: "number", defaultValue: 60 },
{ key: "chat_behavior.intermediate_replies.max_chars", label: "Intermediate Max Chars", type: "number", defaultValue: 180 },
{ key: "chat_behavior.quick_ack.enabled", label: "Quick Acknowledgement", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Override quick acknowledgement for this channel." },
{ key: "chat_behavior.quick_ack.mode", label: "Quick Ack Mode", type: "select", options: quickAckModeOptions, defaultValue: "inherit", help: "Generated acknowledgement uses the configured sidecar provider/model. Fixed template mode is the only mode that sends template text." },
{ key: "chat_behavior.quick_ack.provider", label: "Quick Ack Provider", type: "text", placeholder: "Use agent provider", help: "Optional cheap provider for quick acknowledgements." },
{ key: "chat_behavior.quick_ack.model", label: "Quick Ack Model", type: "text", placeholder: "Use agent model" },
{ key: "chat_behavior.quick_ack.timeout_ms", label: "Quick Ack Timeout (ms)", type: "number", defaultValue: 2500 },
{ key: "chat_behavior.quick_ack.max_tokens", label: "Quick Ack Max Tokens", type: "number", defaultValue: 40 },
{ key: "chat_behavior.quick_ack.max_chars", label: "Quick Ack Max Chars", type: "number", defaultValue: 120 },
{ key: "chat_behavior.final_split.enabled", label: "Final Reply Splitting", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Override final multi-message splitting for this channel." },
{ key: "chat_behavior.enabled", label: "Human-like Delivery", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Channel-level override for delivery behavior. Channel settings win over Agent and Workspace settings." },
{ key: "chat_behavior.intermediate_replies.enabled", label: "Intermediate Replies", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Send natural progress bubbles during long or tool-heavy runs. These messages are not added to session history." },
{ key: "chat_behavior.intermediate_replies.mode", label: "Intermediate Reply Mode", type: "select", options: [{ value: "inherit", label: "Inherit from gateway" }, { value: "sidecar_generated", label: "Sidecar-generated" }, { value: "off", label: "Off" }], defaultValue: "inherit", help: "Choose whether progress updates are generated by the delivery model or disabled for this channel." },
{ key: "chat_behavior.intermediate_replies.provider", label: "Intermediate Provider", type: "text", placeholder: "Use agent provider", help: "Optional provider for progress bubbles. Leave empty to inherit the agent provider; pick a fast, cheap provider here." },
{ key: "chat_behavior.intermediate_replies.model", label: "Intermediate Model", type: "text", placeholder: "Use agent model", help: "Optional model for progress bubbles. Leave empty to inherit the agent model; custom model names are allowed." },
{ key: "chat_behavior.intermediate_replies.timeout_ms", label: "Intermediate Timeout (ms)", type: "number", defaultValue: 2500, help: "Maximum time to wait for the delivery model before skipping an intermediate update." },
{ key: "chat_behavior.intermediate_replies.max_tokens", label: "Intermediate Max Tokens", type: "number", defaultValue: 60, help: "Token cap for each generated progress bubble. Keep this small so updates stay short." },
{ key: "chat_behavior.intermediate_replies.max_chars", label: "Intermediate Max Chars", type: "number", defaultValue: 180, help: "Character cap after generation so progress bubbles do not become full answers." },
{ key: "chat_behavior.quick_ack.enabled", label: "Quick Acknowledgement", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Send a short natural acknowledgement before long work starts. This message is not added to session history." },
{ key: "chat_behavior.quick_ack.mode", label: "Quick Ack Mode", type: "select", options: quickAckModeOptions, defaultValue: "inherit", help: "Generated modes use the configured delivery provider/model. Fixed template mode is the only mode that sends template text." },
{ key: "chat_behavior.quick_ack.provider", label: "Quick Ack Provider", type: "text", placeholder: "Use agent provider", help: "Optional provider for quick acknowledgements. Leave empty to inherit the agent provider; pick a fast, cheap provider here." },
{ key: "chat_behavior.quick_ack.model", label: "Quick Ack Model", type: "text", placeholder: "Use agent model", help: "Optional model for quick acknowledgements. Leave empty to inherit the agent model; custom model names are allowed." },
{ key: "chat_behavior.quick_ack.timeout_ms", label: "Quick Ack Timeout (ms)", type: "number", defaultValue: 2500, help: "Maximum time to wait for the acknowledgement model before skipping generated acknowledgement." },
{ key: "chat_behavior.quick_ack.max_tokens", label: "Quick Ack Max Tokens", type: "number", defaultValue: 40, help: "Token cap for the acknowledgement. Keep this small so it reads like a receipt, not an answer." },
{ key: "chat_behavior.quick_ack.max_chars", label: "Quick Ack Max Chars", type: "number", defaultValue: 120, help: "Character cap after generation so acknowledgement bubbles stay compact." },
{ key: "chat_behavior.final_split.enabled", label: "Final Reply Splitting", type: "select", options: inheritToggleOptions, defaultValue: "inherit", help: "Split long final answers into multiple channel messages. This is separate from reasoning and progress bubbles." },
];
const dmPolicyOptions = [