@nuasite/cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +237 -0
  2. package/dist/src/build-processor.d.ts +20 -0
  3. package/dist/src/build-processor.d.ts.map +1 -0
  4. package/dist/src/collection-scanner.d.ts +6 -0
  5. package/dist/src/collection-scanner.d.ts.map +1 -0
  6. package/dist/src/component-registry.d.ts +63 -0
  7. package/dist/src/component-registry.d.ts.map +1 -0
  8. package/dist/src/config.d.ts +24 -0
  9. package/dist/src/config.d.ts.map +1 -0
  10. package/dist/src/dev-middleware.d.ts +20 -0
  11. package/dist/src/dev-middleware.d.ts.map +1 -0
  12. package/dist/src/editor/ai.d.ts +60 -0
  13. package/dist/src/editor/ai.d.ts.map +1 -0
  14. package/dist/src/editor/api.d.ts +140 -0
  15. package/dist/src/editor/api.d.ts.map +1 -0
  16. package/dist/src/editor/color-utils.d.ts +106 -0
  17. package/dist/src/editor/color-utils.d.ts.map +1 -0
  18. package/dist/src/editor/components/ai-chat.d.ts +11 -0
  19. package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
  20. package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
  21. package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
  22. package/dist/src/editor/components/attribute-editor.d.ts +5 -0
  23. package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
  24. package/dist/src/editor/components/block-editor.d.ts +12 -0
  25. package/dist/src/editor/components/block-editor.d.ts.map +1 -0
  26. package/dist/src/editor/components/collections-browser.d.ts +2 -0
  27. package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
  28. package/dist/src/editor/components/color-toolbar.d.ts +12 -0
  29. package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
  30. package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
  31. package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
  32. package/dist/src/editor/components/create-page-modal.d.ts +2 -0
  33. package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
  34. package/dist/src/editor/components/editable-highlights.d.ts +9 -0
  35. package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
  36. package/dist/src/editor/components/error-boundary.d.ts +32 -0
  37. package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
  38. package/dist/src/editor/components/fields.d.ts +75 -0
  39. package/dist/src/editor/components/fields.d.ts.map +1 -0
  40. package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
  41. package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
  42. package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
  43. package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
  44. package/dist/src/editor/components/image-overlay.d.ts +12 -0
  45. package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
  46. package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
  47. package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
  48. package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
  49. package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
  50. package/dist/src/editor/components/media-library.d.ts +2 -0
  51. package/dist/src/editor/components/media-library.d.ts.map +1 -0
  52. package/dist/src/editor/components/outline.d.ts +21 -0
  53. package/dist/src/editor/components/outline.d.ts.map +1 -0
  54. package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
  55. package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
  56. package/dist/src/editor/components/seo-editor.d.ts +2 -0
  57. package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
  58. package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
  59. package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
  60. package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
  61. package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
  62. package/dist/src/editor/components/toast/toast.d.ts +7 -0
  63. package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
  64. package/dist/src/editor/components/toast/types.d.ts +7 -0
  65. package/dist/src/editor/components/toast/types.d.ts.map +1 -0
  66. package/dist/src/editor/components/toolbar.d.ts +21 -0
  67. package/dist/src/editor/components/toolbar.d.ts.map +1 -0
  68. package/dist/src/editor/config.d.ts +4 -0
  69. package/dist/src/editor/config.d.ts.map +1 -0
  70. package/dist/src/editor/constants.d.ts +101 -0
  71. package/dist/src/editor/constants.d.ts.map +1 -0
  72. package/dist/src/editor/context.d.ts +14 -0
  73. package/dist/src/editor/context.d.ts.map +1 -0
  74. package/dist/src/editor/dom.d.ts +77 -0
  75. package/dist/src/editor/dom.d.ts.map +1 -0
  76. package/dist/src/editor/editor.d.ts +64 -0
  77. package/dist/src/editor/editor.d.ts.map +1 -0
  78. package/dist/src/editor/history.d.ts +20 -0
  79. package/dist/src/editor/history.d.ts.map +1 -0
  80. package/dist/src/editor/hooks/index.d.ts +14 -0
  81. package/dist/src/editor/hooks/index.d.ts.map +1 -0
  82. package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
  83. package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
  84. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
  85. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
  86. package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
  87. package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
  88. package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
  89. package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
  90. package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
  91. package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
  92. package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
  93. package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
  94. package/dist/src/editor/hooks/utils.d.ts +32 -0
  95. package/dist/src/editor/hooks/utils.d.ts.map +1 -0
  96. package/dist/src/editor/index.d.ts +12 -0
  97. package/dist/src/editor/index.d.ts.map +1 -0
  98. package/dist/src/editor/lib/cn.d.ts +3 -0
  99. package/dist/src/editor/lib/cn.d.ts.map +1 -0
  100. package/dist/src/editor/manifest.d.ts +19 -0
  101. package/dist/src/editor/manifest.d.ts.map +1 -0
  102. package/dist/src/editor/markdown-api.d.ts +36 -0
  103. package/dist/src/editor/markdown-api.d.ts.map +1 -0
  104. package/dist/src/editor/signals.d.ts +242 -0
  105. package/dist/src/editor/signals.d.ts.map +1 -0
  106. package/dist/src/editor/storage.d.ts +27 -0
  107. package/dist/src/editor/storage.d.ts.map +1 -0
  108. package/dist/src/editor/text-styling.d.ts +350 -0
  109. package/dist/src/editor/text-styling.d.ts.map +1 -0
  110. package/dist/src/editor/themes.d.ts +38 -0
  111. package/dist/src/editor/themes.d.ts.map +1 -0
  112. package/dist/src/editor/types.d.ts +454 -0
  113. package/dist/src/editor/types.d.ts.map +1 -0
  114. package/dist/src/error-collector.d.ts +56 -0
  115. package/dist/src/error-collector.d.ts.map +1 -0
  116. package/dist/src/handlers/component-ops.d.ts +34 -0
  117. package/dist/src/handlers/component-ops.d.ts.map +1 -0
  118. package/dist/src/handlers/markdown-ops.d.ts +41 -0
  119. package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
  120. package/dist/src/handlers/request-utils.d.ts +20 -0
  121. package/dist/src/handlers/request-utils.d.ts.map +1 -0
  122. package/dist/src/handlers/source-writer.d.ts +51 -0
  123. package/dist/src/handlers/source-writer.d.ts.map +1 -0
  124. package/dist/src/html-processor.d.ts +63 -0
  125. package/dist/src/html-processor.d.ts.map +1 -0
  126. package/dist/src/index.d.ts +41 -0
  127. package/dist/src/index.d.ts.map +1 -0
  128. package/dist/src/manifest-writer.d.ts +111 -0
  129. package/dist/src/manifest-writer.d.ts.map +1 -0
  130. package/dist/src/media/contember.d.ts +15 -0
  131. package/dist/src/media/contember.d.ts.map +1 -0
  132. package/dist/src/media/local.d.ts +9 -0
  133. package/dist/src/media/local.d.ts.map +1 -0
  134. package/dist/src/media/s3.d.ts +12 -0
  135. package/dist/src/media/s3.d.ts.map +1 -0
  136. package/dist/src/media/types.d.ts +40 -0
  137. package/dist/src/media/types.d.ts.map +1 -0
  138. package/dist/src/preview-generator.d.ts +19 -0
  139. package/dist/src/preview-generator.d.ts.map +1 -0
  140. package/dist/src/seo-processor.d.ts +23 -0
  141. package/dist/src/seo-processor.d.ts.map +1 -0
  142. package/dist/src/source-finder/ast-extractors.d.ts +35 -0
  143. package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
  144. package/dist/src/source-finder/ast-parser.d.ts +16 -0
  145. package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
  146. package/dist/src/source-finder/cache.d.ts +18 -0
  147. package/dist/src/source-finder/cache.d.ts.map +1 -0
  148. package/dist/src/source-finder/collection-finder.d.ts +29 -0
  149. package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
  150. package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
  151. package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
  152. package/dist/src/source-finder/element-finder.d.ts +42 -0
  153. package/dist/src/source-finder/element-finder.d.ts.map +1 -0
  154. package/dist/src/source-finder/image-finder.d.ts +24 -0
  155. package/dist/src/source-finder/image-finder.d.ts.map +1 -0
  156. package/dist/src/source-finder/index.d.ts +9 -0
  157. package/dist/src/source-finder/index.d.ts.map +1 -0
  158. package/dist/src/source-finder/search-index.d.ts +27 -0
  159. package/dist/src/source-finder/search-index.d.ts.map +1 -0
  160. package/dist/src/source-finder/snippet-utils.d.ts +90 -0
  161. package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
  162. package/dist/src/source-finder/source-lookup.d.ts +16 -0
  163. package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
  164. package/dist/src/source-finder/types.d.ts +167 -0
  165. package/dist/src/source-finder/types.d.ts.map +1 -0
  166. package/dist/src/source-finder/variable-extraction.d.ts +37 -0
  167. package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
  168. package/dist/src/tailwind-colors.d.ts +54 -0
  169. package/dist/src/tailwind-colors.d.ts.map +1 -0
  170. package/dist/src/tsconfig.tsbuildinfo +1 -0
  171. package/dist/src/types.d.ts +367 -0
  172. package/dist/src/types.d.ts.map +1 -0
  173. package/dist/src/utils.d.ts +61 -0
  174. package/dist/src/utils.d.ts.map +1 -0
  175. package/dist/src/vite-plugin.d.ts +14 -0
  176. package/dist/src/vite-plugin.d.ts.map +1 -0
  177. package/dist/types/tsconfig.tsbuildinfo +1 -0
  178. package/package.json +80 -0
  179. package/src/build-processor.ts +784 -0
  180. package/src/collection-scanner.ts +304 -0
  181. package/src/component-registry.ts +393 -0
  182. package/src/config.ts +74 -0
  183. package/src/dev-middleware.ts +525 -0
  184. package/src/dist/src/tsconfig.tsbuildinfo +1 -0
  185. package/src/editor/ai.ts +185 -0
  186. package/src/editor/api.ts +513 -0
  187. package/src/editor/color-utils.ts +556 -0
  188. package/src/editor/components/ai-chat.tsx +632 -0
  189. package/src/editor/components/ai-tooltip.tsx +179 -0
  190. package/src/editor/components/attribute-editor.tsx +596 -0
  191. package/src/editor/components/block-editor.tsx +546 -0
  192. package/src/editor/components/collections-browser.tsx +248 -0
  193. package/src/editor/components/color-toolbar.tsx +314 -0
  194. package/src/editor/components/confirm-dialog.tsx +69 -0
  195. package/src/editor/components/create-page-modal.tsx +163 -0
  196. package/src/editor/components/editable-highlights.tsx +260 -0
  197. package/src/editor/components/error-boundary.tsx +87 -0
  198. package/src/editor/components/fields.tsx +387 -0
  199. package/src/editor/components/frontmatter-fields.tsx +469 -0
  200. package/src/editor/components/highlight-overlay.ts +229 -0
  201. package/src/editor/components/image-overlay.tsx +230 -0
  202. package/src/editor/components/markdown-editor-overlay.tsx +505 -0
  203. package/src/editor/components/markdown-inline-editor.tsx +780 -0
  204. package/src/editor/components/media-library.tsx +297 -0
  205. package/src/editor/components/outline.tsx +402 -0
  206. package/src/editor/components/redirect-countdown.tsx +45 -0
  207. package/src/editor/components/seo-editor.tsx +498 -0
  208. package/src/editor/components/text-style-toolbar.tsx +362 -0
  209. package/src/editor/components/toast/toast-container.tsx +15 -0
  210. package/src/editor/components/toast/toast.tsx +49 -0
  211. package/src/editor/components/toast/types.ts +7 -0
  212. package/src/editor/components/toolbar.tsx +366 -0
  213. package/src/editor/config.ts +12 -0
  214. package/src/editor/constants.ts +106 -0
  215. package/src/editor/context.tsx +38 -0
  216. package/src/editor/dom.ts +357 -0
  217. package/src/editor/editor.ts +1510 -0
  218. package/src/editor/env.d.ts +4 -0
  219. package/src/editor/history.ts +355 -0
  220. package/src/editor/hooks/index.ts +19 -0
  221. package/src/editor/hooks/useAIHandlers.ts +345 -0
  222. package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
  223. package/src/editor/hooks/useElementDetection.ts +284 -0
  224. package/src/editor/hooks/useImageHoverDetection.ts +102 -0
  225. package/src/editor/hooks/useTextSelection.ts +187 -0
  226. package/src/editor/hooks/useTooltipState.ts +126 -0
  227. package/src/editor/hooks/utils.ts +101 -0
  228. package/src/editor/index.tsx +481 -0
  229. package/src/editor/lib/cn.ts +4 -0
  230. package/src/editor/manifest.ts +25 -0
  231. package/src/editor/markdown-api.ts +209 -0
  232. package/src/editor/signals.ts +1351 -0
  233. package/src/editor/storage.ts +266 -0
  234. package/src/editor/styles.css +465 -0
  235. package/src/editor/text-styling.ts +773 -0
  236. package/src/editor/themes.ts +210 -0
  237. package/src/editor/types.ts +591 -0
  238. package/src/error-collector.ts +106 -0
  239. package/src/handlers/component-ops.ts +463 -0
  240. package/src/handlers/markdown-ops.ts +202 -0
  241. package/src/handlers/request-utils.ts +151 -0
  242. package/src/handlers/source-writer.ts +649 -0
  243. package/src/html-processor.ts +1108 -0
  244. package/src/index.ts +284 -0
  245. package/src/manifest-writer.ts +371 -0
  246. package/src/media/contember.ts +84 -0
  247. package/src/media/local.ts +114 -0
  248. package/src/media/s3.ts +133 -0
  249. package/src/media/types.ts +33 -0
  250. package/src/preview-generator.ts +293 -0
  251. package/src/seo-processor.ts +567 -0
  252. package/src/source-finder/ast-extractors.ts +185 -0
  253. package/src/source-finder/ast-parser.ts +150 -0
  254. package/src/source-finder/cache.ts +76 -0
  255. package/src/source-finder/collection-finder.ts +335 -0
  256. package/src/source-finder/cross-file-tracker.ts +741 -0
  257. package/src/source-finder/element-finder.ts +387 -0
  258. package/src/source-finder/image-finder.ts +283 -0
  259. package/src/source-finder/index.ts +37 -0
  260. package/src/source-finder/search-index.ts +525 -0
  261. package/src/source-finder/snippet-utils.ts +668 -0
  262. package/src/source-finder/source-lookup.ts +200 -0
  263. package/src/source-finder/types.ts +210 -0
  264. package/src/source-finder/variable-extraction.ts +406 -0
  265. package/src/tailwind-colors.ts +874 -0
  266. package/src/tsconfig.json +25 -0
  267. package/src/types.ts +406 -0
  268. package/src/utils.ts +186 -0
  269. package/src/vite-plugin.ts +42 -0
