@djangocfg/ui-tools 2.1.334 → 2.1.336

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 (196) hide show
  1. package/README.md +68 -2
  2. package/dist/ChatRoot-IIYQEWUU.mjs +5 -0
  3. package/dist/ChatRoot-IIYQEWUU.mjs.map +1 -0
  4. package/dist/ChatRoot-PNNGQCYF.css +7 -0
  5. package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
  6. package/dist/ChatRoot-UUKTYM4N.cjs +14 -0
  7. package/dist/ChatRoot-UUKTYM4N.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-OSPUUUHM.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
  18. package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
  19. package/dist/{JsonSchemaForm-TSLX2GRO.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-4IW7GZFQ.cjs → chunk-FEN5S772.cjs} +74 -48
  63. package/dist/chunk-FEN5S772.cjs.map +1 -0
  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-EXGXUK2N.mjs → chunk-GYIO7W7M.mjs} +41 -15
  69. package/dist/chunk-GYIO7W7M.mjs.map +1 -0
  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-KRETIZU6.mjs +2218 -0
  77. package/dist/chunk-KRETIZU6.mjs.map +1 -0
  78. package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
  79. package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
  80. package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
  81. package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
  82. package/dist/chunk-NRXYYO5V.cjs +2257 -0
  83. package/dist/chunk-NRXYYO5V.cjs.map +1 -0
  84. package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
  85. package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
  86. package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
  87. package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
  88. package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
  89. package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
  90. package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
  91. package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
  92. package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
  93. package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
  94. package/dist/chunk-QW4RBGHN.cjs +961 -0
  95. package/dist/chunk-QW4RBGHN.cjs.map +1 -0
  96. package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
  97. package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
  98. package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
  99. package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
  100. package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
  101. package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
  102. package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
  103. package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
  104. package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
  105. package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
  106. package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
  107. package/dist/chunk-ZUFTH5IR.mjs.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 +735 -215
  119. package/dist/index.cjs.map +1 -1
  120. package/dist/index.d.cts +972 -39
  121. package/dist/index.d.ts +972 -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 +201 -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 +122 -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/markdown.ts +56 -0
  156. package/src/tools/Chat/core/payload-dispatch.ts +54 -0
  157. package/src/tools/Chat/core/persona.ts +35 -0
  158. package/src/tools/Chat/core/reducer.ts +335 -0
  159. package/src/tools/Chat/core/transport/http.ts +167 -0
  160. package/src/tools/Chat/core/transport/index.ts +13 -0
  161. package/src/tools/Chat/core/transport/mock.ts +134 -0
  162. package/src/tools/Chat/core/transport/sse.ts +116 -0
  163. package/src/tools/Chat/core/transport/types.ts +24 -0
  164. package/src/tools/Chat/hooks/index.ts +26 -0
  165. package/src/tools/Chat/hooks/useChat.ts +440 -0
  166. package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
  167. package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
  168. package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
  169. package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
  170. package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
  171. package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
  172. package/src/tools/Chat/index.ts +158 -0
  173. package/src/tools/Chat/lazy.tsx +14 -0
  174. package/src/tools/Chat/types.ts +237 -0
  175. package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
  176. package/src/tools/JsonForm/JsonSchemaForm.tsx +32 -1
  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-OSPUUUHM.cjs +0 -13
  181. package/dist/JsonSchemaForm-TSLX2GRO.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-4IW7GZFQ.cjs.map +0 -1
  189. package/dist/chunk-62Y65TGK.mjs.map +0 -1
  190. package/dist/chunk-EXGXUK2N.mjs.map +0 -1
  191. package/dist/chunk-TKSFZHCG.cjs +0 -1597
  192. package/dist/chunk-TKSFZHCG.cjs.map +0 -1
  193. package/dist/components-5UXYNAKR.cjs +0 -22
  194. package/dist/components-CFXOEVPN.mjs +0 -5
  195. package/dist/components-WYEZL5TE.cjs +0 -26
  196. package/dist/components-ZAGG2PBO.mjs +0 -5
