@burdenoff/microfe-proserviceworld 2026.709.1 → 2026.709.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/index.js +2 -1
  2. package/dist/node_modules/clsx/dist/clsx.js +1 -1
  3. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  4. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +130 -130
  5. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  6. package/dist/proserviceworld/ProServiceWorldRoot.js +4 -2
  7. package/dist/proserviceworld/ProServiceWorldRoot.js.map +1 -1
  8. package/dist/proserviceworld/ProServiceWorldRoutes.d.ts.map +1 -1
  9. package/dist/proserviceworld/ProServiceWorldRoutes.js +95 -42
  10. package/dist/proserviceworld/ProServiceWorldRoutes.js.map +1 -1
  11. package/dist/proserviceworld/components/DiscussionThread.js.map +1 -1
  12. package/dist/proserviceworld/components/FeatureFlaggedPage.d.ts +8 -0
  13. package/dist/proserviceworld/components/FeatureFlaggedPage.d.ts.map +1 -0
  14. package/dist/proserviceworld/components/FeatureFlaggedPage.js +17 -0
  15. package/dist/proserviceworld/components/FeatureFlaggedPage.js.map +1 -0
  16. package/dist/proserviceworld/components/LoadMore.d.ts +12 -0
  17. package/dist/proserviceworld/components/LoadMore.d.ts.map +1 -0
  18. package/dist/proserviceworld/components/LoadMore.js +30 -0
  19. package/dist/proserviceworld/components/LoadMore.js.map +1 -0
  20. package/dist/proserviceworld/components/TagPicker.d.ts +11 -0
  21. package/dist/proserviceworld/components/TagPicker.d.ts.map +1 -0
  22. package/dist/proserviceworld/components/TagPicker.js +31 -0
  23. package/dist/proserviceworld/components/TagPicker.js.map +1 -0
  24. package/dist/proserviceworld/components/ui.js.map +1 -1
  25. package/dist/proserviceworld/hooks/useAnalytics.js.map +1 -1
  26. package/dist/proserviceworld/hooks/useEntityComments.js.map +1 -1
  27. package/dist/proserviceworld/hooks/useFiles.js.map +1 -1
  28. package/dist/proserviceworld/hooks/usePageViewAnalytics.d.ts +10 -0
  29. package/dist/proserviceworld/hooks/usePageViewAnalytics.d.ts.map +1 -0
  30. package/dist/proserviceworld/hooks/usePageViewAnalytics.js +24 -0
  31. package/dist/proserviceworld/hooks/usePageViewAnalytics.js.map +1 -0
  32. package/dist/proserviceworld/hooks/useProServiceWorldApi.js +47 -27
  33. package/dist/proserviceworld/hooks/useProServiceWorldApi.js.map +1 -1
  34. package/dist/proserviceworld/i18n/index.d.ts +13 -0
  35. package/dist/proserviceworld/i18n/index.d.ts.map +1 -0
  36. package/dist/proserviceworld/i18n/index.js +171 -0
  37. package/dist/proserviceworld/i18n/index.js.map +1 -0
  38. package/dist/proserviceworld/index.d.ts +1 -0
  39. package/dist/proserviceworld/index.d.ts.map +1 -1
  40. package/dist/proserviceworld/index.js +5 -3
  41. package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
  42. package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -1
  43. package/dist/proserviceworld/pages/ClientsPage.d.ts.map +1 -1
  44. package/dist/proserviceworld/pages/ClientsPage.js +173 -121
  45. package/dist/proserviceworld/pages/ClientsPage.js.map +1 -1
  46. package/dist/proserviceworld/pages/CompliancePage.js.map +1 -1
  47. package/dist/proserviceworld/pages/ConflictsPage.js.map +1 -1
  48. package/dist/proserviceworld/pages/DashboardPage.js.map +1 -1
  49. package/dist/proserviceworld/pages/DocumentsPage.js.map +1 -1
  50. package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -1
  51. package/dist/proserviceworld/pages/EngagementsPage.d.ts.map +1 -1
  52. package/dist/proserviceworld/pages/EngagementsPage.js +168 -120
  53. package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -1
  54. package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -1
  55. package/dist/proserviceworld/pages/MattersPage.d.ts.map +1 -1
  56. package/dist/proserviceworld/pages/MattersPage.js +183 -144
  57. package/dist/proserviceworld/pages/MattersPage.js.map +1 -1
  58. package/dist/proserviceworld/pages/PracticeAreasPage.js.map +1 -1
  59. package/dist/proserviceworld/pages/TimePage.js.map +1 -1
  60. package/dist/proserviceworld/providers/ProServiceWorldProvider.js.map +1 -1
  61. package/dist/proserviceworld/services/assistantApi.js.map +1 -1
  62. package/dist/proserviceworld/services/createSandboxAssistantTransport.js.map +1 -1
  63. package/dist/proserviceworld/utils/cn.js +1 -1
  64. package/dist/proserviceworld/utils/cn.js.map +1 -1
  65. package/dist/proserviceworld/utils/native.d.ts +23 -0
  66. package/dist/proserviceworld/utils/native.d.ts.map +1 -0
  67. package/dist/proserviceworld/utils/native.js +14 -0
  68. package/dist/proserviceworld/utils/native.js.map +1 -0
  69. package/dist/proserviceworld/utils/nav.js.map +1 -1
  70. package/dist/proserviceworld/utils/navigation.js.map +1 -1
  71. package/dist/proserviceworld/utils/pageContext.js +14 -16
  72. package/dist/proserviceworld/utils/pageContext.js.map +1 -1
  73. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"MattersPage.js","names":[],"sources":["../../../src/proserviceworld/pages/MattersPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n Plus,\n Search,\n FolderKanban,\n Lock,\n CheckCircle2,\n CircleDot,\n AlertTriangle,\n} from 'lucide-react';\nimport {\n useMatters,\n useCreateMatter,\n useCloseMatter,\n useClients,\n usePracticeAreas,\n type PswMatterStatus,\n type PswMatter,\n} from '../hooks/useProServiceWorldApi';\nimport { useAnalytics } from '../hooks/useAnalytics';\nimport {\n PageHeader,\n SectionCard,\n MetricCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n NativeSelect,\n Banner,\n TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n} from '../components/ui';\n\nconst STATUSES: PswMatterStatus[] = ['OPEN', 'ON_HOLD', 'CLOSED', 'ARCHIVED'];\nconst PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\n/** Amber padlock chip that flags privileged / confidential matters. */\nconst PrivilegeLock: FC = () => (\n <span\n className=\"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-status-warning-bg-subtle text-status-warning-text\"\n title=\"Privileged / confidential\"\n aria-label=\"Privileged\"\n >\n <Lock className=\"h-3 w-3\" />\n </span>\n);\n\nexport const MattersPage: FC = () => {\n const { emitIntent, emitFact } = useAnalytics();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<PswMatterStatus | ''>('');\n const [creating, setCreating] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const { data, isLoading, error, refetch } = useMatters({\n search: search.trim() || undefined,\n status: statusFilter || undefined,\n });\n const close = useCloseMatter();\n\n const items = data?.items ?? [];\n const openCount = items.filter((m) => m.status === 'OPEN').length;\n const privilegedCount = items.filter((m) => m.isPrivileged).length;\n const highCount = items.filter((m) => m.priority === 'HIGH' || m.priority === 'CRITICAL').length;\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Matters\"\n subtitle=\"The regulated threads of work — legal matters, audits, filings — with their own deadlines and sensitive-document handling.\"\n icon={<FolderKanban className=\"h-6 w-6\" />}\n actions={\n <Button\n onClick={() => {\n emitIntent('psw.matter.create_clicked');\n setCreating(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New matter\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <TextInput\n placeholder=\"Search matters…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && search.trim()) {\n emitFact('psw.search.performed', {\n entityType: 'matter',\n queryLength: search.trim().length,\n });\n }\n }}\n className=\"pl-9\"\n />\n </div>\n <NativeSelect\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as PswMatterStatus | '')}\n className=\"sm:w-48\"\n >\n <option value=\"\">All statuses</option>\n {enumOptions(STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title=\"No matters yet\"\n description=\"Open a matter to track a regulated thread of work for a client.\"\n icon={<FolderKanban className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New matter\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label=\"Matters\"\n value={data.totalCount}\n icon={<FolderKanban className=\"h-4 w-4\" />}\n />\n <MetricCard\n label=\"Open\"\n value={openCount}\n icon={<CircleDot className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Privileged\"\n value={privilegedCount}\n icon={<Lock className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n <MetricCard\n label=\"High priority\"\n value={highCount}\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n tone={highCount ? 'danger' : 'neutral'}\n />\n </div>\n\n <SectionCard title={`${data.totalCount} matter${data.totalCount === 1 ? '' : 's'}`}>\n <TableWrap>\n <thead>\n <tr>\n <Th>Matter</Th>\n <Th>Client</Th>\n <Th>Priority</Th>\n <Th>Status</Th>\n <Th>Due</Th>\n <Th className=\"text-right\">Actions</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((m) => (\n <tr key={m.id} className=\"transition-colors hover:bg-bg-sunken/60\">\n <Td>\n <span className=\"flex items-center gap-2 font-medium text-text-primary\">\n {m.isPrivileged && <PrivilegeLock />}\n <span className=\"truncate\">{m.title}</span>\n </span>\n {m.reference ? (\n <div className=\"mt-0.5 pl-0 text-xs text-text-tertiary\">\n Ref: {m.reference}\n </div>\n ) : null}\n </Td>\n <Td className=\"text-text-primary\">{m.client?.name ?? '—'}</Td>\n <Td>\n <StatusPill status={m.priority} />\n </Td>\n <Td>\n <StatusPill status={m.status} />\n </Td>\n <Td>{fmtDate(m.dueDate)}</Td>\n <Td className=\"text-right\">\n {m.status !== 'CLOSED' && m.status !== 'ARCHIVED' ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={close.isPending}\n onClick={() =>\n close.mutate(m.id, {\n onSuccess: () => {\n emitFact('psw.matter.closed', { matterId: m.id });\n setBanner({ tone: 'success', text: `${m.title} closed.` });\n },\n onError: (e) =>\n setBanner({\n tone: 'danger',\n text: e instanceof Error ? e.message : 'Failed.',\n }),\n })\n }\n >\n <CheckCircle2 className=\"mr-1 h-4 w-4\" /> Close\n </Button>\n ) : (\n <span className=\"text-xs text-text-tertiary\">\n {m.closedAt ? `Closed ${fmtDate(m.closedAt)}` : '—'}\n </span>\n )}\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n </SectionCard>\n </>\n )}\n\n {creating && (\n <MatterModal\n onClose={() => setCreating(false)}\n onSaved={(t) => setBanner({ tone: 'success', text: `${t} opened.` })}\n />\n )}\n </div>\n );\n};\n\nconst MatterModal: FC<{ onClose: () => void; onSaved: (title: string) => void }> = ({\n onClose,\n onSaved,\n}) => {\n const create = useCreateMatter();\n const { emitFact } = useAnalytics();\n const clients = useClients({ pagination: { limit: 100 } });\n const practiceAreas = usePracticeAreas();\n const [form, setForm] = useState({\n clientId: '',\n practiceAreaId: '',\n title: '',\n reference: '',\n description: '',\n priority: 'MEDIUM',\n dueDate: '',\n isPrivileged: false,\n });\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof typeof form) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n if (!form.clientId) return setErr('Select a client.');\n if (!form.title.trim()) return setErr('Matter title is required.');\n create.mutate(\n {\n clientId: form.clientId,\n practiceAreaId: form.practiceAreaId || null,\n title: form.title.trim(),\n reference: form.reference.trim() || null,\n description: form.description.trim() || null,\n priority: form.priority,\n isPrivileged: form.isPrivileged,\n dueDate: form.dueDate ? new Date(form.dueDate).toISOString() : null,\n },\n {\n onSuccess: (created) => {\n const matter = (created as { pswCreateMatter: PswMatter }).pswCreateMatter;\n emitFact('psw.matter.created', {\n matterId: matter.id,\n clientId: form.clientId,\n });\n onSaved(form.title.trim());\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"New matter\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Opening…' : 'Open matter'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Client\" required>\n <NativeSelect value={form.clientId} onChange={set('clientId')}>\n <option value=\"\">Select a client…</option>\n {clients.data?.items.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n <Field label=\"Title\" required>\n <TextInput\n value={form.title}\n onChange={set('title')}\n placeholder=\"Regulatory investigation — SEC inquiry\"\n />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Reference\">\n <TextInput\n value={form.reference}\n onChange={set('reference')}\n placeholder=\"MAT-2026-014\"\n />\n </Field>\n <Field label=\"Priority\">\n <NativeSelect value={form.priority} onChange={set('priority')}>\n {enumOptions(PRIORITIES)}\n </NativeSelect>\n </Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Practice area\">\n <NativeSelect value={form.practiceAreaId} onChange={set('practiceAreaId')}>\n <option value=\"\">Unassigned</option>\n {practiceAreas.data?.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n <Field label=\"Due date\">\n <TextInput type=\"date\" value={form.dueDate} onChange={set('dueDate')} />\n </Field>\n </div>\n <Field label=\"Description\">\n <TextArea value={form.description} onChange={set('description')} />\n </Field>\n <label className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-canvas px-3 py-2.5 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.isPrivileged}\n onChange={(e) => setForm((f) => ({ ...f, isPrivileged: e.target.checked }))}\n />\n <span className=\"flex items-center gap-1.5\">\n <Lock className=\"h-3.5 w-3.5 text-status-warning-text\" />\n Privileged / confidential matter\n </span>\n </label>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"MattersPage.js","names":[],"sources":["../../../src/proserviceworld/pages/MattersPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n Plus,\n Search,\n FolderKanban,\n Lock,\n CheckCircle2,\n CircleDot,\n AlertTriangle,\n} from 'lucide-react';\nimport {\n useMatters,\n useCreateMatter,\n useUpdateMatter,\n useCloseMatter,\n useClients,\n usePracticeAreas,\n type PswMatterStatus,\n type PswMatter,\n} from '../hooks/useProServiceWorldApi';\nimport { useAnalytics } from '../hooks/useAnalytics';\nimport { impactFeedback } from '../utils/native';\nimport {\n PageHeader,\n SectionCard,\n MetricCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n NativeSelect,\n Banner,\n TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n} from '../components/ui';\nimport { TagPicker } from '../components/TagPicker';\nimport { LoadMore } from '../components/LoadMore';\n\nconst STATUSES: PswMatterStatus[] = ['OPEN', 'ON_HOLD', 'CLOSED', 'ARCHIVED'];\nconst PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\n/** Amber padlock chip that flags privileged / confidential matters. */\nconst PrivilegeLock: FC = () => (\n <span\n className=\"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-status-warning-bg-subtle text-status-warning-text\"\n title=\"Privileged / confidential\"\n aria-label=\"Privileged\"\n >\n <Lock className=\"h-3 w-3\" />\n </span>\n);\n\nconst PAGE_SIZE = 25;\n\nexport const MattersPage: FC = () => {\n const { emitIntent, emitFact } = useAnalytics();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<PswMatterStatus | ''>('');\n const [creating, setCreating] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const [pagination, setPagination] = useState({ limit: PAGE_SIZE, offset: 0 });\n const { data, isLoading, error, refetch } = useMatters({\n search: search.trim() || undefined,\n status: statusFilter || undefined,\n pagination,\n });\n const close = useCloseMatter();\n const update = useUpdateMatter();\n\n const items = useMemo(() => data?.items ?? [], [data]);\n const openCount = items.filter((m) => m.status === 'OPEN').length;\n const privilegedCount = items.filter((m) => m.isPrivileged).length;\n const highCount = items.filter((m) => m.priority === 'HIGH' || m.priority === 'CRITICAL').length;\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Matters\"\n subtitle=\"The regulated threads of work — legal matters, audits, filings — with their own deadlines and sensitive-document handling.\"\n icon={<FolderKanban className=\"h-6 w-6\" />}\n actions={\n <Button\n onClick={() => {\n void impactFeedback('light');\n emitIntent('psw.matter.create_clicked');\n setCreating(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New matter\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <TextInput\n placeholder=\"Search matters…\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPagination({ limit: PAGE_SIZE, offset: 0 });\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && search.trim()) {\n emitFact('psw.search.performed', {\n entityType: 'matter',\n queryLength: search.trim().length,\n });\n }\n }}\n className=\"pl-9\"\n />\n </div>\n <NativeSelect\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as PswMatterStatus | '');\n setPagination({ limit: PAGE_SIZE, offset: 0 });\n }}\n className=\"sm:w-48\"\n >\n <option value=\"\">All statuses</option>\n {enumOptions(STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title=\"No matters yet\"\n description=\"Open a matter to track a regulated thread of work for a client.\"\n icon={<FolderKanban className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New matter\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label=\"Matters\"\n value={data.totalCount}\n icon={<FolderKanban className=\"h-4 w-4\" />}\n />\n <MetricCard\n label=\"Open\"\n value={openCount}\n icon={<CircleDot className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Privileged\"\n value={privilegedCount}\n icon={<Lock className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n <MetricCard\n label=\"High priority\"\n value={highCount}\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n tone={highCount ? 'danger' : 'neutral'}\n />\n </div>\n\n <SectionCard title={`${data.totalCount} matter${data.totalCount === 1 ? '' : 's'}`}>\n <TableWrap>\n <thead>\n <tr>\n <Th>Matter</Th>\n <Th>Client</Th>\n <Th>Priority</Th>\n <Th>Status</Th>\n <Th>Due</Th>\n <Th>Tags</Th>\n <Th className=\"text-right\">Actions</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((m) => (\n <tr key={m.id} className=\"transition-colors hover:bg-bg-sunken/60\">\n <Td>\n <span className=\"flex items-center gap-2 font-medium text-text-primary\">\n {m.isPrivileged && <PrivilegeLock />}\n <span className=\"truncate\">{m.title}</span>\n </span>\n {m.reference ? (\n <div className=\"mt-0.5 pl-0 text-xs text-text-tertiary\">\n Ref: {m.reference}\n </div>\n ) : null}\n </Td>\n <Td className=\"text-text-primary\">{m.client?.name ?? '—'}</Td>\n <Td>\n <StatusPill status={m.priority} />\n </Td>\n <Td>\n <StatusPill status={m.status} />\n </Td>\n <Td>{fmtDate(m.dueDate)}</Td>\n <Td>\n <TagPicker\n entityType=\"PswMatter\"\n entityId={m.id}\n tags={m.tags ?? []}\n onChange={async (tags) => {\n await update.mutateAsync({ id: m.id, input: { tags } });\n void refetch();\n }}\n maxTags={5}\n />\n </Td>\n <Td className=\"text-right\">\n {m.status !== 'CLOSED' && m.status !== 'ARCHIVED' ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={close.isPending}\n onClick={() =>\n close.mutate(m.id, {\n onSuccess: () => {\n emitFact('psw.matter.closed', { matterId: m.id });\n setBanner({ tone: 'success', text: `${m.title} closed.` });\n },\n onError: (e) =>\n setBanner({\n tone: 'danger',\n text: e instanceof Error ? e.message : 'Failed.',\n }),\n })\n }\n >\n <CheckCircle2 className=\"mr-1 h-4 w-4\" /> Close\n </Button>\n ) : (\n <span className=\"text-xs text-text-tertiary\">\n {m.closedAt ? `Closed ${fmtDate(m.closedAt)}` : '—'}\n </span>\n )}\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n <LoadMore\n hasMore={data?.pageInfo?.hasMore ?? false}\n loading={isLoading}\n loadedCount={items.length}\n totalCount={data?.totalCount ?? items.length}\n onLoadMore={() =>\n setPagination((p) => ({ limit: PAGE_SIZE, offset: p.offset + PAGE_SIZE }))\n }\n />\n </SectionCard>\n </>\n )}\n\n {creating && (\n <MatterModal\n onClose={() => setCreating(false)}\n onSaved={(t) => setBanner({ tone: 'success', text: `${t} opened.` })}\n />\n )}\n </div>\n );\n};\n\nconst MatterModal: FC<{ onClose: () => void; onSaved: (title: string) => void }> = ({\n onClose,\n onSaved,\n}) => {\n const create = useCreateMatter();\n const { emitFact } = useAnalytics();\n const clients = useClients({ pagination: { limit: 100 } });\n const practiceAreas = usePracticeAreas();\n const [form, setForm] = useState({\n clientId: '',\n practiceAreaId: '',\n title: '',\n reference: '',\n description: '',\n priority: 'MEDIUM',\n dueDate: '',\n isPrivileged: false,\n });\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof typeof form) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n if (!form.clientId) return setErr('Select a client.');\n if (!form.title.trim()) return setErr('Matter title is required.');\n create.mutate(\n {\n clientId: form.clientId,\n practiceAreaId: form.practiceAreaId || null,\n title: form.title.trim(),\n reference: form.reference.trim() || null,\n description: form.description.trim() || null,\n priority: form.priority,\n isPrivileged: form.isPrivileged,\n dueDate: form.dueDate ? new Date(form.dueDate).toISOString() : null,\n },\n {\n onSuccess: (created) => {\n const matter = (created as { pswCreateMatter: PswMatter }).pswCreateMatter;\n emitFact('psw.matter.created', {\n matterId: matter.id,\n clientId: form.clientId,\n });\n onSaved(form.title.trim());\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"New matter\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Opening…' : 'Open matter'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Client\" required>\n <NativeSelect value={form.clientId} onChange={set('clientId')}>\n <option value=\"\">Select a client…</option>\n {clients.data?.items.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n <Field label=\"Title\" required>\n <TextInput\n value={form.title}\n onChange={set('title')}\n placeholder=\"Regulatory investigation — SEC inquiry\"\n />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Reference\">\n <TextInput\n value={form.reference}\n onChange={set('reference')}\n placeholder=\"MAT-2026-014\"\n />\n </Field>\n <Field label=\"Priority\">\n <NativeSelect value={form.priority} onChange={set('priority')}>\n {enumOptions(PRIORITIES)}\n </NativeSelect>\n </Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Practice area\">\n <NativeSelect value={form.practiceAreaId} onChange={set('practiceAreaId')}>\n <option value=\"\">Unassigned</option>\n {practiceAreas.data?.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n <Field label=\"Due date\">\n <TextInput type=\"date\" value={form.dueDate} onChange={set('dueDate')} />\n </Field>\n </div>\n <Field label=\"Description\">\n <TextArea value={form.description} onChange={set('description')} />\n </Field>\n <label className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-canvas px-3 py-2.5 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.isPrivileged}\n onChange={(e) => setForm((f) => ({ ...f, isPrivileged: e.target.checked }))}\n />\n <span className=\"flex items-center gap-1.5\">\n <Lock className=\"h-3.5 w-3.5 text-status-warning-text\" />\n Privileged / confidential matter\n </span>\n </label>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PracticeAreasPage.js","names":[],"sources":["../../../src/proserviceworld/pages/PracticeAreasPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, LayoutGrid, Pencil } from 'lucide-react';\nimport {\n usePracticeAreas,\n useCreatePracticeArea,\n useUpdatePracticeArea,\n type PswPracticeArea,\n} from '../hooks/useProServiceWorldApi';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n Banner,\n} from '../components/ui';\n\nexport const PracticeAreasPage: FC = () => {\n const { data, isLoading, error, refetch } = usePracticeAreas();\n const [creating, setCreating] = useState(false);\n const [editing, setEditing] = useState<PswPracticeArea | null>(null);\n const [banner, setBanner] = useState<string | null>(null);\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Practice areas\"\n subtitle=\"The domains of service your firm offers — Tax, Audit, Litigation, Advisory — that engagements and matters classify against.\"\n icon={<LayoutGrid className=\"h-6 w-6\" />}\n actions={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New area\n </Button>\n }\n />\n\n {banner && (\n <Banner tone=\"success\" onClose={() => setBanner(null)}>\n {banner}\n </Banner>\n )}\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.length === 0 ? (\n <Empty\n title=\"No practice areas\"\n description=\"Define the service domains your firm offers.\"\n icon={<LayoutGrid className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New area\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {data.map((p) => (\n <SectionCard key={p.id}>\n <div className=\"flex items-start justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">{p.name}</h3>\n {p.code ? (\n <span className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-xs text-text-tertiary\">\n {p.code}\n </span>\n ) : null}\n </div>\n <StatusPill status={p.isActive ? 'ACTIVE' : 'INACTIVE'} className=\"mt-1.5\" />\n </div>\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Edit\" onClick={() => setEditing(p)}>\n <Pencil className=\"h-4 w-4\" />\n </Button>\n </div>\n {p.description ? (\n <p className=\"mt-2 text-sm text-text-secondary\">{p.description}</p>\n ) : null}\n </SectionCard>\n ))}\n </div>\n )}\n\n {creating && (\n <AreaModal\n onClose={() => setCreating(false)}\n onDone={() => {\n void refetch();\n setBanner('Practice area created.');\n }}\n />\n )}\n {editing && (\n <AreaModal\n area={editing}\n onClose={() => setEditing(null)}\n onDone={() => {\n void refetch();\n setBanner('Practice area updated.');\n }}\n />\n )}\n </div>\n );\n};\n\nconst AreaModal: FC<{ area?: PswPracticeArea; onClose: () => void; onDone: () => void }> = ({\n area,\n onClose,\n onDone,\n}) => {\n const create = useCreatePracticeArea();\n const update = useUpdatePracticeArea();\n const [form, setForm] = useState({\n name: area?.name ?? '',\n code: area?.code ?? '',\n description: area?.description ?? '',\n isActive: area?.isActive ?? true,\n });\n const [err, setErr] = useState<string | null>(null);\n const pending = create.isPending || update.isPending;\n const set = (k: 'name' | 'code' | 'description') => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n if (!form.name.trim()) return setErr('Name is required.');\n const payload = {\n name: form.name.trim(),\n code: form.code.trim() || null,\n description: form.description.trim() || null,\n isActive: form.isActive,\n };\n const onError = (e: unknown) => setErr(e instanceof Error ? e.message : 'Failed.');\n if (area)\n update.mutate(\n { id: area.id, input: payload },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError,\n }\n );\n else\n create.mutate(payload, {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError,\n });\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title={area ? 'Edit practice area' : 'New practice area'}\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={pending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={pending}>\n {pending ? 'Saving…' : area ? 'Save' : 'Create'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Name\" required>\n <TextInput value={form.name} onChange={set('name')} placeholder=\"Tax Advisory\" />\n </Field>\n <Field label=\"Code\">\n <TextInput value={form.code} onChange={set('code')} placeholder=\"TAX\" />\n </Field>\n <Field label=\"Description\">\n <TextArea value={form.description} onChange={set('description')} />\n </Field>\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.isActive}\n onChange={(e) => setForm((f) => ({ ...f, isActive: e.target.checked }))}\n />\n Active\n </label>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"PracticeAreasPage.js","names":[],"sources":["../../../src/proserviceworld/pages/PracticeAreasPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, LayoutGrid, Pencil } from 'lucide-react';\nimport {\n usePracticeAreas,\n useCreatePracticeArea,\n useUpdatePracticeArea,\n type PswPracticeArea,\n} from '../hooks/useProServiceWorldApi';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n Banner,\n} from '../components/ui';\n\nexport const PracticeAreasPage: FC = () => {\n const { data, isLoading, error, refetch } = usePracticeAreas();\n const [creating, setCreating] = useState(false);\n const [editing, setEditing] = useState<PswPracticeArea | null>(null);\n const [banner, setBanner] = useState<string | null>(null);\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Practice areas\"\n subtitle=\"The domains of service your firm offers — Tax, Audit, Litigation, Advisory — that engagements and matters classify against.\"\n icon={<LayoutGrid className=\"h-6 w-6\" />}\n actions={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New area\n </Button>\n }\n />\n\n {banner && (\n <Banner tone=\"success\" onClose={() => setBanner(null)}>\n {banner}\n </Banner>\n )}\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.length === 0 ? (\n <Empty\n title=\"No practice areas\"\n description=\"Define the service domains your firm offers.\"\n icon={<LayoutGrid className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n New area\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {data.map((p) => (\n <SectionCard key={p.id}>\n <div className=\"flex items-start justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">{p.name}</h3>\n {p.code ? (\n <span className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-xs text-text-tertiary\">\n {p.code}\n </span>\n ) : null}\n </div>\n <StatusPill status={p.isActive ? 'ACTIVE' : 'INACTIVE'} className=\"mt-1.5\" />\n </div>\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Edit\" onClick={() => setEditing(p)}>\n <Pencil className=\"h-4 w-4\" />\n </Button>\n </div>\n {p.description ? (\n <p className=\"mt-2 text-sm text-text-secondary\">{p.description}</p>\n ) : null}\n </SectionCard>\n ))}\n </div>\n )}\n\n {creating && (\n <AreaModal\n onClose={() => setCreating(false)}\n onDone={() => {\n void refetch();\n setBanner('Practice area created.');\n }}\n />\n )}\n {editing && (\n <AreaModal\n area={editing}\n onClose={() => setEditing(null)}\n onDone={() => {\n void refetch();\n setBanner('Practice area updated.');\n }}\n />\n )}\n </div>\n );\n};\n\nconst AreaModal: FC<{ area?: PswPracticeArea; onClose: () => void; onDone: () => void }> = ({\n area,\n onClose,\n onDone,\n}) => {\n const create = useCreatePracticeArea();\n const update = useUpdatePracticeArea();\n const [form, setForm] = useState({\n name: area?.name ?? '',\n code: area?.code ?? '',\n description: area?.description ?? '',\n isActive: area?.isActive ?? true,\n });\n const [err, setErr] = useState<string | null>(null);\n const pending = create.isPending || update.isPending;\n const set = (k: 'name' | 'code' | 'description') => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n if (!form.name.trim()) return setErr('Name is required.');\n const payload = {\n name: form.name.trim(),\n code: form.code.trim() || null,\n description: form.description.trim() || null,\n isActive: form.isActive,\n };\n const onError = (e: unknown) => setErr(e instanceof Error ? e.message : 'Failed.');\n if (area)\n update.mutate(\n { id: area.id, input: payload },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError,\n }\n );\n else\n create.mutate(payload, {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError,\n });\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title={area ? 'Edit practice area' : 'New practice area'}\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={pending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={pending}>\n {pending ? 'Saving…' : area ? 'Save' : 'Create'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Name\" required>\n <TextInput value={form.name} onChange={set('name')} placeholder=\"Tax Advisory\" />\n </Field>\n <Field label=\"Code\">\n <TextInput value={form.code} onChange={set('code')} placeholder=\"TAX\" />\n </Field>\n <Field label=\"Description\">\n <TextArea value={form.description} onChange={set('description')} />\n </Field>\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.isActive}\n onChange={(e) => setForm((f) => ({ ...f, isActive: e.target.checked }))}\n />\n Active\n </label>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimePage.js","names":[],"sources":["../../../src/proserviceworld/pages/TimePage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, Clock, Send, CheckCircle2, ListChecks, Wallet, CircleSlash } from 'lucide-react';\nimport {\n useTimeEntries,\n useLogTimeEntry,\n useSubmitTimeEntry,\n useApproveTimeEntry,\n useEngagements,\n useMatters,\n type PswTimeEntryStatus,\n} from '../hooks/useProServiceWorldApi';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n NativeSelect,\n Banner,\n MetricCard,\n TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n fmtMinutesAsHours,\n} from '../components/ui';\n\nconst STATUSES: PswTimeEntryStatus[] = ['DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED', 'BILLED'];\n\n/** Compact billable / non-billable tag. */\nconst BillableTag: FC<{ billable: boolean }> = ({ billable }) =>\n billable ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current opacity-70\" />\n Billable\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-tertiary\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current opacity-60\" />\n Non-billable\n </span>\n );\n\nexport const TimePage: FC = () => {\n const [statusFilter, setStatusFilter] = useState<PswTimeEntryStatus | ''>('');\n const [logging, setLogging] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const { data, isLoading, error, refetch } = useTimeEntries({ status: statusFilter || undefined });\n const submit = useSubmitTimeEntry();\n const approve = useApproveTimeEntry();\n\n const totalMinutes = (data?.items ?? []).reduce((s, t) => s + t.minutes, 0);\n const billableMinutes = (data?.items ?? [])\n .filter((t) => t.billable)\n .reduce((s, t) => s + t.minutes, 0);\n const utilization = totalMinutes > 0 ? Math.round((billableMinutes / totalMinutes) * 100) : 0;\n\n const act = (\n fn: {\n mutate: (id: string, opts: { onSuccess: () => void; onError: (e: unknown) => void }) => void;\n },\n id: string,\n verb: string\n ) =>\n fn.mutate(id, {\n onSuccess: () => setBanner({ tone: 'success', text: `Time entry ${verb}.` }),\n onError: (e) =>\n setBanner({ tone: 'danger', text: e instanceof Error ? e.message : 'Failed.' }),\n });\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Time\"\n subtitle=\"Capture expert effort against engagements and matters — the raw material of billing and utilization.\"\n icon={<Clock className=\"h-6 w-6\" />}\n actions={\n <Button onClick={() => setLogging(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <MetricCard\n label=\"Entries\"\n value={data?.totalCount ?? 0}\n icon={<ListChecks className=\"h-4 w-4\" />}\n />\n <MetricCard\n label=\"Total hours\"\n value={fmtMinutesAsHours(totalMinutes)}\n icon={<Clock className=\"h-4 w-4\" />}\n />\n <MetricCard\n label=\"Billable hours\"\n value={fmtMinutesAsHours(billableMinutes)}\n hint={`${utilization}% utilization`}\n icon={<Wallet className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Non-billable\"\n value={fmtMinutesAsHours(totalMinutes - billableMinutes)}\n icon={<CircleSlash className=\"h-4 w-4\" />}\n />\n </div>\n\n <div className=\"flex justify-end\">\n <NativeSelect\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as PswTimeEntryStatus | '')}\n className=\"sm:w-52\"\n >\n <option value=\"\">All statuses</option>\n {enumOptions(STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title=\"No time logged\"\n description=\"Log your first time entry against an engagement or matter.\"\n icon={<Clock className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setLogging(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n />\n ) : (\n <SectionCard title=\"Time entries\">\n <TableWrap>\n <thead>\n <tr>\n <Th>Date</Th>\n <Th>Narrative</Th>\n <Th>Hours</Th>\n <Th>Billable</Th>\n <Th>Status</Th>\n <Th className=\"text-right\">Actions</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((t) => (\n <tr key={t.id} className=\"transition-colors hover:bg-bg-sunken/60\">\n <Td className=\"whitespace-nowrap\">{fmtDate(t.date)}</Td>\n <Td className=\"max-w-xs truncate text-text-primary\">{t.narrative || '—'}</Td>\n <Td className=\"whitespace-nowrap font-medium text-text-primary\">\n {fmtMinutesAsHours(t.minutes)}\n </Td>\n <Td>\n <BillableTag billable={t.billable} />\n </Td>\n <Td>\n <StatusPill status={t.status} />\n </Td>\n <Td className=\"text-right\">\n <div className=\"flex justify-end gap-1\">\n {t.status === 'DRAFT' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={submit.isPending}\n onClick={() => act(submit, t.id, 'submitted')}\n >\n <Send className=\"mr-1 h-3.5 w-3.5\" />\n Submit\n </Button>\n )}\n {t.status === 'SUBMITTED' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={approve.isPending}\n onClick={() => act(approve, t.id, 'approved')}\n >\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" />\n Approve\n </Button>\n )}\n </div>\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n </SectionCard>\n )}\n\n {logging && (\n <LogTimeModal\n onClose={() => setLogging(false)}\n onDone={() => setBanner({ tone: 'success', text: 'Time logged.' })}\n />\n )}\n </div>\n );\n};\n\nconst LogTimeModal: FC<{ onClose: () => void; onDone: () => void }> = ({ onClose, onDone }) => {\n const log = useLogTimeEntry();\n const engagements = useEngagements({ pagination: { limit: 100 } });\n const matters = useMatters({ pagination: { limit: 100 } });\n const [form, setForm] = useState({\n against: 'engagement',\n engagementId: '',\n matterId: '',\n date: new Date().toISOString().slice(0, 10),\n hours: '',\n minutes: '',\n rate: '',\n billable: true,\n narrative: '',\n });\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof typeof form) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n const totalMinutes = (Number(form.hours) || 0) * 60 + (Number(form.minutes) || 0);\n if (totalMinutes <= 0) return setErr('Enter the time spent.');\n const ref =\n form.against === 'engagement'\n ? { engagementId: form.engagementId }\n : { matterId: form.matterId };\n if (form.against === 'engagement' && !form.engagementId) return setErr('Select an engagement.');\n if (form.against === 'matter' && !form.matterId) return setErr('Select a matter.');\n log.mutate(\n {\n ...ref,\n date: new Date(form.date).toISOString(),\n minutes: totalMinutes,\n rate: form.rate ? Number(form.rate) : 0,\n billable: form.billable,\n narrative: form.narrative.trim() || null,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Log time\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={log.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={log.isPending}>\n {log.isPending ? 'Logging…' : 'Log time'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Against\">\n <NativeSelect value={form.against} onChange={set('against')}>\n <option value=\"engagement\">Engagement</option>\n <option value=\"matter\">Matter</option>\n </NativeSelect>\n </Field>\n {form.against === 'engagement' ? (\n <Field label=\"Engagement\" required>\n <NativeSelect value={form.engagementId} onChange={set('engagementId')}>\n <option value=\"\">Select an engagement…</option>\n {engagements.data?.items.map((e) => (\n <option key={e.id} value={e.id}>\n {e.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n ) : (\n <Field label=\"Matter\" required>\n <NativeSelect value={form.matterId} onChange={set('matterId')}>\n <option value=\"\">Select a matter…</option>\n {matters.data?.items.map((m) => (\n <option key={m.id} value={m.id}>\n {m.title}\n </option>\n ))}\n </NativeSelect>\n </Field>\n )}\n <div className=\"grid grid-cols-3 gap-3\">\n <Field label=\"Date\">\n <TextInput type=\"date\" value={form.date} onChange={set('date')} />\n </Field>\n <Field label=\"Hours\">\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.hours}\n onChange={set('hours')}\n placeholder=\"2\"\n />\n </Field>\n <Field label=\"Minutes\">\n <TextInput\n type=\"number\"\n min=\"0\"\n max=\"59\"\n value={form.minutes}\n onChange={set('minutes')}\n placeholder=\"30\"\n />\n </Field>\n </div>\n <Field label=\"Rate (per hour)\">\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.rate}\n onChange={set('rate')}\n placeholder=\"450\"\n />\n </Field>\n <label className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-canvas px-3 py-2.5 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.billable}\n onChange={(e) => setForm((f) => ({ ...f, billable: e.target.checked }))}\n />\n <span className=\"flex items-center gap-1.5\">\n <Wallet className=\"h-3.5 w-3.5 text-status-success-text\" />\n Billable\n </span>\n </label>\n <Field label=\"Narrative\">\n <TextArea\n value={form.narrative}\n onChange={set('narrative')}\n placeholder=\"Reviewed working papers and drafted findings memo…\"\n />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimePage.js","names":[],"sources":["../../../src/proserviceworld/pages/TimePage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, Clock, Send, CheckCircle2, ListChecks, Wallet, CircleSlash } from 'lucide-react';\nimport {\n useTimeEntries,\n useLogTimeEntry,\n useSubmitTimeEntry,\n useApproveTimeEntry,\n useEngagements,\n useMatters,\n type PswTimeEntryStatus,\n} from '../hooks/useProServiceWorldApi';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n NativeSelect,\n Banner,\n MetricCard,\n TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n fmtMinutesAsHours,\n} from '../components/ui';\n\nconst STATUSES: PswTimeEntryStatus[] = ['DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED', 'BILLED'];\n\n/** Compact billable / non-billable tag. */\nconst BillableTag: FC<{ billable: boolean }> = ({ billable }) =>\n billable ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current opacity-70\" />\n Billable\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-tertiary\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current opacity-60\" />\n Non-billable\n </span>\n );\n\nexport const TimePage: FC = () => {\n const [statusFilter, setStatusFilter] = useState<PswTimeEntryStatus | ''>('');\n const [logging, setLogging] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const { data, isLoading, error, refetch } = useTimeEntries({ status: statusFilter || undefined });\n const submit = useSubmitTimeEntry();\n const approve = useApproveTimeEntry();\n\n const totalMinutes = (data?.items ?? []).reduce((s, t) => s + t.minutes, 0);\n const billableMinutes = (data?.items ?? [])\n .filter((t) => t.billable)\n .reduce((s, t) => s + t.minutes, 0);\n const utilization = totalMinutes > 0 ? Math.round((billableMinutes / totalMinutes) * 100) : 0;\n\n const act = (\n fn: {\n mutate: (id: string, opts: { onSuccess: () => void; onError: (e: unknown) => void }) => void;\n },\n id: string,\n verb: string\n ) =>\n fn.mutate(id, {\n onSuccess: () => setBanner({ tone: 'success', text: `Time entry ${verb}.` }),\n onError: (e) =>\n setBanner({ tone: 'danger', text: e instanceof Error ? e.message : 'Failed.' }),\n });\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Time\"\n subtitle=\"Capture expert effort against engagements and matters — the raw material of billing and utilization.\"\n icon={<Clock className=\"h-6 w-6\" />}\n actions={\n <Button onClick={() => setLogging(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <MetricCard\n label=\"Entries\"\n value={data?.totalCount ?? 0}\n icon={<ListChecks className=\"h-4 w-4\" />}\n />\n <MetricCard\n label=\"Total hours\"\n value={fmtMinutesAsHours(totalMinutes)}\n icon={<Clock className=\"h-4 w-4\" />}\n />\n <MetricCard\n label=\"Billable hours\"\n value={fmtMinutesAsHours(billableMinutes)}\n hint={`${utilization}% utilization`}\n icon={<Wallet className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Non-billable\"\n value={fmtMinutesAsHours(totalMinutes - billableMinutes)}\n icon={<CircleSlash className=\"h-4 w-4\" />}\n />\n </div>\n\n <div className=\"flex justify-end\">\n <NativeSelect\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as PswTimeEntryStatus | '')}\n className=\"sm:w-52\"\n >\n <option value=\"\">All statuses</option>\n {enumOptions(STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title=\"No time logged\"\n description=\"Log your first time entry against an engagement or matter.\"\n icon={<Clock className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setLogging(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n />\n ) : (\n <SectionCard title=\"Time entries\">\n <TableWrap>\n <thead>\n <tr>\n <Th>Date</Th>\n <Th>Narrative</Th>\n <Th>Hours</Th>\n <Th>Billable</Th>\n <Th>Status</Th>\n <Th className=\"text-right\">Actions</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((t) => (\n <tr key={t.id} className=\"transition-colors hover:bg-bg-sunken/60\">\n <Td className=\"whitespace-nowrap\">{fmtDate(t.date)}</Td>\n <Td className=\"max-w-xs truncate text-text-primary\">{t.narrative || '—'}</Td>\n <Td className=\"whitespace-nowrap font-medium text-text-primary\">\n {fmtMinutesAsHours(t.minutes)}\n </Td>\n <Td>\n <BillableTag billable={t.billable} />\n </Td>\n <Td>\n <StatusPill status={t.status} />\n </Td>\n <Td className=\"text-right\">\n <div className=\"flex justify-end gap-1\">\n {t.status === 'DRAFT' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={submit.isPending}\n onClick={() => act(submit, t.id, 'submitted')}\n >\n <Send className=\"mr-1 h-3.5 w-3.5\" />\n Submit\n </Button>\n )}\n {t.status === 'SUBMITTED' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={approve.isPending}\n onClick={() => act(approve, t.id, 'approved')}\n >\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" />\n Approve\n </Button>\n )}\n </div>\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n </SectionCard>\n )}\n\n {logging && (\n <LogTimeModal\n onClose={() => setLogging(false)}\n onDone={() => setBanner({ tone: 'success', text: 'Time logged.' })}\n />\n )}\n </div>\n );\n};\n\nconst LogTimeModal: FC<{ onClose: () => void; onDone: () => void }> = ({ onClose, onDone }) => {\n const log = useLogTimeEntry();\n const engagements = useEngagements({ pagination: { limit: 100 } });\n const matters = useMatters({ pagination: { limit: 100 } });\n const [form, setForm] = useState({\n against: 'engagement',\n engagementId: '',\n matterId: '',\n date: new Date().toISOString().slice(0, 10),\n hours: '',\n minutes: '',\n rate: '',\n billable: true,\n narrative: '',\n });\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof typeof form) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n const totalMinutes = (Number(form.hours) || 0) * 60 + (Number(form.minutes) || 0);\n if (totalMinutes <= 0) return setErr('Enter the time spent.');\n const ref =\n form.against === 'engagement'\n ? { engagementId: form.engagementId }\n : { matterId: form.matterId };\n if (form.against === 'engagement' && !form.engagementId) return setErr('Select an engagement.');\n if (form.against === 'matter' && !form.matterId) return setErr('Select a matter.');\n log.mutate(\n {\n ...ref,\n date: new Date(form.date).toISOString(),\n minutes: totalMinutes,\n rate: form.rate ? Number(form.rate) : 0,\n billable: form.billable,\n narrative: form.narrative.trim() || null,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Log time\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={log.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={log.isPending}>\n {log.isPending ? 'Logging…' : 'Log time'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Against\">\n <NativeSelect value={form.against} onChange={set('against')}>\n <option value=\"engagement\">Engagement</option>\n <option value=\"matter\">Matter</option>\n </NativeSelect>\n </Field>\n {form.against === 'engagement' ? (\n <Field label=\"Engagement\" required>\n <NativeSelect value={form.engagementId} onChange={set('engagementId')}>\n <option value=\"\">Select an engagement…</option>\n {engagements.data?.items.map((e) => (\n <option key={e.id} value={e.id}>\n {e.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n ) : (\n <Field label=\"Matter\" required>\n <NativeSelect value={form.matterId} onChange={set('matterId')}>\n <option value=\"\">Select a matter…</option>\n {matters.data?.items.map((m) => (\n <option key={m.id} value={m.id}>\n {m.title}\n </option>\n ))}\n </NativeSelect>\n </Field>\n )}\n <div className=\"grid grid-cols-3 gap-3\">\n <Field label=\"Date\">\n <TextInput type=\"date\" value={form.date} onChange={set('date')} />\n </Field>\n <Field label=\"Hours\">\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.hours}\n onChange={set('hours')}\n placeholder=\"2\"\n />\n </Field>\n <Field label=\"Minutes\">\n <TextInput\n type=\"number\"\n min=\"0\"\n max=\"59\"\n value={form.minutes}\n onChange={set('minutes')}\n placeholder=\"30\"\n />\n </Field>\n </div>\n <Field label=\"Rate (per hour)\">\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.rate}\n onChange={set('rate')}\n placeholder=\"450\"\n />\n </Field>\n <label className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-canvas px-3 py-2.5 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.billable}\n onChange={(e) => setForm((f) => ({ ...f, billable: e.target.checked }))}\n />\n <span className=\"flex items-center gap-1.5\">\n <Wallet className=\"h-3.5 w-3.5 text-status-success-text\" />\n Billable\n </span>\n </label>\n <Field label=\"Narrative\">\n <TextArea\n value={form.narrative}\n onChange={set('narrative')}\n placeholder=\"Reviewed working papers and drafted findings memo…\"\n />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProServiceWorldProvider.js","names":[],"sources":["../../../src/proserviceworld/providers/ProServiceWorldProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo } from 'react';\nimport { MemoryRouter } from 'react-router-dom';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport type { ProServiceWorldRootProps } from '../types';\n\ntype ProServiceWorldContextValue = ProServiceWorldRootProps;\n\nconst ProServiceWorldContext = createContext<ProServiceWorldContextValue | null>(null);\n\nexport const useProServiceWorld = () => {\n const context = useContext(ProServiceWorldContext);\n if (!context) throw new Error('useProServiceWorld must be used within ProServiceWorldProvider');\n return context;\n};\n\nexport const ProServiceWorldProvider: FC<PropsWithChildren<ProServiceWorldRootProps>> = ({\n children,\n ...props\n}) => {\n const queryClient = useMemo(\n () =>\n new QueryClient({\n defaultOptions: {\n queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000 },\n },\n }),\n []\n );\n\n const content = (\n <QueryClientProvider client={queryClient}>\n <ProServiceWorldContext.Provider value={props}>{children}</ProServiceWorldContext.Provider>\n </QueryClientProvider>\n );\n\n if (props.basePath) return content;\n return <MemoryRouter>{content}</MemoryRouter>;\n};\n"],"mappings":";;;;;AAQA,IAAM,IAAyB,EAAkD,KAAK,EAEzE,UAA2B;CACtC,IAAM,IAAU,EAAW,EAAuB;AAClD,KAAI,CAAC,EAAS,OAAU,MAAM,iEAAiE;AAC/F,QAAO;GAGI,KAA4E,EACvF,aACA,GAAG,QACC;CAWJ,IAAM,IACJ,kBAAC,GAAD;EAAqB,QAXH,QAEhB,IAAI,EAAY,EACd,gBAAgB,EACd,SAAS;GAAE,OAAO;GAAG,sBAAsB;GAAO,WAAW,MAAS;GAAM,EAC7E,EACF,CAAC,EACJ,EAAE,CACH;YAIG,kBAAC,EAAuB,UAAxB;GAAiC,OAAO;GAAQ;GAA2C,CAAA;EACvE,CAAA;AAIxB,QADI,EAAM,WAAiB,IACpB,kBAAC,GAAD,EAAA,UAAe,GAAuB,CAAA"}
1
+ {"version":3,"file":"ProServiceWorldProvider.js","names":[],"sources":["../../../src/proserviceworld/providers/ProServiceWorldProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo } from 'react';\nimport { MemoryRouter } from 'react-router-dom';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport type { ProServiceWorldRootProps } from '../types';\n\ntype ProServiceWorldContextValue = ProServiceWorldRootProps;\n\nconst ProServiceWorldContext = createContext<ProServiceWorldContextValue | null>(null);\n\nexport const useProServiceWorld = () => {\n const context = useContext(ProServiceWorldContext);\n if (!context) throw new Error('useProServiceWorld must be used within ProServiceWorldProvider');\n return context;\n};\n\nexport const ProServiceWorldProvider: FC<PropsWithChildren<ProServiceWorldRootProps>> = ({\n children,\n ...props\n}) => {\n const queryClient = useMemo(\n () =>\n new QueryClient({\n defaultOptions: {\n queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000 },\n },\n }),\n []\n );\n\n const content = (\n <QueryClientProvider client={queryClient}>\n <ProServiceWorldContext.Provider value={props}>{children}</ProServiceWorldContext.Provider>\n </QueryClientProvider>\n );\n\n if (props.basePath) return content;\n return <MemoryRouter>{content}</MemoryRouter>;\n};\n"],"mappings":";;;;;AAQA,IAAM,IAAyB,EAAkD,IAAI,GAExE,UAA2B;CACtC,IAAM,IAAU,EAAW,CAAsB;CACjD,IAAI,CAAC,GAAS,MAAU,MAAM,gEAAgE;CAC9F,OAAO;AACT,GAEa,KAA4E,EACvF,aACA,GAAG,QACC;CAWJ,IAAM,IACJ,kBAAC,GAAD;EAAqB,QAXH,QAEhB,IAAI,EAAY,EACd,gBAAgB,EACd,SAAS;GAAE,OAAO;GAAG,sBAAsB;GAAO,WAAW,MAAS;EAAK,EAC7E,EACF,CAAC,GACH,CAAC,CAI4B;YAC3B,kBAAC,EAAuB,UAAxB;GAAiC,OAAO;GAAQ;EAA0C,CAAA;CACvE,CAAA;CAIvB,OADI,EAAM,WAAiB,IACpB,kBAAC,GAAD,EAAA,UAAe,EAAsB,CAAA;AAC9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"assistantApi.js","names":[],"sources":["../../../src/proserviceworld/services/assistantApi.ts"],"sourcesContent":["/**\n * AI Assistant API service for ProServiceWorld.\n *\n * All requests go through the workspace GraphQL gateway:\n * Frontend → wspace-public-gateway → wspace-sandbox-svc → ai-assistant container\n *\n * Uses the host app's graphqlFetch helper so auth/workspace headers are attached\n * automatically; extra headers are supplied for sandbox-specific auth context.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport type { AssistantArtifact, AssistantMode, PageContext } from '../types';\n\nconst HARBOR_REGISTRY = 'burdenoffregistry.azurecr.io';\n\nfunction resolveImageTag(): string {\n if (import.meta.env.VITE_APP_ENV === 'prod') return 'prod';\n if (import.meta.env.VITE_DEPLOY_ENV === 'prod') return 'prod';\n if (import.meta.env.PROD) return 'prod';\n return 'alpha';\n}\n\nconst DEFAULT_IMAGE_TAG = resolveImageTag();\nconst ASSISTANT_API_AUTH_STRATEGY = 'sandbox-app-client-headers-v1';\n\nfunction getImageForMode(mode: AssistantMode): string {\n const imageMode = mode === 'assistant' ? 'api-calls' : mode;\n return `${HARBOR_REGISTRY}/wspace/ai-assistant-${imageMode}:${DEFAULT_IMAGE_TAG}`;\n}\n\nfunction getResourcesForMode(): {\n cpuRequest: string;\n cpuLimit: string;\n memoryRequest: string;\n memoryLimit: string;\n} {\n return {\n cpuRequest: '150m',\n cpuLimit: '750m',\n memoryRequest: '384Mi',\n memoryLimit: '1536Mi',\n };\n}\n\nfunction resolveSandboxAssistantMode(sandbox: {\n metadata?: Record<string, unknown> | null;\n name?: string | null;\n}): AssistantMode | null {\n const metadataMode = sandbox.metadata?.assistantMode;\n if (\n metadataMode === 'assistant' ||\n metadataMode === 'general' ||\n metadataMode === 'api-calls' ||\n metadataMode === 'vibe-plugins'\n ) {\n return metadataMode;\n }\n\n const name = sandbox.name ?? '';\n if (name.startsWith('ai-assistant-vibe-plugins-')) return 'vibe-plugins';\n if (name.startsWith('ai-assistant-api-calls-')) return 'api-calls';\n if (name.startsWith('ai-assistant-general-')) return 'general';\n if (name.startsWith('ai-assistant-assistant-')) return 'assistant';\n return null;\n}\n\nexport interface AssistantPromptResult {\n response: string;\n tokens?: { input: number; output: number };\n artifacts?: AssistantArtifact[];\n}\n\nexport interface AssistantRawMessagePart {\n type?: string;\n text?: string;\n tool?: string;\n state?: {\n status?: string;\n input?: unknown;\n output?: unknown;\n raw?: unknown;\n };\n}\n\nexport interface AssistantRawMessage {\n info?: {\n id?: string;\n role?: string;\n providerID?: string;\n modelID?: string;\n tokens?: { input: number; output: number };\n time?: {\n created?: number;\n completed?: number;\n };\n };\n parts?: AssistantRawMessagePart[];\n}\n\nexport interface AssistantSandboxAuthContext {\n accessToken?: string | null;\n workspaceToken?: string | null;\n userId?: string | null;\n organizationId?: string | null;\n}\n\nexport interface AssistantSessionInfo {\n id: string;\n mode: string;\n}\n\ninterface GraphQLResponse<TData> {\n data?: TData;\n errors?: Array<{ message: string }>;\n}\n\nfunction buildContextInput(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): { workspaceId: string; custom?: { organizationId?: string } } {\n return {\n workspaceId,\n ...(authContext?.organizationId\n ? { custom: { organizationId: authContext.organizationId } }\n : {}),\n };\n}\n\nfunction buildExtraHeaders(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Record<string, string> {\n const headers: Record<string, string> = {};\n\n if (authContext?.accessToken) {\n headers.authorization = `Bearer ${authContext.accessToken}`;\n }\n\n if (authContext?.workspaceToken) {\n headers['x-workspace-authorization'] = `Bearer ${authContext.workspaceToken}`;\n }\n\n headers['x-workspace-id'] = workspaceId;\n\n if (authContext?.userId) {\n headers['x-actor-id'] = authContext.userId;\n headers['x-actor-type'] = 'user';\n }\n\n if (authContext?.organizationId) {\n headers['x-org-id'] = authContext.organizationId;\n }\n\n return headers;\n}\n\nasync function executeSandboxGraphQL<TData>(params: {\n query: string;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n baseUrl?: string;\n}): Promise<TData> {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl: params.baseUrl,\n query: params.query,\n variables: params.variables,\n workspaceToken: true,\n extraHeaders: buildExtraHeaders(params.workspaceId, params.authContext),\n suppressGlobalErrorEvent: true,\n });\n\n if (response.errors?.length) {\n const err = new Error(\n response.errors[0]?.message ?? 'Sandbox GraphQL request failed'\n ) as Error & {\n result?: unknown;\n };\n err.result = response as unknown as GraphQLResponse<TData>;\n throw err;\n }\n\n if (!response.data) {\n throw new Error('Sandbox GraphQL returned no data');\n }\n\n return response.data;\n}\n\nexport async function findExistingSandbox(\n baseUrl: string | undefined,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<string | null> {\n const data = await executeSandboxGraphQL<{\n getActiveSandboxes: Array<{\n id: string;\n name: string;\n status: string;\n metadata: Record<string, unknown> | null;\n template: string;\n createdBy: string;\n }>;\n }>({\n query: `\n query GetActiveSandboxes($context: ContextInput!) {\n getActiveSandboxes(context: $context) {\n id\n name\n status\n metadata\n template\n createdBy\n }\n }\n `,\n variables: { context: buildContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n baseUrl,\n });\n const sandboxes = data.getActiveSandboxes ?? [];\n\n return (\n sandboxes.find(\n (s) =>\n (s.status === 'RUNNING' || s.status === 'PROVISIONING' || s.status === 'PENDING') &&\n s.template === 'AI_BUILDER' &&\n resolveSandboxAssistantMode(s) === mode &&\n (!authContext?.userId || s.createdBy === authContext.userId) &&\n (s.metadata?.authStrategy as string | undefined) === ASSISTANT_API_AUTH_STRATEGY &&\n (s.metadata?.imageTag as string | undefined) === DEFAULT_IMAGE_TAG\n )?.id ?? null\n );\n}\n\nexport async function createAssistantSandbox(\n baseUrl: string | undefined,\n mode: AssistantMode,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<string> {\n const resources = getResourcesForMode();\n const env = [\n { name: 'AI_ASSISTANT_MODE', value: mode },\n { name: 'HOSTNAME', value: '0.0.0.0' },\n { name: 'WORKSPACE_ID', value: workspaceId },\n { name: 'AI_ASSISTANT_PRODUCT', value: 'proserviceworld' },\n ];\n\n const data = await executeSandboxGraphQL<{ createSandbox: { id: string } }>({\n query: `\n mutation CreateSandbox($input: CreateSandboxInput!) {\n createSandbox(input: $input) { id }\n }\n `,\n variables: {\n input: {\n name: `ai-assistant-${mode}-${Date.now()}`,\n description: `AI Assistant sandbox (${mode} mode)`,\n template: 'AI_BUILDER',\n mode: 'DEPLOYMENT',\n containers: [\n {\n name: 'ai-assistant',\n image: getImageForMode(mode),\n ports: [{ containerPort: 8080, name: 'http' }],\n env,\n },\n ],\n resources: {\n cpuRequest: resources.cpuRequest,\n cpuLimit: resources.cpuLimit,\n memoryRequest: resources.memoryRequest,\n memoryLimit: resources.memoryLimit,\n },\n ttlSeconds: 600,\n metadata: {\n assistantMode: mode,\n assistantOwnerUserId: authContext?.userId ?? null,\n assistantOrganizationId: authContext?.organizationId ?? null,\n authStrategy: ASSISTANT_API_AUTH_STRATEGY,\n imageTag: DEFAULT_IMAGE_TAG,\n },\n context: buildContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n\n if (!data?.createSandbox?.id) {\n throw new Error('createSandbox returned empty response');\n }\n return data.createSandbox.id;\n}\n\nexport async function waitForSandboxReady(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 300_000,\n onProgress?: (message: string) => void\n): Promise<void> {\n const start = Date.now();\n while (Date.now() - start < maxWaitMs) {\n const data = await executeSandboxGraphQL<{\n getSandbox: { status?: string; errorMessage?: string } | null;\n }>({\n query: `\n query GetSandbox($id: ID!, $context: ContextInput!) {\n getSandbox(id: $id, context: $context) { status errorMessage }\n }\n `,\n variables: { id: sandboxId, context: buildContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n baseUrl,\n });\n const status = data?.getSandbox?.status;\n const errorMessage = data?.getSandbox?.errorMessage;\n if (status === 'RUNNING') return;\n if (status === 'FAILED' || status === 'STOPPED' || status === 'EXPIRED') {\n throw new Error(\n errorMessage\n ? `Sandbox ${status.toLowerCase()}: ${errorMessage}`\n : `Sandbox ${status.toLowerCase()}`\n );\n }\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - start) / 1000));\n onProgress?.(`Starting assistant environment... (${elapsedSeconds}s)`);\n await new Promise((r) => setTimeout(r, 2000));\n }\n throw new Error('Sandbox startup timed out');\n}\n\ninterface ProxyResponse {\n proxySandboxRequest: {\n status: number;\n body: Record<string, unknown>;\n };\n}\n\nasync function proxyToSandbox(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n path: string,\n method: string,\n body?: unknown,\n authContext?: AssistantSandboxAuthContext\n): Promise<Record<string, unknown>> {\n const result = await executeSandboxGraphQL<ProxyResponse>({\n query: `\n mutation ProxySandboxRequest($input: ProxySandboxRequestInput!) {\n proxySandboxRequest(input: $input) { status body }\n }\n `,\n variables: {\n input: {\n sandboxId,\n path,\n method,\n body,\n context: buildContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n\n const proxyResult = result.proxySandboxRequest;\n if (!proxyResult || proxyResult.status >= 400) {\n const errBody = proxyResult?.body as Record<string, unknown> | undefined;\n throw new Error(\n (errBody?.error as string) ?? `Proxy error: ${proxyResult?.status ?? 'unknown'}`\n );\n }\n\n return proxyResult.body;\n}\n\nexport async function waitForAssistantServiceReady(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 90_000,\n onProgress?: (message: string) => void\n): Promise<void> {\n const startedAt = Date.now();\n\n while (Date.now() - startedAt < maxWaitMs) {\n try {\n await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n '/health',\n 'GET',\n undefined,\n authContext\n );\n return;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (\n !/proxy error: 502|proxy error: 503|proxy error: 404|service not available/i.test(message)\n ) {\n throw error;\n }\n }\n\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));\n onProgress?.(`Connecting to assistant... (${elapsedSeconds}s)`);\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n throw new Error('Assistant service did not become healthy before timeout');\n}\n\nexport async function createAssistantSession(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<{ sessionId: string }> {\n const body = await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n '/sessions',\n 'POST',\n { mode },\n authContext\n );\n const session = body.session as Record<string, unknown>;\n return { sessionId: session.id as string };\n}\n\nexport async function sendAssistantPromptAsync(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n prompt: string,\n mode: AssistantMode,\n pageContext?: PageContext,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n const requestBody = {\n mode,\n prompt,\n pageContext: pageContext ?? undefined,\n };\n\n await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n `/sessions/${sessionId}/prompt-async`,\n 'POST',\n requestBody,\n authContext\n );\n}\n\nexport async function getAssistantMessages(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: AssistantSandboxAuthContext,\n limit = 50\n): Promise<AssistantRawMessage[]> {\n const body = await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n `/sessions/${sessionId}/messages?limit=${limit}`,\n 'GET',\n undefined,\n authContext\n );\n\n return (body.messages as AssistantRawMessage[] | undefined) ?? [];\n}\n\nexport async function extendAssistantSandboxTTL(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n additionalSeconds = 600,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await executeSandboxGraphQL<{ extendSandboxTTL: { id: string } }>({\n query: `\n mutation ExtendSandboxTTL($id: ID!, $additionalSeconds: Int!, $context: ContextInput!) {\n extendSandboxTTL(id: $id, additionalSeconds: $additionalSeconds, context: $context) { id }\n }\n `,\n variables: {\n id: sandboxId,\n additionalSeconds,\n context: buildContextInput(workspaceId, authContext),\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n}\n\n/**\n * Pre-flight AI-credit gate. Resolves to `true` when the caller still has\n * AI-credit budget; throws a quota error when the monthly token budget is\n * depleted.\n */\nexport async function checkAssistantAiCredits(\n baseUrl: string | undefined,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<boolean> {\n const data = await executeSandboxGraphQL<{ checkAiCredits: boolean }>({\n query: `mutation CheckAiCredits { checkAiCredits }`,\n variables: {},\n workspaceId,\n authContext,\n baseUrl,\n });\n return data.checkAiCredits;\n}\n\n/**\n * Post-turn charge. Reports raw input/output token counts + the model so the\n * service computes cost-weighted credits server-side. Best-effort — callers\n * must not block the displayed answer on it.\n */\nexport async function reportAssistantAiTokenUsage(\n baseUrl: string | undefined,\n workspaceId: string,\n sessionId: string,\n messageId: string,\n model: string | undefined,\n tokens: { input: number; output: number },\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n const inputTokens = Math.max(0, Math.round(tokens.input ?? 0));\n const outputTokens = Math.max(0, Math.round(tokens.output ?? 0));\n if (inputTokens + outputTokens === 0) return;\n if (!model) return;\n\n await executeSandboxGraphQL<{ reportAiTokenUsage: boolean }>({\n query: `\n mutation ReportAiTokenUsage($input: ReportAiTokenUsageInput!) {\n reportAiTokenUsage(input: $input)\n }\n `,\n variables: {\n input: { sessionId, messageId, model, inputTokens, outputTokens },\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n}\n"],"mappings":"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AM,IAAI,SAAS,MAAY,WAAW,GAAS,IAAK,CAAC;;AAG3D,OAAU,MAAM,0DAA0D;;AAG5E,eAAsB,EACpB,GACA,GACA,GACA,GACA,GACgC;AAWhC,QAAO,EAAE,YAVI,MAAM,EACjB,GACA,GACA,GACA,aACA,QACA,EAAE,SAAM,EACR,EACD,EACoB,QACO,IAAc;;AAG5C,eAAsB,EACpB,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACe;CACf,IAAM,IAAc;EAClB;EACA;EACA,aAAa,KAAe,KAAA;EAC7B;AAED,OAAM,EACJ,GACA,GACA,GACA,aAAa,EAAU,gBACvB,QACA,GACA,EACD;;AAGH,eAAsB,EACpB,GACA,GACA,GACA,GACA,GACA,IAAQ,IACwB;AAWhC,SAVa,MAAM,EACjB,GACA,GACA,GACA,aAAa,EAAU,kBAAkB,KACzC,OACA,KAAA,GACA,EACD,EAEY,YAAkD,EAAE;;AAGnE,eAAsB,EACpB,GACA,GACA,GACA,IAAoB,KACpB,GACe;AACf,OAAM,EAA4D;EAChE,OAAO;EAKP,WAAW;GACT,IAAI;GACJ;GACA,SAAS,EAAkB,GAAa,EAAY;GACrD;EACD;EACA;EACA;EACD,CAAC"}
1
+ {"version":3,"file":"assistantApi.js","names":[],"sources":["../../../src/proserviceworld/services/assistantApi.ts"],"sourcesContent":["/**\n * AI Assistant API service for ProServiceWorld.\n *\n * All requests go through the workspace GraphQL gateway:\n * Frontend → wspace-public-gateway → wspace-sandbox-svc → ai-assistant container\n *\n * Uses the host app's graphqlFetch helper so auth/workspace headers are attached\n * automatically; extra headers are supplied for sandbox-specific auth context.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport type { AssistantArtifact, AssistantMode, PageContext } from '../types';\n\nconst HARBOR_REGISTRY = 'burdenoffregistry.azurecr.io';\n\nfunction resolveImageTag(): string {\n if (import.meta.env.VITE_APP_ENV === 'prod') return 'prod';\n if (import.meta.env.VITE_DEPLOY_ENV === 'prod') return 'prod';\n if (import.meta.env.PROD) return 'prod';\n return 'alpha';\n}\n\nconst DEFAULT_IMAGE_TAG = resolveImageTag();\nconst ASSISTANT_API_AUTH_STRATEGY = 'sandbox-app-client-headers-v1';\n\nfunction getImageForMode(mode: AssistantMode): string {\n const imageMode = mode === 'assistant' ? 'api-calls' : mode;\n return `${HARBOR_REGISTRY}/wspace/ai-assistant-${imageMode}:${DEFAULT_IMAGE_TAG}`;\n}\n\nfunction getResourcesForMode(): {\n cpuRequest: string;\n cpuLimit: string;\n memoryRequest: string;\n memoryLimit: string;\n} {\n return {\n cpuRequest: '150m',\n cpuLimit: '750m',\n memoryRequest: '384Mi',\n memoryLimit: '1536Mi',\n };\n}\n\nfunction resolveSandboxAssistantMode(sandbox: {\n metadata?: Record<string, unknown> | null;\n name?: string | null;\n}): AssistantMode | null {\n const metadataMode = sandbox.metadata?.assistantMode;\n if (\n metadataMode === 'assistant' ||\n metadataMode === 'general' ||\n metadataMode === 'api-calls' ||\n metadataMode === 'vibe-plugins'\n ) {\n return metadataMode;\n }\n\n const name = sandbox.name ?? '';\n if (name.startsWith('ai-assistant-vibe-plugins-')) return 'vibe-plugins';\n if (name.startsWith('ai-assistant-api-calls-')) return 'api-calls';\n if (name.startsWith('ai-assistant-general-')) return 'general';\n if (name.startsWith('ai-assistant-assistant-')) return 'assistant';\n return null;\n}\n\nexport interface AssistantPromptResult {\n response: string;\n tokens?: { input: number; output: number };\n artifacts?: AssistantArtifact[];\n}\n\nexport interface AssistantRawMessagePart {\n type?: string;\n text?: string;\n tool?: string;\n state?: {\n status?: string;\n input?: unknown;\n output?: unknown;\n raw?: unknown;\n };\n}\n\nexport interface AssistantRawMessage {\n info?: {\n id?: string;\n role?: string;\n providerID?: string;\n modelID?: string;\n tokens?: { input: number; output: number };\n time?: {\n created?: number;\n completed?: number;\n };\n };\n parts?: AssistantRawMessagePart[];\n}\n\nexport interface AssistantSandboxAuthContext {\n accessToken?: string | null;\n workspaceToken?: string | null;\n userId?: string | null;\n organizationId?: string | null;\n}\n\nexport interface AssistantSessionInfo {\n id: string;\n mode: string;\n}\n\ninterface GraphQLResponse<TData> {\n data?: TData;\n errors?: Array<{ message: string }>;\n}\n\nfunction buildContextInput(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): { workspaceId: string; custom?: { organizationId?: string } } {\n return {\n workspaceId,\n ...(authContext?.organizationId\n ? { custom: { organizationId: authContext.organizationId } }\n : {}),\n };\n}\n\nfunction buildExtraHeaders(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Record<string, string> {\n const headers: Record<string, string> = {};\n\n if (authContext?.accessToken) {\n headers.authorization = `Bearer ${authContext.accessToken}`;\n }\n\n if (authContext?.workspaceToken) {\n headers['x-workspace-authorization'] = `Bearer ${authContext.workspaceToken}`;\n }\n\n headers['x-workspace-id'] = workspaceId;\n\n if (authContext?.userId) {\n headers['x-actor-id'] = authContext.userId;\n headers['x-actor-type'] = 'user';\n }\n\n if (authContext?.organizationId) {\n headers['x-org-id'] = authContext.organizationId;\n }\n\n return headers;\n}\n\nasync function executeSandboxGraphQL<TData>(params: {\n query: string;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n baseUrl?: string;\n}): Promise<TData> {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl: params.baseUrl,\n query: params.query,\n variables: params.variables,\n workspaceToken: true,\n extraHeaders: buildExtraHeaders(params.workspaceId, params.authContext),\n suppressGlobalErrorEvent: true,\n });\n\n if (response.errors?.length) {\n const err = new Error(\n response.errors[0]?.message ?? 'Sandbox GraphQL request failed'\n ) as Error & {\n result?: unknown;\n };\n err.result = response as unknown as GraphQLResponse<TData>;\n throw err;\n }\n\n if (!response.data) {\n throw new Error('Sandbox GraphQL returned no data');\n }\n\n return response.data;\n}\n\nexport async function findExistingSandbox(\n baseUrl: string | undefined,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<string | null> {\n const data = await executeSandboxGraphQL<{\n getActiveSandboxes: Array<{\n id: string;\n name: string;\n status: string;\n metadata: Record<string, unknown> | null;\n template: string;\n createdBy: string;\n }>;\n }>({\n query: `\n query GetActiveSandboxes($context: ContextInput!) {\n getActiveSandboxes(context: $context) {\n id\n name\n status\n metadata\n template\n createdBy\n }\n }\n `,\n variables: { context: buildContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n baseUrl,\n });\n const sandboxes = data.getActiveSandboxes ?? [];\n\n return (\n sandboxes.find(\n (s) =>\n (s.status === 'RUNNING' || s.status === 'PROVISIONING' || s.status === 'PENDING') &&\n s.template === 'AI_BUILDER' &&\n resolveSandboxAssistantMode(s) === mode &&\n (!authContext?.userId || s.createdBy === authContext.userId) &&\n (s.metadata?.authStrategy as string | undefined) === ASSISTANT_API_AUTH_STRATEGY &&\n (s.metadata?.imageTag as string | undefined) === DEFAULT_IMAGE_TAG\n )?.id ?? null\n );\n}\n\nexport async function createAssistantSandbox(\n baseUrl: string | undefined,\n mode: AssistantMode,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<string> {\n const resources = getResourcesForMode();\n const env = [\n { name: 'AI_ASSISTANT_MODE', value: mode },\n { name: 'HOSTNAME', value: '0.0.0.0' },\n { name: 'WORKSPACE_ID', value: workspaceId },\n { name: 'AI_ASSISTANT_PRODUCT', value: 'proserviceworld' },\n ];\n\n const data = await executeSandboxGraphQL<{ createSandbox: { id: string } }>({\n query: `\n mutation CreateSandbox($input: CreateSandboxInput!) {\n createSandbox(input: $input) { id }\n }\n `,\n variables: {\n input: {\n name: `ai-assistant-${mode}-${Date.now()}`,\n description: `AI Assistant sandbox (${mode} mode)`,\n template: 'AI_BUILDER',\n mode: 'DEPLOYMENT',\n containers: [\n {\n name: 'ai-assistant',\n image: getImageForMode(mode),\n ports: [{ containerPort: 8080, name: 'http' }],\n env,\n },\n ],\n resources: {\n cpuRequest: resources.cpuRequest,\n cpuLimit: resources.cpuLimit,\n memoryRequest: resources.memoryRequest,\n memoryLimit: resources.memoryLimit,\n },\n ttlSeconds: 600,\n metadata: {\n assistantMode: mode,\n assistantOwnerUserId: authContext?.userId ?? null,\n assistantOrganizationId: authContext?.organizationId ?? null,\n authStrategy: ASSISTANT_API_AUTH_STRATEGY,\n imageTag: DEFAULT_IMAGE_TAG,\n },\n context: buildContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n\n if (!data?.createSandbox?.id) {\n throw new Error('createSandbox returned empty response');\n }\n return data.createSandbox.id;\n}\n\nexport async function waitForSandboxReady(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 300_000,\n onProgress?: (message: string) => void\n): Promise<void> {\n const start = Date.now();\n while (Date.now() - start < maxWaitMs) {\n const data = await executeSandboxGraphQL<{\n getSandbox: { status?: string; errorMessage?: string } | null;\n }>({\n query: `\n query GetSandbox($id: ID!, $context: ContextInput!) {\n getSandbox(id: $id, context: $context) { status errorMessage }\n }\n `,\n variables: { id: sandboxId, context: buildContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n baseUrl,\n });\n const status = data?.getSandbox?.status;\n const errorMessage = data?.getSandbox?.errorMessage;\n if (status === 'RUNNING') return;\n if (status === 'FAILED' || status === 'STOPPED' || status === 'EXPIRED') {\n throw new Error(\n errorMessage\n ? `Sandbox ${status.toLowerCase()}: ${errorMessage}`\n : `Sandbox ${status.toLowerCase()}`\n );\n }\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - start) / 1000));\n onProgress?.(`Starting assistant environment... (${elapsedSeconds}s)`);\n await new Promise((r) => setTimeout(r, 2000));\n }\n throw new Error('Sandbox startup timed out');\n}\n\ninterface ProxyResponse {\n proxySandboxRequest: {\n status: number;\n body: Record<string, unknown>;\n };\n}\n\nasync function proxyToSandbox(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n path: string,\n method: string,\n body?: unknown,\n authContext?: AssistantSandboxAuthContext\n): Promise<Record<string, unknown>> {\n const result = await executeSandboxGraphQL<ProxyResponse>({\n query: `\n mutation ProxySandboxRequest($input: ProxySandboxRequestInput!) {\n proxySandboxRequest(input: $input) { status body }\n }\n `,\n variables: {\n input: {\n sandboxId,\n path,\n method,\n body,\n context: buildContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n\n const proxyResult = result.proxySandboxRequest;\n if (!proxyResult || proxyResult.status >= 400) {\n const errBody = proxyResult?.body as Record<string, unknown> | undefined;\n throw new Error(\n (errBody?.error as string) ?? `Proxy error: ${proxyResult?.status ?? 'unknown'}`\n );\n }\n\n return proxyResult.body;\n}\n\nexport async function waitForAssistantServiceReady(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 90_000,\n onProgress?: (message: string) => void\n): Promise<void> {\n const startedAt = Date.now();\n\n while (Date.now() - startedAt < maxWaitMs) {\n try {\n await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n '/health',\n 'GET',\n undefined,\n authContext\n );\n return;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (\n !/proxy error: 502|proxy error: 503|proxy error: 404|service not available/i.test(message)\n ) {\n throw error;\n }\n }\n\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));\n onProgress?.(`Connecting to assistant... (${elapsedSeconds}s)`);\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n throw new Error('Assistant service did not become healthy before timeout');\n}\n\nexport async function createAssistantSession(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<{ sessionId: string }> {\n const body = await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n '/sessions',\n 'POST',\n { mode },\n authContext\n );\n const session = body.session as Record<string, unknown>;\n return { sessionId: session.id as string };\n}\n\nexport async function sendAssistantPromptAsync(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n prompt: string,\n mode: AssistantMode,\n pageContext?: PageContext,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n const requestBody = {\n mode,\n prompt,\n pageContext: pageContext ?? undefined,\n };\n\n await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n `/sessions/${sessionId}/prompt-async`,\n 'POST',\n requestBody,\n authContext\n );\n}\n\nexport async function getAssistantMessages(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: AssistantSandboxAuthContext,\n limit = 50\n): Promise<AssistantRawMessage[]> {\n const body = await proxyToSandbox(\n baseUrl,\n sandboxId,\n workspaceId,\n `/sessions/${sessionId}/messages?limit=${limit}`,\n 'GET',\n undefined,\n authContext\n );\n\n return (body.messages as AssistantRawMessage[] | undefined) ?? [];\n}\n\nexport async function extendAssistantSandboxTTL(\n baseUrl: string | undefined,\n sandboxId: string,\n workspaceId: string,\n additionalSeconds = 600,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await executeSandboxGraphQL<{ extendSandboxTTL: { id: string } }>({\n query: `\n mutation ExtendSandboxTTL($id: ID!, $additionalSeconds: Int!, $context: ContextInput!) {\n extendSandboxTTL(id: $id, additionalSeconds: $additionalSeconds, context: $context) { id }\n }\n `,\n variables: {\n id: sandboxId,\n additionalSeconds,\n context: buildContextInput(workspaceId, authContext),\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n}\n\n/**\n * Pre-flight AI-credit gate. Resolves to `true` when the caller still has\n * AI-credit budget; throws a quota error when the monthly token budget is\n * depleted.\n */\nexport async function checkAssistantAiCredits(\n baseUrl: string | undefined,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<boolean> {\n const data = await executeSandboxGraphQL<{ checkAiCredits: boolean }>({\n query: `mutation CheckAiCredits { checkAiCredits }`,\n variables: {},\n workspaceId,\n authContext,\n baseUrl,\n });\n return data.checkAiCredits;\n}\n\n/**\n * Post-turn charge. Reports raw input/output token counts + the model so the\n * service computes cost-weighted credits server-side. Best-effort — callers\n * must not block the displayed answer on it.\n */\nexport async function reportAssistantAiTokenUsage(\n baseUrl: string | undefined,\n workspaceId: string,\n sessionId: string,\n messageId: string,\n model: string | undefined,\n tokens: { input: number; output: number },\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n const inputTokens = Math.max(0, Math.round(tokens.input ?? 0));\n const outputTokens = Math.max(0, Math.round(tokens.output ?? 0));\n if (inputTokens + outputTokens === 0) return;\n if (!model) return;\n\n await executeSandboxGraphQL<{ reportAiTokenUsage: boolean }>({\n query: `\n mutation ReportAiTokenUsage($input: ReportAiTokenUsageInput!) {\n reportAiTokenUsage(input: $input)\n }\n `,\n variables: {\n input: { sessionId, messageId, model, inputTokens, outputTokens },\n },\n workspaceId,\n authContext,\n baseUrl,\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"createSandboxAssistantTransport.js","names":[],"sources":["../../../src/proserviceworld/services/createSandboxAssistantTransport.ts"],"sourcesContent":["/**\n * ProServiceWorld adapter for the shared fe-libs AssistantWidget.\n *\n * The floating widget (Layer 1) is backend-agnostic — it calls an injected\n * `AssistantTransport`. This hook builds a transport that reuses the sandbox\n * `api-calls` agent: it provisions/reuses a sandbox, opens a session, dispatches\n * the prompt async, then polls for the streamed answer. The agent introspects\n * the GraphQL schema and performs API calls on the user's behalf using their\n * workspace token, contextual to the current screen (via `gatherPageContext`).\n */\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\nimport {\n createAssistantSandbox,\n createAssistantSession,\n extendAssistantSandboxTTL,\n findExistingSandbox,\n getAssistantMessages,\n sendAssistantPromptAsync,\n waitForAssistantServiceReady,\n waitForSandboxReady,\n type AssistantRawMessage,\n type AssistantSandboxAuthContext,\n} from './assistantApi';\nimport { gatherPageContext } from '../utils/pageContext';\nimport type { AssistantMode } from '../types';\n\ninterface AssistantSendArgs {\n prompt: string;\n onProgress: (partialText: string) => void;\n signal: AbortSignal;\n}\n\nexport interface AssistantTransport {\n sendPrompt: (args: AssistantSendArgs) => Promise<{ text: string }>;\n}\n\nconst MODE: AssistantMode = 'assistant';\nconst STREAM_BUDGET_MS = 180_000;\nconst POLL_INTERVAL_MS = 1500;\nconst TTL_EXTEND_INTERVAL_MS = 30_000;\n\nfunction getProfileContextValue(key: 'workspaceId' | 'organizationId'): string {\n try {\n const activeContextKey =\n key === 'workspaceId'\n ? 'burdenoff-active-context-workspace'\n : 'burdenoff-active-context-organization';\n const activeContextValue = localStorage.getItem(activeContextKey);\n if (activeContextValue) return activeContextValue;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n if (!activeProfileId) return '';\n const raw = localStorage.getItem(`bf-p-${activeProfileId}-context`);\n if (!raw) return '';\n const context = JSON.parse(raw) as { workspaceId?: string; organizationId?: string };\n return context[key] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction getWorkspaceId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('workspace') ?? getProfileContextValue('workspaceId');\n}\n\nfunction getOrganizationId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('org') ?? getProfileContextValue('organizationId');\n}\n\nfunction getRawMessageCreatedAt(message: AssistantRawMessage): number {\n return message.info?.time?.created ?? 0;\n}\n\nfunction getAssistantText(parts: AssistantRawMessage['parts']): string {\n return (parts ?? [])\n .filter((part) => part.type === 'text' && typeof part.text === 'string')\n .map((part) => part.text?.trim() ?? '')\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction getToolProgress(parts: AssistantRawMessage['parts']): string[] {\n return (parts ?? [])\n .filter((part) => part.type === 'tool' && part.tool)\n .map((part) => {\n const status = part.state?.status ?? 'running';\n const tool = part.tool ?? 'tool';\n if (status === 'completed') return `Completed: ${tool}`;\n if (status === 'failed') return `Failed: ${tool}`;\n return `Running: ${tool}`;\n });\n}\n\nfunction buildProgress(\n messages: AssistantRawMessage[],\n sinceMs: number\n): { content: string; done: boolean } {\n const relevant = messages\n .filter(\n (message) => message.info?.role === 'assistant' && getRawMessageCreatedAt(message) >= sinceMs\n )\n .sort((left, right) => getRawMessageCreatedAt(left) - getRawMessageCreatedAt(right));\n\n if (relevant.length === 0) return { content: 'Thinking…', done: false };\n\n const latest = relevant[relevant.length - 1]!;\n const text = getAssistantText(latest.parts);\n const toolProgress = getToolProgress(latest.parts);\n const content = [text, ...toolProgress].filter(Boolean).join('\\n\\n') || 'Thinking…';\n\n return {\n content,\n done: Boolean(latest.info?.time?.completed) && (Boolean(text) || toolProgress.length > 0),\n };\n}\n\nfunction sanitize(response: string): string {\n return response\n .replace(/(Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/(X-Workspace-Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9._-]+\\.[A-Za-z0-9._-]+\\b/g, '[REDACTED_JWT]')\n .replace(/\\bsk-ant-[A-Za-z0-9-]+\\b/g, '[REDACTED_API_KEY]');\n}\n\nfunction isRecoverable(error: unknown): boolean {\n const message =\n error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n if (\n message.includes('rate limit exceeded') ||\n message.includes('unauthorized') ||\n message.includes('k8s api error 401')\n ) {\n return false;\n }\n return [\n 'sandbox not found',\n 'sandbox is not running',\n 'sandbox service not available yet',\n 'sandbox failed',\n 'sandbox startup timed out',\n 'assistant service did not become healthy',\n 'proxy error: 404',\n 'proxy error: 502',\n 'proxy error: 503',\n ].some((fragment) => message.includes(fragment));\n}\n\n/**\n * Returns a memoized `AssistantTransport` wired to the ProServiceWorld sandbox\n * `api-calls` agent. The sandbox + session are cached in refs so follow-up\n * turns reuse the warm environment for the lifetime of the host shell.\n */\nexport function useSandboxAssistantTransport(): AssistantTransport {\n const { apiGatewayUrl } = useProServiceWorld();\n const { getAccessToken, getWorkspaceToken, userId } = useAuthToken();\n\n const sandboxIdRef = useRef<string | null>(null);\n const sessionIdRef = useRef<string | null>(null);\n\n const ensureRuntime = useCallback(\n async (\n workspaceId: string,\n authContext: AssistantSandboxAuthContext\n ): Promise<{ sandboxId: string; sessionId: string }> => {\n let sandboxId = sandboxIdRef.current;\n if (!sandboxId) {\n sandboxId = await findExistingSandbox(apiGatewayUrl, workspaceId, MODE, authContext);\n if (!sandboxId) {\n if (!getAccessToken()) {\n throw new Error(\n 'API Calls mode requires an authenticated session. Please sign in again.'\n );\n }\n sandboxId = await createAssistantSandbox(apiGatewayUrl, MODE, workspaceId, authContext);\n }\n await waitForSandboxReady(apiGatewayUrl, sandboxId, workspaceId, authContext);\n await waitForAssistantServiceReady(apiGatewayUrl, sandboxId, workspaceId, authContext);\n sandboxIdRef.current = sandboxId;\n }\n\n let sessionId = sessionIdRef.current;\n if (!sessionId) {\n const result = await createAssistantSession(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n MODE,\n authContext\n );\n sessionId = result.sessionId;\n sessionIdRef.current = sessionId;\n }\n\n return { sandboxId, sessionId };\n },\n [apiGatewayUrl, getAccessToken]\n );\n\n const sendPrompt = useCallback(\n async ({ prompt, onProgress, signal }: AssistantSendArgs): Promise<{ text: string }> => {\n const workspaceId = getWorkspaceId();\n if (!workspaceId) {\n throw new Error('The assistant needs an active workspace. Open a workspace and try again.');\n }\n const authContext: AssistantSandboxAuthContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n };\n\n const run = async (attempt: 1 | 2): Promise<{ text: string }> => {\n try {\n const { sandboxId, sessionId } = await ensureRuntime(workspaceId, authContext);\n\n const startedAt = Date.now();\n await sendAssistantPromptAsync(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n sessionId,\n prompt,\n MODE,\n gatherPageContext(),\n authContext\n );\n\n const timeoutAt = Date.now() + STREAM_BUDGET_MS;\n let lastTtlExtensionAt = Date.now();\n let progress = buildProgress([], startedAt);\n\n while (Date.now() < timeoutAt) {\n if (signal.aborted) throw new Error('Cancelled');\n\n const messages = await getAssistantMessages(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n sessionId,\n authContext,\n 50\n );\n progress = buildProgress(messages, startedAt);\n onProgress(sanitize(progress.content));\n if (progress.done) break;\n\n if (Date.now() - lastTtlExtensionAt > TTL_EXTEND_INTERVAL_MS) {\n await extendAssistantSandboxTTL(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n 600,\n authContext\n ).catch(() => undefined);\n lastTtlExtensionAt = Date.now();\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (!progress.done) {\n throw new Error('The assistant timed out while responding. Please try again.');\n }\n\n return { text: sanitize(progress.content) };\n } catch (error) {\n if (attempt === 1 && isRecoverable(error)) {\n sandboxIdRef.current = null;\n sessionIdRef.current = null;\n return run(2);\n }\n throw error;\n }\n };\n\n return run(1);\n },\n [apiGatewayUrl, ensureRuntime, getAccessToken, getWorkspaceToken, userId]\n );\n\n return useMemo<AssistantTransport>(() => ({ sendPrompt }), [sendPrompt]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"createSandboxAssistantTransport.js","names":[],"sources":["../../../src/proserviceworld/services/createSandboxAssistantTransport.ts"],"sourcesContent":["/**\n * ProServiceWorld adapter for the shared fe-libs AssistantWidget.\n *\n * The floating widget (Layer 1) is backend-agnostic — it calls an injected\n * `AssistantTransport`. This hook builds a transport that reuses the sandbox\n * `api-calls` agent: it provisions/reuses a sandbox, opens a session, dispatches\n * the prompt async, then polls for the streamed answer. The agent introspects\n * the GraphQL schema and performs API calls on the user's behalf using their\n * workspace token, contextual to the current screen (via `gatherPageContext`).\n */\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\nimport {\n createAssistantSandbox,\n createAssistantSession,\n extendAssistantSandboxTTL,\n findExistingSandbox,\n getAssistantMessages,\n sendAssistantPromptAsync,\n waitForAssistantServiceReady,\n waitForSandboxReady,\n type AssistantRawMessage,\n type AssistantSandboxAuthContext,\n} from './assistantApi';\nimport { gatherPageContext } from '../utils/pageContext';\nimport type { AssistantMode } from '../types';\n\ninterface AssistantSendArgs {\n prompt: string;\n onProgress: (partialText: string) => void;\n signal: AbortSignal;\n}\n\nexport interface AssistantTransport {\n sendPrompt: (args: AssistantSendArgs) => Promise<{ text: string }>;\n}\n\nconst MODE: AssistantMode = 'assistant';\nconst STREAM_BUDGET_MS = 180_000;\nconst POLL_INTERVAL_MS = 1500;\nconst TTL_EXTEND_INTERVAL_MS = 30_000;\n\nfunction getProfileContextValue(key: 'workspaceId' | 'organizationId'): string {\n try {\n const activeContextKey =\n key === 'workspaceId'\n ? 'burdenoff-active-context-workspace'\n : 'burdenoff-active-context-organization';\n const activeContextValue = localStorage.getItem(activeContextKey);\n if (activeContextValue) return activeContextValue;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n if (!activeProfileId) return '';\n const raw = localStorage.getItem(`bf-p-${activeProfileId}-context`);\n if (!raw) return '';\n const context = JSON.parse(raw) as { workspaceId?: string; organizationId?: string };\n return context[key] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction getWorkspaceId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('workspace') ?? getProfileContextValue('workspaceId');\n}\n\nfunction getOrganizationId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('org') ?? getProfileContextValue('organizationId');\n}\n\nfunction getRawMessageCreatedAt(message: AssistantRawMessage): number {\n return message.info?.time?.created ?? 0;\n}\n\nfunction getAssistantText(parts: AssistantRawMessage['parts']): string {\n return (parts ?? [])\n .filter((part) => part.type === 'text' && typeof part.text === 'string')\n .map((part) => part.text?.trim() ?? '')\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction getToolProgress(parts: AssistantRawMessage['parts']): string[] {\n return (parts ?? [])\n .filter((part) => part.type === 'tool' && part.tool)\n .map((part) => {\n const status = part.state?.status ?? 'running';\n const tool = part.tool ?? 'tool';\n if (status === 'completed') return `Completed: ${tool}`;\n if (status === 'failed') return `Failed: ${tool}`;\n return `Running: ${tool}`;\n });\n}\n\nfunction buildProgress(\n messages: AssistantRawMessage[],\n sinceMs: number\n): { content: string; done: boolean } {\n const relevant = messages\n .filter(\n (message) => message.info?.role === 'assistant' && getRawMessageCreatedAt(message) >= sinceMs\n )\n .sort((left, right) => getRawMessageCreatedAt(left) - getRawMessageCreatedAt(right));\n\n if (relevant.length === 0) return { content: 'Thinking…', done: false };\n\n const latest = relevant[relevant.length - 1]!;\n const text = getAssistantText(latest.parts);\n const toolProgress = getToolProgress(latest.parts);\n const content = [text, ...toolProgress].filter(Boolean).join('\\n\\n') || 'Thinking…';\n\n return {\n content,\n done: Boolean(latest.info?.time?.completed) && (Boolean(text) || toolProgress.length > 0),\n };\n}\n\nfunction sanitize(response: string): string {\n return response\n .replace(/(Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/(X-Workspace-Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9._-]+\\.[A-Za-z0-9._-]+\\b/g, '[REDACTED_JWT]')\n .replace(/\\bsk-ant-[A-Za-z0-9-]+\\b/g, '[REDACTED_API_KEY]');\n}\n\nfunction isRecoverable(error: unknown): boolean {\n const message =\n error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n if (\n message.includes('rate limit exceeded') ||\n message.includes('unauthorized') ||\n message.includes('k8s api error 401')\n ) {\n return false;\n }\n return [\n 'sandbox not found',\n 'sandbox is not running',\n 'sandbox service not available yet',\n 'sandbox failed',\n 'sandbox startup timed out',\n 'assistant service did not become healthy',\n 'proxy error: 404',\n 'proxy error: 502',\n 'proxy error: 503',\n ].some((fragment) => message.includes(fragment));\n}\n\n/**\n * Returns a memoized `AssistantTransport` wired to the ProServiceWorld sandbox\n * `api-calls` agent. The sandbox + session are cached in refs so follow-up\n * turns reuse the warm environment for the lifetime of the host shell.\n */\nexport function useSandboxAssistantTransport(): AssistantTransport {\n const { apiGatewayUrl } = useProServiceWorld();\n const { getAccessToken, getWorkspaceToken, userId } = useAuthToken();\n\n const sandboxIdRef = useRef<string | null>(null);\n const sessionIdRef = useRef<string | null>(null);\n\n const ensureRuntime = useCallback(\n async (\n workspaceId: string,\n authContext: AssistantSandboxAuthContext\n ): Promise<{ sandboxId: string; sessionId: string }> => {\n let sandboxId = sandboxIdRef.current;\n if (!sandboxId) {\n sandboxId = await findExistingSandbox(apiGatewayUrl, workspaceId, MODE, authContext);\n if (!sandboxId) {\n if (!getAccessToken()) {\n throw new Error(\n 'API Calls mode requires an authenticated session. Please sign in again.'\n );\n }\n sandboxId = await createAssistantSandbox(apiGatewayUrl, MODE, workspaceId, authContext);\n }\n await waitForSandboxReady(apiGatewayUrl, sandboxId, workspaceId, authContext);\n await waitForAssistantServiceReady(apiGatewayUrl, sandboxId, workspaceId, authContext);\n sandboxIdRef.current = sandboxId;\n }\n\n let sessionId = sessionIdRef.current;\n if (!sessionId) {\n const result = await createAssistantSession(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n MODE,\n authContext\n );\n sessionId = result.sessionId;\n sessionIdRef.current = sessionId;\n }\n\n return { sandboxId, sessionId };\n },\n [apiGatewayUrl, getAccessToken]\n );\n\n const sendPrompt = useCallback(\n async ({ prompt, onProgress, signal }: AssistantSendArgs): Promise<{ text: string }> => {\n const workspaceId = getWorkspaceId();\n if (!workspaceId) {\n throw new Error('The assistant needs an active workspace. Open a workspace and try again.');\n }\n const authContext: AssistantSandboxAuthContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n };\n\n const run = async (attempt: 1 | 2): Promise<{ text: string }> => {\n try {\n const { sandboxId, sessionId } = await ensureRuntime(workspaceId, authContext);\n\n const startedAt = Date.now();\n await sendAssistantPromptAsync(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n sessionId,\n prompt,\n MODE,\n gatherPageContext(),\n authContext\n );\n\n const timeoutAt = Date.now() + STREAM_BUDGET_MS;\n let lastTtlExtensionAt = Date.now();\n let progress = buildProgress([], startedAt);\n\n while (Date.now() < timeoutAt) {\n if (signal.aborted) throw new Error('Cancelled');\n\n const messages = await getAssistantMessages(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n sessionId,\n authContext,\n 50\n );\n progress = buildProgress(messages, startedAt);\n onProgress(sanitize(progress.content));\n if (progress.done) break;\n\n if (Date.now() - lastTtlExtensionAt > TTL_EXTEND_INTERVAL_MS) {\n await extendAssistantSandboxTTL(\n apiGatewayUrl,\n sandboxId,\n workspaceId,\n 600,\n authContext\n ).catch(() => undefined);\n lastTtlExtensionAt = Date.now();\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (!progress.done) {\n throw new Error('The assistant timed out while responding. Please try again.');\n }\n\n return { text: sanitize(progress.content) };\n } catch (error) {\n if (attempt === 1 && isRecoverable(error)) {\n sandboxIdRef.current = null;\n sessionIdRef.current = null;\n return run(2);\n }\n throw error;\n }\n };\n\n return run(1);\n },\n [apiGatewayUrl, ensureRuntime, getAccessToken, getWorkspaceToken, userId]\n );\n\n return useMemo<AssistantTransport>(() => ({ sendPrompt }), [sendPrompt]);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import e from "../../node_modules/clsx/dist/clsx.js";
1
+ import { clsx as e } from "../../node_modules/clsx/dist/clsx.js";
2
2
  import { twMerge as t } from "../../node_modules/tailwind-merge/dist/bundle-mjs.js";
3
3
  //#region src/proserviceworld/utils/cn.ts
4
4
  function n(...n) {
@@ -1 +1 @@
1
- {"version":3,"file":"cn.js","names":[],"sources":["../../../src/proserviceworld/utils/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;AAGA,SAAgB,EAAG,GAAG,GAAsB;AAC1C,QAAO,EAAQ,EAAK,EAAO,CAAC"}
1
+ {"version":3,"file":"cn.js","names":[],"sources":["../../../src/proserviceworld/utils/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;AAGA,SAAgB,EAAG,GAAG,GAAsB;CAC1C,OAAO,EAAQ,EAAK,CAAM,CAAC;AAC7B"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ProServiceWorld native-bridge helpers.
3
+ *
4
+ * The host app shell owns the Capacitor bridge and exposes it at
5
+ * `window.__burdenoffNativeBridge`. This module provides typed, safe wrappers
6
+ * that no-op gracefully on web or when the bridge is unavailable.
7
+ */
8
+ export type HapticImpactStyle = 'light' | 'medium' | 'heavy';
9
+ declare global {
10
+ interface Window {
11
+ __burdenoffNativeBridge?: {
12
+ impact?: (style: HapticImpactStyle) => Promise<void>;
13
+ notify?: (type: 'success' | 'warning' | 'error') => Promise<void>;
14
+ toast?: (message: string, duration?: 'short' | 'long') => Promise<boolean>;
15
+ isNativePlatform?: () => boolean;
16
+ };
17
+ }
18
+ }
19
+ export declare function impactFeedback(style?: HapticImpactStyle): Promise<void>;
20
+ export declare function notifyFeedback(type: 'success' | 'warning' | 'error'): Promise<void>;
21
+ export declare function nativeToast(message: string, duration?: 'short' | 'long'): Promise<void>;
22
+ export declare function isNativePlatform(): boolean;
23
+ //# sourceMappingURL=native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../../src/proserviceworld/utils/native.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,uBAAuB,CAAC,EAAE;YACxB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;YACrD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;YAClE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;YAC3E,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;SAClC,CAAC;KACH;CACF;AAOD,wBAAsB,cAAc,CAAC,KAAK,GAAE,iBAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQzF;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,OAAO,GAAG,MAAgB,GACnC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C"}
@@ -0,0 +1,14 @@
1
+ //#region src/proserviceworld/utils/native.ts
2
+ function e() {
3
+ if (!(typeof window > "u")) return window.__burdenoffNativeBridge;
4
+ }
5
+ async function t(t = "light") {
6
+ let n = e();
7
+ if (n?.impact) try {
8
+ await n.impact(t);
9
+ } catch {}
10
+ }
11
+ //#endregion
12
+ export { t as impactFeedback };
13
+
14
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.js","names":[],"sources":["../../../src/proserviceworld/utils/native.ts"],"sourcesContent":["/**\n * ProServiceWorld native-bridge helpers.\n *\n * The host app shell owns the Capacitor bridge and exposes it at\n * `window.__burdenoffNativeBridge`. This module provides typed, safe wrappers\n * that no-op gracefully on web or when the bridge is unavailable.\n */\n\nexport type HapticImpactStyle = 'light' | 'medium' | 'heavy';\n\ndeclare global {\n interface Window {\n __burdenoffNativeBridge?: {\n impact?: (style: HapticImpactStyle) => Promise<void>;\n notify?: (type: 'success' | 'warning' | 'error') => Promise<void>;\n toast?: (message: string, duration?: 'short' | 'long') => Promise<boolean>;\n isNativePlatform?: () => boolean;\n };\n }\n}\n\nfunction getBridge() {\n if (typeof window === 'undefined') return undefined;\n return window.__burdenoffNativeBridge;\n}\n\nexport async function impactFeedback(style: HapticImpactStyle = 'light'): Promise<void> {\n const bridge = getBridge();\n if (!bridge?.impact) return;\n try {\n await bridge.impact(style);\n } catch {\n /* no-op on web / unsupported */\n }\n}\n\nexport async function notifyFeedback(type: 'success' | 'warning' | 'error'): Promise<void> {\n const bridge = getBridge();\n if (!bridge?.notify) return;\n try {\n await bridge.notify(type);\n } catch {\n /* no-op */\n }\n}\n\nexport async function nativeToast(\n message: string,\n duration: 'short' | 'long' = 'short'\n): Promise<void> {\n const bridge = getBridge();\n if (!bridge?.toast) return;\n try {\n await bridge.toast(message, duration);\n } catch {\n /* no-op */\n }\n}\n\nexport function isNativePlatform(): boolean {\n const bridge = getBridge();\n return bridge?.isNativePlatform?.() ?? false;\n}\n"],"mappings":";AAqBA,SAAS,IAAY;CACf,aAAO,SAAW,MACtB,OAAO,OAAO;AAChB;AAEA,eAAsB,EAAe,IAA2B,SAAwB;CACtF,IAAM,IAAS,EAAU;CACpB,OAAQ,QACb,IAAI;EACF,MAAM,EAAO,OAAO,CAAK;CAC3B,QAAQ,CAER;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"nav.js","names":[],"sources":["../../../src/proserviceworld/utils/nav.ts"],"sourcesContent":["import { useNavigate } from 'react-router-dom';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\n\n/**\n * Navigate to a path relative to the MFE's basePath. Works both mounted in a\n * host shell (basePath set, shell BrowserRouter) and standalone (MemoryRouter).\n */\nexport function useNav(): (relative: string) => void {\n const navigate = useNavigate();\n const { basePath = '' } = useProServiceWorld();\n return (relative: string) => navigate(`${basePath}/${relative}`.replace(/\\/{2,}/g, '/'));\n}\n"],"mappings":";;;AAOA,SAAgB,IAAqC;CACnD,IAAM,IAAW,GAAa,EACxB,EAAE,cAAW,OAAO,GAAoB;AAC9C,SAAQ,MAAqB,EAAS,GAAG,EAAS,GAAG,IAAW,QAAQ,WAAW,IAAI,CAAC"}
1
+ {"version":3,"file":"nav.js","names":[],"sources":["../../../src/proserviceworld/utils/nav.ts"],"sourcesContent":["import { useNavigate } from 'react-router-dom';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\n\n/**\n * Navigate to a path relative to the MFE's basePath. Works both mounted in a\n * host shell (basePath set, shell BrowserRouter) and standalone (MemoryRouter).\n */\nexport function useNav(): (relative: string) => void {\n const navigate = useNavigate();\n const { basePath = '' } = useProServiceWorld();\n return (relative: string) => navigate(`${basePath}/${relative}`.replace(/\\/{2,}/g, '/'));\n}\n"],"mappings":";;;AAOA,SAAgB,IAAqC;CACnD,IAAM,IAAW,EAAY,GACvB,EAAE,cAAW,OAAO,EAAmB;CAC7C,QAAQ,MAAqB,EAAS,GAAG,EAAS,GAAG,IAAW,QAAQ,WAAW,GAAG,CAAC;AACzF"}
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","names":[],"sources":["../../../src/proserviceworld/utils/navigation.ts"],"sourcesContent":["export function joinPath(basePath: string, relativePath: string): string {\n const cleanBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n const cleanPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;\n return cleanBase + cleanPath;\n}\n"],"mappings":";AAAA,SAAgB,EAAS,GAAkB,GAA8B;AAGvE,SAFkB,EAAS,SAAS,IAAI,GAAG,EAAS,MAAM,GAAG,GAAG,GAAG,MACjD,EAAa,WAAW,IAAI,GAAG,IAAe,IAAI"}
1
+ {"version":3,"file":"navigation.js","names":[],"sources":["../../../src/proserviceworld/utils/navigation.ts"],"sourcesContent":["export function joinPath(basePath: string, relativePath: string): string {\n const cleanBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n const cleanPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;\n return cleanBase + cleanPath;\n}\n"],"mappings":";AAAA,SAAgB,EAAS,GAAkB,GAA8B;CAGvE,QAFkB,EAAS,SAAS,GAAG,IAAI,EAAS,MAAM,GAAG,EAAE,IAAI,MACjD,EAAa,WAAW,GAAG,IAAI,IAAe,IAAI;AAEtE"}