@djangocfg/ui-tools 2.1.335 → 2.1.337

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 (194) hide show
  1. package/README.md +68 -2
  2. package/dist/ChatRoot-PNNGQCYF.css +7 -0
  3. package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
  4. package/dist/ChatRoot-XV2QXMV4.mjs +5 -0
  5. package/dist/ChatRoot-XV2QXMV4.mjs.map +1 -0
  6. package/dist/ChatRoot-YX4RLHQX.cjs +14 -0
  7. package/dist/ChatRoot-YX4RLHQX.cjs.map +1 -0
  8. package/dist/{CronScheduler.client-3O3VU4CI.mjs → CronScheduler.client-DLMXCPAJ.mjs} +4 -4
  9. package/dist/{CronScheduler.client-3O3VU4CI.mjs.map → CronScheduler.client-DLMXCPAJ.mjs.map} +1 -1
  10. package/dist/{CronScheduler.client-A4GO6YBY.cjs → CronScheduler.client-WEJF4PWQ.cjs} +14 -14
  11. package/dist/{CronScheduler.client-A4GO6YBY.cjs.map → CronScheduler.client-WEJF4PWQ.cjs.map} +1 -1
  12. package/dist/{DocsLayout-XLDB6CJ2.cjs → DocsLayout-N5ZJZPBY.cjs} +200 -199
  13. package/dist/DocsLayout-N5ZJZPBY.cjs.map +1 -0
  14. package/dist/{DocsLayout-CTJINVBM.mjs → DocsLayout-VFPPNKSQ.mjs} +7 -6
  15. package/dist/DocsLayout-VFPPNKSQ.mjs.map +1 -0
  16. package/dist/JsonSchemaForm-DD7CLRIG.cjs +13 -0
  17. package/dist/{JsonSchemaForm-6WMS4CIY.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
  18. package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
  19. package/dist/{JsonSchemaForm-KX4JT3M4.mjs.map → JsonSchemaForm-XKUIVELK.mjs.map} +1 -1
  20. package/dist/JsonTree-55625VVH.mjs +5 -0
  21. package/dist/{JsonTree-F27RMYSI.cjs.map → JsonTree-55625VVH.mjs.map} +1 -1
  22. package/dist/JsonTree-DCM5QGWF.cjs +11 -0
  23. package/dist/{JsonTree-QTJYSHCV.mjs.map → JsonTree-DCM5QGWF.cjs.map} +1 -1
  24. package/dist/{LottiePlayer.client-6WVWDO75.cjs → LottiePlayer.client-2S7ISJ2S.cjs} +6 -6
  25. package/dist/{LottiePlayer.client-6WVWDO75.cjs.map → LottiePlayer.client-2S7ISJ2S.cjs.map} +1 -1
  26. package/dist/{LottiePlayer.client-B4I6WNZM.mjs → LottiePlayer.client-5LDSSJWS.mjs} +4 -4
  27. package/dist/{LottiePlayer.client-B4I6WNZM.mjs.map → LottiePlayer.client-5LDSSJWS.mjs.map} +1 -1
  28. package/dist/{MapContainer-RYG4HPH4.cjs → MapContainer-76YL2JXL.cjs} +8 -8
  29. package/dist/{MapContainer-RYG4HPH4.cjs.map → MapContainer-76YL2JXL.cjs.map} +1 -1
  30. package/dist/{MapContainer-GXQLP5WY.mjs → MapContainer-7HXBI3OH.mjs} +3 -3
  31. package/dist/{MapContainer-GXQLP5WY.mjs.map → MapContainer-7HXBI3OH.mjs.map} +1 -1
  32. package/dist/{Mermaid.client-SXRRI2YW.mjs → Mermaid.client-NL4SVR7F.mjs} +4 -4
  33. package/dist/{Mermaid.client-SXRRI2YW.mjs.map → Mermaid.client-NL4SVR7F.mjs.map} +1 -1
  34. package/dist/{Mermaid.client-W76R5AKJ.cjs → Mermaid.client-NNTI6DFX.cjs} +26 -26
  35. package/dist/{Mermaid.client-W76R5AKJ.cjs.map → Mermaid.client-NNTI6DFX.cjs.map} +1 -1
  36. package/dist/Player-BRV7XTWR.mjs +4 -0
  37. package/dist/{Player-M3GC3VPE.mjs.map → Player-BRV7XTWR.mjs.map} +1 -1
  38. package/dist/Player-PM7F7DD7.cjs +13 -0
  39. package/dist/{Player-ZL2X5LGG.cjs.map → Player-PM7F7DD7.cjs.map} +1 -1
  40. package/dist/{PrettyCode.client-RPDIE5CH.cjs → PrettyCode.client-KOHDVPPN.cjs} +13 -13
  41. package/dist/{PrettyCode.client-RPDIE5CH.cjs.map → PrettyCode.client-KOHDVPPN.cjs.map} +1 -1
  42. package/dist/{PrettyCode.client-SPMTQEG4.mjs → PrettyCode.client-ZGYGKE7G.mjs} +4 -4
  43. package/dist/{PrettyCode.client-SPMTQEG4.mjs.map → PrettyCode.client-ZGYGKE7G.mjs.map} +1 -1
  44. package/dist/TreeRoot-N72OYKXU.cjs +19 -0
  45. package/dist/{TreeRoot-A3J65L6F.mjs.map → TreeRoot-N72OYKXU.cjs.map} +1 -1
  46. package/dist/TreeRoot-VGAIXCUA.mjs +4 -0
  47. package/dist/{TreeRoot-DSK5JILT.cjs.map → TreeRoot-VGAIXCUA.mjs.map} +1 -1
  48. package/dist/chunk-2ZLKZ5VR.mjs +631 -0
  49. package/dist/chunk-2ZLKZ5VR.mjs.map +1 -0
  50. package/dist/{chunk-LFWQ36LJ.mjs → chunk-5G5YBFS6.mjs} +4 -4
  51. package/dist/{chunk-LFWQ36LJ.mjs.map → chunk-5G5YBFS6.mjs.map} +1 -1
  52. package/dist/{chunk-IHAY6FO6.cjs → chunk-5I5QNGUG.cjs} +17 -17
  53. package/dist/{chunk-IHAY6FO6.cjs.map → chunk-5I5QNGUG.cjs.map} +1 -1
  54. package/dist/{chunk-F2CMIIOH.cjs → chunk-76NNDZH6.cjs} +42 -42
  55. package/dist/{chunk-F2CMIIOH.cjs.map → chunk-76NNDZH6.cjs.map} +1 -1
  56. package/dist/chunk-B5AWZOHJ.cjs +649 -0
  57. package/dist/chunk-B5AWZOHJ.cjs.map +1 -0
  58. package/dist/{chunk-KR6B3LVY.mjs → chunk-B6IR5KSC.mjs} +3 -3
  59. package/dist/{chunk-KR6B3LVY.mjs.map → chunk-B6IR5KSC.mjs.map} +1 -1
  60. package/dist/{chunk-5LBDYFWH.mjs → chunk-C6GXVH5J.mjs} +3 -3
  61. package/dist/{chunk-5LBDYFWH.mjs.map → chunk-C6GXVH5J.mjs.map} +1 -1
  62. package/dist/{chunk-NRKD4F5X.cjs → chunk-FEN5S772.cjs} +36 -36
  63. package/dist/{chunk-NRKD4F5X.cjs.map → chunk-FEN5S772.cjs.map} +1 -1
  64. package/dist/{chunk-2SMCH62O.cjs → chunk-FP2RLYQZ.cjs} +11 -11
  65. package/dist/{chunk-2SMCH62O.cjs.map → chunk-FP2RLYQZ.cjs.map} +1 -1
  66. package/dist/{chunk-MOME6KYD.mjs → chunk-G5IEC7SR.mjs} +3 -3
  67. package/dist/{chunk-MOME6KYD.mjs.map → chunk-G5IEC7SR.mjs.map} +1 -1
  68. package/dist/{chunk-SE5IERVH.mjs → chunk-GYIO7W7M.mjs} +3 -3
  69. package/dist/{chunk-SE5IERVH.mjs.map → chunk-GYIO7W7M.mjs.map} +1 -1
  70. package/dist/{chunk-3Z3A7FHA.cjs → chunk-IEEAENLX.cjs} +48 -48
  71. package/dist/{chunk-3Z3A7FHA.cjs.map → chunk-IEEAENLX.cjs.map} +1 -1
  72. package/dist/{chunk-DFTVB66S.cjs → chunk-KNDLV4PI.cjs} +85 -85
  73. package/dist/{chunk-DFTVB66S.cjs.map → chunk-KNDLV4PI.cjs.map} +1 -1
  74. package/dist/{chunk-SSUOENAZ.mjs → chunk-KNEQRUBA.mjs} +3 -3
  75. package/dist/{chunk-SSUOENAZ.mjs.map → chunk-KNEQRUBA.mjs.map} +1 -1
  76. package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
  77. package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
  78. package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
  79. package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
  80. package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
  81. package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
  82. package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
  83. package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
  84. package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
  85. package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
  86. package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
  87. package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
  88. package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
  89. package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
  90. package/dist/chunk-QW4RBGHN.cjs +961 -0
  91. package/dist/chunk-QW4RBGHN.cjs.map +1 -0
  92. package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
  93. package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
  94. package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
  95. package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
  96. package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
  97. package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
  98. package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
  99. package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
  100. package/dist/chunk-YWSQDBNU.mjs +2339 -0
  101. package/dist/chunk-YWSQDBNU.mjs.map +1 -0
  102. package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
  103. package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
  104. package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
  105. package/dist/chunk-ZUFTH5IR.mjs.map +1 -0
  106. package/dist/chunk-ZWPBBAR2.cjs +2379 -0
  107. package/dist/chunk-ZWPBBAR2.cjs.map +1 -0
  108. package/dist/components-EHOGXATG.cjs +22 -0
  109. package/dist/{components-5UXYNAKR.cjs.map → components-EHOGXATG.cjs.map} +1 -1
  110. package/dist/components-MQ6DR7TX.cjs +26 -0
  111. package/dist/{components-CFXOEVPN.mjs.map → components-MQ6DR7TX.cjs.map} +1 -1
  112. package/dist/components-XRX7QGLB.mjs +5 -0
  113. package/dist/{components-WYEZL5TE.cjs.map → components-XRX7QGLB.mjs.map} +1 -1
  114. package/dist/components-YATKRWLH.mjs +5 -0
  115. package/dist/{components-ZAGG2PBO.mjs.map → components-YATKRWLH.mjs.map} +1 -1
  116. package/dist/file-icon/index.cjs +6 -6
  117. package/dist/file-icon/index.mjs +1 -1
  118. package/dist/index.cjs +739 -215
  119. package/dist/index.cjs.map +1 -1
  120. package/dist/index.d.cts +1025 -39
  121. package/dist/index.d.ts +1025 -39
  122. package/dist/index.mjs +387 -31
  123. package/dist/index.mjs.map +1 -1
  124. package/dist/tree/index.cjs +38 -38
  125. package/dist/tree/index.d.cts +2 -2
  126. package/dist/tree/index.d.ts +2 -2
  127. package/dist/tree/index.mjs +3 -3
  128. package/package.json +6 -6
  129. package/src/index.ts +5 -0
  130. package/src/stories/index.ts +3 -1
  131. package/src/tools/Chat/Chat.story.tsx +1006 -0
  132. package/src/tools/Chat/README.md +528 -0
  133. package/src/tools/Chat/components/Attachments.tsx +192 -0
  134. package/src/tools/Chat/components/ChatRoot.tsx +208 -0
  135. package/src/tools/Chat/components/Composer.tsx +134 -0
  136. package/src/tools/Chat/components/EmptyState.tsx +47 -0
  137. package/src/tools/Chat/components/ErrorBanner.tsx +47 -0
  138. package/src/tools/Chat/components/JumpToLatest.tsx +30 -0
  139. package/src/tools/Chat/components/MessageActions.tsx +72 -0
  140. package/src/tools/Chat/components/MessageBubble.tsx +228 -0
  141. package/src/tools/Chat/components/MessageList.tsx +82 -0
  142. package/src/tools/Chat/components/Sources.tsx +55 -0
  143. package/src/tools/Chat/components/StreamingIndicator.tsx +29 -0
  144. package/src/tools/Chat/components/ToolCalls.tsx +172 -0
  145. package/src/tools/Chat/components/index.ts +24 -0
  146. package/src/tools/Chat/config.ts +55 -0
  147. package/src/tools/Chat/context/ChatProvider.tsx +126 -0
  148. package/src/tools/Chat/context/index.ts +9 -0
  149. package/src/tools/Chat/core/audio/audioBus.ts +172 -0
  150. package/src/tools/Chat/core/audio/index.ts +8 -0
  151. package/src/tools/Chat/core/audio/preferences.ts +68 -0
  152. package/src/tools/Chat/core/audio/types.ts +49 -0
  153. package/src/tools/Chat/core/ids.ts +16 -0
  154. package/src/tools/Chat/core/index.ts +5 -0
  155. package/src/tools/Chat/core/logger.ts +73 -0
  156. package/src/tools/Chat/core/markdown.ts +56 -0
  157. package/src/tools/Chat/core/payload-dispatch.ts +54 -0
  158. package/src/tools/Chat/core/persona.ts +35 -0
  159. package/src/tools/Chat/core/reducer.ts +335 -0
  160. package/src/tools/Chat/core/transport/http.ts +167 -0
  161. package/src/tools/Chat/core/transport/index.ts +13 -0
  162. package/src/tools/Chat/core/transport/mock.ts +134 -0
  163. package/src/tools/Chat/core/transport/sse.ts +116 -0
  164. package/src/tools/Chat/core/transport/types.ts +24 -0
  165. package/src/tools/Chat/hooks/index.ts +26 -0
  166. package/src/tools/Chat/hooks/useChat.ts +555 -0
  167. package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
  168. package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
  169. package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
  170. package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
  171. package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
  172. package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
  173. package/src/tools/Chat/index.ts +161 -0
  174. package/src/tools/Chat/lazy.tsx +14 -0
  175. package/src/tools/Chat/types.ts +237 -0
  176. package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
  177. package/src/tools/Map/README.md +384 -0
  178. package/dist/DocsLayout-CTJINVBM.mjs.map +0 -1
  179. package/dist/DocsLayout-XLDB6CJ2.cjs.map +0 -1
  180. package/dist/JsonSchemaForm-6WMS4CIY.cjs +0 -13
  181. package/dist/JsonSchemaForm-KX4JT3M4.mjs +0 -4
  182. package/dist/JsonTree-F27RMYSI.cjs +0 -11
  183. package/dist/JsonTree-QTJYSHCV.mjs +0 -5
  184. package/dist/Player-M3GC3VPE.mjs +0 -4
  185. package/dist/Player-ZL2X5LGG.cjs +0 -13
  186. package/dist/TreeRoot-A3J65L6F.mjs +0 -4
  187. package/dist/TreeRoot-DSK5JILT.cjs +0 -19
  188. package/dist/chunk-62Y65TGK.mjs.map +0 -1
  189. package/dist/chunk-TKSFZHCG.cjs +0 -1597
  190. package/dist/chunk-TKSFZHCG.cjs.map +0 -1
  191. package/dist/components-5UXYNAKR.cjs +0 -22
  192. package/dist/components-CFXOEVPN.mjs +0 -5
  193. package/dist/components-WYEZL5TE.cjs +0 -26
  194. package/dist/components-ZAGG2PBO.mjs +0 -5
@@ -0,0 +1,54 @@
1
+ // Tiny dispatcher for tool-call payload renderers.
2
+ //
3
+ // Hosts compose matchers (predicate + render) and a fallback. The result is a
4
+ // `renderPayload` function compatible with `<ToolCalls renderPayload>`.
5
+ //
6
+ // Pure — no React, no DOM. The matchers themselves return ReactNode.
7
+
8
+ import type { ReactNode } from 'react';
9
+
10
+ import type { ChatToolCall } from '../types';
11
+ import type { ToolPayloadKind } from '../components/ToolCalls';
12
+
13
+ export interface ToolPayloadMatcher {
14
+ /** Cheap predicate. First match wins. */
15
+ match: (value: unknown, kind: ToolPayloadKind, call: ChatToolCall) => boolean;
16
+ render: (value: unknown, kind: ToolPayloadKind, call: ChatToolCall) => ReactNode;
17
+ }
18
+
19
+ export type ToolPayloadFallback = (
20
+ value: unknown,
21
+ kind: ToolPayloadKind,
22
+ call: ChatToolCall,
23
+ ) => ReactNode;
24
+
25
+ export function dispatchToolPayload(
26
+ matchers: ToolPayloadMatcher[],
27
+ fallback: ToolPayloadFallback,
28
+ ): ToolPayloadFallback {
29
+ return (value, kind, call) => {
30
+ for (const m of matchers) {
31
+ if (m.match(value, kind, call)) return m.render(value, kind, call);
32
+ }
33
+ return fallback(value, kind, call);
34
+ };
35
+ }
36
+
37
+ // ---- Common matcher predicates -------------------------------------------
38
+ // Re-export as building blocks. Hosts can use directly or compose.
39
+
40
+ export function isPlainObject(v: unknown): v is Record<string, unknown> {
41
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
42
+ }
43
+
44
+ export function isLatLng(v: unknown): v is { lat: number; lng: number } {
45
+ return isPlainObject(v) && typeof v.lat === 'number' && typeof v.lng === 'number';
46
+ }
47
+
48
+ export function isGeoJSONFeatureCollection(v: unknown): v is { type: 'FeatureCollection'; features: unknown[] } {
49
+ return isPlainObject(v) && v.type === 'FeatureCollection' && Array.isArray(v.features);
50
+ }
51
+
52
+ export function isStringValue(v: unknown): v is string {
53
+ return typeof v === 'string';
54
+ }
@@ -0,0 +1,35 @@
1
+ // Persona resolution helpers — pure, no React.
2
+ //
3
+ // Resolution cascade for a bubble's persona:
4
+ // 1. message.sender (per-message override; multi-user chats)
5
+ // 2. config.user / config.assistant (provider-level default)
6
+ // 3. role-based fallback ('You' / 'AI')
7
+
8
+ import type { ChatAssistantContext, ChatMessage, ChatPersona, ChatUserContext } from '../types';
9
+
10
+ const FALLBACK_USER: ChatPersona = { name: 'You', initials: 'You' };
11
+ const FALLBACK_ASSISTANT: ChatPersona = { name: 'AI', initials: 'AI' };
12
+
13
+ export function resolvePersona(
14
+ message: Pick<ChatMessage, 'role' | 'sender'>,
15
+ user?: ChatUserContext,
16
+ assistant?: ChatAssistantContext,
17
+ ): ChatPersona {
18
+ if (message.sender) return message.sender;
19
+ if (message.role === 'user') return user ?? FALLBACK_USER;
20
+ if (message.role === 'assistant') return assistant ?? FALLBACK_ASSISTANT;
21
+ return { name: message.role };
22
+ }
23
+
24
+ /** Compute initials for an avatar fallback. */
25
+ export function deriveInitials(persona: ChatPersona, role?: string): string {
26
+ if (persona.initials) return persona.initials;
27
+ if (persona.name) {
28
+ const parts = persona.name.trim().split(/\s+/);
29
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
30
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
31
+ }
32
+ if (role === 'user') return 'You';
33
+ if (role === 'assistant') return 'AI';
34
+ return '?';
35
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Pure chat state machine. Zero React. Zero I/O.
3
+ *
4
+ * Invariants:
5
+ * - At most one message has `isStreaming === true` at any time. It is always
6
+ * the last message in the array.
7
+ * - Messages are immutable; updates produce new objects.
8
+ * - `version` bumps on edit so memo keys invalidate.
9
+ */
10
+
11
+ import type { ChatAttachment, ChatMessage, ChatSource, ChatToolCall } from '../types';
12
+
13
+ export interface ChatState {
14
+ sessionId: string | null;
15
+ messages: ChatMessage[];
16
+ /** Initial history load in flight. */
17
+ isLoading: boolean;
18
+ /** Assistant is generating a reply. */
19
+ isStreaming: boolean;
20
+ /** Older history page in flight. */
21
+ isLoadingMore: boolean;
22
+ hasMore: boolean;
23
+ oldestCursor: string | null;
24
+ error: string | null;
25
+ }
26
+
27
+ export const initialState: ChatState = {
28
+ sessionId: null,
29
+ messages: [],
30
+ isLoading: false,
31
+ isStreaming: false,
32
+ isLoadingMore: false,
33
+ hasMore: true,
34
+ oldestCursor: null,
35
+ error: null,
36
+ };
37
+
38
+ export type ChatAction =
39
+ | {
40
+ type: 'SESSION_SET';
41
+ sessionId: string;
42
+ messages?: ChatMessage[];
43
+ hasMore?: boolean;
44
+ cursor?: string | null;
45
+ }
46
+ | { type: 'HISTORY_LOAD_START' }
47
+ | {
48
+ type: 'HISTORY_LOAD_DONE';
49
+ messages: ChatMessage[];
50
+ hasMore: boolean;
51
+ cursor: string | null;
52
+ }
53
+ | { type: 'HISTORY_MORE_START' }
54
+ | {
55
+ type: 'HISTORY_MORE_DONE';
56
+ messages: ChatMessage[];
57
+ hasMore: boolean;
58
+ cursor: string | null;
59
+ }
60
+ | { type: 'MESSAGE_USER_ADD'; message: ChatMessage }
61
+ | { type: 'STREAM_START'; id: string; createdAt?: number }
62
+ | { type: 'STREAM_CHUNK'; delta: string }
63
+ | { type: 'STREAM_TOOL_ACTIVITY'; tool: string }
64
+ | { type: 'TOOL_CALL_START'; messageId: string; toolCall: ChatToolCall }
65
+ | { type: 'TOOL_CALL_DELTA'; messageId: string; toolId: string; delta: string }
66
+ | {
67
+ type: 'TOOL_CALL_END';
68
+ messageId: string;
69
+ toolId: string;
70
+ output: unknown;
71
+ status: 'success' | 'error';
72
+ }
73
+ | {
74
+ type: 'STREAM_DONE';
75
+ id: string;
76
+ tokensIn?: number;
77
+ tokensOut?: number;
78
+ sources?: ChatSource[];
79
+ }
80
+ | { type: 'STREAM_CANCELLED'; id: string; partialText: string; label?: string }
81
+ | { type: 'STREAM_ERROR'; id?: string; message: string }
82
+ | { type: 'MESSAGE_EDIT'; id: string; content: string }
83
+ | { type: 'MESSAGE_DELETE'; id: string }
84
+ | { type: 'MESSAGES_CLEAR' }
85
+ | { type: 'ERROR_SET'; error: string | null }
86
+ | {
87
+ type: 'ATTACHMENT_PROGRESS';
88
+ messageId: string;
89
+ attachmentId: string;
90
+ progress?: number;
91
+ status?: ChatAttachment['status'];
92
+ };
93
+
94
+ function updateLastStreaming(
95
+ messages: ChatMessage[],
96
+ patch: (m: ChatMessage) => ChatMessage,
97
+ ): ChatMessage[] {
98
+ const idx = findLastStreamingIndex(messages);
99
+ if (idx === -1) return messages;
100
+ const next = messages.slice();
101
+ next[idx] = patch(messages[idx]);
102
+ return next;
103
+ }
104
+
105
+ function findLastStreamingIndex(messages: ChatMessage[]): number {
106
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
107
+ if (messages[i].isStreaming) return i;
108
+ }
109
+ return -1;
110
+ }
111
+
112
+ function patchMessageById(
113
+ messages: ChatMessage[],
114
+ id: string,
115
+ patch: (m: ChatMessage) => ChatMessage,
116
+ ): ChatMessage[] {
117
+ const idx = messages.findIndex((m) => m.id === id);
118
+ if (idx === -1) return messages;
119
+ const next = messages.slice();
120
+ next[idx] = patch(messages[idx]);
121
+ return next;
122
+ }
123
+
124
+ export function reducer(state: ChatState, action: ChatAction): ChatState {
125
+ switch (action.type) {
126
+ case 'SESSION_SET':
127
+ return {
128
+ ...state,
129
+ sessionId: action.sessionId,
130
+ messages: action.messages ?? state.messages,
131
+ hasMore: action.hasMore ?? state.hasMore,
132
+ oldestCursor: action.cursor ?? state.oldestCursor,
133
+ error: null,
134
+ };
135
+
136
+ case 'HISTORY_LOAD_START':
137
+ return { ...state, isLoading: true, error: null };
138
+
139
+ case 'HISTORY_LOAD_DONE':
140
+ return {
141
+ ...state,
142
+ isLoading: false,
143
+ messages: action.messages,
144
+ hasMore: action.hasMore,
145
+ oldestCursor: action.cursor,
146
+ };
147
+
148
+ case 'HISTORY_MORE_START':
149
+ return { ...state, isLoadingMore: true };
150
+
151
+ case 'HISTORY_MORE_DONE':
152
+ return {
153
+ ...state,
154
+ isLoadingMore: false,
155
+ // Older messages prepend to the top.
156
+ messages: [...action.messages, ...state.messages],
157
+ hasMore: action.hasMore,
158
+ oldestCursor: action.cursor,
159
+ };
160
+
161
+ case 'MESSAGE_USER_ADD':
162
+ return {
163
+ ...state,
164
+ messages: [...state.messages, action.message],
165
+ error: null,
166
+ };
167
+
168
+ case 'STREAM_START': {
169
+ if (state.isStreaming) {
170
+ if (typeof console !== 'undefined') {
171
+ // Soft guard — UI should disable the composer; this catches bugs.
172
+ // eslint-disable-next-line no-console
173
+ console.warn('[chat] STREAM_START while already streaming, ignoring');
174
+ }
175
+ return state;
176
+ }
177
+ const placeholder: ChatMessage = {
178
+ id: action.id,
179
+ role: 'assistant',
180
+ content: '',
181
+ createdAt: action.createdAt ?? Date.now(),
182
+ isStreaming: true,
183
+ };
184
+ return {
185
+ ...state,
186
+ isStreaming: true,
187
+ messages: [...state.messages, placeholder],
188
+ };
189
+ }
190
+
191
+ case 'STREAM_CHUNK': {
192
+ const messages = updateLastStreaming(state.messages, (m) => ({
193
+ ...m,
194
+ content: m.content + action.delta,
195
+ }));
196
+ return { ...state, messages };
197
+ }
198
+
199
+ case 'STREAM_TOOL_ACTIVITY': {
200
+ const messages = updateLastStreaming(state.messages, (m) => ({
201
+ ...m,
202
+ toolActivity: action.tool,
203
+ }));
204
+ return { ...state, messages };
205
+ }
206
+
207
+ case 'TOOL_CALL_START': {
208
+ const messages = patchMessageById(state.messages, action.messageId, (m) => ({
209
+ ...m,
210
+ toolCalls: [...(m.toolCalls ?? []), action.toolCall],
211
+ }));
212
+ return { ...state, messages };
213
+ }
214
+
215
+ case 'TOOL_CALL_DELTA': {
216
+ const messages = patchMessageById(state.messages, action.messageId, (m) => {
217
+ if (!m.toolCalls) return m;
218
+ return {
219
+ ...m,
220
+ toolCalls: m.toolCalls.map((tc) =>
221
+ tc.id === action.toolId
222
+ ? { ...tc, streamingText: (tc.streamingText ?? '') + action.delta }
223
+ : tc,
224
+ ),
225
+ };
226
+ });
227
+ return { ...state, messages };
228
+ }
229
+
230
+ case 'TOOL_CALL_END': {
231
+ const messages = patchMessageById(state.messages, action.messageId, (m) => {
232
+ if (!m.toolCalls) return m;
233
+ return {
234
+ ...m,
235
+ toolCalls: m.toolCalls.map((tc) =>
236
+ tc.id === action.toolId
237
+ ? {
238
+ ...tc,
239
+ output: action.output,
240
+ status: action.status,
241
+ streamingText: undefined,
242
+ endedAt: Date.now(),
243
+ }
244
+ : tc,
245
+ ),
246
+ };
247
+ });
248
+ return { ...state, messages };
249
+ }
250
+
251
+ case 'STREAM_DONE': {
252
+ const messages = patchMessageById(state.messages, action.id, (m) => ({
253
+ ...m,
254
+ isStreaming: false,
255
+ tokensIn: action.tokensIn ?? m.tokensIn,
256
+ tokensOut: action.tokensOut ?? m.tokensOut,
257
+ sources: action.sources ?? m.sources,
258
+ }));
259
+ return { ...state, isStreaming: false, messages };
260
+ }
261
+
262
+ case 'STREAM_CANCELLED': {
263
+ const suffix = action.label ?? '[cancelled]';
264
+ const messages = patchMessageById(state.messages, action.id, (m) => ({
265
+ ...m,
266
+ isStreaming: false,
267
+ content: action.partialText + (action.partialText ? `\n\n_${suffix}_` : `_${suffix}_`),
268
+ }));
269
+ return { ...state, isStreaming: false, messages };
270
+ }
271
+
272
+ case 'STREAM_ERROR': {
273
+ const messages = action.id
274
+ ? patchMessageById(state.messages, action.id, (m) => ({
275
+ ...m,
276
+ isStreaming: false,
277
+ isError: true,
278
+ }))
279
+ : state.messages;
280
+ return { ...state, isStreaming: false, error: action.message, messages };
281
+ }
282
+
283
+ case 'MESSAGE_EDIT': {
284
+ const messages = patchMessageById(state.messages, action.id, (m) => ({
285
+ ...m,
286
+ content: action.content,
287
+ version: (m.version ?? 0) + 1,
288
+ }));
289
+ return { ...state, messages };
290
+ }
291
+
292
+ case 'MESSAGE_DELETE':
293
+ return {
294
+ ...state,
295
+ messages: state.messages.filter((m) => m.id !== action.id),
296
+ };
297
+
298
+ case 'MESSAGES_CLEAR':
299
+ return {
300
+ ...state,
301
+ messages: [],
302
+ isStreaming: false,
303
+ error: null,
304
+ };
305
+
306
+ case 'ERROR_SET':
307
+ return { ...state, error: action.error };
308
+
309
+ case 'ATTACHMENT_PROGRESS': {
310
+ const messages = patchMessageById(state.messages, action.messageId, (m) => {
311
+ if (!m.attachments) return m;
312
+ return {
313
+ ...m,
314
+ attachments: m.attachments.map((a) =>
315
+ a.id === action.attachmentId
316
+ ? {
317
+ ...a,
318
+ progress: action.progress ?? a.progress,
319
+ status: action.status ?? a.status,
320
+ }
321
+ : a,
322
+ ),
323
+ };
324
+ });
325
+ return { ...state, messages };
326
+ }
327
+
328
+ default: {
329
+ // Exhaustiveness check.
330
+ const _exhaustive: never = action;
331
+ void _exhaustive;
332
+ return state;
333
+ }
334
+ }
335
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * HTTP + SSE transport. Default implementation for web hosts.
3
+ *
4
+ * Backend contract (see @dev/@refactoring7-chat/06-integration.md):
5
+ * POST /sessions → SessionInfo (JSON)
6
+ * GET /sessions/:id/history?cursor= → HistoryPage (JSON)
7
+ * POST /sessions/:id/messages → SSE stream of ChatStreamEvent
8
+ * POST /sessions/:id/messages/buffered → ChatMessage (JSON, fallback)
9
+ * DELETE /sessions/:id → 204
10
+ */
11
+
12
+ import type {
13
+ ChatMessage,
14
+ ChatStreamEvent,
15
+ ChatTransport,
16
+ CreateSessionOptions,
17
+ HistoryPage,
18
+ SendOptions,
19
+ SessionInfo,
20
+ StreamOptions,
21
+ } from '../../types';
22
+ import { TransportError } from './types';
23
+ import { parseSSE } from './sse';
24
+
25
+ export interface HttpTransportConfig {
26
+ /** Base URL without trailing slash, e.g. '/api/chat' or 'https://api.example.com/v1/chat'. */
27
+ baseUrl: string;
28
+ /** Optional slug appended/forwarded as project identifier. */
29
+ slug?: string;
30
+ /** Returns headers applied to every request — e.g. Authorization. */
31
+ getAuthHeader?: () => Record<string, string> | Promise<Record<string, string>>;
32
+ /** Default fetch timeout (per non-streaming request). */
33
+ timeoutMs?: number;
34
+ /** Override fetch implementation (useful for tests or custom retry layers). */
35
+ fetchImpl?: typeof fetch;
36
+ }
37
+
38
+ const DEFAULT_TIMEOUT = 20_000;
39
+
40
+ async function jsonOrThrow<T>(res: Response, label: string): Promise<T> {
41
+ if (!res.ok) {
42
+ const text = await res.text().catch(() => '');
43
+ throw new TransportError(
44
+ `${label} failed (${res.status}): ${text || res.statusText}`,
45
+ mapStatusToCode(res.status),
46
+ );
47
+ }
48
+ try {
49
+ return (await res.json()) as T;
50
+ } catch {
51
+ throw new TransportError(`${label} returned invalid JSON`, 'invalid_response');
52
+ }
53
+ }
54
+
55
+ function mapStatusToCode(status: number): string {
56
+ if (status === 401) return 'unauthorized';
57
+ if (status === 403) return 'forbidden';
58
+ if (status === 404) return 'not_found';
59
+ if (status === 429) return 'rate_limited';
60
+ if (status >= 500) return 'server_error';
61
+ return 'http_error';
62
+ }
63
+
64
+ function withTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
65
+ const ctrl = new AbortController();
66
+ const onAbort = () => ctrl.abort();
67
+ signal?.addEventListener('abort', onAbort, { once: true });
68
+ setTimeout(() => ctrl.abort(), timeoutMs);
69
+ return ctrl.signal;
70
+ }
71
+
72
+ export function createHttpTransport(config: HttpTransportConfig): ChatTransport {
73
+ const fetchImpl = config.fetchImpl ?? fetch;
74
+ const timeout = config.timeoutMs ?? DEFAULT_TIMEOUT;
75
+ const base = config.baseUrl.replace(/\/$/, '');
76
+
77
+ async function buildHeaders(extra?: Record<string, string>): Promise<Record<string, string>> {
78
+ const auth = (await config.getAuthHeader?.()) ?? {};
79
+ return {
80
+ 'Content-Type': 'application/json',
81
+ ...auth,
82
+ ...(extra ?? {}),
83
+ };
84
+ }
85
+
86
+ return {
87
+ async createSession(opts?: CreateSessionOptions): Promise<SessionInfo> {
88
+ const res = await fetchImpl(`${base}/sessions`, {
89
+ method: 'POST',
90
+ headers: await buildHeaders(),
91
+ body: JSON.stringify({ slug: config.slug, metadata: opts?.metadata ?? {} }),
92
+ signal: withTimeout(undefined, timeout),
93
+ });
94
+ return jsonOrThrow<SessionInfo>(res, 'createSession');
95
+ },
96
+
97
+ async loadHistory(sessionId, cursor, limit): Promise<HistoryPage> {
98
+ const params = new URLSearchParams();
99
+ if (cursor) params.set('cursor', cursor);
100
+ if (limit) params.set('limit', String(limit));
101
+ const url = `${base}/sessions/${encodeURIComponent(sessionId)}/history${
102
+ params.toString() ? `?${params.toString()}` : ''
103
+ }`;
104
+ const res = await fetchImpl(url, {
105
+ method: 'GET',
106
+ headers: await buildHeaders(),
107
+ signal: withTimeout(undefined, timeout),
108
+ });
109
+ return jsonOrThrow<HistoryPage>(res, 'loadHistory');
110
+ },
111
+
112
+ async *stream(
113
+ sessionId: string,
114
+ content: string,
115
+ options: StreamOptions,
116
+ ): AsyncGenerator<ChatStreamEvent, void, void> {
117
+ const url = `${base}/sessions/${encodeURIComponent(sessionId)}/messages`;
118
+ const res = await fetchImpl(url, {
119
+ method: 'POST',
120
+ headers: await buildHeaders({ Accept: 'text/event-stream' }),
121
+ body: JSON.stringify({
122
+ content,
123
+ attachments: options.attachments ?? [],
124
+ metadata: options.metadata ?? {},
125
+ }),
126
+ signal: options.signal,
127
+ });
128
+
129
+ if (!res.ok) {
130
+ const text = await res.text().catch(() => '');
131
+ throw new TransportError(
132
+ `stream failed (${res.status}): ${text || res.statusText}`,
133
+ mapStatusToCode(res.status),
134
+ );
135
+ }
136
+
137
+ yield* parseSSE(res, { signal: options.signal });
138
+ },
139
+
140
+ async send(sessionId, content, options?: SendOptions): Promise<ChatMessage> {
141
+ const url = `${base}/sessions/${encodeURIComponent(sessionId)}/messages/buffered`;
142
+ const res = await fetchImpl(url, {
143
+ method: 'POST',
144
+ headers: await buildHeaders(),
145
+ body: JSON.stringify({
146
+ content,
147
+ attachments: options?.attachments ?? [],
148
+ metadata: options?.metadata ?? {},
149
+ }),
150
+ signal: options?.signal ?? withTimeout(undefined, timeout),
151
+ });
152
+ return jsonOrThrow<ChatMessage>(res, 'send');
153
+ },
154
+
155
+ async closeSession(sessionId: string): Promise<void> {
156
+ const url = `${base}/sessions/${encodeURIComponent(sessionId)}`;
157
+ const res = await fetchImpl(url, {
158
+ method: 'DELETE',
159
+ headers: await buildHeaders(),
160
+ signal: withTimeout(undefined, timeout),
161
+ });
162
+ if (!res.ok && res.status !== 404) {
163
+ throw new TransportError(`closeSession failed (${res.status})`, mapStatusToCode(res.status));
164
+ }
165
+ },
166
+ };
167
+ }
@@ -0,0 +1,13 @@
1
+ export { createHttpTransport, type HttpTransportConfig } from './http';
2
+ export { createMockTransport, type MockTransportOptions } from './mock';
3
+ export { parseSSE, type ParseSSEOptions } from './sse';
4
+ export { TransportError } from './types';
5
+ export type {
6
+ ChatTransport,
7
+ ChatStreamEvent,
8
+ CreateSessionOptions,
9
+ SessionInfo,
10
+ HistoryPage,
11
+ StreamOptions,
12
+ SendOptions,
13
+ } from './types';