@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.
- package/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- 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
|
+
}
|