@djangocfg/ui-tools 2.1.380 → 2.1.382

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 (183) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-2ZLQWLYV.mjs +4 -0
  7. package/dist/DictationField-2ZLQWLYV.mjs.map +1 -0
  8. package/dist/DictationField-IPPJ54CU.cjs +13 -0
  9. package/dist/DictationField-IPPJ54CU.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4LXG3NBV.mjs +833 -0
  15. package/dist/chunk-4LXG3NBV.mjs.map +1 -0
  16. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  17. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  18. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  19. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  20. package/dist/chunk-KMSBGNVC.cjs +835 -0
  21. package/dist/chunk-KMSBGNVC.cjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1532 -100
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1148 -107
  29. package/dist/index.d.ts +1148 -107
  30. package/dist/index.mjs +1421 -51
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -8
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/stories/index.ts +32 -2
  37. package/src/tools/Chat/README.md +347 -530
  38. package/src/tools/Chat/components/Attachments.tsx +6 -1
  39. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  40. package/src/tools/Chat/components/Composer.tsx +20 -3
  41. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  42. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  43. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  44. package/src/tools/Chat/components/MessageList.tsx +87 -1
  45. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  46. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  47. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  48. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  49. package/src/tools/Chat/core/audio/index.ts +1 -0
  50. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  51. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  57. package/src/tools/Chat/core/audio/types.ts +28 -0
  58. package/src/tools/Chat/core/reducer.ts +33 -0
  59. package/src/tools/Chat/core/transport/index.ts +13 -0
  60. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  61. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  62. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  63. package/src/tools/Chat/core/transport/sse.ts +18 -5
  64. package/src/tools/Chat/hooks/index.ts +25 -0
  65. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  66. package/src/tools/Chat/hooks/useChat.ts +28 -0
  67. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  68. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  69. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  70. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  71. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  72. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  73. package/src/tools/Chat/index.ts +69 -1
  74. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  75. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  76. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  77. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  78. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  79. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  80. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  81. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  82. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  83. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  84. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  85. package/src/tools/Chat/launcher/index.ts +46 -0
  86. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  87. package/src/tools/Chat/stories/01-basic.story.tsx +64 -0
  88. package/src/tools/Chat/stories/02-bubbles.story.tsx +21 -0
  89. package/src/tools/Chat/stories/03-tool-calls.story.tsx +59 -0
  90. package/src/tools/Chat/stories/04-personas.story.tsx +78 -0
  91. package/src/tools/Chat/stories/05-launcher.story.tsx +321 -0
  92. package/src/tools/Chat/stories/06-header.story.tsx +147 -0
  93. package/src/tools/Chat/stories/07-audio-actions.story.tsx +112 -0
  94. package/src/tools/Chat/stories/shared/Frame.tsx +21 -0
  95. package/src/tools/Chat/stories/shared/index.ts +5 -0
  96. package/src/tools/Chat/stories/shared/messages.ts +39 -0
  97. package/src/tools/Chat/stories/shared/personas.ts +13 -0
  98. package/src/tools/Chat/stories/shared/seeds.ts +92 -0
  99. package/src/tools/Chat/stories/shared/transports.ts +36 -0
  100. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  101. package/src/tools/Chat/styles/index.ts +16 -0
  102. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  103. package/src/tools/Chat/types/attachment.ts +25 -0
  104. package/src/tools/Chat/types/config.ts +48 -0
  105. package/src/tools/Chat/types/events.ts +35 -0
  106. package/src/tools/Chat/types/index.ts +34 -0
  107. package/src/tools/Chat/types/labels.ts +38 -0
  108. package/src/tools/Chat/types/message.ts +32 -0
  109. package/src/tools/Chat/types/persona.ts +31 -0
  110. package/src/tools/Chat/types/session.ts +43 -0
  111. package/src/tools/Chat/types/tool-call.ts +17 -0
  112. package/src/tools/Chat/types/transport.ts +28 -0
  113. package/src/tools/Chat/types.ts +5 -240
  114. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  115. package/src/tools/MarkdownEditor/index.ts +1 -1
  116. package/src/tools/SpeechRecognition/README.md +336 -0
  117. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  118. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  119. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  120. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  121. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  122. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  123. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  124. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  125. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  126. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  127. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  128. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  129. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  130. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  131. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  132. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  133. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  134. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  135. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  136. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  137. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  138. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  139. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  140. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  141. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  142. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  143. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  144. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  145. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  146. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  147. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  148. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  149. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  150. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  151. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  152. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  153. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  154. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  155. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  156. package/src/tools/SpeechRecognition/index.ts +82 -0
  157. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  158. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  159. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  160. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +32 -0
  161. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +32 -0
  162. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +27 -0
  163. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +35 -0
  164. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +40 -0
  165. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +48 -0
  166. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +57 -0
  167. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +25 -0
  168. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +90 -0
  169. package/src/tools/SpeechRecognition/stories/shared.tsx +123 -0
  170. package/src/tools/SpeechRecognition/types.ts +133 -0
  171. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  172. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  173. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  174. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  175. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  176. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  177. package/dist/chunk-NWUT327A.mjs.map +0 -1
  178. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  179. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  180. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  181. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  182. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  183. package/src/tools/Chat/Chat.story.tsx +0 -1457