@@ -0,0 +1,345 @@
1
+ import { useCallback, useMemo, useRef } from 'preact/hooks'
2
+ import { AIService, type CmsAiAction } from '../ai'
3
+ import { getChatHistory } from '../api'
4
+ import { getEditableTextFromElement, logDebug } from '../dom'
5
+ import { handleElementChange } from '../editor'
6
+ import { getComponentDefinition, getComponentInstance, getManifestEntry } from '../manifest'
7
+ import * as signals from '../signals'
8
+ import type { AIStatusType, ChatMessage, CmsConfig } from '../types'
9
+
10
+ export interface AIHandlersOptions {
11
+ config: CmsConfig
12
+ showToast: (message: string, type?: 'info' | 'success' | 'error') => void
13
+ onTooltipHide: () => void
14
+ onUIUpdate?: () => void
15
+ }
16
+
17
+ // Counter for generating unique message IDs
18
+ let messageIdCounter = 0
19
+ function nextMessageId(prefix: string): string {
20
+ return `${prefix}-${Date.now()}-${++messageIdCounter}`
21
+ }
22
+
23
+ /**
24
+ * Hook providing AI-related handlers for the CMS editor.
25
+ * Uses signals directly for state management.
26
+ */
27
+ export function useAIHandlers({
28
+ config,
29
+ showToast,
30
+ onTooltipHide,
31
+ onUIUpdate,
32
+ }: AIHandlersOptions) {
33
+ // Create AI service instance - memoized to avoid recreation on each render
34
+ const aiService = useMemo(() => new AIService(config), [config])
35
+
36
+ // Guard against stale chat history responses
37
+ const historyRequestRef = useRef(0)
38
+
39
+ /**
40
+ * Toggle AI chat visibility
41
+ */
42
+ const handleAIChatToggle = useCallback(async () => {
43
+ if (signals.isChatOpen.value) {
44
+ signals.setAIChatOpen(false)
45
+ } else {
46
+ signals.setAIChatOpen(true)
47
+ const currentId = signals.currentEditingId.value
48
+ if (currentId) {
49
+ signals.setChatContextElement(currentId)
50
+ }
51
+
52
+ // Load chat history when opening
53
+ const requestId = ++historyRequestRef.current
54
+ try {
55
+ const history = await getChatHistory(config.apiBase)
56
+ // Discard if a newer request has been made
57
+ if (requestId !== historyRequestRef.current) return
58
+ if (history.messages && history.messages.length > 0) {
59
+ // Convert API messages to ChatMessage format
60
+ const chatMessages: ChatMessage[] = history.messages
61
+ .filter((msg) => {
62
+ // Skip tool-role messages that may have slipped through
63
+ if (msg.role === 'tool') return false
64
+ const content = msg.content?.trim() || ''
65
+ // Skip empty messages
66
+ if (!content) return false
67
+ return true
68
+ })
69
+ .map((msg) => ({
70
+ id: msg.id,
71
+ role: msg.role as 'user' | 'assistant',
72
+ content: msg.content || '',
73
+ timestamp: new Date(msg.created_at).getTime(),
74
+ }))
75
+ signals.setChatMessages(chatMessages)
76
+ }
77
+ } catch (error) {
78
+ logDebug(config.debug, 'Failed to load chat history:', error)
79
+ }
80
+ }
81
+ }, [config])
82
+
83
+ /**
84
+ * Close AI chat
85
+ */
86
+ const handleChatClose = useCallback(() => {
87
+ signals.setAIChatOpen(false)
88
+ }, [])
89
+
90
+ /**
91
+ * Cancel in-progress AI request
92
+ */
93
+ const handleChatCancel = useCallback(() => {
94
+ aiService.abort()
95
+ signals.setAIProcessing(false)
96
+ signals.clearAIStatus()
97
+ // Clean up empty assistant message
98
+ const messages = signals.chatMessages.value
99
+ const lastMsg = messages[messages.length - 1]
100
+ if (lastMsg?.role === 'assistant' && !lastMsg.content.trim()) {
101
+ signals.setChatMessages(messages.filter((m) => m.id !== lastMsg.id))
102
+ }
103
+ }, [aiService])
104
+
105
+ /**
106
+ * Handle AI prompt submission from tooltip
107
+ */
108
+ const handleTooltipPromptSubmit = useCallback(
109
+ async (prompt: string, elementId: string) => {
110
+ const change = signals.getPendingChange(elementId)
111
+ if (!change) {
112
+ showToast('Element not found', 'error')
113
+ return
114
+ }
115
+
116
+ const currentContent = getEditableTextFromElement(change.element)
117
+ const manifest = signals.manifest.value
118
+
119
+ signals.setAIProcessing(true)
120
+
121
+ logDebug(
122
+ config.debug,
123
+ 'Tooltip AI request for element:',
124
+ elementId,
125
+ 'prompt:',
126
+ prompt,
127
+ )
128
+
129
+ try {
130
+ await aiService.streamRequest(
131
+ {
132
+ prompt,
133
+ elementId,
134
+ currentContent,
135
+ context: getManifestEntry(manifest, elementId)?.sourcePath,
136
+ },
137
+ {
138
+ onToken: (_token, fullText) => {
139
+ change.element.textContent = fullText
140
+ },
141
+ onComplete: (finalText) => {
142
+ logDebug(config.debug, 'Tooltip AI completed:', finalText)
143
+ change.element.textContent = finalText
144
+ handleElementChange(
145
+ config,
146
+ elementId,
147
+ change.element,
148
+ onUIUpdate,
149
+ )
150
+
151
+ signals.setAIProcessing(false)
152
+ onTooltipHide()
153
+ showToast('AI edit applied', 'success')
154
+ },
155
+ onError: (error) => {
156
+ logDebug(config.debug, 'Tooltip AI error:', error)
157
+ signals.setAIProcessing(false)
158
+ showToast(`AI error: ${error.message}`, 'error')
159
+ },
160
+ },
161
+ )
162
+ } catch (error) {
163
+ signals.setAIProcessing(false)
164
+ showToast('AI request failed', 'error')
165
+ }
166
+ },
167
+ [config, aiService, showToast, onTooltipHide, onUIUpdate],
168
+ )
169
+
170
+ /**
171
+ * Handle chat message send
172
+ */
173
+ const handleChatSend = useCallback(
174
+ async (message: string, elementId?: string) => {
175
+ const userMessage: ChatMessage = {
176
+ id: nextMessageId('user'),
177
+ role: 'user',
178
+ content: message,
179
+ elementId,
180
+ timestamp: Date.now(),
181
+ }
182
+ signals.addChatMessage(userMessage)
183
+
184
+ const assistantMessageId = nextMessageId('assistant')
185
+ const assistantMessage: ChatMessage = {
186
+ id: assistantMessageId,
187
+ role: 'assistant',
188
+ content: '',
189
+ elementId,
190
+ timestamp: Date.now(),
191
+ }
192
+
193
+ signals.setAIProcessing(true)
194
+ signals.clearAIStatus()
195
+
196
+ const manifest = signals.manifest.value
197
+ const componentInstance = elementId ? getComponentInstance(manifest, elementId) : null
198
+
199
+ let currentContent: string | undefined
200
+ let context: string | undefined
201
+
202
+ if (componentInstance) {
203
+ // Component context: send component name, props, and source file
204
+ currentContent = JSON.stringify({
205
+ component: componentInstance.componentName,
206
+ props: componentInstance.props,
207
+ })
208
+ context = componentInstance.file
209
+ } else {
210
+ const entry = elementId ? getManifestEntry(manifest, elementId) : null
211
+ const parentComponent = entry?.parentComponentId
212
+ ? getComponentInstance(manifest, entry.parentComponentId)
213
+ : null
214
+
215
+ const change = elementId ? signals.getPendingChange(elementId) : null
216
+ currentContent = change
217
+ ? getEditableTextFromElement(change.element)
218
+ : undefined
219
+ // Use the entry's source file, or fall back to parent component's file
220
+ context = entry?.sourcePath
221
+ ?? parentComponent?.file
222
+ ?? undefined
223
+ }
224
+
225
+ const handleAction = (action: CmsAiAction) => {
226
+ logDebug(config.debug, 'AI action received:', action)
227
+ if (action.name === 'preview' && action.url) {
228
+ // Open preview in new tab
229
+ window.open(action.url, '_blank')
230
+ } else if (action.name === 'refresh') {
231
+ // Refresh the current page to show new content
232
+ window.location.reload()
233
+ }
234
+ }
235
+
236
+ let hasStarted = false
237
+
238
+ try {
239
+ await aiService.streamRequest(
240
+ {
241
+ prompt: message,
242
+ elementId: elementId || '',
243
+ currentContent: currentContent || '',
244
+ context,
245
+ },
246
+ {
247
+ onStart: () => {
248
+ if (!hasStarted) {
249
+ signals.addChatMessage(assistantMessage)
250
+ hasStarted = true
251
+ }
252
+ },
253
+ onToken: (_token, fullText) => {
254
+ if (!hasStarted) {
255
+ signals.addChatMessage(assistantMessage)
256
+ hasStarted = true
257
+ }
258
+ // Update the message content using the new helper
259
+ signals.updateChatMessage(assistantMessageId, fullText)
260
+ },
261
+ onStatus: (status, statusMessage) => {
262
+ // Map SSE status strings to AIStatusType
263
+ const statusMap: Record<string, AIStatusType> = {
264
+ thinking: 'thinking',
265
+ coding: 'coding',
266
+ building: 'building',
267
+ deploying: 'deploying',
268
+ complete: 'complete',
269
+ }
270
+ const mappedStatus = statusMap[status] ?? null
271
+ signals.setAIStatus(mappedStatus, statusMessage)
272
+ },
273
+ onAction: handleAction,
274
+ onComplete: (finalText) => {
275
+ if (hasStarted && !finalText.trim()) {
276
+ // Remove empty assistant message instead of leaving an empty bubble
277
+ signals.setChatMessages(
278
+ signals.chatMessages.value.filter((m) => m.id !== assistantMessageId),
279
+ )
280
+ } else {
281
+ signals.updateChatMessage(assistantMessageId, finalText)
282
+ }
283
+ signals.setAIProcessing(false)
284
+ signals.clearAIStatus()
285
+ },
286
+ onError: (error) => {
287
+ // Remove empty assistant message, keep if it has partial content
288
+ const msg = signals.chatMessages.value.find((m) => m.id === assistantMessageId)
289
+ if (msg && !msg.content.trim()) {
290
+ signals.setChatMessages(
291
+ signals.chatMessages.value.filter((m) => m.id !== assistantMessageId),
292
+ )
293
+ }
294
+ signals.setAIProcessing(false)
295
+ signals.clearAIStatus()
296
+ showToast(`AI error: ${error.message}`, 'error')
297
+ },
298
+ },
299
+ )
300
+ } catch (error) {
301
+ if (hasStarted) {
302
+ // Remove the empty assistant message if no content was received
303
+ const msg = signals.chatMessages.value.find((m) => m.id === assistantMessageId)
304
+ if (msg && !msg.content.trim()) {
305
+ signals.setChatMessages(
306
+ signals.chatMessages.value.filter((m) => m.id !== assistantMessageId),
307
+ )
308
+ }
309
+ }
310
+ signals.setAIProcessing(false)
311
+ signals.clearAIStatus()
312
+ showToast('AI request failed', 'error')
313
+ }
314
+ },
315
+ [config, aiService, showToast],
316
+ )
317
+
318
+ /**
319
+ * Apply chat content to an element
320
+ */
321
+ const handleApplyToElement = useCallback(
322
+ (content: string, elementId: string) => {
323
+ const change = signals.getPendingChange(elementId)
324
+ if (!change) {
325
+ showToast('Element not found', 'error')
326
+ return
327
+ }
328
+
329
+ change.element.textContent = content
330
+ handleElementChange(config, elementId, change.element, onUIUpdate)
331
+ showToast('Content applied to element', 'success')
332
+ },
333
+ [config, showToast, onUIUpdate],
334
+ )
335
+
336
+ return {
337
+ aiService,
338
+ handleAIChatToggle,
339
+ handleChatClose,
340
+ handleChatCancel,
341
+ handleTooltipPromptSubmit,
342
+ handleChatSend,
343
+ handleApplyToElement,
344
+ }
345
+ }
@@ -0,0 +1,206 @@
1
+ import { useCallback, useState } from 'preact/hooks'
2
+ import { logDebug } from '../dom'
3
+ import { startDeploymentPolling } from '../editor'
4
+ import * as signals from '../signals'
5
+ import type { CmsConfig, InsertPosition } from '../types'
6
+
7
+ /** Collapse a DOM element with a smooth height transition */
8
+ function collapseElement(el: Element) {
9
+ const htmlEl = el as HTMLElement
10
+ htmlEl.style.overflow = 'hidden'
11
+ htmlEl.style.height = `${htmlEl.offsetHeight}px`
12
+ htmlEl.style.transition = 'height 0.3s ease, opacity 0.3s ease'
13
+ // Force reflow before changing values
14
+ void htmlEl.offsetHeight
15
+ htmlEl.style.height = '0'
16
+ htmlEl.style.opacity = '0'
17
+ }
18
+
19
+ export interface BlockEditorHandlersOptions {
20
+ config: CmsConfig
21
+ showToast: (message: string, type?: 'info' | 'success' | 'error') => void
22
+ }
23
+
24
+ /**
25
+ * Hook providing block editor handlers for the CMS editor.
26
+ * Uses signals directly for state management.
27
+ */
28
+ export function useBlockEditorHandlers({
29
+ config,
30
+ showToast,
31
+ }: BlockEditorHandlersOptions) {
32
+ const [blockEditorRect, setBlockEditorRect] = useState<DOMRect | null>(null)
33
+
34
+ /**
35
+ * Open block editor for a component
36
+ */
37
+ const handleComponentSelect = useCallback(
38
+ (componentId: string, rect: DOMRect) => {
39
+ signals.setCurrentComponentId(componentId)
40
+ signals.setBlockEditorOpen(true)
41
+ setBlockEditorRect(rect)
42
+ },
43
+ [],
44
+ )
45
+
46
+ /**
47
+ * Close block editor
48
+ */
49
+ const handleBlockEditorClose = useCallback(() => {
50
+ signals.setBlockEditorOpen(false)
51
+ signals.setCurrentComponentId(null)
52
+ setBlockEditorRect(null)
53
+ }, [])
54
+
55
+ /**
56
+ * Update component props
57
+ */
58
+ const handleUpdateProps = useCallback(
59
+ (componentId: string, props: Record<string, any>) => {
60
+ logDebug(config.debug, 'Update props for component:', componentId, props)
61
+ // TODO: Implement prop update logic - this will require server-side file modification
62
+ showToast('Props updated (preview only)', 'info')
63
+ },
64
+ [config.debug, showToast],
65
+ )
66
+
67
+ /**
68
+ * Insert a new component
69
+ */
70
+ const handleInsertComponent = useCallback(
71
+ async (
72
+ position: InsertPosition,
73
+ referenceComponentId: string,
74
+ componentName: string,
75
+ props: Record<string, any>,
76
+ ) => {
77
+ logDebug(
78
+ config.debug,
79
+ 'Insert component:',
80
+ componentName,
81
+ position,
82
+ referenceComponentId,
83
+ 'props:',
84
+ props,
85
+ )
86
+
87
+ // Clone the existing mock preview before the block editor unmounts and removes it
88
+ const existingMock = document.querySelector('[data-cms-preview-mock]') as HTMLElement | null
89
+ let previewEl: HTMLElement | null = null
90
+ if (existingMock) {
91
+ previewEl = existingMock.cloneNode(true) as HTMLElement
92
+ previewEl.removeAttribute('data-cms-preview-mock')
93
+ previewEl.style.outline = 'none'
94
+ previewEl.style.outlineOffset = ''
95
+ previewEl.style.opacity = '1'
96
+ existingMock.parentNode?.insertBefore(previewEl, existingMock.nextSibling)
97
+ }
98
+
99
+ // Call API to insert the component in source code
100
+ try {
101
+ const response = await fetch(`${config.apiBase}/insert-component`, {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ credentials: 'include',
105
+ body: JSON.stringify({
106
+ position,
107
+ referenceComponentId,
108
+ componentName,
109
+ props,
110
+ meta: {
111
+ source: 'inline-editor',
112
+ url: window.location.href,
113
+ },
114
+ }),
115
+ })
116
+
117
+ if (!response.ok) {
118
+ const error = await response.text()
119
+ throw new Error(error || 'Failed to insert component')
120
+ }
121
+
122
+ showToast(`${componentName} inserted ${position} component`, 'success')
123
+
124
+ // Trigger deployment polling after successful insert
125
+ startDeploymentPolling(config)
126
+ } catch (error) {
127
+ console.error('[CMS] Failed to insert component:', error)
128
+
129
+ // Remove the preview on failure
130
+ previewEl?.remove()
131
+
132
+ showToast('Failed to insert component', 'error')
133
+ }
134
+ },
135
+ [config.apiBase, config.debug, config, showToast],
136
+ )
137
+
138
+ /**
139
+ * Remove a block/component
140
+ */
141
+ const handleRemoveBlock = useCallback(
142
+ async (componentId: string) => {
143
+ logDebug(config.debug, 'Remove block:', componentId)
144
+
145
+ // Find the element in the DOM
146
+ const componentEl = document.querySelector(
147
+ `[data-cms-component-id="${componentId}"]`,
148
+ )
149
+
150
+ // Dim the component while the API call is in progress
151
+ if (componentEl) {
152
+ ;(componentEl as HTMLElement).style.opacity = '0.4'
153
+ ;(componentEl as HTMLElement).style.pointerEvents = 'none'
154
+ }
155
+
156
+ try {
157
+ const response = await fetch(`${config.apiBase}/remove-component`, {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ credentials: 'include',
161
+ body: JSON.stringify({
162
+ componentId,
163
+ meta: {
164
+ source: 'inline-editor',
165
+ url: window.location.href,
166
+ },
167
+ }),
168
+ })
169
+
170
+ if (!response.ok) {
171
+ const error = await response.text()
172
+ throw new Error(error || 'Failed to remove component')
173
+ }
174
+
175
+ showToast('Component removed', 'success')
176
+
177
+ // Trigger deployment polling after successful remove
178
+ startDeploymentPolling(config)
179
+
180
+ // Visually collapse and hide the component until page refreshes after deploy
181
+ if (componentEl) {
182
+ collapseElement(componentEl)
183
+ }
184
+ } catch (error) {
185
+ console.error('[CMS] Failed to remove component:', error)
186
+ showToast('Failed to remove component', 'error')
187
+
188
+ // Restore the component's appearance on failure
189
+ if (componentEl) {
190
+ ;(componentEl as HTMLElement).style.opacity = ''
191
+ ;(componentEl as HTMLElement).style.pointerEvents = ''
192
+ }
193
+ }
194
+ },
195
+ [config.apiBase, config.debug, config, showToast],
196
+ )
197
+
198
+ return {
199
+ blockEditorRect,
200
+ handleComponentSelect,
201
+ handleBlockEditorClose,
202
+ handleUpdateProps,
203
+ handleInsertComponent,
204
+ handleRemoveBlock,
205
+ }
206
+ }