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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
182 lines
4.3 KiB
TypeScript
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}</>;
|
|
}
|