@@ -0,0 +1,38 @@
1
+ /**
2
+ * UI label strings — i18n is the host's job. Pass overrides via
3
+ * `ChatConfig.labels` and merge with `DEFAULT_LABELS`.
4
+ */
5
+
6
+ export interface ChatLabels {
7
+ send: string;
8
+ cancel: string;
9
+ copy: string;
10
+ regenerate: string;
11
+ edit: string;
12
+ delete: string;
13
+ retry: string;
14
+ newChat: string;
15
+ loadMore: string;
16
+ jumpToLatest: string;
17
+ attach: string;
18
+ voice: string;
19
+ errorGeneric: string;
20
+ cancelledSuffix: string;
21
+ }
22
+
23
+ export const DEFAULT_LABELS: ChatLabels = {
24
+ send: 'Send',
25
+ cancel: 'Stop',
26
+ copy: 'Copy',
27
+ regenerate: 'Regenerate',
28
+ edit: 'Edit',
29
+ delete: 'Delete',
30
+ retry: 'Retry',
31
+ newChat: 'New chat',
32
+ loadMore: 'Load more',
33
+ jumpToLatest: 'Jump to latest',
34
+ attach: 'Attach files',
35
+ voice: 'Voice input',
36
+ errorGeneric: 'Something went wrong',
37
+ cancelledSuffix: '[cancelled]',
38
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The atomic unit — a single message in a chat session.
3
+ *
4
+ * Composed of role + content + optional rich-data slots (attachments,
5
+ * sources, tool calls). Serializable: no Date, no Map.
6
+ */
7
+
8
+ import type { ChatAttachment, ChatSource } from './attachment';
9
+ import type { ChatPersona, ChatRole } from './persona';
10
+ import type { ChatToolCall } from './tool-call';
11
+
12
+ export interface ChatMessage {
13
+ id: string;
14
+ role: ChatRole;
15
+ content: string;
16
+ /** epoch ms — serializable. */
17
+ createdAt: number;
18
+ isStreaming?: boolean;
19
+ isError?: boolean;
20
+ /** Bumps on edit so memo keys invalidate. */
21
+ version?: number;
22
+ /** Per-message persona override (multi-user / multi-bot). Falls back to
23
+ * `ChatConfig.user` / `ChatConfig.assistant` when absent. */
24
+ sender?: ChatPersona;
25
+ /** Simple status indicator above message ("Searching knowledge base..."). */
26
+ toolActivity?: string;
27
+ toolCalls?: ChatToolCall[];
28
+ attachments?: ChatAttachment[];
29
+ sources?: ChatSource[];
30
+ tokensIn?: number;
31
+ tokensOut?: number;
32
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Persona — display identity attached to messages and config.
3
+ *
4
+ * Used by `ChatMessage.sender`, `ChatConfig.user`, `ChatConfig.assistant`.
5
+ */
6
+
7
+ export type ChatRole = 'user' | 'assistant' | 'system';
8
+
9
+ export interface ChatPersona {
10
+ /** Display name. */
11
+ name?: string;
12
+ /** Avatar image URL. */
13
+ avatarUrl?: string;
14
+ /** Initials fallback when no avatar. Auto-derived from `name` if absent. */
15
+ initials?: string;
16
+ /** Tooltip / aria description. */
17
+ description?: string;
18
+ }
19
+
20
+ export interface ChatUserContext extends ChatPersona {
21
+ email?: string;
22
+ language?: string;
23
+ role?: string;
24
+ /** Free-form custom data forwarded to transport metadata. */
25
+ custom?: Record<string, unknown>;
26
+ }
27
+
28
+ export interface ChatAssistantContext extends ChatPersona {
29
+ /** Model identifier (gpt-4o, claude-opus, …). */
30
+ model?: string;
31
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Session info + history page shapes returned by transports.
3
+ */
4
+
5
+ import type { ChatAttachment } from './attachment';
6
+ import type { ChatMessage } from './message';
7
+
8
+ export interface CreateSessionOptions {
9
+ metadata?: Record<string, unknown>;
10
+ }
11
+
12
+ export interface SessionInfo {
13
+ sessionId: string;
14
+ /** If the server resumed an existing session, it may return prior messages. */
15
+ messages?: ChatMessage[];
16
+ hasMore?: boolean;
17
+ cursor?: string | null;
18
+ resumed?: boolean;
19
+ /**
20
+ * Optional human-readable title (typically derived from the first
21
+ * user message). Hosts that render a session-list sidebar can read
22
+ * this directly instead of crawling messages. Plan64.
23
+ */
24
+ title?: string;
25
+ }
26
+
27
+ export interface HistoryPage {
28
+ messages: ChatMessage[];
29
+ hasMore: boolean;
30
+ nextCursor: string | null;
31
+ }
32
+
33
+ export interface StreamOptions {
34
+ signal: AbortSignal;
35
+ attachments?: ChatAttachment[];
36
+ metadata?: Record<string, unknown>;
37
+ }
38
+
39
+ export interface SendOptions {
40
+ signal?: AbortSignal;
41
+ attachments?: ChatAttachment[];
42
+ metadata?: Record<string, unknown>;
43
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Tool calls — assistant-initiated function/tool invocations during a turn.
3
+ */
4
+
5
+ export interface ChatToolCall {
6
+ id: string;
7
+ name: string;
8
+ input: unknown;
9
+ output?: unknown;
10
+ /** Live buffer accumulated during streaming. Replaced by `output` on completion. */
11
+ streamingText?: string;
12
+ status: 'running' | 'success' | 'error' | 'cancelled';
13
+ startedAt: number;
14
+ endedAt?: number;
15
+ /** Optional grouping label for parallel/fan-out execution. */
16
+ sourceHostname?: string;
17
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Transport interface — what every chat backend adapter implements.
3
+ *
4
+ * Default web implementation lives in `core/transport/http.ts`.
5
+ * Pydantic-AI adapter lives in `core/transport/pydantic-ai-transport.ts`.
6
+ */
7
+
8
+ import type { ChatMessage } from './message';
9
+ import type { ChatStreamEvent } from './events';
10
+ import type {
11
+ CreateSessionOptions,
12
+ HistoryPage,
13
+ SendOptions,
14
+ SessionInfo,
15
+ StreamOptions,
16
+ } from './session';
17
+
18
+ export interface ChatTransport {
19
+ createSession(opts?: CreateSessionOptions): Promise<SessionInfo>;
20
+ loadHistory(sessionId: string, cursor?: string | null, limit?: number): Promise<HistoryPage>;
21
+ stream(
22
+ sessionId: string,
23
+ content: string,
24
+ options: StreamOptions,
25
+ ): AsyncGenerator<ChatStreamEvent, void, void>;
26
+ send(sessionId: string, content: string, options?: SendOptions): Promise<ChatMessage>;
27
+ closeSession(sessionId: string): Promise<void>;
28
+ }
@@ -1,244 +1,9 @@
1
1
  /**
2
- * Public types for the Chat tool.
2
+ * Backwards-compatibility re-export.
3
3
  *
4
- * Shapes here are the contract between transport adapters, the reducer,
5
- * the hooks layer and the components. Keep serializable (no Date, no Map).
4
+ * Types live in `./types/` (one file per domain). Import directly from
5
+ * `@djangocfg/ui-tools/Chat` in new code; this barrel exists so old
6
+ * `import … from '.../types'` paths keep working.
6
7
  */
7
8
 
8
- export type ChatRole = 'user' | 'assistant' | 'system';
9
-
10
- export interface ChatToolCall {
11
- id: string;
12
- name: string;
13
- input: unknown;
14
- output?: unknown;
15
- /** Live buffer accumulated during streaming. Replaced by `output` on completion. */
16
- streamingText?: string;
17
- status: 'running' | 'success' | 'error' | 'cancelled';
18
- startedAt: number;
19
- endedAt?: number;
20
- /** Optional grouping label for parallel/fan-out execution. */
21
- sourceHostname?: string;
22
- }
23
-
24
- export interface ChatAttachment {
25
- id: string;
26
- type: 'image' | 'file' | 'audio' | 'video';
27
- url: string;
28
- thumbnailUrl?: string;
29
- name?: string;
30
- mimeType?: string;
31
- sizeBytes?: number;
32
- status?: 'uploading' | 'ready' | 'error';
33
- /** 0..1 while uploading. */
34
- progress?: number;
35
- /** Extracted text from images (OCR). */
36
- ocrText?: string;
37
- }
38
-
39
- export interface ChatSource {
40
- title: string;
41
- url: string;
42
- snippet?: string;
43
- chunkIndex?: number;
44
- }
45
-
46
- export interface ChatPersona {
47
- /** Display name. */
48
- name?: string;
49
- /** Avatar image URL. */
50
- avatarUrl?: string;
51
- /** Initials fallback when no avatar. Auto-derived from `name` if absent. */
52
- initials?: string;
53
- /** Tooltip / aria description. */
54
- description?: string;
55
- }
56
-
57
- export interface ChatMessage {
58
- id: string;
59
- role: ChatRole;
60
- content: string;
61
- /** epoch ms — serializable. */
62
- createdAt: number;
63
- isStreaming?: boolean;
64
- isError?: boolean;
65
- /** Bumps on edit so memo keys invalidate. */
66
- version?: number;
67
- /** Per-message persona override (multi-user / multi-bot). Falls back to
68
- * `ChatConfig.user` / `ChatConfig.assistant` when absent. */
69
- sender?: ChatPersona;
70
- /** Simple status indicator above message ("Searching knowledge base..."). */
71
- toolActivity?: string;
72
- toolCalls?: ChatToolCall[];
73
- attachments?: ChatAttachment[];
74
- sources?: ChatSource[];
75
- tokensIn?: number;
76
- tokensOut?: number;
77
- }
78
-
79
- export type ChatDisplayMode = 'closed' | 'embedded' | 'floating' | 'sidebar' | 'fullscreen';
80
-
81
- export interface ChatUserContext extends ChatPersona {
82
- email?: string;
83
- language?: string;
84
- role?: string;
85
- /** Free-form custom data forwarded to transport metadata. */
86
- custom?: Record<string, unknown>;
87
- }
88
-
89
- export interface ChatAssistantContext extends ChatPersona {
90
- /** Model identifier (gpt-4o, claude-opus, …). */
91
- model?: string;
92
- }
93
-
94
- export interface ChatPrefs {
95
- /** Submit hotkey for the composer. */
96
- submitOn?: 'enter' | 'cmd+enter';
97
- /** UI density. */
98
- density?: 'comfortable' | 'compact';
99
- /** Locale forwarded to transport metadata. */
100
- locale?: string;
101
- /** Show timestamps on each bubble. */
102
- showTimestamps?: boolean;
103
- }
104
-
105
- export interface ChatConfig {
106
- /** Window title / aria-label. */
107
- title?: string;
108
- /** Composer placeholder. */
109
- placeholder?: string;
110
- /** Empty-state greeting. */
111
- greeting?: string;
112
- /** Empty-state description. */
113
- description?: string;
114
- /** Suggested prompts shown on empty conversation. */
115
- suggestions?: Array<{ label: string; prompt: string }>;
116
- /** Locale forwarded to the transport via stream metadata. */
117
- locale?: string;
118
- /** Project / chat slug forwarded to the transport. */
119
- slug?: string;
120
- /** UI density. Use `prefs.density` for the same effect; this stays for
121
- * backwards compatibility. */
122
- density?: 'comfortable' | 'compact';
123
- /** Identity of the human author. Renders avatar / name on user bubbles
124
- * and gets stamped on outgoing messages as `message.sender`. */
125
- user?: ChatUserContext;
126
- /** Identity of the assistant. Renders avatar / name on assistant bubbles. */
127
- assistant?: ChatAssistantContext;
128
- /** UI preferences. */
129
- prefs?: ChatPrefs;
130
- /** Visual labels (i18n is the host's job). */
131
- labels?: Partial<ChatLabels>;
132
- }
133
-
134
- export interface ChatLabels {
135
- send: string;
136
- cancel: string;
137
- copy: string;
138
- regenerate: string;
139
- edit: string;
140
- delete: string;
141
- retry: string;
142
- newChat: string;
143
- loadMore: string;
144
- jumpToLatest: string;
145
- attach: string;
146
- voice: string;
147
- errorGeneric: string;
148
- cancelledSuffix: string;
149
- }
150
-
151
- export const DEFAULT_LABELS: ChatLabels = {
152
- send: 'Send',
153
- cancel: 'Stop',
154
- copy: 'Copy',
155
- regenerate: 'Regenerate',
156
- edit: 'Edit',
157
- delete: 'Delete',
158
- retry: 'Retry',
159
- newChat: 'New chat',
160
- loadMore: 'Load more',
161
- jumpToLatest: 'Jump to latest',
162
- attach: 'Attach files',
163
- voice: 'Voice input',
164
- errorGeneric: 'Something went wrong',
165
- cancelledSuffix: '[cancelled]',
166
- };
167
-
168
- // ---- Transport ------------------------------------------------------------
169
-
170
- export interface CreateSessionOptions {
171
- metadata?: Record<string, unknown>;
172
- }
173
-
174
- export interface SessionInfo {
175
- sessionId: string;
176
- /** If the server resumed an existing session, it may return prior messages. */
177
- messages?: ChatMessage[];
178
- hasMore?: boolean;
179
- cursor?: string | null;
180
- resumed?: boolean;
181
- /**
182
- * Optional human-readable title (typically derived from the first
183
- * user message). Hosts that render a session-list sidebar can read
184
- * this directly instead of crawling messages. Plan64.
185
- */
186
- title?: string;
187
- }
188
-
189
- export interface HistoryPage {
190
- messages: ChatMessage[];
191
- hasMore: boolean;
192
- nextCursor: string | null;
193
- }
194
-
195
- export interface StreamOptions {
196
- signal: AbortSignal;
197
- attachments?: ChatAttachment[];
198
- metadata?: Record<string, unknown>;
199
- }
200
-
201
- export interface SendOptions {
202
- signal?: AbortSignal;
203
- attachments?: ChatAttachment[];
204
- metadata?: Record<string, unknown>;
205
- }
206
-
207
- export type ChatStreamEvent =
208
- | { type: 'message_start'; messageId: string; sessionId: string }
209
- | { type: 'resume_start' }
210
- | { type: 'chunk'; delta: string }
211
- | { type: 'tool_activity'; tool: string; status: string }
212
- | {
213
- type: 'tool_call_start';
214
- toolId: string;
215
- name: string;
216
- input: unknown;
217
- sourceHostname?: string;
218
- }
219
- | { type: 'tool_call_delta'; toolId: string; delta: string }
220
- | {
221
- type: 'tool_call_end';
222
- toolId: string;
223
- output: unknown;
224
- status: 'success' | 'error';
225
- }
226
- | {
227
- type: 'message_end';
228
- tokensIn?: number;
229
- tokensOut?: number;
230
- sources?: ChatSource[];
231
- }
232
- | { type: 'error'; code: string; message: string };
233
-
234
- export interface ChatTransport {
235
- createSession(opts?: CreateSessionOptions): Promise<SessionInfo>;
236
- loadHistory(sessionId: string, cursor?: string | null, limit?: number): Promise<HistoryPage>;
237
- stream(
238
- sessionId: string,
239
- content: string,
240
- options: StreamOptions,
241
- ): AsyncGenerator<ChatStreamEvent, void, void>;
242
- send(sessionId: string, content: string, options?: SendOptions): Promise<ChatMessage>;
243
- closeSession(sessionId: string): Promise<void>;
244
- }
9
+ export * from './types/index';
@@ -6,7 +6,7 @@ import Placeholder from '@tiptap/extension-placeholder';
6
6
  import Mention from '@tiptap/extension-mention';
7
7
  import { Markdown } from '@tiptap/markdown';
8
8
  import type { AnyExtension } from '@tiptap/core';
9
- import { useEffect, useRef, useMemo } from 'react';
9
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
10
10
  import {
11
11
  Bold, Italic, Strikethrough, Heading1, Heading2, Heading3,
12
12
  List, ListOrdered, Quote, Minus, Code, type LucideIcon,
@@ -90,20 +90,39 @@ export interface MarkdownEditorProps {
90
90
  onSubmit?: () => boolean | void;
91
91
  }
92
92
 
93
+ /**
94
+ * Imperative handle exposed via `ref`. Matches `ComposerHandle` from
95
+ * `@djangocfg/ui-tools/chat` so consumers can forward it straight into
96
+ * `useRegisterComposer({ focus, moveCursorToEnd })` — that's what makes
97
+ * voice dictation (`VoiceComposerSlot`) push live text into a TipTap
98
+ * composer.
99
+ */
100
+ export interface MarkdownEditorHandle {
101
+ /** Move keyboard focus into the editor. */
102
+ focus: () => void;
103
+ /** Place the caret at the end of the document (and focus). */
104
+ moveCursorToEnd: () => void;
105
+ /** Escape hatch — the underlying TipTap `Editor` instance. */
106
+ getEditor: () => Editor | null;
107
+ }
108
+
93
109
  // ── Component ──
94
110
 
95
- export function MarkdownEditor({
96
- value,
97
- onChange,
98
- placeholder = 'Write markdown...',
99
- minHeight = 120,
100
- className = '',
101
- disabled = false,
102
- showToolbar = true,
103
- mentions,
104
- onMentionIdsChange,
105
- onSubmit,
106
- }: MarkdownEditorProps) {
111
+ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(function MarkdownEditor(
112
+ {
113
+ value,
114
+ onChange,
115
+ placeholder = 'Write markdown...',
116
+ minHeight = 120,
117
+ className = '',
118
+ disabled = false,
119
+ showToolbar = true,
120
+ mentions,
121
+ onMentionIdsChange,
122
+ onSubmit,
123
+ },
124
+ ref,
125
+ ) {
107
126
  // Keep the latest onSubmit in a ref so the Tiptap extension's
108
127
  // keymap closure always calls the freshest handler — Tiptap's
109
128
  // useEditor initialises extensions ONCE on first render. Without
@@ -244,6 +263,23 @@ export function MarkdownEditor({
244
263
  }
245
264
  }, [value, editor]);
246
265
 
266
+ // Imperative API for hosts that drive the editor without owning a
267
+ // TipTap ref directly — chat composer registration, voice slot,
268
+ // focus-on-stream-end.
269
+ useImperativeHandle(
270
+ ref,
271
+ (): MarkdownEditorHandle => ({
272
+ focus: () => {
273
+ editor?.commands.focus();
274
+ },
275
+ moveCursorToEnd: () => {
276
+ editor?.commands.focus('end');
277
+ },
278
+ getEditor: () => editor ?? null,
279
+ }),
280
+ [editor],
281
+ );
282
+
247
283
  const wrapperClass = `markdown-editor rounded-md border border-input bg-background ${disabled ? 'opacity-60' : ''} ${className}`.trim();
248
284
 
249
285
  return (
@@ -254,7 +290,7 @@ export function MarkdownEditor({
254
290
  </div>
255
291
  </div>
256
292
  );
257
- }
293
+ });
258
294
 
259
295
  // ── Toolbar ──
260
296
 
@@ -1,5 +1,5 @@
1
1
  export { MarkdownEditor } from './MarkdownEditor';
2
- export type { MarkdownEditorProps } from './MarkdownEditor';
2
+ export type { MarkdownEditorProps, MarkdownEditorHandle } from './MarkdownEditor';
3
3
  export type {
4
4
  MentionItem,
5
5
  MentionConfig,