@@ -0,0 +1,72 @@
1
+ 'use client';
2
+
3
+ import { Copy, Pencil, RefreshCw, Trash } from 'lucide-react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ import type { ChatRole } from '../types';
8
+
9
+ export interface MessageActionsProps {
10
+ role: ChatRole;
11
+ onCopy?: () => void;
12
+ onRegenerate?: () => void;
13
+ onEdit?: () => void;
14
+ onDelete?: () => void;
15
+ hideOn?: Array<ChatRole>;
16
+ className?: string;
17
+ }
18
+
19
+ export function MessageActions({
20
+ role,
21
+ onCopy,
22
+ onRegenerate,
23
+ onEdit,
24
+ onDelete,
25
+ hideOn,
26
+ className,
27
+ }: MessageActionsProps) {
28
+ if (hideOn?.includes(role)) return null;
29
+ return (
30
+ <div
31
+ className={cn(
32
+ 'mt-1 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/msg:opacity-100 focus-within:opacity-100',
33
+ className,
34
+ )}
35
+ >
36
+ {onCopy ? <ActionButton onClick={onCopy} label="Copy" icon={Copy} /> : null}
37
+ {onRegenerate && role === 'assistant' ? (
38
+ <ActionButton onClick={onRegenerate} label="Regenerate" icon={RefreshCw} />
39
+ ) : null}
40
+ {onEdit && role === 'user' ? (
41
+ <ActionButton onClick={onEdit} label="Edit" icon={Pencil} />
42
+ ) : null}
43
+ {onDelete ? <ActionButton onClick={onDelete} label="Delete" icon={Trash} destructive /> : null}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ function ActionButton({
49
+ onClick,
50
+ label,
51
+ icon: Icon,
52
+ destructive,
53
+ }: {
54
+ onClick: () => void;
55
+ label: string;
56
+ icon: typeof Copy;
57
+ destructive?: boolean;
58
+ }) {
59
+ return (
60
+ <button
61
+ type="button"
62
+ onClick={onClick}
63
+ aria-label={label}
64
+ className={cn(
65
+ 'rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground',
66
+ destructive && 'hover:bg-destructive/15 hover:text-destructive',
67
+ )}
68
+ >
69
+ <Icon aria-hidden className="size-3" />
70
+ </button>
71
+ );
72
+ }
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import { memo, type ReactNode } from 'react';
4
+
5
+ import { Avatar, AvatarFallback, AvatarImage } from '@djangocfg/ui-core/components';
6
+ import { cn } from '@djangocfg/ui-core/lib';
7
+
8
+ import { MarkdownMessage } from '../../../components/markdown';
9
+ import type {
10
+ ChatAssistantContext,
11
+ ChatAttachment,
12
+ ChatMessage,
13
+ ChatSource,
14
+ ChatToolCall,
15
+ ChatUserContext,
16
+ } from '../types';
17
+ import { resolvePersona, deriveInitials } from '../core/persona';
18
+ import { useChatContextOptional } from '../context';
19
+ import { StreamingIndicator } from './StreamingIndicator';
20
+ import { Sources } from './Sources';
21
+ import { ToolCalls } from './ToolCalls';
22
+ import {
23
+ AttachmentsGrid,
24
+ AttachmentsList,
25
+ type AttachmentRendererMap,
26
+ } from './Attachments';
27
+ import { MessageActions } from './MessageActions';
28
+ import type { ToolCallsProps } from './ToolCalls';
29
+
30
+ export interface MessageBubbleProps {
31
+ message: ChatMessage;
32
+ isUser?: boolean;
33
+ showAvatar?: boolean;
34
+ /** Override avatar URL (skips persona resolution). */
35
+ avatarSrc?: string;
36
+ /** Override avatar fallback (skips persona resolution). */
37
+ avatarFallback?: ReactNode;
38
+ /** Personas — when provided, take precedence over context. */
39
+ user?: ChatUserContext;
40
+ assistant?: ChatAssistantContext;
41
+ showTimestamp?: boolean;
42
+ showActions?: boolean;
43
+ isCompact?: boolean;
44
+ className?: string;
45
+ beforeContent?: ReactNode;
46
+ afterContent?: ReactNode;
47
+ toolCallsRenderer?: (calls: ChatToolCall[]) => ReactNode;
48
+ /** Forwarded to the default `<ToolCalls>` when `toolCallsRenderer` is not set. */
49
+ toolCallsProps?: Omit<ToolCallsProps, 'calls'>;
50
+ sourcesRenderer?: (sources: ChatSource[]) => ReactNode;
51
+ attachmentsRenderer?: (atts: ChatAttachment[]) => ReactNode;
52
+ /** Per-type attachment renderers forwarded to default `<Attachments>`. */
53
+ attachmentRenderers?: AttachmentRendererMap;
54
+ /** Click handler for attachment tiles (e.g. open lightbox). */
55
+ onAttachmentOpen?: (a: ChatAttachment) => void;
56
+ onCopy?: () => void;
57
+ onRegenerate?: () => void;
58
+ onEdit?: () => void;
59
+ onDelete?: () => void;
60
+ }
61
+
62
+ const MessageBubbleInner = ({
63
+ message,
64
+ isUser: isUserProp,
65
+ showAvatar = true,
66
+ avatarSrc,
67
+ avatarFallback,
68
+ user,
69
+ assistant,
70
+ showTimestamp = false,
71
+ showActions = true,
72
+ isCompact = false,
73
+ className,
74
+ beforeContent,
75
+ afterContent,
76
+ toolCallsRenderer,
77
+ toolCallsProps,
78
+ sourcesRenderer,
79
+ attachmentsRenderer,
80
+ attachmentRenderers,
81
+ onAttachmentOpen,
82
+ onCopy,
83
+ onRegenerate,
84
+ onEdit,
85
+ onDelete,
86
+ }: MessageBubbleProps) => {
87
+ const isUser = isUserProp ?? message.role === 'user';
88
+ const isStreaming = !!message.isStreaming;
89
+ const isErr = !!message.isError;
90
+
91
+ const ctx = useChatContextOptional();
92
+ const persona = resolvePersona(
93
+ message,
94
+ user ?? ctx?.config.user,
95
+ assistant ?? ctx?.config.assistant,
96
+ );
97
+ const initials = deriveInitials(persona, message.role);
98
+ const personaName = persona.name ?? (isUser ? 'You' : 'Assistant');
99
+
100
+ return (
101
+ <div
102
+ role="article"
103
+ aria-label={`${personaName} said: ${message.content.slice(0, 80)}`}
104
+ aria-busy={isStreaming || undefined}
105
+ data-role={message.role}
106
+ className={cn(
107
+ 'group/msg flex gap-2.5 px-2.5 py-2',
108
+ isUser ? 'flex-row-reverse' : 'flex-row',
109
+ className,
110
+ )}
111
+ >
112
+ {showAvatar ? (
113
+ <Avatar
114
+ className="size-7 shrink-0"
115
+ title={persona.description ?? personaName}
116
+ >
117
+ {avatarSrc || persona.avatarUrl ? (
118
+ <AvatarImage src={avatarSrc ?? persona.avatarUrl} alt={personaName} />
119
+ ) : null}
120
+ <AvatarFallback className="text-[10px]">
121
+ {avatarFallback ?? initials}
122
+ </AvatarFallback>
123
+ </Avatar>
124
+ ) : null}
125
+
126
+ <div className={cn('min-w-0 flex-1', isUser && 'flex flex-col items-end')}>
127
+ {beforeContent}
128
+ {message.attachments?.length
129
+ ? attachmentsRenderer
130
+ ? attachmentsRenderer(message.attachments)
131
+ : (
132
+ <div className="mb-1.5 w-full">
133
+ {attachmentRenderers ? (
134
+ <AttachmentsList
135
+ attachments={message.attachments}
136
+ renderers={attachmentRenderers}
137
+ onClick={onAttachmentOpen}
138
+ className={isUser ? 'items-end' : undefined}
139
+ />
140
+ ) : (
141
+ <AttachmentsGrid
142
+ attachments={message.attachments}
143
+ onClick={onAttachmentOpen}
144
+ className={isUser ? 'justify-end' : undefined}
145
+ />
146
+ )}
147
+ </div>
148
+ )
149
+ : null}
150
+
151
+ <div
152
+ className={cn(
153
+ 'inline-block max-w-full rounded-2xl px-3.5 py-2 text-sm',
154
+ isUser
155
+ ? 'bg-primary text-primary-foreground rounded-tr-md'
156
+ : isErr
157
+ ? 'bg-destructive/10 text-destructive rounded-tl-md border border-destructive/30'
158
+ : 'bg-muted text-foreground rounded-tl-md',
159
+ )}
160
+ >
161
+ {isStreaming && message.toolActivity ? (
162
+ <div className="mb-1.5">
163
+ <StreamingIndicator label={message.toolActivity} />
164
+ </div>
165
+ ) : null}
166
+
167
+ {message.content || !isStreaming ? (
168
+ <MarkdownMessage
169
+ content={message.content || (isErr ? '*Failed to generate a response.*' : '')}
170
+ isUser={isUser}
171
+ isCompact={isCompact}
172
+ plainText={isStreaming}
173
+ />
174
+ ) : (
175
+ <StreamingIndicator />
176
+ )}
177
+ </div>
178
+
179
+ {message.toolCalls?.length
180
+ ? toolCallsRenderer
181
+ ? toolCallsRenderer(message.toolCalls)
182
+ : <ToolCalls calls={message.toolCalls} {...toolCallsProps} />
183
+ : null}
184
+
185
+ {message.sources?.length && !isStreaming
186
+ ? sourcesRenderer
187
+ ? sourcesRenderer(message.sources)
188
+ : <Sources sources={message.sources} />
189
+ : null}
190
+
191
+ {showActions && !isStreaming ? (
192
+ <MessageActions
193
+ role={message.role}
194
+ onCopy={onCopy}
195
+ onRegenerate={onRegenerate}
196
+ onEdit={onEdit}
197
+ onDelete={onDelete}
198
+ />
199
+ ) : null}
200
+
201
+ {showTimestamp ? (
202
+ <div className="mt-1 text-[10px] text-muted-foreground">
203
+ {new Date(message.createdAt).toLocaleTimeString()}
204
+ </div>
205
+ ) : null}
206
+
207
+ {afterContent}
208
+ </div>
209
+ </div>
210
+ );
211
+ };
212
+
213
+ export const MessageBubble = memo(MessageBubbleInner, (prev, next) => {
214
+ const a = prev.message;
215
+ const b = next.message;
216
+ return (
217
+ a.id === b.id &&
218
+ a.content === b.content &&
219
+ a.isStreaming === b.isStreaming &&
220
+ a.isError === b.isError &&
221
+ (a.version ?? 0) === (b.version ?? 0) &&
222
+ a.toolActivity === b.toolActivity &&
223
+ a.toolCalls === b.toolCalls &&
224
+ a.sources === b.sources &&
225
+ a.attachments === b.attachments
226
+ );
227
+ });
228
+ MessageBubble.displayName = 'MessageBubble';
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+
3
+ import { type RefObject, type ReactNode, forwardRef, useCallback } from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+ import { Spinner } from '@djangocfg/ui-core/components';
7
+
8
+ import type { ChatMessage } from '../types';
9
+ import { useChatContextOptional } from '../context';
10
+ import { MessageBubble } from './MessageBubble';
11
+
12
+ export interface MessageListProps {
13
+ messages?: ChatMessage[];
14
+ renderItem?: (m: ChatMessage, i: number) => ReactNode;
15
+ renderEmpty?: () => ReactNode;
16
+ isLoadingMore?: boolean;
17
+ topSentinelRef?: RefObject<HTMLDivElement | null>;
18
+ bottomRef?: RefObject<HTMLDivElement | null>;
19
+ className?: string;
20
+ itemClassName?: string;
21
+ }
22
+
23
+ export const MessageList = forwardRef<HTMLDivElement, MessageListProps>(function MessageList(
24
+ {
25
+ messages: messagesProp,
26
+ renderItem,
27
+ renderEmpty,
28
+ isLoadingMore: isLoadingMoreProp,
29
+ topSentinelRef,
30
+ bottomRef,
31
+ className,
32
+ itemClassName,
33
+ },
34
+ ref,
35
+ ) {
36
+ const ctx = useChatContextOptional();
37
+ const messages = messagesProp ?? ctx?.messages ?? [];
38
+ const isLoadingMore = isLoadingMoreProp ?? ctx?.isLoadingMore ?? false;
39
+
40
+ const defaultRenderItem = useCallback(
41
+ (m: ChatMessage) => (
42
+ <div className={itemClassName} key={m.id}>
43
+ <MessageBubble
44
+ message={m}
45
+ onCopy={() => copy(m.content)}
46
+ onRegenerate={ctx ? () => void ctx.regenerate(m.id) : undefined}
47
+ onDelete={ctx ? () => ctx.deleteMessage(m.id) : undefined}
48
+ />
49
+ </div>
50
+ ),
51
+ [itemClassName, ctx],
52
+ );
53
+
54
+ const itemRenderer = renderItem ?? defaultRenderItem;
55
+
56
+ return (
57
+ <div
58
+ ref={ref}
59
+ role="log"
60
+ aria-live="polite"
61
+ aria-atomic="false"
62
+ className={cn('flex-1 overflow-y-auto', className)}
63
+ >
64
+ <div ref={topSentinelRef} aria-hidden />
65
+ {isLoadingMore ? (
66
+ <div className="flex justify-center py-2">
67
+ <Spinner className="size-4 text-muted-foreground" />
68
+ </div>
69
+ ) : null}
70
+ {messages.length === 0
71
+ ? renderEmpty?.() ?? null
72
+ : messages.map((m, i) => itemRenderer(m, i))}
73
+ <div ref={bottomRef} aria-hidden />
74
+ </div>
75
+ );
76
+ });
77
+
78
+ function copy(text: string) {
79
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
80
+ void navigator.clipboard.writeText(text);
81
+ }
82
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { ExternalLink } from 'lucide-react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ import type { ChatSource } from '../types';
8
+
9
+ export interface SourcesProps {
10
+ sources: ChatSource[];
11
+ layout?: 'inline' | 'grid';
12
+ maxVisible?: number;
13
+ onClick?: (source: ChatSource) => void;
14
+ className?: string;
15
+ }
16
+
17
+ export function Sources({ sources, layout = 'inline', maxVisible, onClick, className }: SourcesProps) {
18
+ if (!sources?.length) return null;
19
+ const visible = maxVisible ? sources.slice(0, maxVisible) : sources;
20
+ const remaining = maxVisible ? Math.max(0, sources.length - maxVisible) : 0;
21
+
22
+ return (
23
+ <div
24
+ className={cn(
25
+ 'mt-2 flex flex-wrap gap-1.5',
26
+ layout === 'grid' && 'grid grid-cols-2',
27
+ className,
28
+ )}
29
+ >
30
+ {visible.map((s, i) => {
31
+ const handle = onClick ? () => onClick(s) : undefined;
32
+ const Tag = handle ? 'button' : 'a';
33
+ const props = handle
34
+ ? ({ type: 'button', onClick: handle } as const)
35
+ : ({ href: s.url, target: '_blank', rel: 'noopener noreferrer' } as const);
36
+ return (
37
+ <Tag
38
+ key={`${s.url}-${i}`}
39
+ {...props}
40
+ className="inline-flex max-w-full items-center gap-1 rounded-md border border-border bg-background/60 px-2 py-1 text-xs text-foreground/80 hover:bg-accent hover:text-foreground"
41
+ title={s.snippet ?? s.title}
42
+ >
43
+ <span className="truncate">{s.title || s.url}</span>
44
+ <ExternalLink aria-hidden className="size-3 shrink-0 opacity-60" />
45
+ </Tag>
46
+ );
47
+ })}
48
+ {remaining > 0 ? (
49
+ <span className="inline-flex items-center rounded-md border border-dashed border-border px-2 py-1 text-xs text-muted-foreground">
50
+ +{remaining}
51
+ </span>
52
+ ) : null}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@djangocfg/ui-core/lib';
4
+
5
+ export interface StreamingIndicatorProps {
6
+ variant?: 'dots' | 'pulse';
7
+ label?: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function StreamingIndicator({ variant = 'dots', label, className }: StreamingIndicatorProps) {
12
+ return (
13
+ <span
14
+ className={cn('inline-flex items-center gap-1.5 text-xs text-muted-foreground', className)}
15
+ aria-live="off"
16
+ >
17
+ {variant === 'dots' ? (
18
+ <span className="inline-flex gap-0.5" aria-hidden>
19
+ <span className="size-1 animate-bounce rounded-full bg-current [animation-delay:-0.2s]" />
20
+ <span className="size-1 animate-bounce rounded-full bg-current [animation-delay:-0.1s]" />
21
+ <span className="size-1 animate-bounce rounded-full bg-current" />
22
+ </span>
23
+ ) : (
24
+ <span className="inline-block size-1.5 animate-pulse rounded-full bg-current" aria-hidden />
25
+ )}
26
+ {label ? <span className="italic">{label}</span> : null}
27
+ </span>
28
+ );
29
+ }
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode, useEffect, useRef, useState } from 'react';
4
+ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
5
+
6
+ import { cn } from '@djangocfg/ui-core/lib';
7
+
8
+ import type { ChatToolCall } from '../types';
9
+
10
+ export type ToolPayloadKind = 'input' | 'output' | 'streaming';
11
+
12
+ export interface ToolCallsProps {
13
+ calls: ChatToolCall[];
14
+ /** Open every panel up-front. Default: false (panels are closed). */
15
+ defaultExpanded?: boolean;
16
+ /** Auto-open while a tool is running, then auto-close on completion.
17
+ * User toggles after that are remembered. Default: true. */
18
+ expandWhileStreaming?: boolean;
19
+ /** Override how the tool input payload is rendered. Receives the raw value. */
20
+ renderInput?: (input: unknown, call: ChatToolCall) => ReactNode;
21
+ /** Override how the tool output payload is rendered. */
22
+ renderOutput?: (output: unknown, call: ChatToolCall) => ReactNode;
23
+ /** Override how the live `streamingText` is rendered. */
24
+ renderStreaming?: (text: string, call: ChatToolCall) => ReactNode;
25
+ /** Single override for all three; specific renderers above take precedence. */
26
+ renderPayload?: (value: unknown, kind: ToolPayloadKind, call: ChatToolCall) => ReactNode;
27
+ className?: string;
28
+ }
29
+
30
+ export function ToolCalls({
31
+ calls,
32
+ defaultExpanded = false,
33
+ expandWhileStreaming = true,
34
+ renderInput,
35
+ renderOutput,
36
+ renderStreaming,
37
+ renderPayload,
38
+ className,
39
+ }: ToolCallsProps) {
40
+ if (!calls?.length) return null;
41
+ return (
42
+ <div className={cn('mt-2 space-y-1.5', className)}>
43
+ {calls.map((call) => (
44
+ <ToolCallItem
45
+ key={call.id}
46
+ call={call}
47
+ defaultExpanded={defaultExpanded}
48
+ expandWhileStreaming={expandWhileStreaming}
49
+ renderInput={renderInput}
50
+ renderOutput={renderOutput}
51
+ renderStreaming={renderStreaming}
52
+ renderPayload={renderPayload}
53
+ />
54
+ ))}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ interface ItemProps {
60
+ call: ChatToolCall;
61
+ defaultExpanded: boolean;
62
+ expandWhileStreaming: boolean;
63
+ renderInput?: ToolCallsProps['renderInput'];
64
+ renderOutput?: ToolCallsProps['renderOutput'];
65
+ renderStreaming?: ToolCallsProps['renderStreaming'];
66
+ renderPayload?: ToolCallsProps['renderPayload'];
67
+ }
68
+
69
+ function ToolCallItem({
70
+ call,
71
+ defaultExpanded,
72
+ expandWhileStreaming,
73
+ renderInput,
74
+ renderOutput,
75
+ renderStreaming,
76
+ renderPayload,
77
+ }: ItemProps) {
78
+ const isRunning = call.status === 'running';
79
+ const initialOpen = defaultExpanded || (expandWhileStreaming && isRunning);
80
+ const [open, setOpen] = useState(initialOpen);
81
+ // Remember manual interaction so completion doesn't override it.
82
+ const userToggledRef = useRef(false);
83
+ const wasRunningRef = useRef(isRunning);
84
+
85
+ // Auto-collapse on running → completed transition, unless user has interacted.
86
+ useEffect(() => {
87
+ if (wasRunningRef.current && !isRunning) {
88
+ if (!userToggledRef.current && !defaultExpanded) {
89
+ setOpen(false);
90
+ }
91
+ }
92
+ wasRunningRef.current = isRunning;
93
+ }, [isRunning, defaultExpanded]);
94
+
95
+ const handleToggle = () => {
96
+ userToggledRef.current = true;
97
+ setOpen((v) => !v);
98
+ };
99
+
100
+ const Icon = open ? ChevronDown : ChevronRight;
101
+ const statusColor =
102
+ call.status === 'success'
103
+ ? 'text-emerald-500'
104
+ : call.status === 'error'
105
+ ? 'text-destructive'
106
+ : call.status === 'cancelled'
107
+ ? 'text-muted-foreground'
108
+ : 'text-amber-500';
109
+
110
+ const renderValue = (value: unknown, kind: ToolPayloadKind): ReactNode => {
111
+ if (kind === 'input' && renderInput) return renderInput(value, call);
112
+ if (kind === 'output' && renderOutput) return renderOutput(value, call);
113
+ if (kind === 'streaming' && renderStreaming)
114
+ return renderStreaming(typeof value === 'string' ? value : String(value), call);
115
+ if (renderPayload) return renderPayload(value, kind, call);
116
+ return <DefaultPayload value={value} kind={kind} />;
117
+ };
118
+
119
+ return (
120
+ <div className="overflow-hidden rounded-md border border-border bg-muted/30">
121
+ <button
122
+ type="button"
123
+ onClick={handleToggle}
124
+ aria-expanded={open}
125
+ className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs hover:bg-muted/60"
126
+ >
127
+ <Icon aria-hidden className="size-3 shrink-0 text-muted-foreground" />
128
+ {isRunning ? (
129
+ <Loader2 aria-hidden className="size-3 shrink-0 animate-spin text-amber-500" />
130
+ ) : (
131
+ <span className={cn('size-2 shrink-0 rounded-full', statusColor.replace('text-', 'bg-'))} />
132
+ )}
133
+ <span className="font-mono text-foreground">{call.name}</span>
134
+ <span className={cn('ml-auto', statusColor)}>{call.status}</span>
135
+ </button>
136
+ {open ? (
137
+ <div className="space-y-1 border-t border-border px-2 py-1.5 text-[11px]">
138
+ {call.input != null ? renderValue(call.input, 'input') : null}
139
+ {call.streamingText != null
140
+ ? renderValue(call.streamingText, 'streaming')
141
+ : call.output !== undefined
142
+ ? renderValue(call.output, 'output')
143
+ : null}
144
+ </div>
145
+ ) : null}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function DefaultPayload({ value, kind }: { value: unknown; kind: ToolPayloadKind }) {
151
+ const isStreamingOrString = kind === 'streaming' || typeof value === 'string';
152
+ const muted = kind === 'input';
153
+ return (
154
+ <pre
155
+ className={cn(
156
+ 'overflow-auto rounded bg-background/60 p-1.5 font-mono',
157
+ kind === 'input' ? 'max-h-32' : 'max-h-48',
158
+ muted ? 'text-muted-foreground' : 'text-foreground/90',
159
+ )}
160
+ >
161
+ {isStreamingOrString ? String(value) : safeStringify(value)}
162
+ </pre>
163
+ );
164
+ }
165
+
166
+ function safeStringify(value: unknown): string {
167
+ try {
168
+ return JSON.stringify(value, null, 2);
169
+ } catch {
170
+ return String(value);
171
+ }
172
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ export { ChatRoot, type ChatRootProps } from './ChatRoot';
4
+ export { MessageList, type MessageListProps } from './MessageList';
5
+ export { MessageBubble, type MessageBubbleProps } from './MessageBubble';
6
+ export { MessageActions, type MessageActionsProps } from './MessageActions';
7
+ export { Composer, type ComposerProps } from './Composer';
8
+ export { Sources, type SourcesProps } from './Sources';
9
+ export { ToolCalls, type ToolCallsProps, type ToolPayloadKind } from './ToolCalls';
10
+ export {
11
+ Attachments,
12
+ AttachmentsGrid,
13
+ AttachmentsList,
14
+ type AttachmentsProps,
15
+ type AttachmentsGridProps,
16
+ type AttachmentsListProps,
17
+ type AttachmentRenderer,
18
+ type AttachmentRendererArgs,
19
+ type AttachmentRendererMap,
20
+ } from './Attachments';
21
+ export { EmptyState, type EmptyStateProps } from './EmptyState';
22
+ export { ErrorBanner, type ErrorBannerProps } from './ErrorBanner';
23
+ export { JumpToLatest, type JumpToLatestProps } from './JumpToLatest';
24
+ export { StreamingIndicator, type StreamingIndicatorProps } from './StreamingIndicator';