mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
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:
@@ -15,6 +15,7 @@ import { useData } from '@/providers/data-provider';
|
|||||||
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
||||||
import { useNetworkStatus } from '@/hooks/use-network-status';
|
import { useNetworkStatus } from '@/hooks/use-network-status';
|
||||||
// import { GlobalSearch } from '@/components/shared/global-search';
|
// import { GlobalSearch } from '@/components/shared/global-search';
|
||||||
|
import { AiFloatingButton } from '@/components/shared/ai-floating-button';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { cx } from '@/utils/cx';
|
import { cx } from '@/utils/cx';
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ interface AppShellProps {
|
|||||||
|
|
||||||
export const AppShell = ({ children }: AppShellProps) => {
|
export const AppShell = ({ children }: AppShellProps) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { isCCAgent } = useAuth();
|
const { isCCAgent, isAdmin } = useAuth();
|
||||||
const { isOpen, activeAction, close } = useMaintShortcuts();
|
const { isOpen, activeAction, close } = useMaintShortcuts();
|
||||||
const { connectionStatus, isRegistered } = useSip();
|
const { connectionStatus, isRegistered } = useSip();
|
||||||
const networkQuality = useNetworkStatus();
|
const networkQuality = useNetworkStatus();
|
||||||
@@ -143,6 +144,7 @@ export const AppShell = ({ children }: AppShellProps) => {
|
|||||||
<main className="flex flex-1 flex-col overflow-hidden">{children}</main>
|
<main className="flex flex-1 flex-col overflow-hidden">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
|
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
|
||||||
|
{isAdmin && <AiFloatingButton />}
|
||||||
</div>
|
</div>
|
||||||
<MaintOtpModal
|
<MaintOtpModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
|||||||
50
src/components/shared/ai-floating-button.tsx
Normal file
50
src/components/shared/ai-floating-button.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faSidebarFlip, faSidebar } from '@fortawesome/pro-duotone-svg-icons';
|
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { AiChatPanel } from '@/components/call-desk/ai-chat-panel';
|
|
||||||
import { DashboardKpi } from '@/components/dashboard/kpi-cards';
|
import { DashboardKpi } from '@/components/dashboard/kpi-cards';
|
||||||
import { MissedQueue } from '@/components/dashboard/missed-queue';
|
import { MissedQueue } from '@/components/dashboard/missed-queue';
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +26,6 @@ const getDateRangeStart = (range: DateRange): Date => {
|
|||||||
export const TeamDashboardPage = () => {
|
export const TeamDashboardPage = () => {
|
||||||
const { calls, leads, campaigns, loading } = useData();
|
const { calls, leads, campaigns, loading } = useData();
|
||||||
const [dateRange, setDateRange] = useState<DateRange>('week');
|
const [dateRange, setDateRange] = useState<DateRange>('week');
|
||||||
const [aiOpen, setAiOpen] = useState(true);
|
|
||||||
|
|
||||||
// Pull the richer supervisor rollup (NPS, idle, time breakdown, alerts)
|
// Pull the richer supervisor rollup (NPS, idle, time breakdown, alerts)
|
||||||
// from the sidecar. Only `today`/`week`/`month` overlap with the rollup's
|
// from the sidecar. Only `today`/`week`/`month` overlap with the rollup's
|
||||||
@@ -61,29 +57,20 @@ export const TeamDashboardPage = () => {
|
|||||||
subtitle={dateRangeLabel}
|
subtitle={dateRangeLabel}
|
||||||
infoText="Aggregated call metrics, agent performance, and operational alerts."
|
infoText="Aggregated call metrics, agent performance, and operational alerts."
|
||||||
controls={
|
controls={
|
||||||
<>
|
<div className="flex rounded-lg border border-secondary overflow-hidden">
|
||||||
<div className="flex rounded-lg border border-secondary overflow-hidden">
|
{(['today', 'week', 'month'] as const).map((range) => (
|
||||||
{(['today', 'week', 'month'] as const).map((range) => (
|
<button
|
||||||
<button
|
key={range}
|
||||||
key={range}
|
onClick={() => setDateRange(range)}
|
||||||
onClick={() => setDateRange(range)}
|
className={cx(
|
||||||
className={cx(
|
"px-3 py-1 text-xs font-medium transition duration-100 ease-linear",
|
||||||
"px-3 py-1 text-xs font-medium transition duration-100 ease-linear",
|
dateRange === range ? 'bg-active text-brand-secondary' : 'text-tertiary hover:bg-primary_hover',
|
||||||
dateRange === range ? 'bg-active text-brand-secondary' : 'text-tertiary hover:bg-primary_hover',
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{range === 'today' ? 'Today' : range === 'week' ? 'Week' : 'Month'}
|
||||||
{range === 'today' ? 'Today' : range === 'week' ? 'Week' : 'Month'}
|
</button>
|
||||||
</button>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setAiOpen(!aiOpen)}
|
|
||||||
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
|
|
||||||
title={aiOpen ? 'Hide AI panel' : 'Show AI panel'}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={aiOpen ? faSidebarFlip : faSidebar} className="size-4" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -154,17 +141,6 @@ export const TeamDashboardPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI panel — collapsible */}
|
|
||||||
<div className={cx(
|
|
||||||
"shrink-0 border-l border-secondary bg-primary flex flex-col overflow-hidden transition-all duration-200 ease-linear",
|
|
||||||
aiOpen ? "w-[380px]" : "w-0 border-l-0",
|
|
||||||
)}>
|
|
||||||
{aiOpen && (
|
|
||||||
<div className="flex h-full flex-col p-4">
|
|
||||||
<AiChatPanel callerContext={{ type: 'supervisor' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user