PSA/packages/product-chat/context.tsx
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

182 lines
4.3 KiB
TypeScript

'use client';
import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { usePathname } from 'next/navigation';
export type AIChatRecordType =
| 'ticket'
| 'project'
| 'client'
| 'contact'
| 'asset';
export interface AIChatUiContext {
pathname: string;
screen: {
key: string;
label: string;
};
record?: {
type: AIChatRecordType;
id: string;
};
}
export interface AIChatContextOverride {
pathname?: string;
screen?: Partial<AIChatUiContext['screen']>;
record?: AIChatUiContext['record'];
}
type AIChatContextValue = {
uiContext: AIChatUiContext;
setOverride: (override: AIChatContextOverride | null) => void;
};
const AIChatContext = createContext<AIChatContextValue | null>(null);
function toTitleCase(value: string): string {
return value
.split(/[-_]/g)
.filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join(' ');
}
function deriveScreenFromPathname(pathname: string | null): AIChatUiContext['screen'] {
const fallback = {
key: 'msp',
label: 'MSP Portal',
};
if (!pathname) {
return fallback;
}
const stripped = pathname.replace(/^\/+|\/+$/g, '');
const segments = stripped.split('/').filter(Boolean);
if (segments.length === 0) {
return fallback;
}
const withoutPortal = segments[0] === 'msp' ? segments.slice(1) : segments;
if (withoutPortal.length === 0) {
return fallback;
}
const [primary, secondary] = withoutPortal;
const key = secondary ? `${primary}.${secondary}` : primary;
if (primary === 'dashboard') {
return { key, label: 'Dashboard' };
}
if (primary === 'tickets') {
return { key, label: secondary ? 'Ticket Details' : 'Tickets' };
}
if (primary === 'projects') {
return { key, label: secondary ? 'Project Details' : 'Projects' };
}
if (primary === 'clients') {
return { key, label: secondary ? 'Client Details' : 'Clients' };
}
if (primary === 'contacts') {
return { key, label: secondary ? 'Contact Details' : 'Contacts' };
}
if (primary === 'assets') {
return { key, label: secondary ? 'Asset Details' : 'Assets' };
}
if (primary === 'billing') {
return { key, label: 'Billing' };
}
if (primary === 'settings') {
return { key, label: secondary ? `Settings: ${toTitleCase(secondary)}` : 'Settings' };
}
if (primary === 'chat') {
return { key, label: 'Chat' };
}
return {
key,
label: toTitleCase(secondary ?? primary),
};
}
export function AIChatContextProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const [override, setOverride] = useState<AIChatContextOverride | null>(null);
const uiContext = useMemo<AIChatUiContext>(() => {
const resolvedPath = override?.pathname ?? pathname ?? '/msp';
const derivedScreen = deriveScreenFromPathname(resolvedPath);
return {
pathname: resolvedPath,
screen: {
key: override?.screen?.key ?? derivedScreen.key,
label: override?.screen?.label ?? derivedScreen.label,
},
...(override?.record ? { record: override.record } : {}),
};
}, [override, pathname]);
const value = useMemo<AIChatContextValue>(
() => ({
uiContext,
setOverride,
}),
[uiContext],
);
return <AIChatContext.Provider value={value}>{children}</AIChatContext.Provider>;
}
export function useAIChatContext(): AIChatUiContext {
const context = useContext(AIChatContext);
if (!context) {
throw new Error('useAIChatContext must be used within AIChatContextProvider');
}
return context.uiContext;
}
export function useAIChatContextOverride(override: AIChatContextOverride | null) {
const context = useContext(AIChatContext);
useEffect(() => {
if (!context) {
return;
}
context.setOverride(override);
return () => {
context.setOverride(null);
};
}, [context, override]);
}
export function AIChatContextBoundary({
children,
value,
}: {
children: React.ReactNode;
value: AIChatContextOverride;
}) {
const stableValue = useMemo<AIChatContextOverride>(
() => ({
pathname: value.pathname,
screen: value.screen,
record: value.record,
}),
[value.pathname, value.record?.id, value.record?.type, value.screen?.key, value.screen?.label],
);
useAIChatContextOverride(stableValue);
return <>{children}</>;
}