mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
fix: stabilize native streaming and improve bot controls
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user