@nuasite/cms 0.19.1 → 0.20.2
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/dist/editor.js +12615 -12689
- package/package.json +3 -3
- package/src/build-processor.ts +4 -4
- package/src/dev-middleware.ts +185 -189
- package/src/editor/api.ts +0 -251
- package/src/editor/components/fields.tsx +6 -6
- package/src/editor/components/markdown-editor-overlay.tsx +46 -70
- package/src/editor/components/markdown-inline-editor.tsx +34 -165
- package/src/editor/components/mdx-block-view.tsx +351 -47
- package/src/editor/components/mdx-component-picker.tsx +35 -11
- package/src/editor/components/media-library.tsx +1 -15
- package/src/editor/components/modal-shell.tsx +1 -1
- package/src/editor/components/toolbar.tsx +0 -75
- package/src/editor/constants.ts +0 -4
- package/src/editor/editor.ts +2 -192
- package/src/editor/hooks/index.ts +0 -3
- package/src/editor/hooks/useBlockEditorHandlers.ts +1 -8
- package/src/editor/hooks/useTooltipState.ts +1 -2
- package/src/editor/index.tsx +2 -18
- package/src/editor/milkdown-mdx-plugin.tsx +116 -19
- package/src/editor/milkdown-utils.ts +174 -0
- package/src/editor/post-message.ts +0 -6
- package/src/editor/signals.ts +0 -183
- package/src/editor/styles.css +0 -108
- package/src/editor/types.ts +0 -76
- package/src/html-processor.ts +9 -7
- package/src/source-finder/cache.ts +47 -0
- package/src/source-finder/collection-finder.ts +181 -0
- package/src/source-finder/index.ts +5 -2
- package/src/source-finder/search-index.ts +79 -0
- package/src/source-finder/snippet-utils.ts +36 -61
- package/src/types.ts +0 -4
- package/src/utils.ts +10 -0
- package/src/vite-plugin.ts +24 -4
- package/src/editor/ai.ts +0 -185
- package/src/editor/components/ai-chat.tsx +0 -631
- package/src/editor/components/ai-tooltip.tsx +0 -180
- package/src/editor/components/mdx-props-editor.tsx +0 -94
- package/src/editor/hooks/useAIHandlers.ts +0 -345
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
-
import { Z_INDEX } from '../constants'
|
|
3
|
-
|
|
4
|
-
export interface AITooltipCallbacks {
|
|
5
|
-
onPromptSubmit: (prompt: string, elementId: string) => void
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface AITooltipProps {
|
|
9
|
-
callbacks: AITooltipCallbacks
|
|
10
|
-
visible: boolean
|
|
11
|
-
elementId: string | null
|
|
12
|
-
rect: DOMRect | null
|
|
13
|
-
processing: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function AITooltip({ callbacks, visible, elementId, rect, processing }: AITooltipProps) {
|
|
17
|
-
const [isExpanded, setIsExpanded] = useState(false)
|
|
18
|
-
const [prompt, setPrompt] = useState('')
|
|
19
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!visible) {
|
|
23
|
-
setIsExpanded(false)
|
|
24
|
-
setPrompt('')
|
|
25
|
-
}
|
|
26
|
-
}, [visible])
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (isExpanded && inputRef.current) {
|
|
30
|
-
setTimeout(() => inputRef.current?.focus(), 50)
|
|
31
|
-
}
|
|
32
|
-
}, [isExpanded])
|
|
33
|
-
|
|
34
|
-
if (!visible || !rect || !elementId) {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Don't show empty tooltip when not expanded and not processing
|
|
39
|
-
// (the content for this state is not yet implemented)
|
|
40
|
-
if (!isExpanded && !processing) {
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Calculate position
|
|
45
|
-
const tooltipWidth = 200
|
|
46
|
-
const tooltipHeight = 50
|
|
47
|
-
let left = rect.left + rect.width / 2 - tooltipWidth / 2
|
|
48
|
-
let top = rect.top - tooltipHeight - 8
|
|
49
|
-
|
|
50
|
-
const padding = 10
|
|
51
|
-
const maxLeft = window.innerWidth - tooltipWidth - padding
|
|
52
|
-
const minLeft = padding
|
|
53
|
-
|
|
54
|
-
left = Math.max(minLeft, Math.min(left, maxLeft))
|
|
55
|
-
|
|
56
|
-
if (top < padding) {
|
|
57
|
-
top = rect.bottom + 8
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const maxTop = window.innerHeight - tooltipHeight - padding
|
|
61
|
-
top = Math.min(top, maxTop)
|
|
62
|
-
|
|
63
|
-
const handleExpand = (e: MouseEvent) => {
|
|
64
|
-
e.preventDefault()
|
|
65
|
-
e.stopPropagation()
|
|
66
|
-
if (!isExpanded && elementId) {
|
|
67
|
-
setIsExpanded(true)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const handleCancel = (e: MouseEvent) => {
|
|
72
|
-
e.preventDefault()
|
|
73
|
-
e.stopPropagation()
|
|
74
|
-
setIsExpanded(false)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const handleSubmit = (e: Event) => {
|
|
78
|
-
e.preventDefault()
|
|
79
|
-
e.stopPropagation()
|
|
80
|
-
if (prompt.trim() && elementId) {
|
|
81
|
-
callbacks.onPromptSubmit(prompt.trim(), elementId)
|
|
82
|
-
setIsExpanded(false)
|
|
83
|
-
setPrompt('')
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const handleMouseDown = (e: MouseEvent) => {
|
|
88
|
-
// Stop propagation to prevent the click-outside handler from hiding the tooltip
|
|
89
|
-
e.stopPropagation()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<div
|
|
94
|
-
data-cms-ui
|
|
95
|
-
onMouseDown={handleMouseDown}
|
|
96
|
-
onClick={(e: MouseEvent) => e.stopPropagation()}
|
|
97
|
-
style={{
|
|
98
|
-
position: 'fixed',
|
|
99
|
-
left: `${left}px`,
|
|
100
|
-
top: `${top}px`,
|
|
101
|
-
zIndex: Z_INDEX.OVERLAY,
|
|
102
|
-
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
|
103
|
-
fontSize: '12px',
|
|
104
|
-
}}
|
|
105
|
-
>
|
|
106
|
-
<div
|
|
107
|
-
class={`tooltip ${isExpanded ? 'expanded' : ''} ${
|
|
108
|
-
processing ? 'processing' : ''
|
|
109
|
-
} bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.3)] border border-white/10 text-white cursor-pointer transition-all duration-200 pointer-events-auto select-none rounded-cms-lg`}
|
|
110
|
-
onClick={handleExpand}
|
|
111
|
-
onMouseDown={handleMouseDown}
|
|
112
|
-
style={{
|
|
113
|
-
padding: isExpanded ? '16px' : '10px 14px',
|
|
114
|
-
minWidth: isExpanded ? '280px' : 'auto',
|
|
115
|
-
maxWidth: isExpanded ? '320px' : 'auto',
|
|
116
|
-
}}
|
|
117
|
-
>
|
|
118
|
-
{
|
|
119
|
-
// TODO: Implement AI tooltip
|
|
120
|
-
/*{!isExpanded && !processing && (
|
|
121
|
-
<div class="flex items-center gap-1.5 text-cms-secondary">
|
|
122
|
-
<svg
|
|
123
|
-
width="14"
|
|
124
|
-
height="14"
|
|
125
|
-
viewBox="0 0 24 24"
|
|
126
|
-
fill="none"
|
|
127
|
-
stroke="currentColor"
|
|
128
|
-
stroke-width="2.5"
|
|
129
|
-
stroke-linecap="round"
|
|
130
|
-
stroke-linejoin="round"
|
|
131
|
-
>
|
|
132
|
-
<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" />
|
|
133
|
-
</svg>
|
|
134
|
-
<span class="font-medium">Ask AI to edit</span>
|
|
135
|
-
</div>
|
|
136
|
-
)}*/
|
|
137
|
-
}
|
|
138
|
-
{processing && (
|
|
139
|
-
<div class="flex items-center gap-2 text-white/70">
|
|
140
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="animate-spin">
|
|
141
|
-
<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" />
|
|
142
|
-
</svg>
|
|
143
|
-
<span class="font-medium">Processing...</span>
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
{isExpanded && !processing && (
|
|
147
|
-
<form class="prompt-form flex flex-col gap-3" onSubmit={handleSubmit}>
|
|
148
|
-
<label class="text-[11px] text-white/50 font-medium">What would you like to change?</label>
|
|
149
|
-
<input
|
|
150
|
-
ref={inputRef}
|
|
151
|
-
type="text"
|
|
152
|
-
placeholder="e.g., make it shorter..."
|
|
153
|
-
value={prompt}
|
|
154
|
-
onInput={(e) => setPrompt((e.target as HTMLInputElement).value)}
|
|
155
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
156
|
-
class="w-full px-3 py-2 border border-white/20 bg-white/10 text-white text-xs font-sans outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10 transition-all rounded-cms-sm placeholder:text-white/40"
|
|
157
|
-
/>
|
|
158
|
-
<div class="flex gap-2 justify-end">
|
|
159
|
-
<button
|
|
160
|
-
type="button"
|
|
161
|
-
onClick={handleCancel}
|
|
162
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
163
|
-
class="px-3 py-1.5 text-[11px] font-medium cursor-pointer transition-all bg-white/10 text-white/80 hover:bg-white/20 hover:text-white rounded-cms-pill"
|
|
164
|
-
>
|
|
165
|
-
Cancel
|
|
166
|
-
</button>
|
|
167
|
-
<button
|
|
168
|
-
type="submit"
|
|
169
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
170
|
-
class="px-3 py-1.5 text-[11px] font-medium cursor-pointer transition-all bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover rounded-cms-pill"
|
|
171
|
-
>
|
|
172
|
-
Apply
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
175
|
-
</form>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
)
|
|
180
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'preact/hooks'
|
|
2
|
-
import { clampPanelPosition, Z_INDEX } from '../constants'
|
|
3
|
-
import { getComponentDefinition } from '../manifest'
|
|
4
|
-
import { closeMdxPropsEditor, manifest, mdxPropsEditorState } from '../signals'
|
|
5
|
-
import { MdxComponentIcon } from './mdx-block-view'
|
|
6
|
-
import { CancelButton, CloseButton } from './modal-shell'
|
|
7
|
-
import { PropEditor } from './prop-editor'
|
|
8
|
-
|
|
9
|
-
export function MdxPropsEditor({
|
|
10
|
-
onUpdateProps,
|
|
11
|
-
}: {
|
|
12
|
-
onUpdateProps: (nodePos: number, props: Record<string, string>) => void
|
|
13
|
-
}) {
|
|
14
|
-
const state = mdxPropsEditorState.value
|
|
15
|
-
const isVisible = state.isOpen && state.componentName !== null && state.nodePos !== null
|
|
16
|
-
|
|
17
|
-
const definition = isVisible ? getComponentDefinition(manifest.value, state.componentName!) : undefined
|
|
18
|
-
const [propValues, setPropValues] = useState<Record<string, string>>(state.props)
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (isVisible) setPropValues(state.props)
|
|
22
|
-
}, [state.nodePos, state.componentName, state.props, isVisible])
|
|
23
|
-
|
|
24
|
-
if (!isVisible) return null
|
|
25
|
-
|
|
26
|
-
const panelStyle = state.cursorPos ? clampPanelPosition(state.cursorPos, 360) : {}
|
|
27
|
-
|
|
28
|
-
const handleSave = () => {
|
|
29
|
-
if (state.nodePos !== null) {
|
|
30
|
-
onUpdateProps(state.nodePos, propValues)
|
|
31
|
-
closeMdxPropsEditor()
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<>
|
|
37
|
-
<div
|
|
38
|
-
data-cms-ui
|
|
39
|
-
onClick={closeMdxPropsEditor}
|
|
40
|
-
style={{ zIndex: Z_INDEX.SELECTION }}
|
|
41
|
-
class="fixed inset-0"
|
|
42
|
-
/>
|
|
43
|
-
|
|
44
|
-
<div
|
|
45
|
-
data-cms-ui
|
|
46
|
-
onClick={(e: MouseEvent) => e.stopPropagation()}
|
|
47
|
-
class="fixed w-90 bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] font-sans text-sm overflow-hidden flex flex-col rounded-cms-xl border border-white/10"
|
|
48
|
-
style={{ ...panelStyle, zIndex: Z_INDEX.MODAL }}
|
|
49
|
-
>
|
|
50
|
-
<div class="px-5 py-4 flex justify-between items-center border-b border-white/10">
|
|
51
|
-
<div class="flex items-center gap-2">
|
|
52
|
-
<MdxComponentIcon />
|
|
53
|
-
<span class="font-semibold text-white">{state.componentName}</span>
|
|
54
|
-
</div>
|
|
55
|
-
<CloseButton onClick={closeMdxPropsEditor} />
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<div class="p-5 overflow-y-auto flex-1">
|
|
59
|
-
{definition
|
|
60
|
-
? (
|
|
61
|
-
definition.props.map((prop) => (
|
|
62
|
-
<PropEditor
|
|
63
|
-
key={prop.name}
|
|
64
|
-
prop={prop}
|
|
65
|
-
value={propValues[prop.name] || ''}
|
|
66
|
-
onChange={(value) => setPropValues((prev) => ({ ...prev, [prop.name]: value }))}
|
|
67
|
-
/>
|
|
68
|
-
))
|
|
69
|
-
)
|
|
70
|
-
: (
|
|
71
|
-
<div class="text-white/50 text-[13px]">
|
|
72
|
-
<div class="mb-3">Unknown component — props not editable.</div>
|
|
73
|
-
<div class="font-mono text-[11px] text-white/30 bg-white/5 p-3 rounded-cms-md break-all">
|
|
74
|
-
{Object.entries(propValues).map(([k, v]) => <div key={k}>{k}="{v}"</div>)}
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
)}
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
{definition && (
|
|
81
|
-
<div class="px-5 py-4 border-t border-white/10 flex gap-2 justify-end">
|
|
82
|
-
<CancelButton onClick={closeMdxPropsEditor} />
|
|
83
|
-
<button
|
|
84
|
-
onClick={handleSave}
|
|
85
|
-
class="px-4 py-2.5 bg-cms-primary text-cms-primary-text rounded-cms-pill cursor-pointer hover:bg-cms-primary-hover transition-all font-medium"
|
|
86
|
-
>
|
|
87
|
-
Save
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
</>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
@@ -1,345 +0,0 @@
|
|
|
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
|
-
}
|