@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,632 @@
1
+ import { marked } from 'marked'
2
+ import { useEffect, useRef, useState } from 'preact/hooks'
3
+ import { CSS } from '../constants'
4
+ import { getComponentInstance } from '../manifest'
5
+ import * as signals from '../signals'
6
+
7
+ // Configure marked for safe HTML output
8
+ marked.setOptions({
9
+ breaks: true, // Convert \n to <br>
10
+ gfm: true, // GitHub Flavored Markdown
11
+ })
12
+
13
+ /**
14
+ * Sanitize HTML output to prevent XSS from AI-generated content.
15
+ * Strips dangerous tags and event handler attributes.
16
+ */
17
+ function sanitizeHtml(html: string): string {
18
+ const doc = new DOMParser().parseFromString(html, 'text/html')
19
+ for (const el of doc.querySelectorAll('script,style,iframe,object,embed,form')) {
20
+ el.remove()
21
+ }
22
+ for (const el of doc.querySelectorAll('*')) {
23
+ for (const attr of Array.from(el.attributes)) {
24
+ if (attr.name.startsWith('on') || attr.name === 'srcdoc') {
25
+ el.removeAttribute(attr.name)
26
+ }
27
+ if (
28
+ ['href', 'src', 'action'].includes(attr.name)
29
+ && attr.value.trim().toLowerCase().startsWith('javascript:')
30
+ ) {
31
+ el.removeAttribute(attr.name)
32
+ }
33
+ }
34
+ }
35
+ return doc.body.innerHTML
36
+ }
37
+
38
+ /**
39
+ * Escape HTML entities for safe rendering as text
40
+ */
41
+ function escapeHtml(text: string): string {
42
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
43
+ }
44
+
45
+ /**
46
+ * Render markdown to sanitized HTML
47
+ */
48
+ function renderMarkdown(content: string): string {
49
+ try {
50
+ const html = marked.parse(content, { async: false }) as string
51
+ return sanitizeHtml(html)
52
+ } catch {
53
+ return escapeHtml(content)
54
+ }
55
+ }
56
+
57
+ export interface AIChatCallbacks {
58
+ onSend: (message: string, elementId?: string) => void
59
+ onClose: () => void
60
+ onCancel: () => void
61
+ onApplyToElement: (content: string, elementId: string) => void
62
+ }
63
+
64
+ export interface AIChatProps {
65
+ callbacks: AIChatCallbacks
66
+ }
67
+
68
+ /**
69
+ * Get a friendly label for the context element
70
+ */
71
+ function getContextLabel(elementId: string): string {
72
+ const manifest = signals.manifest.value
73
+ const instance = getComponentInstance(manifest, elementId)
74
+ if (instance) {
75
+ return instance.componentName
76
+ }
77
+ return elementId
78
+ }
79
+
80
+ export const AIChat = ({ callbacks }: AIChatProps) => {
81
+ const [message, setMessage] = useState('')
82
+ const [appliedMessages, setAppliedMessages] = useState<Set<string>>(
83
+ new Set(),
84
+ )
85
+ const [position, setPosition] = useState<'left' | 'right'>('right')
86
+ const [isMinimized, setIsMinimized] = useState(false)
87
+ const [isDragging, setIsDragging] = useState(false)
88
+ const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null)
89
+ const dragOffsetRef = useRef({ x: 0, y: 0 })
90
+ const messagesEndRef = useRef<HTMLDivElement>(null)
91
+ const textareaRef = useRef<HTMLTextAreaElement>(null)
92
+ const containerRef = useRef<HTMLDivElement>(null)
93
+
94
+ const open = signals.isChatOpen.value
95
+ const messages = signals.chatMessages.value
96
+ const contextElementId = signals.chatContextElementId.value
97
+ const inputDisabled = signals.isAIProcessing.value
98
+ const currentStatus = signals.currentStatus.value
99
+ const statusMessage = signals.statusMessage.value
100
+
101
+ // Reset applied messages when chat opens (history may have changed)
102
+ useEffect(() => {
103
+ if (open) {
104
+ setAppliedMessages(new Set())
105
+ }
106
+ }, [open])
107
+
108
+ // Handle drag start
109
+ const handleDragStart = (e: MouseEvent) => {
110
+ if (!containerRef.current) return
111
+ const rect = containerRef.current.getBoundingClientRect()
112
+ dragOffsetRef.current = {
113
+ x: e.clientX - rect.left,
114
+ y: e.clientY - rect.top,
115
+ }
116
+ setDragPosition({ x: rect.left, y: rect.top })
117
+ setIsDragging(true)
118
+ }
119
+
120
+ // Handle drag move and end
121
+ useEffect(() => {
122
+ if (!isDragging) return
123
+
124
+ const handleMouseMove = (e: MouseEvent) => {
125
+ setDragPosition({
126
+ x: e.clientX - dragOffsetRef.current.x,
127
+ y: e.clientY - dragOffsetRef.current.y,
128
+ })
129
+ }
130
+
131
+ const handleMouseUp = (e: MouseEvent) => {
132
+ setIsDragging(false)
133
+ // Snap to left or right based on position
134
+ const windowCenter = window.innerWidth / 2
135
+ const currentX = e.clientX - dragOffsetRef.current.x
136
+ const containerWidth = containerRef.current?.offsetWidth || 400
137
+ const containerCenter = currentX + containerWidth / 2
138
+
139
+ if (containerCenter < windowCenter) {
140
+ setPosition('left')
141
+ } else {
142
+ setPosition('right')
143
+ }
144
+ setDragPosition(null)
145
+ }
146
+
147
+ document.addEventListener('mousemove', handleMouseMove)
148
+ document.addEventListener('mouseup', handleMouseUp)
149
+
150
+ return () => {
151
+ document.removeEventListener('mousemove', handleMouseMove)
152
+ document.removeEventListener('mouseup', handleMouseUp)
153
+ }
154
+ }, [isDragging])
155
+
156
+ // biome-ignore lint/correctness/useExhaustiveDependencies: need to scroll to the bottom when messages change
157
+ useEffect(() => {
158
+ if (messagesEndRef.current) {
159
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
160
+ }
161
+ }, [messages, currentStatus])
162
+
163
+ useEffect(() => {
164
+ if (open && textareaRef.current && !inputDisabled) {
165
+ setTimeout(() => textareaRef.current?.focus(), 50)
166
+ }
167
+ }, [open, inputDisabled])
168
+
169
+ // Highlight the selected context element on the page
170
+ useEffect(() => {
171
+ if (!open || !contextElementId) return
172
+
173
+ const el = document.querySelector(`[${CSS.COMPONENT_ID_ATTRIBUTE}="${contextElementId}"]`)
174
+ if (!el) return
175
+
176
+ const htmlEl = el as HTMLElement
177
+ const prev = htmlEl.style.outline
178
+ const prevOffset = htmlEl.style.outlineOffset
179
+ htmlEl.style.outline = '2px solid rgba(99, 102, 241, 0.7)'
180
+ htmlEl.style.outlineOffset = '2px'
181
+
182
+ return () => {
183
+ htmlEl.style.outline = prev
184
+ htmlEl.style.outlineOffset = prevOffset
185
+ }
186
+ }, [open, contextElementId])
187
+
188
+ const contextLabel = contextElementId ? getContextLabel(contextElementId) : null
189
+
190
+ const handleSubmit = (e: Event) => {
191
+ e.preventDefault()
192
+ if (message.trim() && !inputDisabled) {
193
+ callbacks.onSend(message.trim(), contextElementId || undefined)
194
+ setMessage('')
195
+ if (textareaRef.current) {
196
+ textareaRef.current.style.height = 'auto'
197
+ }
198
+ }
199
+ }
200
+
201
+ const handleTextareaInput = (e: Event) => {
202
+ const target = e.target as HTMLTextAreaElement
203
+ setMessage(target.value)
204
+ target.style.height = 'auto'
205
+ target.style.height = `${Math.min(target.scrollHeight, 120)}px`
206
+ }
207
+
208
+ const handleTextareaKeyDown = (e: KeyboardEvent) => {
209
+ if (e.key === 'Enter' && !e.shiftKey) {
210
+ e.preventDefault()
211
+ handleSubmit(e)
212
+ }
213
+ }
214
+
215
+ const handleApply = (
216
+ messageId: string,
217
+ content: string,
218
+ elementId: string,
219
+ ) => {
220
+ callbacks.onApplyToElement(content, elementId)
221
+ setAppliedMessages(new Set(appliedMessages).add(messageId))
222
+ }
223
+
224
+ if (!open) {
225
+ return null
226
+ }
227
+
228
+ const stopPropagation = (e: Event) => e.stopPropagation()
229
+
230
+ const containerStyle = dragPosition
231
+ ? {
232
+ left: `${dragPosition.x}px`,
233
+ top: `${dragPosition.y}px`,
234
+ right: 'auto',
235
+ bottom: 'auto',
236
+ height: isMinimized ? 'auto' : 'calc(100vh - 40px)',
237
+ }
238
+ : undefined
239
+
240
+ const positionClass = position === 'left' ? 'left-5' : 'right-5'
241
+
242
+ return (
243
+ <div
244
+ ref={containerRef}
245
+ class={`fixed ${dragPosition ? '' : positionClass} top-5 ${
246
+ isMinimized ? '' : 'bottom-5'
247
+ } w-100 max-w-[calc(100vw-40px)] bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] rounded-cms-xl border border-white/10 z-2147483645 flex flex-col font-sans overflow-hidden ${
248
+ isDragging ? '' : 'transition-all duration-300'
249
+ }`}
250
+ style={containerStyle}
251
+ data-cms-ui
252
+ onMouseDown={stopPropagation}
253
+ onClick={stopPropagation}
254
+ >
255
+ <div
256
+ class={`px-5 py-4 flex items-center justify-between border-b border-white/10 ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
257
+ onMouseDown={handleDragStart}
258
+ >
259
+ <div class="flex items-center gap-2.5">
260
+ <div class="flex items-center text-white">
261
+ <svg
262
+ width="20"
263
+ height="20"
264
+ viewBox="0 0 24 24"
265
+ fill="none"
266
+ stroke="currentColor"
267
+ stroke-width="2.5"
268
+ stroke-linecap="round"
269
+ stroke-linejoin="round"
270
+ >
271
+ <path d="M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3z" />
272
+ </svg>
273
+ </div>
274
+ <h3 class="m-0 text-base font-semibold text-white">
275
+ AI Assistant
276
+ </h3>
277
+ </div>
278
+ <div class="flex items-center gap-1">
279
+ <button
280
+ onClick={(e) => {
281
+ e.stopPropagation()
282
+ setIsMinimized(!isMinimized)
283
+ }}
284
+ onMouseDown={(e) => e.stopPropagation()}
285
+ class="bg-white/10 border-none text-white/80 text-sm cursor-pointer p-1.5 leading-none transition-all w-8 h-8 flex items-center justify-center hover:bg-white/20 hover:text-white rounded-full"
286
+ title={isMinimized ? 'Expand' : 'Minimize'}
287
+ >
288
+ {isMinimized
289
+ ? (
290
+ <svg
291
+ width="14"
292
+ height="14"
293
+ viewBox="0 0 24 24"
294
+ fill="none"
295
+ stroke="currentColor"
296
+ stroke-width="2.5"
297
+ stroke-linecap="round"
298
+ stroke-linejoin="round"
299
+ >
300
+ <polyline points="15 3 21 3 21 9" />
301
+ <polyline points="9 21 3 21 3 15" />
302
+ <line x1="21" y1="3" x2="14" y2="10" />
303
+ <line x1="3" y1="21" x2="10" y2="14" />
304
+ </svg>
305
+ )
306
+ : (
307
+ <svg
308
+ width="14"
309
+ height="14"
310
+ viewBox="0 0 24 24"
311
+ fill="none"
312
+ stroke="currentColor"
313
+ stroke-width="2.5"
314
+ stroke-linecap="round"
315
+ stroke-linejoin="round"
316
+ >
317
+ <polyline points="4 14 10 14 10 20" />
318
+ <polyline points="20 10 14 10 14 4" />
319
+ <line x1="14" y1="10" x2="21" y2="3" />
320
+ <line x1="3" y1="21" x2="10" y2="14" />
321
+ </svg>
322
+ )}
323
+ </button>
324
+ <button
325
+ onClick={(e) => {
326
+ e.stopPropagation()
327
+ callbacks.onClose()
328
+ }}
329
+ onMouseDown={(e) => e.stopPropagation()}
330
+ class="bg-white/10 border-none text-white/80 text-xl cursor-pointer p-1.5 leading-none transition-all w-8 h-8 flex items-center justify-center hover:bg-white/20 hover:text-white rounded-full"
331
+ >
332
+ &times;
333
+ </button>
334
+ </div>
335
+ </div>
336
+
337
+ {!isMinimized && (
338
+ <>
339
+ <div class="flex-1 overflow-y-auto p-4 flex flex-col gap-4 bg-black/20">
340
+ {messages.length === 0
341
+ ? (
342
+ <div class="flex flex-col items-center justify-center h-full text-white text-center p-10">
343
+ <svg
344
+ width="48"
345
+ height="48"
346
+ viewBox="0 0 24 24"
347
+ fill="none"
348
+ stroke="currentColor"
349
+ stroke-width="2"
350
+ stroke-linecap="round"
351
+ stroke-linejoin="round"
352
+ class="mb-4 text-white/30"
353
+ >
354
+ <path d="M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3z" />
355
+ </svg>
356
+ <div class="text-sm font-semibold mb-2 text-white">
357
+ Start a conversation
358
+ </div>
359
+ <div class="text-xs text-white/50">
360
+ Ask AI to help you edit content
361
+ </div>
362
+ </div>
363
+ )
364
+ : (
365
+ messages.map((msg) => (
366
+ <div
367
+ key={msg.id}
368
+ class="flex flex-col gap-1.5 animate-[slideIn_0.2s_ease]"
369
+ >
370
+ {msg.role === 'assistant' && !msg.content.trim()
371
+ ? (
372
+ <div class="px-4 py-3 text-[13px] leading-relaxed max-w-[85%] bg-white/10 text-white/50 self-start rounded-cms-lg rounded-bl-cms-sm border border-white/10 flex items-center gap-1.5">
373
+ <span class="inline-block w-1.5 h-1.5 bg-white/40 rounded-full animate-bounce [animation-delay:0ms]" />
374
+ <span class="inline-block w-1.5 h-1.5 bg-white/40 rounded-full animate-bounce [animation-delay:150ms]" />
375
+ <span class="inline-block w-1.5 h-1.5 bg-white/40 rounded-full animate-bounce [animation-delay:300ms]" />
376
+ </div>
377
+ )
378
+ : (
379
+ <div
380
+ class={`px-4 py-3 text-[13px] leading-relaxed wrap-break-word max-w-[85%] ${
381
+ msg.role === 'user'
382
+ ? 'bg-cms-primary text-cms-primary-text self-end rounded-cms-lg rounded-br-cms-sm'
383
+ : 'bg-white/10 text-white self-start rounded-cms-lg rounded-bl-cms-sm cms-markdown border border-white/10'
384
+ }`}
385
+
386
+ dangerouslySetInnerHTML={msg.role === 'assistant'
387
+ ? { __html: renderMarkdown(msg.content) }
388
+ : undefined}
389
+ >
390
+ {msg.role === 'user' ? msg.content : undefined}
391
+ </div>
392
+ )}
393
+ {msg.elementId && (
394
+ <div
395
+ class={`text-[10px] text-white/40 font-mono px-1 ${msg.role === 'user' ? 'self-end' : 'self-start'}`}
396
+ >
397
+ {msg.elementId}
398
+ </div>
399
+ )}
400
+ {
401
+ /* TODO: Re-enable when we can apply partial content instead of whole message
402
+ {msg.role === 'assistant' && msg.elementId && (
403
+ <button
404
+ onClick={() => handleApply(msg.id, msg.content, msg.elementId!)}
405
+ disabled={appliedMessages.has(msg.id)}
406
+ class={`px-3 py-1.5 text-[11px] font-medium cursor-pointer self-start transition-all mt-1 rounded-cms-pill ${
407
+ appliedMessages.has(msg.id)
408
+ ? 'bg-white/10 text-white/50 cursor-not-allowed'
409
+ : 'bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover'
410
+ }`}
411
+ >
412
+ {appliedMessages.has(msg.id)
413
+ ? '✓ Applied'
414
+ : 'Apply to element'}
415
+ </button>
416
+ )}
417
+ */
418
+ }
419
+ </div>
420
+ ))
421
+ )}
422
+
423
+ {/* Status indicator */}
424
+ {currentStatus && (
425
+ <div class="flex items-center gap-2 px-3 py-1.5 text-[11px] text-white/40 self-start animate-[slideIn_0.2s_ease]">
426
+ <StatusIcon status={currentStatus} />
427
+ <span>
428
+ {statusMessage || getDefaultStatusMessage(currentStatus)}
429
+ </span>
430
+ </div>
431
+ )}
432
+
433
+ <div ref={messagesEndRef} />
434
+ </div>
435
+
436
+ <div class="p-4 border-t border-white/10 bg-cms-dark rounded-b-cms-xl">
437
+ {contextElementId && contextLabel
438
+ ? (
439
+ <div class="px-3 py-2 bg-white/10 rounded-cms-md mb-3 text-[11px] text-white/60 relative">
440
+ <button
441
+ onClick={() => signals.setChatContextElement(null)}
442
+ class="absolute top-2 right-2 bg-none border-none text-white/50 cursor-pointer p-0 text-sm leading-none hover:text-white"
443
+ >
444
+ &times;
445
+ </button>
446
+ <div class="font-medium mb-0.5">Editing:</div>
447
+ <div class="text-white font-medium">{contextLabel}</div>
448
+ </div>
449
+ )
450
+ : (
451
+ <div class="px-3 py-2 rounded-cms-md mb-3 text-[11px] text-white/30">
452
+ Click on section on the page to focus the conversation
453
+ </div>
454
+ )}
455
+ <form onSubmit={handleSubmit} class="flex gap-2">
456
+ <textarea
457
+ ref={textareaRef}
458
+ placeholder="Ask AI anything..."
459
+ rows={1}
460
+ value={message}
461
+ onInput={handleTextareaInput}
462
+ onKeyDown={handleTextareaKeyDown}
463
+ disabled={inputDisabled}
464
+ class={`flex-1 px-4 py-3 border border-white/20 text-[13px] font-sans resize-none max-h-30 transition-all outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10 rounded-cms-lg placeholder:text-white/40 ${
465
+ inputDisabled
466
+ ? 'bg-white/5 text-white/50 opacity-60'
467
+ : 'bg-white/10 text-white'
468
+ }`}
469
+ />
470
+ {inputDisabled
471
+ ? (
472
+ <button
473
+ type="button"
474
+ onClick={() => callbacks.onCancel()}
475
+ class="px-4 cursor-pointer transition-all flex items-center justify-center rounded-cms-lg bg-red-500/20 text-red-300 hover:bg-red-500/30 hover:text-red-200 border border-red-500/30"
476
+ title="Cancel request"
477
+ >
478
+ <svg
479
+ width="16"
480
+ height="16"
481
+ viewBox="0 0 24 24"
482
+ fill="none"
483
+ stroke="currentColor"
484
+ stroke-width="2.5"
485
+ stroke-linecap="round"
486
+ stroke-linejoin="round"
487
+ >
488
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
489
+ </svg>
490
+ </button>
491
+ )
492
+ : (
493
+ <button
494
+ type="submit"
495
+ class="px-4 cursor-pointer transition-all flex items-center justify-center rounded-cms-lg bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover"
496
+ >
497
+ <svg
498
+ width="16"
499
+ height="16"
500
+ viewBox="0 0 24 24"
501
+ fill="none"
502
+ stroke="currentColor"
503
+ stroke-width="2.5"
504
+ stroke-linecap="round"
505
+ stroke-linejoin="round"
506
+ >
507
+ <line x1="22" y1="2" x2="11" y2="13"></line>
508
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
509
+ </svg>
510
+ </button>
511
+ )}
512
+ </form>
513
+ </div>
514
+ </>
515
+ )}
516
+ </div>
517
+ )
518
+ }
519
+
520
+ /**
521
+ * Get default status message for a status type
522
+ */
523
+ function getDefaultStatusMessage(status: string): string {
524
+ switch (status) {
525
+ case 'thinking':
526
+ return 'Thinking...'
527
+ case 'coding':
528
+ return 'Writing code...'
529
+ case 'building':
530
+ return 'Building preview...'
531
+ case 'deploying':
532
+ return 'Deploying...'
533
+ case 'complete':
534
+ return 'Done!'
535
+ default:
536
+ return 'Processing...'
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Status indicator icon component
542
+ */
543
+ function StatusIcon({ status }: { status: string }) {
544
+ switch (status) {
545
+ case 'thinking':
546
+ return (
547
+ <svg
548
+ width="16"
549
+ height="16"
550
+ viewBox="0 0 24 24"
551
+ fill="none"
552
+ stroke="currentColor"
553
+ stroke-width="2"
554
+ class="animate-pulse text-purple-600"
555
+ >
556
+ <circle cx="12" cy="12" r="10" />
557
+ <path d="M12 16v-4m0-4h.01" />
558
+ </svg>
559
+ )
560
+ case 'coding':
561
+ return (
562
+ <svg
563
+ width="16"
564
+ height="16"
565
+ viewBox="0 0 24 24"
566
+ fill="none"
567
+ stroke="currentColor"
568
+ stroke-width="2"
569
+ class="text-blue-600"
570
+ >
571
+ <polyline points="16 18 22 12 16 6" />
572
+ <polyline points="8 6 2 12 8 18" />
573
+ </svg>
574
+ )
575
+ case 'building':
576
+ return (
577
+ <svg
578
+ width="16"
579
+ height="16"
580
+ viewBox="0 0 24 24"
581
+ fill="none"
582
+ stroke="currentColor"
583
+ stroke-width="2"
584
+ class="animate-spin text-orange-600"
585
+ >
586
+ <path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" />
587
+ </svg>
588
+ )
589
+ case 'deploying':
590
+ return (
591
+ <svg
592
+ width="16"
593
+ height="16"
594
+ viewBox="0 0 24 24"
595
+ fill="none"
596
+ stroke="currentColor"
597
+ stroke-width="2"
598
+ class="animate-bounce text-green-600"
599
+ >
600
+ <path d="M12 19V5m-7 7l7-7 7 7" />
601
+ </svg>
602
+ )
603
+ case 'complete':
604
+ return (
605
+ <svg
606
+ width="16"
607
+ height="16"
608
+ viewBox="0 0 24 24"
609
+ fill="none"
610
+ stroke="currentColor"
611
+ stroke-width="2"
612
+ class="text-green-600"
613
+ >
614
+ <path d="M20 6L9 17l-5-5" />
615
+ </svg>
616
+ )
617
+ default:
618
+ return (
619
+ <svg
620
+ width="16"
621
+ height="16"
622
+ viewBox="0 0 24 24"
623
+ fill="none"
624
+ stroke="currentColor"
625
+ stroke-width="2"
626
+ class="animate-spin text-slate-600"
627
+ >
628
+ <path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" />
629
+ </svg>
630
+ )
631
+ }
632
+ }