whale-code 6.4.0 → 6.5.0

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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -0,0 +1,411 @@
1
+ // server/lib/coa-renderer.ts — React-PDF COA renderer
2
+ // Ported from CoaGenerator edge function. Produces pixel-perfect branded COA PDFs.
3
+ import React from "react";
4
+ import { Document, Page, Text, View, Image, StyleSheet, renderToBuffer, Svg, Path, } from "@react-pdf/renderer";
5
+ const e = React.createElement;
6
+ // ============================================================================
7
+ // Design Tokens
8
+ // ============================================================================
9
+ const colors = {
10
+ black: "#000000",
11
+ gray900: "#111827",
12
+ gray800: "#1f2937",
13
+ gray700: "#374151",
14
+ gray600: "#4b5563",
15
+ gray500: "#6b7280",
16
+ gray400: "#9ca3af",
17
+ gray300: "#d1d5db",
18
+ gray200: "#e5e7eb",
19
+ gray100: "#f3f4f6",
20
+ gray50: "#f9fafb",
21
+ green700: "#15803d",
22
+ green500: "#10b981",
23
+ green50: "#f0fdf4",
24
+ white: "#ffffff",
25
+ };
26
+ const pieColors = {
27
+ THCa: "#10B981",
28
+ "D9-THC": "#F59E0B",
29
+ "D8-THC": "#EF4444",
30
+ THCP: "#8B5CF6",
31
+ CBD: "#3B82F6",
32
+ CBDa: "#06B6D4",
33
+ CBG: "#14B8A6",
34
+ CBGa: "#059669",
35
+ CBC: "#EC4899",
36
+ CBN: "#F97316",
37
+ CBDV: "#6366F1",
38
+ THCV: "#A855F7",
39
+ CBL: "#84CC16",
40
+ CBCA: "#D946EF",
41
+ CBNA: "#F43F5E",
42
+ Other: "#9CA3AF",
43
+ };
44
+ // ============================================================================
45
+ // StyleSheets
46
+ // ============================================================================
47
+ const styles = StyleSheet.create({
48
+ page: {
49
+ paddingTop: 15,
50
+ paddingBottom: 15,
51
+ paddingLeft: 10,
52
+ paddingRight: 10,
53
+ fontSize: 8,
54
+ fontFamily: "Helvetica",
55
+ color: colors.gray900,
56
+ backgroundColor: colors.white,
57
+ },
58
+ header: {
59
+ flexDirection: "row",
60
+ justifyContent: "space-between",
61
+ borderBottomWidth: 1,
62
+ borderBottomColor: colors.gray200,
63
+ paddingBottom: 6,
64
+ marginBottom: 6,
65
+ },
66
+ headerLeft: { flexDirection: "row", alignItems: "center" },
67
+ logo: { width: 45, height: 45 },
68
+ companyInfo: { marginLeft: 8 },
69
+ companyName: { fontSize: 22, fontFamily: "Helvetica-Bold", color: colors.black },
70
+ companySubname: { fontSize: 17, color: colors.black, marginTop: -2 },
71
+ divider: { width: 1, height: 45, backgroundColor: colors.gray300, marginHorizontal: 10 },
72
+ labContact: { fontSize: 8, color: colors.gray700, lineHeight: 1.4 },
73
+ docTitle: { fontSize: 11, color: colors.gray700, marginTop: 6 },
74
+ sampleSection: {
75
+ flexDirection: "row",
76
+ marginBottom: 8,
77
+ paddingBottom: 6,
78
+ borderBottomWidth: 1,
79
+ borderBottomColor: colors.gray200,
80
+ },
81
+ sampleCol: { flex: 1, paddingHorizontal: 8 },
82
+ sampleTitle: { fontSize: 11, fontFamily: "Helvetica-Bold", marginBottom: 4 },
83
+ infoRow: { flexDirection: "row", marginBottom: 2 },
84
+ infoLabel: { fontSize: 7.5, fontFamily: "Helvetica-Bold", color: colors.gray900 },
85
+ infoValue: { fontSize: 7.5, color: colors.gray700, marginLeft: 3 },
86
+ sectionHeader: { flexDirection: "row", justifyContent: "space-between", marginBottom: 4 },
87
+ sectionTitle: { fontSize: 10, fontFamily: "Helvetica-Bold" },
88
+ sectionStatus: { fontSize: 8, color: colors.gray700 },
89
+ tableContainer: { flexDirection: "row" },
90
+ table: { flex: 1, borderWidth: 1, borderColor: colors.gray200 },
91
+ tableHeader: {
92
+ flexDirection: "row",
93
+ backgroundColor: colors.gray50,
94
+ borderBottomWidth: 1,
95
+ borderBottomColor: colors.gray200,
96
+ },
97
+ tableHeaderCell: {
98
+ padding: 6, fontSize: 9, fontFamily: "Helvetica-Bold", textAlign: "center",
99
+ borderRightWidth: 1, borderRightColor: colors.gray200,
100
+ },
101
+ tableHeaderCellLast: {
102
+ padding: 6, fontSize: 9, fontFamily: "Helvetica-Bold", textAlign: "center",
103
+ },
104
+ tableRow: {
105
+ flexDirection: "row", borderBottomWidth: 1, borderBottomColor: colors.gray200, minHeight: 22,
106
+ },
107
+ tableRowAlt: {
108
+ flexDirection: "row", borderBottomWidth: 1, borderBottomColor: colors.gray200,
109
+ backgroundColor: colors.gray50, minHeight: 22,
110
+ },
111
+ tableCell: {
112
+ padding: 6, fontSize: 9, textAlign: "center",
113
+ borderRightWidth: 1, borderRightColor: colors.gray200, color: colors.gray700,
114
+ },
115
+ tableCellLast: { padding: 6, fontSize: 9, textAlign: "center", color: colors.gray700 },
116
+ tableCellLeft: {
117
+ padding: 6, fontSize: 9, textAlign: "left",
118
+ borderRightWidth: 1, borderRightColor: colors.gray200, fontFamily: "Helvetica-Bold",
119
+ },
120
+ tableSummaryRow: {
121
+ flexDirection: "row", backgroundColor: colors.gray100, minHeight: 22,
122
+ },
123
+ summaryContainer: { width: 145, marginLeft: 12 },
124
+ summaryBox: {
125
+ borderWidth: 2, borderColor: colors.green500, backgroundColor: colors.green50,
126
+ padding: 10, marginBottom: 8, alignItems: "center",
127
+ },
128
+ summaryBoxInactive: {
129
+ borderWidth: 2, borderColor: colors.gray300, backgroundColor: colors.gray50,
130
+ padding: 10, marginBottom: 8, alignItems: "center",
131
+ },
132
+ summaryValue: { fontSize: 18, fontFamily: "Helvetica-Bold", color: colors.green700 },
133
+ summaryValueInactive: { fontSize: 18, fontFamily: "Helvetica-Bold", color: colors.gray500 },
134
+ summaryLabel: { fontSize: 9, fontFamily: "Helvetica-Bold", color: colors.green700, marginTop: 3 },
135
+ summaryLabelInactive: { fontSize: 9, fontFamily: "Helvetica-Bold", color: colors.gray500, marginTop: 3 },
136
+ summaryTableTitle: { fontSize: 10, fontFamily: "Helvetica-Bold", marginTop: 10, marginBottom: 4 },
137
+ summaryTable: { borderWidth: 1, borderColor: colors.gray200 },
138
+ summaryTableRow: {
139
+ flexDirection: "row", borderBottomWidth: 1, borderBottomColor: colors.gray200, minHeight: 18,
140
+ },
141
+ summaryTableCell: { padding: 4, fontSize: 8, flex: 1, textAlign: "center" },
142
+ summaryTableCellLeft: {
143
+ padding: 4, fontSize: 8, flex: 1, textAlign: "left", fontFamily: "Helvetica-Bold",
144
+ },
145
+ methodology: { backgroundColor: colors.gray50, padding: 8, marginTop: 8, marginBottom: 8 },
146
+ methodologyTitle: { fontSize: 9, fontFamily: "Helvetica-Bold", marginBottom: 4 },
147
+ methodologyText: { fontSize: 7, color: colors.gray700, lineHeight: 1.5 },
148
+ footer: {
149
+ flexDirection: "row", borderTopWidth: 1, borderTopColor: colors.gray200,
150
+ paddingTop: 8, marginTop: 6,
151
+ },
152
+ footerLeft: { flex: 1, flexDirection: "row" },
153
+ qrSection: { alignItems: "center", marginRight: 12 },
154
+ qrCode: { width: 60, height: 60, borderWidth: 1, borderColor: colors.gray300 },
155
+ qrLabel: { fontSize: 7, color: colors.gray700, marginTop: 3 },
156
+ disclaimer: { flex: 1, fontSize: 7, color: colors.gray700, lineHeight: 1.4 },
157
+ footerRight: { width: 150, alignItems: "flex-end" },
158
+ footerCompany: { fontSize: 9, textAlign: "right", marginBottom: 8 },
159
+ signatureSection: {
160
+ borderTopWidth: 1, borderTopColor: colors.gray300, paddingTop: 8, alignItems: "flex-end",
161
+ },
162
+ signatureName: { fontSize: 9, fontFamily: "Helvetica-Bold" },
163
+ signatureTitle: { fontSize: 8, color: colors.gray700 },
164
+ });
165
+ const panelStyles = StyleSheet.create({
166
+ pageHeader: {
167
+ flexDirection: "row", justifyContent: "space-between", alignItems: "center",
168
+ borderBottomWidth: 2, borderBottomColor: colors.green500, paddingBottom: 6, marginBottom: 8,
169
+ },
170
+ pageHeaderLeft: { flexDirection: "row", alignItems: "center" },
171
+ pageHeaderLogo: { width: 28, height: 28 },
172
+ pageHeaderTitle: { marginLeft: 8 },
173
+ pageHeaderCompany: { fontSize: 14, fontFamily: "Helvetica-Bold" },
174
+ pageHeaderSubtitle: { fontSize: 9, color: colors.gray600 },
175
+ pageHeaderRight: { textAlign: "right" },
176
+ pageTitle: { fontSize: 12, fontFamily: "Helvetica-Bold" },
177
+ pageSampleInfo: { fontSize: 8, color: colors.gray600 },
178
+ panelSection: { marginBottom: 6 },
179
+ summaryCard: {
180
+ backgroundColor: colors.green50, borderWidth: 2, borderColor: colors.green500,
181
+ borderRadius: 8, padding: 8, marginBottom: 8,
182
+ },
183
+ summaryCardTitle: { fontSize: 10, fontFamily: "Helvetica-Bold", color: colors.green700, marginBottom: 6 },
184
+ summaryCardText: { fontSize: 8, color: colors.gray700, lineHeight: 1.4 },
185
+ panelHeader: {
186
+ flexDirection: "row", justifyContent: "space-between", alignItems: "center",
187
+ backgroundColor: colors.gray100, padding: 8, marginBottom: 1,
188
+ },
189
+ panelTitle: { fontSize: 11, fontFamily: "Helvetica-Bold", flexShrink: 0 },
190
+ statusBadge: {
191
+ backgroundColor: colors.green500, color: colors.white,
192
+ paddingHorizontal: 8, paddingVertical: 3, borderRadius: 3,
193
+ fontSize: 8, fontFamily: "Helvetica-Bold",
194
+ },
195
+ statusBadgeFail: {
196
+ backgroundColor: "#EF4444", color: colors.white,
197
+ paddingHorizontal: 8, paddingVertical: 3, borderRadius: 3,
198
+ fontSize: 8, fontFamily: "Helvetica-Bold",
199
+ },
200
+ panelTable: { borderWidth: 1, borderColor: colors.gray200 },
201
+ panelTableHeader: {
202
+ flexDirection: "row", backgroundColor: colors.gray50,
203
+ borderBottomWidth: 1, borderBottomColor: colors.gray200,
204
+ },
205
+ panelTableHeaderCell: {
206
+ padding: 6, fontSize: 9, fontFamily: "Helvetica-Bold", textAlign: "center",
207
+ borderRightWidth: 1, borderRightColor: colors.gray200,
208
+ },
209
+ panelTableRow: {
210
+ flexDirection: "row", borderBottomWidth: 1, borderBottomColor: colors.gray200, minHeight: 18,
211
+ },
212
+ panelTableCell: {
213
+ padding: 3, fontSize: 7, textAlign: "center",
214
+ borderRightWidth: 1, borderRightColor: colors.gray200,
215
+ },
216
+ panelTableCellLeft: {
217
+ padding: 3, fontSize: 7, textAlign: "left",
218
+ borderRightWidth: 1, borderRightColor: colors.gray200,
219
+ },
220
+ pesticideTableRow: {
221
+ flexDirection: "row", borderBottomWidth: 0.5, borderBottomColor: colors.gray200, minHeight: 14,
222
+ },
223
+ pesticideTableCell: {
224
+ padding: 2, fontSize: 6.5, textAlign: "center",
225
+ borderRightWidth: 0.5, borderRightColor: colors.gray200,
226
+ },
227
+ pesticideTableCellLeft: {
228
+ padding: 2, fontSize: 6.5, textAlign: "left",
229
+ borderRightWidth: 0.5, borderRightColor: colors.gray200,
230
+ },
231
+ passText: { color: colors.green700, fontFamily: "Helvetica-Bold" },
232
+ failText: { color: "#EF4444", fontFamily: "Helvetica-Bold" },
233
+ twoColumnContainer: { flexDirection: "row", gap: 12 },
234
+ columnHalf: { flex: 1 },
235
+ pageFooter: {
236
+ position: "absolute", bottom: 20, left: 20, right: 20,
237
+ flexDirection: "row", justifyContent: "space-between",
238
+ borderTopWidth: 1, borderTopColor: colors.gray200, paddingTop: 8,
239
+ },
240
+ footerText: { fontSize: 7, color: colors.gray600 },
241
+ methodology: { marginTop: 8, padding: 6, backgroundColor: colors.gray50 },
242
+ methodologyTitle: { fontSize: 8, fontFamily: "Helvetica-Bold", marginBottom: 4 },
243
+ methodologyText: { fontSize: 7, color: colors.gray600, lineHeight: 1.4 },
244
+ categoryBadge: {
245
+ backgroundColor: colors.green500, color: colors.white,
246
+ paddingHorizontal: 6, paddingVertical: 2, fontSize: 7, fontFamily: "Helvetica-Bold",
247
+ },
248
+ categoryTitle: { fontSize: 9, fontFamily: "Helvetica-Bold", marginLeft: 8 },
249
+ categorySubtitle: { fontSize: 7, color: colors.gray600, marginLeft: 8 },
250
+ });
251
+ // ============================================================================
252
+ // Pie Chart
253
+ // ============================================================================
254
+ const createPieSlice = (cx, cy, r, startAngle, endAngle) => {
255
+ const start = {
256
+ x: cx + r * Math.cos((startAngle * Math.PI) / 180),
257
+ y: cy + r * Math.sin((startAngle * Math.PI) / 180),
258
+ };
259
+ const end = {
260
+ x: cx + r * Math.cos((endAngle * Math.PI) / 180),
261
+ y: cy + r * Math.sin((endAngle * Math.PI) / 180),
262
+ };
263
+ const largeArc = endAngle - startAngle > 180 ? 1 : 0;
264
+ return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y} Z`;
265
+ };
266
+ const PieChart = ({ cannabinoids }) => {
267
+ const detected = cannabinoids
268
+ .filter((c) => c.result !== "ND" && c.percentWeight > 0)
269
+ .sort((a, b) => b.percentWeight - a.percentWeight);
270
+ const cx = 35, cy = 35, r = 30;
271
+ if (detected.length === 0 || detected.reduce((sum, c) => sum + c.percentWeight, 0) === 0) {
272
+ const placeholderData = [
273
+ { name: "D9-THC", pct: 65 }, { name: "THCa", pct: 20 },
274
+ { name: "CBD", pct: 10 }, { name: "Other", pct: 5 },
275
+ ];
276
+ let currentAngle = -90;
277
+ const placeholderSlices = placeholderData.map((item, i) => {
278
+ const angle = (item.pct / 100) * 360;
279
+ const path = createPieSlice(cx, cy, r, currentAngle, currentAngle + angle);
280
+ const color = pieColors[item.name] || pieColors.Other;
281
+ currentAngle += angle;
282
+ return e(Path, { key: String(i), d: path, fill: color, opacity: 0.4 });
283
+ });
284
+ const placeholderLegend = placeholderData.map((item, i) => {
285
+ const color = pieColors[item.name] || pieColors.Other;
286
+ return e(View, { key: String(i), style: { flexDirection: "row", alignItems: "center", marginBottom: 2 } }, e(View, { style: { width: 6, height: 6, backgroundColor: color, opacity: 0.4, marginRight: 3 } }), e(Text, { style: { fontSize: 6, color: colors.gray400 } }, `${item.name}: --%`));
287
+ });
288
+ return e(View, { style: { flexDirection: "row", alignItems: "flex-start" } }, e(Svg, { width: 70, height: 70, viewBox: "0 0 70 70" }, ...placeholderSlices), e(View, { style: { marginLeft: 4 } }, ...placeholderLegend));
289
+ }
290
+ const total = detected.reduce((sum, c) => sum + c.percentWeight, 0);
291
+ let currentAngle = -90;
292
+ const slices = detected.map((c, i) => {
293
+ const angle = (c.percentWeight / total) * 360;
294
+ const path = createPieSlice(cx, cy, r, currentAngle, currentAngle + angle);
295
+ const color = pieColors[c.name] || pieColors.Other;
296
+ currentAngle += angle;
297
+ return e(Path, { key: String(i), d: path, fill: color });
298
+ });
299
+ const legend = detected.slice(0, 6).map((c, i) => {
300
+ const color = pieColors[c.name] || pieColors.Other;
301
+ const displayName = c.name.replace(/Δ/g, "D").replace(/\u0394/g, "D").replace(/Delta/gi, "D");
302
+ return e(View, { key: String(i), style: { flexDirection: "row", alignItems: "center", marginBottom: 2 } }, e(View, { style: { width: 6, height: 6, backgroundColor: color, marginRight: 3 } }), e(Text, { style: { fontSize: 6, color: colors.gray700 } }, `${displayName}: ${c.percentWeight.toFixed(2)}%`));
303
+ });
304
+ return e(View, { style: { flexDirection: "row", alignItems: "flex-start" } }, e(Svg, { width: 70, height: 70, viewBox: "0 0 70 70" }, ...slices), e(View, { style: { marginLeft: 4 } }, ...legend));
305
+ };
306
+ // ============================================================================
307
+ // Shared Components (Pages 2-5)
308
+ // ============================================================================
309
+ const PanelPageHeader = ({ data, title }) => {
310
+ return e(View, null, e(View, { style: styles.header }, e(View, { style: styles.headerLeft }, data.logoUrl
311
+ ? e(Image, { src: data.logoUrl, style: styles.logo })
312
+ : e(View, { style: { ...styles.logo, backgroundColor: colors.green500, borderRadius: 8 } }), e(View, { style: styles.companyInfo }, e(Text, { style: styles.companyName }, (data.labName || "Lab").split(" ")[0]), e(Text, { style: styles.companySubname }, (data.labName || "").split(" ").slice(1).join(" ") || "")), e(View, { style: styles.divider }), e(Text, { style: styles.labContact }, data.labContact || "")), e(Text, { style: { ...styles.docTitle, fontSize: 10, fontFamily: "Helvetica-Bold" } }, title)), e(View, { style: { ...styles.sampleSection, marginBottom: 8 } }, e(View, { style: styles.sampleCol }, e(Text, { style: styles.sampleTitle }, data.sampleName), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Sample ID:"), e(Text, { style: styles.infoValue }, data.sampleId)), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Matrix:"), e(Text, { style: styles.infoValue }, data.sampleType || "Plant"))), e(View, { style: styles.sampleCol }, e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Collected:"), e(Text, { style: styles.infoValue }, data.dateCollected || "\u2014")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Received:"), e(Text, { style: styles.infoValue }, data.dateReceived || "\u2014")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Completed:"), e(Text, { style: styles.infoValue }, data.dateTested || "\u2014")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Batch #:"), e(Text, { style: styles.infoValue }, data.batchId || "\u2014"))), e(View, { style: styles.sampleCol }, e(Text, { style: styles.infoLabel }, "Client:"), e(Text, { style: { ...styles.infoValue, fontFamily: "Helvetica-Bold", marginLeft: 0 } }, data.clientName), data.clientAddress ? e(Text, { style: { ...styles.infoValue, marginLeft: 0, fontSize: 6.5 } }, data.clientAddress) : null, data.licenseNumber ? e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Lic #:"), e(Text, { style: styles.infoValue }, data.licenseNumber)) : null), e(View, { style: { ...styles.sampleCol, alignItems: "center", justifyContent: "center" } }, e(Text, { style: { ...styles.infoLabel, marginBottom: 4, textAlign: "center" } }, "Test Result"), e(View, { style: {
313
+ width: 70, height: 40, borderWidth: 3, borderColor: colors.green500,
314
+ borderRadius: 4, backgroundColor: colors.white, alignItems: "center", justifyContent: "center",
315
+ } }, e(Text, { style: { fontSize: 18, fontFamily: "Helvetica-Bold", color: colors.green500, letterSpacing: 2 } }, "PASS")))));
316
+ };
317
+ const PanelPageFooter = ({ data, pageNum, totalPages }) => {
318
+ const methodText = pageNum === 2
319
+ ? "All safety and compliance testing is performed in accordance with ISO 17025 accredited laboratory standards. Heavy metals via ICP-MS. Microbial via qPCR. Solvents via HS-GC-MS. Mycotoxins via LC-MS/MS. ND = Not Detected (below Limit of Detection)."
320
+ : pageNum >= 3
321
+ ? "Comprehensive pesticide screening performed using LC-MS/MS in accordance with ISO 17025 standards. Category I pesticides are banned substances with zero tolerance. Category II pesticides have established action levels. All results in ppm. ND = Not Detected."
322
+ : "Testing performed using validated methods per ISO 17025 standards. This Certificate applies only to the sample(s) tested.";
323
+ return e(View, null, e(View, { style: styles.methodology }, e(Text, { style: styles.methodologyTitle }, "Methodology & Quality Control"), e(Text, { style: styles.methodologyText }, methodText)), e(View, { style: styles.footer }, e(View, { style: styles.footerLeft }, data.qrCodeDataUrl ? e(View, { style: styles.qrSection }, e(Image, { src: data.qrCodeDataUrl, style: styles.qrCode }), e(Text, { style: styles.qrLabel }, "Scan for digital copy")) : null, e(Text, { style: styles.disclaimer }, `${data.labName || "This laboratory"} performs analytical testing using validated internal methodologies and quality control protocols. All results apply only to the sample(s) tested. This Certificate is not a declaration of product safety, efficacy, or regulatory compliance. Testing performed in accordance with applicable state regulations.\n\nThis report may not be reproduced except in full without written approval from ${data.labName || "the laboratory"}.`), e(Text, { style: { ...styles.disclaimer, marginTop: 4 } }, `For inquiries: support@quantixanalytics.com | Raleigh, NC | ${data.labWebsite || "www.quantixanalytics.com"}`)), e(View, { style: styles.footerRight }, e(View, { style: styles.footerCompany }, e(Text, { style: { fontFamily: "Helvetica-Bold" } }, data.labName || "Laboratory"), e(Text, {}, "All Rights Reserved"), e(Text, { style: { fontSize: 7, color: colors.gray600 } }, data.labWebsite || "www.quantixanalytics.com")), e(View, { style: styles.signatureSection }, data.signatureUrl ? e(Image, { src: data.signatureUrl, style: { width: 180, height: 70, marginBottom: 4 } }) : null, e(Text, { style: styles.signatureName }, data.labDirector || "Sarah Mitchell"), e(Text, { style: styles.signatureTitle }, data.directorTitle || "Laboratory Director"), e(Text, { style: styles.signatureTitle }, data.approvalDate || data.dateTested || "\u2014"), e(Text, { style: { fontSize: 7, color: colors.gray500, marginTop: 8, textAlign: "right" } }, `Page ${pageNum} of ${totalPages}`)))));
324
+ };
325
+ // ============================================================================
326
+ // Page 2: Safety & Compliance
327
+ // ============================================================================
328
+ const SafetyTestingPage = ({ data }) => {
329
+ const microbialPassed = !data.microbialResults?.some((r) => r.status === "Fail");
330
+ const heavyMetalsPassed = !data.heavyMetalsResults?.some((r) => r.status === "Fail");
331
+ const mycotoxinsPassed = !data.mycotoxinResults?.some((r) => r.status === "Fail");
332
+ return e(Page, { size: "A4", style: styles.page }, e(PanelPageHeader, { data, title: "Safety & Compliance Testing" }), e(View, { style: panelStyles.summaryCard }, e(Text, { style: panelStyles.summaryCardTitle }, "\u2713 All Safety Tests Passed"), e(Text, { style: panelStyles.summaryCardText }, "This sample has been tested for microbial contaminants, heavy metals, and mycotoxins. All results are within acceptable limits as defined by state regulations and industry standards.")), e(View, { style: panelStyles.twoColumnContainer }, e(View, { style: panelStyles.columnHalf }, e(View, { style: panelStyles.panelSection }, e(View, { style: panelStyles.panelHeader }, e(Text, { style: panelStyles.panelTitle }, "Microbial Contaminants"), e(Text, { style: microbialPassed ? panelStyles.statusBadge : panelStyles.statusBadgeFail }, microbialPassed ? "Pass" : "Fail")), e(View, { style: panelStyles.panelTable }, e(View, { style: panelStyles.panelTableHeader }, e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "45%", textAlign: "left" } }, "Test"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "20%" } }, "Result"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "20%" } }, "Limit"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "15%", borderRightWidth: 0 } }, "Status")), ...(data.microbialResults || []).map((r, i) => e(View, { key: String(i), style: panelStyles.panelTableRow }, e(Text, { style: { ...panelStyles.panelTableCellLeft, width: "45%" } }, r.analyte || r.test || ""), e(Text, { style: { ...panelStyles.panelTableCell, width: "20%", fontFamily: "Helvetica-Bold" } }, r.result === "ND" ? "ND" : String(r.result)), e(Text, { style: { ...panelStyles.panelTableCell, width: "20%" } }, String(r.limit || "")), e(Text, { style: {
333
+ ...panelStyles.panelTableCell, width: "15%", borderRightWidth: 0,
334
+ ...(r.status === "Pass" ? panelStyles.passText : panelStyles.failText),
335
+ } }, r.status)))))), e(View, { style: panelStyles.columnHalf }, e(View, { style: panelStyles.panelSection }, e(View, { style: panelStyles.panelHeader }, e(Text, { style: panelStyles.panelTitle }, "Heavy Metals Analysis"), e(Text, { style: heavyMetalsPassed ? panelStyles.statusBadge : panelStyles.statusBadgeFail }, heavyMetalsPassed ? "Pass" : "Fail")), e(View, { style: panelStyles.panelTable }, e(View, { style: panelStyles.panelTableHeader }, e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "28%", textAlign: "left" } }, "Analyte"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "13%" } }, "LOD"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "13%" } }, "LOQ"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "15%" } }, "Result"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "13%" } }, "Limit"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "18%", borderRightWidth: 0 } }, "Status")), ...(data.heavyMetalsResults || []).map((r, i) => e(View, { key: String(i), style: panelStyles.panelTableRow }, e(Text, { style: { ...panelStyles.panelTableCellLeft, width: "28%" } }, r.analyte), e(Text, { style: { ...panelStyles.panelTableCell, width: "13%" } }, String(r.lod)), e(Text, { style: { ...panelStyles.panelTableCell, width: "13%" } }, String(r.loq)), e(Text, { style: { ...panelStyles.panelTableCell, width: "15%", fontFamily: "Helvetica-Bold" } }, r.result === "ND" ? "ND" : String(r.result)), e(Text, { style: { ...panelStyles.panelTableCell, width: "13%" } }, `<${r.limit}`), e(Text, { style: {
336
+ ...panelStyles.panelTableCell, width: "18%", borderRightWidth: 0,
337
+ ...(r.status === "Pass" ? panelStyles.passText : panelStyles.failText),
338
+ } }, r.status))))), e(View, { style: panelStyles.panelSection }, e(View, { style: panelStyles.panelHeader }, e(Text, { style: panelStyles.panelTitle }, "Mycotoxins"), e(Text, { style: mycotoxinsPassed ? panelStyles.statusBadge : panelStyles.statusBadgeFail }, mycotoxinsPassed ? "Pass" : "Fail")), e(View, { style: panelStyles.panelTable }, e(View, { style: panelStyles.panelTableHeader }, e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "28%", textAlign: "left" } }, "Analyte"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "13%" } }, "LOD"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "13%" } }, "LOQ"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "15%" } }, "Result"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "13%" } }, "Limit"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "18%", borderRightWidth: 0 } }, "Status")), ...(data.mycotoxinResults || []).map((r, i) => e(View, { key: String(i), style: panelStyles.panelTableRow }, e(Text, { style: { ...panelStyles.panelTableCellLeft, width: "28%" } }, r.analyte), e(Text, { style: { ...panelStyles.panelTableCell, width: "13%" } }, String(r.lod)), e(Text, { style: { ...panelStyles.panelTableCell, width: "13%" } }, String(r.loq)), e(Text, { style: { ...panelStyles.panelTableCell, width: "15%", fontFamily: "Helvetica-Bold" } }, r.result === "ND" ? "ND" : String(r.result)), e(Text, { style: { ...panelStyles.panelTableCell, width: "13%" } }, `<${r.limit}`), e(Text, { style: {
339
+ ...panelStyles.panelTableCell, width: "18%", borderRightWidth: 0,
340
+ ...(r.status === "Pass" ? panelStyles.passText : panelStyles.failText),
341
+ } }, r.status))))))), e(PanelPageFooter, { data, pageNum: 2, totalPages: data.totalPages || 4 }));
342
+ };
343
+ // ============================================================================
344
+ // Pages 3 & 4: Pesticide tables (shared renderer)
345
+ // ============================================================================
346
+ const PesticidePage = ({ data, category, pesticides, pageNum }) => {
347
+ const allPassed = !pesticides.some((r) => r.status === "Fail");
348
+ const half = Math.ceil(pesticides.length / 2);
349
+ const leftColumn = pesticides.slice(0, half);
350
+ const rightColumn = pesticides.slice(half);
351
+ const isCat1 = category === "I";
352
+ const renderColumn = (items) => e(View, { style: panelStyles.columnHalf }, e(View, { style: panelStyles.panelTable }, e(View, { style: panelStyles.panelTableHeader }, e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "60%", textAlign: "left" } }, "Analyte"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "20%" } }, "Result"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "20%", borderRightWidth: 0 } }, "Status")), ...items.map((r, i) => e(View, { key: String(i), style: panelStyles.pesticideTableRow }, e(Text, { style: { ...panelStyles.pesticideTableCellLeft, width: "60%" } }, r.analyte), e(Text, { style: { ...panelStyles.pesticideTableCell, width: "20%", fontFamily: "Helvetica-Bold" } }, r.result === "ND" ? "ND" : String(r.result)), e(Text, { style: {
353
+ ...panelStyles.pesticideTableCell, width: "20%", borderRightWidth: 0,
354
+ ...(r.status === "Pass" ? panelStyles.passText : panelStyles.failText),
355
+ } }, r.status)))));
356
+ return e(Page, { size: "A4", style: styles.page }, e(PanelPageHeader, { data, title: isCat1 ? "Pesticide Screening" : "Pesticide Screening (Continued)" }), e(View, { style: { ...panelStyles.summaryCard, backgroundColor: allPassed ? colors.green50 : "#FEF2F2" } }, e(Text, { style: panelStyles.summaryCardTitle }, allPassed ? "\u2713 All Pesticide Tests Passed" : "\u26A0 Pesticide Detection"), e(Text, { style: panelStyles.summaryCardText }, isCat1
357
+ ? "Category I pesticides are banned substances with zero tolerance. Any detection of these pesticides results in automatic failure and product cannot be sold."
358
+ : "Category II pesticides have established action levels. Results must be below the regulatory limits shown. These pesticides are restricted but allowed at specified concentrations.")), e(View, { style: { flexDirection: "row", alignItems: "center", marginBottom: 8 } }, e(Text, { style: panelStyles.categoryBadge }, `CATEGORY ${category}`), e(Text, { style: panelStyles.categoryTitle }, isCat1 ? `Zero Tolerance - ${pesticides.length} Analytes` : `Action Level - ${pesticides.length} Analytes`), e(Text, { style: panelStyles.categorySubtitle }, isCat1 ? "Any detection = FAIL" : "Must be below established limits")), isCat1 ? e(View, { style: { ...panelStyles.panelHeader, marginBottom: 8 } }, e(Text, { style: panelStyles.panelTitle }, "Pesticide Analysis - 66 Analytes"), e(Text, { style: allPassed ? panelStyles.statusBadge : panelStyles.statusBadgeFail }, allPassed ? "Pass" : "Fail")) : null, isCat1 ? e(Text, { style: { fontSize: 7, color: colors.gray600, marginBottom: 8 } }, "Method: LC-MS/MS | LOD: 0.005-0.05 ppm | LOQ: 0.01-0.1 ppm") : null, e(View, { style: panelStyles.twoColumnContainer }, renderColumn(leftColumn), renderColumn(rightColumn)), e(PanelPageFooter, { data, pageNum, totalPages: data.totalPages || 4 }));
359
+ };
360
+ // ============================================================================
361
+ // Page 5: Residual Solvents
362
+ // ============================================================================
363
+ const ResidualSolventsPage = ({ data }) => {
364
+ const allPassed = !data.testsResidualSolvents?.some((r) => r.status === "Fail");
365
+ const solvents = data.testsResidualSolvents || [];
366
+ const half = Math.ceil(solvents.length / 2);
367
+ const leftColumn = solvents.slice(0, half);
368
+ const rightColumn = solvents.slice(half);
369
+ const renderColumn = (items) => e(View, { style: panelStyles.columnHalf }, e(View, { style: panelStyles.panelTable }, e(View, { style: panelStyles.panelTableHeader }, e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "45%", textAlign: "left" } }, "Solvent"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "25%" } }, "Result (ppm)"), e(Text, { style: { ...panelStyles.panelTableHeaderCell, width: "30%", borderRightWidth: 0 } }, "Limit (ppm)")), ...items.map((r, i) => e(View, { key: String(i), style: panelStyles.pesticideTableRow }, e(Text, { style: { ...panelStyles.pesticideTableCellLeft, width: "45%" } }, r.analyte), e(Text, { style: { ...panelStyles.pesticideTableCell, width: "25%", fontFamily: "Helvetica-Bold" } }, typeof r.result === "string" ? r.result : String(r.result)), e(Text, { style: { ...panelStyles.pesticideTableCell, width: "30%", borderRightWidth: 0 } }, String(r.limit))))));
370
+ return e(Page, { size: "A4", style: styles.page }, e(PanelPageHeader, { data, title: "Residual Solvents Analysis" }), e(View, { style: { ...panelStyles.summaryCard, backgroundColor: allPassed ? colors.green50 : "#FEF2F2" } }, e(Text, { style: panelStyles.summaryCardTitle }, allPassed ? "\u2713 All Solvent Tests Passed" : "\u26A0 Solvent Detection"), e(Text, { style: panelStyles.summaryCardText }, "Residual solvents testing per USP <467> standards. Class 1 solvents (carcinogens) have strict limits. Class 2/3 solvents have higher allowable limits. All results in ppm (parts per million).")), e(View, { style: { flexDirection: "row", alignItems: "center", marginBottom: 12 } }, e(Text, { style: panelStyles.categoryBadge }, "USP <467>"), e(Text, { style: panelStyles.categoryTitle }, `Residual Solvents - ${solvents.length} Analytes`), e(Text, { style: panelStyles.categorySubtitle }, "HS-GC-MS Analysis")), e(View, { style: panelStyles.twoColumnContainer }, renderColumn(leftColumn), renderColumn(rightColumn)), e(PanelPageFooter, { data, pageNum: 5, totalPages: data.totalPages || 5 }));
371
+ };
372
+ // ============================================================================
373
+ // Main Document
374
+ // ============================================================================
375
+ const CannabisCOADocument = ({ data }) => {
376
+ const totalTHC = data.totalTHC && data.totalTHC > 0 ? data.totalTHC.toFixed(2) : "ND";
377
+ const totalCBD = data.totalCBD || 0;
378
+ const totalCBDDisplay = totalCBD > 0 ? totalCBD.toFixed(2) : "ND";
379
+ const totalCannabinoids = data.totalCannabinoids && data.totalCannabinoids > 0 ? data.totalCannabinoids.toFixed(2) : "ND";
380
+ const hasResidualSolvents = data.testsResidualSolvents && data.testsResidualSolvents.length > 0;
381
+ const totalPages = data.fullPanel ? (hasResidualSolvents ? 5 : 4) : 1;
382
+ const top4 = (data.cannabinoids || [])
383
+ .filter((c) => c.result !== "ND" && c.percentWeight > 0)
384
+ .sort((a, b) => b.percentWeight - a.percentWeight)
385
+ .slice(0, 4);
386
+ const pages = [];
387
+ // Page 1: Cannabinoid Profile
388
+ pages.push(e(Page, { key: "page1", size: "A4", style: styles.page }, e(View, { style: styles.header }, e(View, { style: styles.headerLeft }, data.logoUrl
389
+ ? e(Image, { src: data.logoUrl, style: styles.logo })
390
+ : e(View, { style: { ...styles.logo, backgroundColor: colors.green500, borderRadius: 8 } }), e(View, { style: styles.companyInfo }, e(Text, { style: styles.companyName }, (data.labName || "Lab").split(" ")[0]), e(Text, { style: styles.companySubname }, (data.labName || "").split(" ").slice(1).join(" ") || "")), e(View, { style: styles.divider }), e(Text, { style: styles.labContact }, data.labContact || "")), e(Text, { style: styles.docTitle }, "Certificate of Analysis")), e(View, { style: styles.sampleSection }, e(View, { style: styles.sampleCol }, e(Text, { style: styles.sampleTitle }, data.sampleName), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Sample ID:"), e(Text, { style: styles.infoValue }, data.sampleId)), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Matrix:"), e(Text, { style: styles.infoValue }, data.sampleType || "Plant")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Sample Size:"), e(Text, { style: styles.infoValue }, data.sampleSize || "\u2014"))), e(View, { style: styles.sampleCol }, e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Collected:"), e(Text, { style: styles.infoValue }, data.dateCollected || "\u2014")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Received:"), e(Text, { style: styles.infoValue }, data.dateReceived || "\u2014")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Completed:"), e(Text, { style: styles.infoValue }, data.dateTested || "\u2014")), e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Batch #:"), e(Text, { style: styles.infoValue }, data.batchId || "\u2014"))), e(View, { style: styles.sampleCol }, e(Text, { style: styles.infoLabel }, "Client:"), e(Text, { style: { ...styles.infoValue, fontFamily: "Helvetica-Bold", marginLeft: 0 } }, data.clientName), data.clientAddress ? e(Text, { style: { ...styles.infoValue, marginLeft: 0, fontSize: 6.5 } }, data.clientAddress) : null, data.licenseNumber ? e(View, { style: styles.infoRow }, e(Text, { style: styles.infoLabel }, "Lic #:"), e(Text, { style: styles.infoValue }, data.licenseNumber)) : null), e(View, { style: { ...styles.sampleCol, alignItems: "center" } }, e(Text, { style: { ...styles.infoLabel, marginBottom: 4 } }, "Cannabinoid Distribution"), e(PieChart, { cannabinoids: data.cannabinoids || [] }))), e(View, { style: styles.sectionHeader }, e(Text, { style: styles.sectionTitle }, "Cannabinoids"), e(Text, { style: styles.sectionStatus }, "Complete")), e(View, { style: styles.tableContainer }, e(View, { style: styles.table }, e(View, { style: styles.tableHeader }, e(Text, { style: { ...styles.tableHeaderCell, width: "28%", textAlign: "left" } }, "Analyte"), e(Text, { style: { ...styles.tableHeaderCell, width: "14%" } }, "LOD mg/g"), e(Text, { style: { ...styles.tableHeaderCell, width: "14%" } }, "LOQ mg/g"), e(Text, { style: { ...styles.tableHeaderCell, width: "22%" } }, "Result %"), e(Text, { style: { ...styles.tableHeaderCellLast, width: "22%" } }, "Result mg/g")), ...(data.cannabinoids || []).map((c, i) => {
391
+ const displayName = (c.name || "").replace(/Δ/g, "D").replace(/\u0394/g, "D").replace(/Delta/gi, "D");
392
+ return e(View, { key: String(i), style: i % 2 === 0 ? styles.tableRow : styles.tableRowAlt }, e(Text, { style: { ...styles.tableCellLeft, width: "28%" } }, displayName), e(Text, { style: { ...styles.tableCell, width: "14%" } }, c.lod.toFixed(2)), e(Text, { style: { ...styles.tableCell, width: "14%" } }, c.loq.toFixed(2)), e(Text, { style: { ...styles.tableCell, width: "22%", fontFamily: "Helvetica-Bold" } }, c.result === "ND" ? "ND" : c.percentWeight.toFixed(2)), e(Text, { style: { ...styles.tableCellLast, width: "22%" } }, c.result === "ND" ? "ND" : c.mgPerG.toFixed(2)));
393
+ }), e(View, { style: styles.tableSummaryRow }, e(Text, { style: { ...styles.tableCellLeft, width: "28%" } }, "Total THC"), e(Text, { style: { ...styles.tableCell, width: "14%" } }, ""), e(Text, { style: { ...styles.tableCell, width: "14%" } }, ""), e(Text, { style: { ...styles.tableCell, width: "22%", fontFamily: "Helvetica-Bold" } }, totalTHC), e(Text, { style: { ...styles.tableCellLast, width: "22%" } }, totalTHC === "ND" ? "ND" : (parseFloat(totalTHC) * 10).toFixed(2))), e(View, { style: styles.tableSummaryRow }, e(Text, { style: { ...styles.tableCellLeft, width: "28%" } }, "Total CBD"), e(Text, { style: { ...styles.tableCell, width: "14%" } }, ""), e(Text, { style: { ...styles.tableCell, width: "14%" } }, ""), e(Text, { style: { ...styles.tableCell, width: "22%", fontFamily: "Helvetica-Bold" } }, totalCBDDisplay), e(Text, { style: { ...styles.tableCellLast, width: "22%" } }, totalCBD > 0 ? (totalCBD * 10).toFixed(2) : "ND")), e(View, { style: { ...styles.tableSummaryRow, borderBottomWidth: 0 } }, e(Text, { style: { ...styles.tableCellLeft, width: "28%" } }, "Total Cannabinoids"), e(Text, { style: { ...styles.tableCell, width: "14%" } }, ""), e(Text, { style: { ...styles.tableCell, width: "14%" } }, ""), e(Text, { style: { ...styles.tableCell, width: "22%", fontFamily: "Helvetica-Bold" } }, totalCannabinoids), e(Text, { style: { ...styles.tableCellLast, width: "22%" } }, totalCannabinoids === "ND" ? "ND" : (parseFloat(totalCannabinoids) * 10).toFixed(2)))), e(View, { style: styles.summaryContainer }, ...top4.map((c, i) => e(View, { key: String(i), style: styles.summaryBox }, e(Text, { style: styles.summaryValue }, `${c.percentWeight.toFixed(2)}%`), e(Text, { style: styles.summaryLabel }, c.name.replace(/Δ/g, "D").replace(/\u0394/g, "D")))), e(Text, { style: styles.summaryTableTitle }, "Summary"), e(View, { style: styles.summaryTable }, e(View, { style: { ...styles.summaryTableRow, backgroundColor: colors.gray50 } }, e(Text, { style: styles.summaryTableCellLeft }, "Test"), e(Text, { style: styles.summaryTableCell }, "Date"), e(Text, { style: styles.summaryTableCell }, "Result")), e(View, { style: styles.summaryTableRow }, e(Text, { style: styles.summaryTableCellLeft }, "Batch"), e(Text, { style: styles.summaryTableCell }, data.testsBatch ? data.dateTested || "\u2014" : "\u2014"), e(Text, { style: styles.summaryTableCell }, data.testsBatch ? "Complete" : "Not Submitted")), e(View, { style: { ...styles.summaryTableRow, backgroundColor: colors.gray50 } }, e(Text, { style: styles.summaryTableCellLeft }, "Cannabinoids"), e(Text, { style: styles.summaryTableCell }, data.testsCannabinoids ? data.dateTested || "\u2014" : "\u2014"), e(Text, { style: styles.summaryTableCell }, data.testsCannabinoids ? "Complete" : "Not Tested")), e(View, { style: { ...styles.summaryTableRow, borderBottomWidth: 0 } }, e(Text, { style: styles.summaryTableCellLeft }, "Moisture"), e(Text, { style: styles.summaryTableCell }, data.testsMoisture ? data.dateTested || "\u2014" : "\u2014"), e(Text, { style: styles.summaryTableCell }, data.testsMoisture ? `${data.moisture ? data.moisture.toFixed(2) : "0.00"}%` : "Not Tested"))))), e(View, { style: styles.methodology }, e(Text, { style: styles.methodologyTitle }, data.notes || "Methodology & Quality Control"), e(Text, { style: styles.methodologyText }, "Total THC = D9-THC + 0.877 \u00D7 THCa. Total CBD = CBD + 0.877 \u00D7 CBDa. Total Cannabinoids = Sum of individual cannabinoids detected. Testing performed using validated HPLC-UV/MS methods per ISO 17025 standards. Unless otherwise stated, all quality control samples performed within specifications. This Certificate applies only to the sample(s) tested and is not a declaration of product safety, efficacy, or regulatory compliance.")), e(View, { style: styles.footer }, e(View, { style: styles.footerLeft }, data.qrCodeDataUrl ? e(View, { style: styles.qrSection }, e(Image, { src: data.qrCodeDataUrl, style: styles.qrCode }), e(Text, { style: styles.qrLabel }, "Scan for digital copy")) : null, e(Text, { style: styles.disclaimer }, `${data.labName || "This laboratory"} performs analytical testing using validated internal methodologies and quality control protocols. All results apply only to the sample(s) tested. This Certificate is not a declaration of product safety, efficacy, or regulatory compliance. Testing performed in accordance with applicable state regulations.\n\nThis report may not be reproduced except in full without written approval from ${data.labName || "the laboratory"}.`), e(Text, { style: { ...styles.disclaimer, marginTop: 4 } }, `For inquiries: support@quantixanalytics.com | Raleigh, NC | ${data.labWebsite || "www.quantixanalytics.com"}`)), e(View, { style: styles.footerRight }, e(View, { style: styles.footerCompany }, e(Text, { style: { fontFamily: "Helvetica-Bold" } }, data.labName || "Laboratory"), e(Text, {}, "All Rights Reserved"), e(Text, { style: { fontSize: 7, color: colors.gray600 } }, data.labWebsite || "www.quantixanalytics.com")), e(View, { style: styles.signatureSection }, data.signatureUrl ? e(Image, { src: data.signatureUrl, style: { width: 180, height: 70, marginBottom: 4 } }) : null, e(Text, { style: styles.signatureName }, data.labDirector || "Sarah Mitchell"), e(Text, { style: styles.signatureTitle }, data.directorTitle || "Laboratory Director"), e(Text, { style: styles.signatureTitle }, data.approvalDate || data.dateTested || "\u2014"), data.fullPanel ? e(Text, { style: { fontSize: 7, color: colors.gray500, marginTop: 8, textAlign: "right" } }, `Page 1 of ${totalPages}`) : null)))));
394
+ if (data.fullPanel) {
395
+ pages.push(e(SafetyTestingPage, { key: "page2", data: { ...data, totalPages } }));
396
+ pages.push(e(PesticidePage, { key: "page3", data: { ...data, totalPages }, category: "I", pesticides: data.pesticidesCat1 || [], pageNum: 3 }));
397
+ pages.push(e(PesticidePage, { key: "page4", data: { ...data, totalPages }, category: "II", pesticides: data.pesticidesCat2 || [], pageNum: 4 }));
398
+ if (hasResidualSolvents) {
399
+ pages.push(e(ResidualSolventsPage, { key: "page5", data: { ...data, totalPages } }));
400
+ }
401
+ }
402
+ return e(Document, {}, ...pages);
403
+ };
404
+ // ============================================================================
405
+ // Public API
406
+ // ============================================================================
407
+ export async function renderCOAToPdf(data) {
408
+ const doc = React.createElement(CannabisCOADocument, { data });
409
+ const buffer = await renderToBuffer(doc);
410
+ return Buffer.from(buffer);
411
+ }
@@ -50,7 +50,51 @@ export function preCompact(messages) {
50
50
  });
51
51
  return { ...msg, content: newContent };
52
52
  });
53
- // 2. Remove thinking blocks older than 3 turns from the end
53
+ // 2. Deduplicate tool results detect identical content (e.g. same file read multiple times)
54
+ // and replace duplicates with a pointer to the first occurrence.
55
+ const contentHashes = new Map();
56
+ for (let i = 0; i < compacted.length; i++) {
57
+ const msg = compacted[i];
58
+ if (msg.role !== "user" || !Array.isArray(msg.content))
59
+ continue;
60
+ for (const block of msg.content) {
61
+ if (block.type !== "tool_result")
62
+ continue;
63
+ const rc = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
64
+ if (rc.length < 200)
65
+ continue; // Skip small results — not worth deduping
66
+ const hashKey = `${rc.length}:${rc.slice(0, 200)}`;
67
+ const existing = contentHashes.get(hashKey);
68
+ if (existing) {
69
+ existing.count++;
70
+ }
71
+ else {
72
+ contentHashes.set(hashKey, { count: 1, firstIdx: i });
73
+ }
74
+ }
75
+ }
76
+ // Second pass: replace duplicates (not the first occurrence) with pointer
77
+ for (let i = 0; i < compacted.length; i++) {
78
+ const msg = compacted[i];
79
+ if (msg.role !== "user" || !Array.isArray(msg.content))
80
+ continue;
81
+ const newContent = msg.content.map((block) => {
82
+ if (block.type !== "tool_result")
83
+ return block;
84
+ const rc = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
85
+ if (rc.length < 200)
86
+ return block;
87
+ const hashKey = `${rc.length}:${rc.slice(0, 200)}`;
88
+ const entry = contentHashes.get(hashKey);
89
+ if (entry && entry.count >= 2 && i !== entry.firstIdx) {
90
+ bytesRemoved += rc.length - 50;
91
+ return { ...block, content: "[Duplicate — same content as earlier read]" };
92
+ }
93
+ return block;
94
+ });
95
+ compacted[i] = { ...msg, content: newContent };
96
+ }
97
+ // 3. Remove thinking blocks older than 3 turns from the end
54
98
  const thinkingCutoff = Math.max(0, compacted.length - 6); // 3 turns = ~6 messages
55
99
  const result = compacted.map((msg, idx) => {
56
100
  if (idx >= thinkingCutoff)
@@ -0,0 +1,143 @@
1
+ interface GenerationRule {
2
+ field: string;
3
+ type: "pattern" | "random_choice" | "random_range" | "relative_date" | "constant";
4
+ pattern?: string;
5
+ choices?: string[];
6
+ resolve_pattern?: boolean;
7
+ min?: number;
8
+ max?: number;
9
+ decimals?: number;
10
+ base_field?: string;
11
+ offset?: number | number[];
12
+ value?: unknown;
13
+ }
14
+ export declare function applyGenerationRules(rules: GenerationRule[] | Record<string, unknown> | undefined | null, data: Record<string, unknown>): Record<string, unknown>;
15
+ interface CannabinoidConfig {
16
+ [analyte: string]: {
17
+ min?: number;
18
+ max?: number;
19
+ nd?: boolean;
20
+ };
21
+ }
22
+ interface CannabinoidRow {
23
+ name: string;
24
+ result: string | number;
25
+ percentWeight: number;
26
+ mgPerG: number;
27
+ lod: number;
28
+ loq: number;
29
+ }
30
+ export declare function generateCannabinoidData(profileConfig: CannabinoidConfig | undefined | null, constants: Record<string, unknown>): CannabinoidRow[];
31
+ export declare function generateFullPanelData(constants: Record<string, unknown>): {
32
+ microbialResults: {
33
+ test: string;
34
+ result: string | number;
35
+ limit: number | string;
36
+ status: string;
37
+ }[];
38
+ heavyMetalsResults: {
39
+ analyte: string;
40
+ lod: number;
41
+ loq: number;
42
+ result: string | number;
43
+ limit: number;
44
+ status: string;
45
+ }[];
46
+ mycotoxinResults: {
47
+ analyte: string;
48
+ lod: number;
49
+ loq: number;
50
+ result: "ND";
51
+ limit: number;
52
+ status: "Pass";
53
+ }[];
54
+ pesticidesCat1: {
55
+ analyte: string;
56
+ result: "ND";
57
+ status: "Pass";
58
+ }[];
59
+ pesticidesCat2: {
60
+ analyte: string;
61
+ result: "ND";
62
+ status: "Pass";
63
+ }[];
64
+ residualSolventsResults: {
65
+ analyte: string;
66
+ result: "<LOQ";
67
+ limit: number;
68
+ loq: number;
69
+ unit: string;
70
+ status: "Pass";
71
+ }[];
72
+ };
73
+ interface Calculation {
74
+ field: string;
75
+ formula: string;
76
+ decimals?: number;
77
+ }
78
+ export declare function applyCalculations(calculations: Calculation[] | undefined | null, constants: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
79
+ interface ValidationRule {
80
+ name: string;
81
+ formula: string;
82
+ severity: "error" | "warning";
83
+ message: string;
84
+ }
85
+ export interface ValidationResult {
86
+ name: string;
87
+ severity: "error" | "warning";
88
+ message: string;
89
+ passed: boolean;
90
+ }
91
+ export declare function runValidation(rules: ValidationRule[] | undefined | null, data: Record<string, unknown>): ValidationResult[];
92
+ export interface LayoutElement {
93
+ type: string;
94
+ content?: string;
95
+ text?: string;
96
+ fontSize?: number;
97
+ color?: string;
98
+ bold?: boolean;
99
+ align?: string;
100
+ backgroundColor?: string;
101
+ padding?: string | number;
102
+ fontWeight?: string;
103
+ src?: string;
104
+ bind?: string;
105
+ width?: number | string;
106
+ height?: number | string;
107
+ thickness?: number;
108
+ columns?: LayoutElement[];
109
+ children?: LayoutElement[];
110
+ items?: LayoutElement[] | {
111
+ label: string;
112
+ bind: string;
113
+ }[];
114
+ headers?: string[];
115
+ rows?: unknown[];
116
+ border?: string;
117
+ background?: string;
118
+ label?: string;
119
+ value?: string;
120
+ url?: string;
121
+ data?: string;
122
+ size?: number;
123
+ }
124
+ export interface PageConfig {
125
+ size?: string;
126
+ orientation?: string;
127
+ margins?: {
128
+ top?: string;
129
+ right?: string;
130
+ bottom?: string;
131
+ left?: string;
132
+ };
133
+ }
134
+ export interface StyleConfig {
135
+ fontFamily?: string;
136
+ fontSize?: number;
137
+ color?: string;
138
+ }
139
+ export declare function resolveBinding(bind: string, data: Record<string, unknown>): unknown;
140
+ export declare function resolveText(text: string | undefined, data: Record<string, unknown>): string;
141
+ export declare function generateQRSvg(text: string, size?: number): string;
142
+ export declare function renderLayoutToHtml(layout: LayoutElement[] | Record<string, unknown> | undefined | null, pageConfig: PageConfig | undefined | null, styles: StyleConfig | undefined | null, data: Record<string, unknown>): string;
143
+ export {};