feat: global AI assistant floating button for supervisors (#578)

- AiFloatingButton: FAB (bottom-right) opens a slide-in drawer with
  the supervisor AI chat panel. Close button collapses drawer, FAB
  reappears. Chat state persists across open/close and page navigation.
- app-shell: mounts FAB for admin users (isAdmin), same pattern as
  CallWidget for agents.
- team-dashboard: removed inline AI panel + toggle button — replaced
  by the global FAB. Dashboard content reclaims the full width.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 10:45:45 +05:30
parent 7e5d910197
commit d0e34fa9dd
3 changed files with 67 additions and 39 deletions

View File

@@ -15,6 +15,7 @@ import { useData } from '@/providers/data-provider';
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
import { useNetworkStatus } from '@/hooks/use-network-status';
// import { GlobalSearch } from '@/components/shared/global-search';
import { AiFloatingButton } from '@/components/shared/ai-floating-button';
import { apiClient } from '@/lib/api-client';
import { cx } from '@/utils/cx';
@@ -24,7 +25,7 @@ interface AppShellProps {
export const AppShell = ({ children }: AppShellProps) => {
const { pathname } = useLocation();
const { isCCAgent } = useAuth();
const { isCCAgent, isAdmin } = useAuth();
const { isOpen, activeAction, close } = useMaintShortcuts();
const { connectionStatus, isRegistered } = useSip();
const networkQuality = useNetworkStatus();
@@ -143,6 +144,7 @@ export const AppShell = ({ children }: AppShellProps) => {
<main className="flex flex-1 flex-col overflow-hidden">{children}</main>
</div>
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
{isAdmin && <AiFloatingButton />}
</div>
<MaintOtpModal
isOpen={isOpen}

View File

@@ -0,0 +1,50 @@
import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSparkles, faXmark } from '@fortawesome/pro-duotone-svg-icons';
import { AiChatPanel } from '@/components/call-desk/ai-chat-panel';
import { cx } from '@/utils/cx';
export const AiFloatingButton = () => {
const [open, setOpen] = useState(false);
return (
<>
{/* FAB — bottom right, hidden when drawer is open */}
{!open && (
<button
onClick={() => setOpen(true)}
className="fixed bottom-6 right-6 z-50 flex size-12 items-center justify-center rounded-full bg-brand-solid text-white shadow-lg hover:bg-brand-solid_hover transition duration-100 ease-linear"
title="AI Assistant"
>
<FontAwesomeIcon icon={faSparkles} className="size-5" />
</button>
)}
{/* Drawer — slides in from right */}
<div className={cx(
'fixed top-0 right-0 z-50 h-full bg-primary border-l border-secondary shadow-xl transition-all duration-200 ease-linear flex flex-col',
open ? 'w-[400px]' : 'w-0 overflow-hidden border-l-0',
)}>
{open && (
<>
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-4 py-3">
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faSparkles} className="size-3.5 text-fg-brand-primary" />
<span className="text-sm font-semibold text-primary">AI Assistant</span>
</div>
<button
onClick={() => setOpen(false)}
className="flex size-7 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
>
<FontAwesomeIcon icon={faXmark} className="size-4" />
</button>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
<AiChatPanel callerContext={{ type: 'supervisor' }} />
</div>
</>
)}
</div>
</>
);
};