fix: stabilize native streaming and improve bot controls

This commit is contained in:
Hyu
2026-10-10 18:02:42 +08:00
parent e8da99be63
commit a2b7f6b7bb
32 changed files with 583 additions and 55 deletions
+27 -12
View File
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
@@ -22,7 +22,7 @@ import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-ses
import { httpClient } from '@/app/infra/http/HttpClient';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import { useTranslation } from 'react-i18next';
import { Settings, FileText, Users, RefreshCw, Trash2 } from 'lucide-react';
import { Settings, FileText, Users, RefreshCw, Trash2, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { showBotError } from './bot-error';
@@ -72,6 +72,7 @@ export default function BotDetailContent({ id }: { id: string }) {
// Enable state managed here so the header switch works
const [botEnabled, setBotEnabled] = useState(true);
const [isTogglingEnable, setIsTogglingEnable] = useState(false);
const [enableLoaded, setEnableLoaded] = useState(false);
// Fetch bot enable state
@@ -91,20 +92,25 @@ export default function BotDetailContent({ id }: { id: string }) {
const handleEnableToggle = useCallback(
async (checked: boolean) => {
if (isTogglingEnable) return;
const prev = botEnabled;
setIsTogglingEnable(true);
setBotEnabled(checked);
try {
await httpClient.updateBot(id, { enable: checked });
setBot((current) =>
current ? { ...current, enable: checked } : current,
);
toast.success(t(checked ? 'bots.enableConfirmed' : 'bots.disableConfirmed'));
refreshBots();
} catch (error) {
setBotEnabled(prev);
showBotError(error, t('bots.setBotEnableError'), t);
} finally {
setIsTogglingEnable(false);
}
},
[id, botEnabled, refreshBots, t],
[id, botEnabled, isTogglingEnable, refreshBots, t],
);
function handleFormSubmit() {
@@ -210,18 +216,27 @@ export default function BotDetailContent({ id }: { id: string }) {
</div>
{enableLoaded && (
<div className="flex items-center gap-2">
<Badge
status={isTogglingEnable ? 'pending' : botEnabled ? 'enabled' : 'disabled'}
className="gap-1.5 rounded-full"
role="status"
aria-live="polite"
>
{isTogglingEnable
? <Loader2 className="animate-spin" aria-hidden="true" />
: <span className="size-1.5 rounded-full bg-current" aria-hidden="true" />}
{t(isTogglingEnable
? botEnabled ? 'bots.enabling' : 'bots.disabling'
: botEnabled ? 'bots.enableConfirmed' : 'bots.disableConfirmed')}
</Badge>
<Switch
id="bot-enable-switch"
checked={botEnabled}
onCheckedChange={handleEnableToggle}
disabled={!canManage}
disabled={!canManage || isTogglingEnable}
aria-label={t('common.enable')}
aria-busy={isTogglingEnable}
/>
<Label
htmlFor="bot-enable-switch"
className="text-sm text-muted-foreground cursor-pointer"
>
{t('common.enable')}
</Label>
</div>
)}
</div>
@@ -237,11 +252,11 @@ export default function BotDetailContent({ id }: { id: string }) {
<Button
type="submit"
form="bot-form"
disabled={!enableLoaded || (!formDirty && botEnabled)}
disabled={!enableLoaded || !formDirty || isTogglingEnable}
className={activeTab !== 'config' ? 'invisible' : ''}
data-guide="bot-config-save"
>
{t(botEnabled ? 'common.save' : 'bots.saveAndEnable')}
{t(formDirty && !botEnabled ? 'bots.saveAndEnable' : 'common.save')}
</Button>
<Button
type="button"
@@ -800,7 +800,7 @@ function NavItems({
{(isBot || isMCP) && (
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-popover',
'absolute -bottom-1 -right-1 size-3 rounded-full border-2 border-popover',
isMCP
? mcpStatusColor(item)
: item.enabled === false
@@ -868,7 +868,7 @@ function NavItems({
{(isBot || isMCP) && (
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-sidebar',
'absolute -bottom-1 -right-1 size-3 rounded-full border-2 border-sidebar',
isMCP
? mcpStatusColor(item)
: item.enabled === false
@@ -43,7 +43,8 @@ export function useMonitoringData(filterState: FilterState) {
requestId === requestIdRef.current &&
getCurrentWorkspaceSnapshot()?.workspace.uuid === workspaceUuid;
setLoading(true);
setError(null);
// Keep the failure view during retries; restore the dashboard only after
// a successful response, without briefly restarting its polling timer.
try {
const { startTime, endTime } = resolveMonitoringWindow(
@@ -385,6 +386,7 @@ export function useMonitoringData(filterState: FilterState) {
].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
setData(transformedData);
setError(null);
setRequestScope(scope);
} catch (err) {
if (!isCurrent()) return;
+5 -3
View File
@@ -167,7 +167,7 @@ function MonitoringPageContent() {
intervalRef.current = refreshInterval;
countdown.current = refreshInterval;
setRemainingSeconds(refreshInterval);
if (!refreshInterval) return;
if (!refreshInterval || error) return;
const timer = window.setInterval(() => {
if (document.visibilityState === 'visible' && !refreshState.current.busy) {
countdown.current = Math.max(0, countdown.current - 1);
@@ -179,7 +179,7 @@ function MonitoringPageContent() {
}
}, 1000);
return () => window.clearInterval(timer);
}, [refreshInterval]);
}, [refreshInterval, error]);
// State for expanded errors
const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
@@ -204,7 +204,7 @@ function MonitoringPageContent() {
return (
<div className="w-full h-full overflow-y-auto overflow-x-hidden">
{/* Filters and Refresh Button - Sticky */}
<div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
{!error && <div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
<div>
<Card className="flex flex-col gap-4 p-4 xl:flex-row xl:items-end">
<MonitoringFilters
@@ -264,6 +264,8 @@ function MonitoringPageContent() {
</div>
</div>
}
{/* Content Area */}
{error ? (
<LoadErrorState title={t('monitoring.loadError')} onRetry={handleRefresh} busy={refreshing} />
+4
View File
@@ -822,6 +822,10 @@ const enUS = {
earlier: 'Earlier',
dateFormat: '{{month}}/{{day}}',
setBotEnableError: 'Failed to set bot enable status',
enabling: 'Enabling…',
disabling: 'Disabling…',
enableConfirmed: 'Enabled',
disableConfirmed: 'Disabled',
log: 'Log',
configuration: 'Configuration',
logs: 'Logs',
+4
View File
@@ -838,6 +838,10 @@ const esES = {
earlier: 'Anterior',
dateFormat: '{{day}}/{{month}}',
setBotEnableError: 'Error al establecer el estado de activación del Bot',
enabling: 'Activando…',
disabling: 'Desactivando…',
enableConfirmed: 'Activado',
disableConfirmed: 'Desactivado',
log: 'Registro',
configuration: 'Configuración',
logs: 'Registros',
+4
View File
@@ -825,6 +825,10 @@ const jaJP = {
earlier: 'それ以前',
dateFormat: '{{month}}月{{day}}日',
setBotEnableError: 'ボットの有効状態の設定に失敗しました',
enabling: '有効にしています…',
disabling: '無効にしています…',
enableConfirmed: '有効',
disableConfirmed: '無効',
log: 'ログ',
configuration: '設定',
logs: 'ログ',
+4
View File
@@ -833,6 +833,10 @@ const ruRU = {
earlier: 'Ранее',
dateFormat: '{{day}}.{{month}}',
setBotEnableError: 'Не удалось изменить статус бота',
enabling: 'Включение…',
disabling: 'Отключение…',
enableConfirmed: 'Включён',
disableConfirmed: 'Отключён',
log: 'Журнал',
configuration: 'Настройки',
logs: 'Журналы',
+4
View File
@@ -809,6 +809,10 @@ const thTH = {
earlier: 'ก่อนหน้า',
dateFormat: '{{day}}/{{month}}',
setBotEnableError: 'ไม่สามารถตั้งค่าสถานะเปิดใช้งาน Bot ได้',
enabling: 'กำลังเปิดใช้งาน…',
disabling: 'กำลังปิดใช้งาน…',
enableConfirmed: 'เปิดใช้งานแล้ว',
disableConfirmed: 'ปิดใช้งานแล้ว',
log: 'บันทึก',
configuration: 'การกำหนดค่า',
logs: 'บันทึก',
+4
View File
@@ -823,6 +823,10 @@ const viVN = {
earlier: 'Trước đó',
dateFormat: '{{day}}/{{month}}',
setBotEnableError: 'Thiết lập trạng thái bật Bot thất bại',
enabling: 'Đang bật…',
disabling: 'Đang tắt…',
enableConfirmed: 'Đã bật',
disableConfirmed: 'Đã tắt',
log: 'Nhật ký',
configuration: 'Cấu hình',
logs: 'Nhật ký',
+4
View File
@@ -775,6 +775,10 @@ const zhHans = {
earlier: '更久之前',
dateFormat: '{{month}}月{{day}}日',
setBotEnableError: '设置机器人启用状态失败',
enabling: '正在启用…',
disabling: '正在禁用…',
enableConfirmed: '已启用',
disableConfirmed: '已禁用',
log: '日志',
configuration: '配置',
logs: '日志',
+4
View File
@@ -773,6 +773,10 @@ const zhHant = {
earlier: '更久之前',
dateFormat: '{{month}}月{{day}}日',
setBotEnableError: '設定機器人啟用狀態失敗',
enabling: '正在啟用…',
disabling: '正在停用…',
enableConfirmed: '已啟用',
disableConfirmed: '已停用',
log: '日誌',
configuration: '設定',
logs: '日誌',