@actuate-media/cms-admin 0.11.0 → 0.12.1
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/LICENSE +21 -21
- package/dist/__tests__/components/comment-mark.test.d.ts +2 -0
- package/dist/__tests__/components/comment-mark.test.d.ts.map +1 -0
- package/dist/__tests__/components/comment-mark.test.js +89 -0
- package/dist/__tests__/components/comment-mark.test.js.map +1 -0
- package/dist/__tests__/components/comment-side-panel.test.d.ts +2 -0
- package/dist/__tests__/components/comment-side-panel.test.d.ts.map +1 -0
- package/dist/__tests__/components/comment-side-panel.test.js +291 -0
- package/dist/__tests__/components/comment-side-panel.test.js.map +1 -0
- package/dist/__tests__/components/notification-bell.test.d.ts +2 -0
- package/dist/__tests__/components/notification-bell.test.d.ts.map +1 -0
- package/dist/__tests__/components/notification-bell.test.js +200 -0
- package/dist/__tests__/components/notification-bell.test.js.map +1 -0
- package/dist/__tests__/components/offline-status.test.d.ts +2 -0
- package/dist/__tests__/components/offline-status.test.d.ts.map +1 -0
- package/dist/__tests__/components/offline-status.test.js +56 -0
- package/dist/__tests__/components/offline-status.test.js.map +1 -0
- package/dist/__tests__/components/presence-chips-helpers.test.d.ts +2 -0
- package/dist/__tests__/components/presence-chips-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/components/presence-chips-helpers.test.js +35 -0
- package/dist/__tests__/components/presence-chips-helpers.test.js.map +1 -0
- package/dist/__tests__/fields/component-block-helpers.test.d.ts +7 -0
- package/dist/__tests__/fields/component-block-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/fields/component-block-helpers.test.js +592 -0
- package/dist/__tests__/fields/component-block-helpers.test.js.map +1 -0
- package/dist/__tests__/lib/active-editor.test.d.ts +2 -0
- package/dist/__tests__/lib/active-editor.test.d.ts.map +1 -0
- package/dist/__tests__/lib/active-editor.test.js +57 -0
- package/dist/__tests__/lib/active-editor.test.js.map +1 -0
- package/dist/__tests__/lib/coauthor-client.test.d.ts +2 -0
- package/dist/__tests__/lib/coauthor-client.test.d.ts.map +1 -0
- package/dist/__tests__/lib/coauthor-client.test.js +173 -0
- package/dist/__tests__/lib/coauthor-client.test.js.map +1 -0
- package/dist/__tests__/lib/collaboration-provider.test.d.ts +2 -0
- package/dist/__tests__/lib/collaboration-provider.test.d.ts.map +1 -0
- package/dist/__tests__/lib/collaboration-provider.test.js +335 -0
- package/dist/__tests__/lib/collaboration-provider.test.js.map +1 -0
- package/dist/__tests__/lib/comment-anchor.test.d.ts +2 -0
- package/dist/__tests__/lib/comment-anchor.test.d.ts.map +1 -0
- package/dist/__tests__/lib/comment-anchor.test.js +173 -0
- package/dist/__tests__/lib/comment-anchor.test.js.map +1 -0
- package/dist/__tests__/lib/comments-client.test.d.ts +2 -0
- package/dist/__tests__/lib/comments-client.test.d.ts.map +1 -0
- package/dist/__tests__/lib/comments-client.test.js +202 -0
- package/dist/__tests__/lib/comments-client.test.js.map +1 -0
- package/dist/__tests__/lib/notifications-client.test.d.ts +2 -0
- package/dist/__tests__/lib/notifications-client.test.d.ts.map +1 -0
- package/dist/__tests__/lib/notifications-client.test.js +155 -0
- package/dist/__tests__/lib/notifications-client.test.js.map +1 -0
- package/dist/__tests__/lib/slash-command.test.d.ts +2 -0
- package/dist/__tests__/lib/slash-command.test.d.ts.map +1 -0
- package/dist/__tests__/lib/slash-command.test.js +123 -0
- package/dist/__tests__/lib/slash-command.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/AICoauthorBubbleMenu.d.ts +18 -0
- package/dist/components/AICoauthorBubbleMenu.d.ts.map +1 -0
- package/dist/components/AICoauthorBubbleMenu.js +113 -0
- package/dist/components/AICoauthorBubbleMenu.js.map +1 -0
- package/dist/components/CoauthorResultPopover.d.ts +28 -0
- package/dist/components/CoauthorResultPopover.d.ts.map +1 -0
- package/dist/components/CoauthorResultPopover.js +42 -0
- package/dist/components/CoauthorResultPopover.js.map +1 -0
- package/dist/components/CommandPalette.d.ts +1 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +138 -40
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CommentMark.d.ts +54 -0
- package/dist/components/CommentMark.d.ts.map +1 -0
- package/dist/components/CommentMark.js +104 -0
- package/dist/components/CommentMark.js.map +1 -0
- package/dist/components/CommentSidePanel.d.ts +86 -0
- package/dist/components/CommentSidePanel.d.ts.map +1 -0
- package/dist/components/CommentSidePanel.js +165 -0
- package/dist/components/CommentSidePanel.js.map +1 -0
- package/dist/components/LivePreview.d.ts.map +1 -1
- package/dist/components/LivePreview.js +80 -16
- package/dist/components/LivePreview.js.map +1 -1
- package/dist/components/NotificationBell.d.ts +45 -0
- package/dist/components/NotificationBell.d.ts.map +1 -0
- package/dist/components/NotificationBell.js +90 -0
- package/dist/components/NotificationBell.js.map +1 -0
- package/dist/components/OfflineStatus.d.ts +39 -0
- package/dist/components/OfflineStatus.d.ts.map +1 -0
- package/dist/components/OfflineStatus.js +47 -0
- package/dist/components/OfflineStatus.js.map +1 -0
- package/dist/components/PresenceChips.d.ts +31 -0
- package/dist/components/PresenceChips.d.ts.map +1 -0
- package/dist/components/PresenceChips.js +58 -0
- package/dist/components/PresenceChips.js.map +1 -0
- package/dist/components/TipTapEditor.d.ts +61 -1
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/TipTapEditor.js +211 -8
- package/dist/components/TipTapEditor.js.map +1 -1
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/fields/ComponentBlockField.d.ts +25 -0
- package/dist/fields/ComponentBlockField.d.ts.map +1 -0
- package/dist/fields/ComponentBlockField.js +74 -0
- package/dist/fields/ComponentBlockField.js.map +1 -0
- package/dist/fields/FieldRenderer.d.ts +3 -0
- package/dist/fields/FieldRenderer.d.ts.map +1 -1
- package/dist/fields/FieldRenderer.js +3 -1
- package/dist/fields/FieldRenderer.js.map +1 -1
- package/dist/fields/PropInput.d.ts +14 -0
- package/dist/fields/PropInput.d.ts.map +1 -0
- package/dist/fields/PropInput.js +163 -0
- package/dist/fields/PropInput.js.map +1 -0
- package/dist/fields/component-block-helpers.d.ts +96 -0
- package/dist/fields/component-block-helpers.d.ts.map +1 -0
- package/dist/fields/component-block-helpers.js +323 -0
- package/dist/fields/component-block-helpers.js.map +1 -0
- package/dist/fields/index.d.ts +4 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +2 -0
- package/dist/fields/index.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/active-editor.d.ts +6 -0
- package/dist/lib/active-editor.d.ts.map +1 -0
- package/dist/lib/active-editor.js +35 -0
- package/dist/lib/active-editor.js.map +1 -0
- package/dist/lib/coauthor-client.d.ts +72 -0
- package/dist/lib/coauthor-client.d.ts.map +1 -0
- package/dist/lib/coauthor-client.js +98 -0
- package/dist/lib/coauthor-client.js.map +1 -0
- package/dist/lib/collaboration-provider.d.ts +142 -0
- package/dist/lib/collaboration-provider.d.ts.map +1 -0
- package/dist/lib/collaboration-provider.js +168 -0
- package/dist/lib/collaboration-provider.js.map +1 -0
- package/dist/lib/comment-anchor.d.ts +86 -0
- package/dist/lib/comment-anchor.d.ts.map +1 -0
- package/dist/lib/comment-anchor.js +139 -0
- package/dist/lib/comment-anchor.js.map +1 -0
- package/dist/lib/comments-client.d.ts +106 -0
- package/dist/lib/comments-client.d.ts.map +1 -0
- package/dist/lib/comments-client.js +138 -0
- package/dist/lib/comments-client.js.map +1 -0
- package/dist/lib/notifications-client.d.ts +68 -0
- package/dist/lib/notifications-client.d.ts.map +1 -0
- package/dist/lib/notifications-client.js +85 -0
- package/dist/lib/notifications-client.js.map +1 -0
- package/dist/lib/slash-command.d.ts +39 -0
- package/dist/lib/slash-command.d.ts.map +1 -0
- package/dist/lib/slash-command.js +50 -0
- package/dist/lib/slash-command.js.map +1 -0
- package/package.json +20 -4
- package/src/__tests__/components/comment-mark.test.ts +99 -0
- package/src/__tests__/components/comment-side-panel.test.tsx +343 -0
- package/src/__tests__/components/notification-bell.test.tsx +213 -0
- package/src/__tests__/components/offline-status.test.tsx +66 -0
- package/src/__tests__/components/presence-chips-helpers.test.ts +44 -0
- package/src/__tests__/fields/component-block-helpers.test.ts +674 -0
- package/src/__tests__/lib/active-editor.test.ts +70 -0
- package/src/__tests__/lib/coauthor-client.test.ts +213 -0
- package/src/__tests__/lib/collaboration-provider.test.ts +425 -0
- package/src/__tests__/lib/comment-anchor.test.ts +188 -0
- package/src/__tests__/lib/comments-client.test.ts +239 -0
- package/src/__tests__/lib/notifications-client.test.ts +195 -0
- package/src/__tests__/lib/slash-command.test.ts +146 -0
- package/src/components/AICoauthorBubbleMenu.tsx +187 -0
- package/src/components/CoauthorResultPopover.tsx +193 -0
- package/src/components/CommandPalette.tsx +397 -209
- package/src/components/CommentMark.ts +138 -0
- package/src/components/CommentSidePanel.tsx +521 -0
- package/src/components/LivePreview.tsx +117 -11
- package/src/components/NotificationBell.tsx +242 -0
- package/src/components/OfflineStatus.tsx +90 -0
- package/src/components/PresenceChips.tsx +150 -0
- package/src/components/TipTapEditor.tsx +310 -12
- package/src/fields/ComponentBlockField.tsx +179 -0
- package/src/fields/FieldRenderer.tsx +8 -0
- package/src/fields/PropInput.tsx +552 -0
- package/src/fields/component-block-helpers.ts +341 -0
- package/src/fields/index.ts +4 -0
- package/src/index.ts +27 -0
- package/src/lib/active-editor.ts +39 -0
- package/src/lib/coauthor-client.ts +160 -0
- package/src/lib/collaboration-provider.ts +306 -0
- package/src/lib/comment-anchor.ts +179 -0
- package/src/lib/comments-client.ts +208 -0
- package/src/lib/notifications-client.ts +146 -0
- package/src/lib/slash-command.ts +67 -0
|
@@ -16,8 +16,29 @@ import {
|
|
|
16
16
|
Upload,
|
|
17
17
|
BarChart3,
|
|
18
18
|
Clock,
|
|
19
|
+
Maximize2,
|
|
20
|
+
Minimize2,
|
|
21
|
+
Sparkles,
|
|
22
|
+
SpellCheck,
|
|
23
|
+
Wand2,
|
|
19
24
|
} from 'lucide-react'
|
|
25
|
+
import { toast } from 'sonner'
|
|
20
26
|
import { cmsApi } from '../lib/api.js'
|
|
27
|
+
import {
|
|
28
|
+
coauthorActionDescription,
|
|
29
|
+
coauthorActionLabel,
|
|
30
|
+
coauthorText,
|
|
31
|
+
type CoauthorAction,
|
|
32
|
+
type ProofreadChange,
|
|
33
|
+
} from '../lib/coauthor-client.js'
|
|
34
|
+
import {
|
|
35
|
+
isAiSlashCommandInput,
|
|
36
|
+
orderedAiActions,
|
|
37
|
+
parseAiSlashCommand,
|
|
38
|
+
} from '../lib/slash-command.js'
|
|
39
|
+
import { getActiveEditor } from '../lib/active-editor.js'
|
|
40
|
+
import { CoauthorResultPopover } from './CoauthorResultPopover.js'
|
|
41
|
+
import type { Editor } from '@tiptap/react'
|
|
21
42
|
|
|
22
43
|
interface SearchResultDocument {
|
|
23
44
|
id: string
|
|
@@ -80,6 +101,39 @@ const staticActions = [
|
|
|
80
101
|
{ id: 'action-open-settings', label: 'Open settings', icon: Settings, action: '/settings' },
|
|
81
102
|
]
|
|
82
103
|
|
|
104
|
+
const AI_ACTION_ICONS: Record<CoauthorAction, typeof Sparkles> = {
|
|
105
|
+
rewrite: Wand2,
|
|
106
|
+
expand: Maximize2,
|
|
107
|
+
compress: Minimize2,
|
|
108
|
+
proofread: SpellCheck,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface AiResultState {
|
|
112
|
+
open: boolean
|
|
113
|
+
loading: boolean
|
|
114
|
+
action: CoauthorAction
|
|
115
|
+
before: string
|
|
116
|
+
after: string
|
|
117
|
+
error?: string
|
|
118
|
+
changes?: ProofreadChange[]
|
|
119
|
+
/** Editor captured at submission time; null means clipboard-only. */
|
|
120
|
+
target: Editor | null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const INITIAL_AI_RESULT: AiResultState = {
|
|
124
|
+
open: false,
|
|
125
|
+
loading: false,
|
|
126
|
+
action: 'rewrite',
|
|
127
|
+
before: '',
|
|
128
|
+
after: '',
|
|
129
|
+
target: null,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function truncatePreview(text: string, max = 60): string {
|
|
133
|
+
if (text.length <= max) return text
|
|
134
|
+
return `${text.slice(0, max - 1)}…`
|
|
135
|
+
}
|
|
136
|
+
|
|
83
137
|
const navigationCommands = [
|
|
84
138
|
{ id: 'nav-dashboard', label: 'Dashboard', icon: LayoutDashboard, action: '/' },
|
|
85
139
|
{ id: 'nav-posts', label: 'Posts', icon: FileText, action: '/posts' },
|
|
@@ -104,6 +158,7 @@ export function CommandPalette({ open, onOpenChange, onNavigate }: CommandPalett
|
|
|
104
158
|
const [users, setUsers] = useState<SearchResultUser[]>([])
|
|
105
159
|
const [recentItems, setRecentItems] = useState<RecentItem[]>([])
|
|
106
160
|
const [loading, setLoading] = useState(false)
|
|
161
|
+
const [aiResult, setAiResult] = useState<AiResultState>(INITIAL_AI_RESULT)
|
|
107
162
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
108
163
|
|
|
109
164
|
useEffect(() => {
|
|
@@ -153,7 +208,7 @@ export function CommandPalette({ open, onOpenChange, onNavigate }: CommandPalett
|
|
|
153
208
|
|
|
154
209
|
useEffect(() => {
|
|
155
210
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
156
|
-
if (!search.trim()) {
|
|
211
|
+
if (!search.trim() || isAiSlashCommandInput(search)) {
|
|
157
212
|
setDocuments([])
|
|
158
213
|
setMedia([])
|
|
159
214
|
setUsers([])
|
|
@@ -174,227 +229,360 @@ export function CommandPalette({ open, onOpenChange, onNavigate }: CommandPalett
|
|
|
174
229
|
setSearch('')
|
|
175
230
|
}
|
|
176
231
|
|
|
177
|
-
|
|
232
|
+
const runSlashAction = useCallback(
|
|
233
|
+
async (action: CoauthorAction, text: string) => {
|
|
234
|
+
if (!text.trim()) {
|
|
235
|
+
toast.message(`Provide text after the command: /ai ${action} <your text>`)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
const target = getActiveEditor()
|
|
239
|
+
onOpenChange(false)
|
|
240
|
+
setSearch('')
|
|
241
|
+
setAiResult({
|
|
242
|
+
open: true,
|
|
243
|
+
loading: true,
|
|
244
|
+
action,
|
|
245
|
+
before: text,
|
|
246
|
+
after: '',
|
|
247
|
+
target,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const outcome = await coauthorText(action, text)
|
|
251
|
+
if (!outcome.ok) {
|
|
252
|
+
setAiResult((prev) => ({ ...prev, loading: false, error: outcome.error }))
|
|
253
|
+
toast.error(outcome.error)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
setAiResult((prev) => ({
|
|
257
|
+
...prev,
|
|
258
|
+
loading: false,
|
|
259
|
+
after: outcome.result.text,
|
|
260
|
+
changes: outcome.result.changes,
|
|
261
|
+
error: undefined,
|
|
262
|
+
}))
|
|
263
|
+
},
|
|
264
|
+
[onOpenChange],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const handleAiApply = useCallback(() => {
|
|
268
|
+
const { target, after, action } = aiResult
|
|
269
|
+
if (!target) {
|
|
270
|
+
void navigator.clipboard
|
|
271
|
+
?.writeText(after)
|
|
272
|
+
.then(() => toast.success('Copied to clipboard.'))
|
|
273
|
+
.catch(() => toast.error('Clipboard unavailable — copy manually.'))
|
|
274
|
+
setAiResult(INITIAL_AI_RESULT)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
const { from, to, empty } = target.state.selection
|
|
278
|
+
const chain = target.chain().focus()
|
|
279
|
+
if (!empty) {
|
|
280
|
+
chain.insertContentAt({ from, to }, after).run()
|
|
281
|
+
} else {
|
|
282
|
+
chain.insertContent(after).run()
|
|
283
|
+
}
|
|
284
|
+
toast.success(`${coauthorActionLabel(action)} applied.`)
|
|
285
|
+
setAiResult(INITIAL_AI_RESULT)
|
|
286
|
+
}, [aiResult])
|
|
287
|
+
|
|
288
|
+
const handleAiCopy = useCallback(async () => {
|
|
289
|
+
try {
|
|
290
|
+
await navigator.clipboard.writeText(aiResult.after)
|
|
291
|
+
toast.success('Copied to clipboard.')
|
|
292
|
+
} catch {
|
|
293
|
+
toast.error('Clipboard unavailable — copy manually.')
|
|
294
|
+
}
|
|
295
|
+
}, [aiResult.after])
|
|
296
|
+
|
|
297
|
+
const handleAiDiscard = useCallback(() => {
|
|
298
|
+
setAiResult(INITIAL_AI_RESULT)
|
|
299
|
+
}, [])
|
|
178
300
|
|
|
179
301
|
const hasQuery = search.trim().length > 0
|
|
180
302
|
const hasResults = documents.length > 0 || media.length > 0 || users.length > 0
|
|
303
|
+
const isAiMode = isAiSlashCommandInput(search)
|
|
304
|
+
const aiCommand = parseAiSlashCommand(search)
|
|
305
|
+
const aiPayload = aiCommand?.text ?? ''
|
|
306
|
+
const aiActions = orderedAiActions(search)
|
|
307
|
+
|
|
308
|
+
const popover = (
|
|
309
|
+
<CoauthorResultPopover
|
|
310
|
+
open={aiResult.open}
|
|
311
|
+
loading={aiResult.loading}
|
|
312
|
+
action={aiResult.action}
|
|
313
|
+
before={aiResult.before}
|
|
314
|
+
after={aiResult.after}
|
|
315
|
+
error={aiResult.error}
|
|
316
|
+
changes={aiResult.changes}
|
|
317
|
+
applyLabel={aiResult.target ? 'Apply to editor' : 'Copy to clipboard'}
|
|
318
|
+
onApply={handleAiApply}
|
|
319
|
+
onCopy={handleAiCopy}
|
|
320
|
+
onDiscard={handleAiDiscard}
|
|
321
|
+
/>
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if (!open) return popover
|
|
181
325
|
|
|
182
326
|
return (
|
|
183
|
-
|
|
184
|
-
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 px-4 pt-[20vh]"
|
|
185
|
-
onClick={() => {
|
|
186
|
-
onOpenChange(false)
|
|
187
|
-
setSearch('')
|
|
188
|
-
}}
|
|
189
|
-
role="presentation"
|
|
190
|
-
>
|
|
327
|
+
<>
|
|
191
328
|
<div
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
329
|
+
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 px-4 pt-[20vh]"
|
|
330
|
+
onClick={() => {
|
|
331
|
+
onOpenChange(false)
|
|
332
|
+
setSearch('')
|
|
333
|
+
}}
|
|
334
|
+
role="presentation"
|
|
196
335
|
>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
setSearch('')
|
|
203
|
-
}
|
|
204
|
-
}}
|
|
205
|
-
shouldFilter={!hasQuery}
|
|
336
|
+
<div
|
|
337
|
+
role="dialog"
|
|
338
|
+
aria-modal="true"
|
|
339
|
+
className="w-full max-w-2xl"
|
|
340
|
+
onClick={(e) => e.stopPropagation()}
|
|
206
341
|
>
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
{
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{loading
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
value={m.filename}
|
|
282
|
-
onSelect={() =>
|
|
283
|
-
handleSelect(`/media?selected=${m.id}`, {
|
|
284
|
-
id: `media-${m.id}`,
|
|
285
|
-
label: m.filename,
|
|
286
|
-
path: `/media?selected=${m.id}`,
|
|
287
|
-
type: 'media',
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
291
|
-
>
|
|
292
|
-
<Image className="h-4 w-4 text-gray-500" />
|
|
293
|
-
<div className="flex min-w-0 flex-col">
|
|
294
|
-
<span className="truncate">{m.filename}</span>
|
|
295
|
-
<span className="text-xs text-gray-400">{m.mimeType}</span>
|
|
296
|
-
</div>
|
|
297
|
-
</Command.Item>
|
|
298
|
-
))}
|
|
299
|
-
</Command.Group>
|
|
300
|
-
)}
|
|
301
|
-
|
|
302
|
-
{hasQuery && users.length > 0 && (
|
|
303
|
-
<Command.Group heading="Users" className="mt-1 px-2 py-2">
|
|
304
|
-
<div className="mb-2 text-xs font-medium text-gray-500">USERS</div>
|
|
305
|
-
{users.map((u) => (
|
|
306
|
-
<Command.Item
|
|
307
|
-
key={`user-${u.id}`}
|
|
308
|
-
value={`${u.name} ${u.email}`}
|
|
309
|
-
onSelect={() =>
|
|
310
|
-
handleSelect(`/users?selected=${u.id}`, {
|
|
311
|
-
id: `user-${u.id}`,
|
|
312
|
-
label: u.name,
|
|
313
|
-
path: `/users?selected=${u.id}`,
|
|
314
|
-
type: 'user',
|
|
315
|
-
})
|
|
316
|
-
}
|
|
317
|
-
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
318
|
-
>
|
|
319
|
-
<Users className="h-4 w-4 text-gray-500" />
|
|
320
|
-
<div className="flex min-w-0 flex-col">
|
|
321
|
-
<span className="truncate">{u.name}</span>
|
|
322
|
-
<span className="text-xs text-gray-400">
|
|
323
|
-
{u.email} · {u.role}
|
|
324
|
-
</span>
|
|
325
|
-
</div>
|
|
326
|
-
</Command.Item>
|
|
327
|
-
))}
|
|
328
|
-
</Command.Group>
|
|
329
|
-
)}
|
|
330
|
-
|
|
331
|
-
{(!hasQuery || !hasResults) && (
|
|
332
|
-
<Command.Group heading="Navigation" className="px-2 py-2">
|
|
333
|
-
<div className="mb-2 text-xs font-medium text-gray-500">NAVIGATION</div>
|
|
334
|
-
{navigationCommands.map((cmd) => {
|
|
335
|
-
const Icon = cmd.icon
|
|
336
|
-
return (
|
|
342
|
+
<Command
|
|
343
|
+
className="w-full overflow-hidden rounded-lg bg-white shadow-2xl"
|
|
344
|
+
onKeyDown={(e) => {
|
|
345
|
+
if (e.key === 'Escape') {
|
|
346
|
+
onOpenChange(false)
|
|
347
|
+
setSearch('')
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
shouldFilter={!hasQuery}
|
|
351
|
+
>
|
|
352
|
+
<div className="flex items-center border-b border-gray-200 px-4">
|
|
353
|
+
<Search className="mr-3 h-5 w-5 shrink-0 text-gray-400" />
|
|
354
|
+
<Command.Input
|
|
355
|
+
value={search}
|
|
356
|
+
onValueChange={setSearch}
|
|
357
|
+
placeholder="Search, jump to, or type /ai to use the AI co-author…"
|
|
358
|
+
className="w-full bg-transparent py-4 text-base placeholder:text-gray-400 focus:outline-none"
|
|
359
|
+
/>
|
|
360
|
+
{loading && (
|
|
361
|
+
<div className="mr-2 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
|
|
362
|
+
)}
|
|
363
|
+
<kbd className="hidden shrink-0 rounded bg-gray-100 px-2 py-1 font-mono text-xs text-gray-600 sm:inline-block">
|
|
364
|
+
ESC
|
|
365
|
+
</kbd>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<Command.List className="max-h-[400px] overflow-y-auto p-2">
|
|
369
|
+
<Command.Empty className="py-6 text-center text-sm text-gray-500">
|
|
370
|
+
{loading ? 'Searching...' : 'No results found.'}
|
|
371
|
+
</Command.Empty>
|
|
372
|
+
|
|
373
|
+
{isAiMode && (
|
|
374
|
+
<Command.Group heading="AI co-author" className="px-2 py-2">
|
|
375
|
+
<div className="mb-2 flex items-center gap-1 text-xs font-medium text-gray-500">
|
|
376
|
+
<Sparkles className="h-3 w-3" aria-hidden /> AI CO-AUTHOR
|
|
377
|
+
</div>
|
|
378
|
+
{aiActions.map((action) => {
|
|
379
|
+
const Icon = AI_ACTION_ICONS[action]
|
|
380
|
+
const isExactMatch = aiCommand?.action === action
|
|
381
|
+
const previewable = isExactMatch && aiPayload.length > 0
|
|
382
|
+
return (
|
|
383
|
+
<Command.Item
|
|
384
|
+
key={`ai-${action}`}
|
|
385
|
+
value={`ai ${action}`}
|
|
386
|
+
onSelect={() => runSlashAction(action, aiPayload)}
|
|
387
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
388
|
+
>
|
|
389
|
+
<Icon className="h-4 w-4 text-blue-600" />
|
|
390
|
+
<div className="flex min-w-0 flex-col">
|
|
391
|
+
<span className="font-medium">
|
|
392
|
+
{coauthorActionLabel(action)}
|
|
393
|
+
{previewable && (
|
|
394
|
+
<span className="ml-1 font-normal text-gray-500">
|
|
395
|
+
“{truncatePreview(aiPayload)}”
|
|
396
|
+
</span>
|
|
397
|
+
)}
|
|
398
|
+
</span>
|
|
399
|
+
<span className="text-xs text-gray-400">
|
|
400
|
+
{coauthorActionDescription(action)}
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
<kbd className="ml-auto rounded bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-500">
|
|
404
|
+
/ai {action}
|
|
405
|
+
</kbd>
|
|
406
|
+
</Command.Item>
|
|
407
|
+
)
|
|
408
|
+
})}
|
|
409
|
+
</Command.Group>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{!isAiMode && !hasQuery && recentItems.length > 0 && (
|
|
413
|
+
<Command.Group heading="Recent" className="px-2 py-2">
|
|
414
|
+
<div className="mb-2 text-xs font-medium text-gray-500">RECENT</div>
|
|
415
|
+
{recentItems.map((item) => (
|
|
337
416
|
<Command.Item
|
|
338
|
-
key={
|
|
339
|
-
value={
|
|
340
|
-
onSelect={() => handleSelect(
|
|
417
|
+
key={`recent-${item.id}`}
|
|
418
|
+
value={`recent ${item.label}`}
|
|
419
|
+
onSelect={() => handleSelect(item.path, item)}
|
|
341
420
|
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
342
421
|
>
|
|
343
|
-
<
|
|
344
|
-
<span>{
|
|
422
|
+
<Clock className="h-4 w-4 text-gray-400" />
|
|
423
|
+
<span>{item.label}</span>
|
|
424
|
+
<span className="ml-auto text-xs text-gray-400 capitalize">{item.type}</span>
|
|
345
425
|
</Command.Item>
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
426
|
+
))}
|
|
427
|
+
</Command.Group>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{!isAiMode && hasQuery && documents.length > 0 && (
|
|
431
|
+
<Command.Group heading="Documents" className="px-2 py-2">
|
|
432
|
+
<div className="mb-2 text-xs font-medium text-gray-500">DOCUMENTS</div>
|
|
433
|
+
{documents.map((doc) => (
|
|
434
|
+
<Command.Item
|
|
435
|
+
key={`doc-${doc.id}`}
|
|
436
|
+
value={doc.title ?? doc.slug ?? doc.id}
|
|
437
|
+
onSelect={() =>
|
|
438
|
+
handleSelect(`/collections/${doc.collection}/${doc.id}`, {
|
|
439
|
+
id: `doc-${doc.id}`,
|
|
440
|
+
label: doc.title ?? doc.slug ?? doc.id,
|
|
441
|
+
path: `/collections/${doc.collection}/${doc.id}`,
|
|
442
|
+
type: 'document',
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
446
|
+
>
|
|
447
|
+
<FileText className="h-4 w-4 text-gray-500" />
|
|
448
|
+
<div className="flex min-w-0 flex-col">
|
|
449
|
+
<span className="truncate">{doc.title ?? doc.slug ?? doc.id}</span>
|
|
450
|
+
<span className="text-xs text-gray-400">
|
|
451
|
+
{doc.collection} · {doc.status}
|
|
452
|
+
</span>
|
|
453
|
+
</div>
|
|
454
|
+
</Command.Item>
|
|
455
|
+
))}
|
|
456
|
+
</Command.Group>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{!isAiMode && hasQuery && media.length > 0 && (
|
|
460
|
+
<Command.Group heading="Media" className="mt-1 px-2 py-2">
|
|
461
|
+
<div className="mb-2 text-xs font-medium text-gray-500">MEDIA</div>
|
|
462
|
+
{media.map((m) => (
|
|
463
|
+
<Command.Item
|
|
464
|
+
key={`media-${m.id}`}
|
|
465
|
+
value={m.filename}
|
|
466
|
+
onSelect={() =>
|
|
467
|
+
handleSelect(`/media?selected=${m.id}`, {
|
|
468
|
+
id: `media-${m.id}`,
|
|
469
|
+
label: m.filename,
|
|
470
|
+
path: `/media?selected=${m.id}`,
|
|
471
|
+
type: 'media',
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
475
|
+
>
|
|
476
|
+
<Image className="h-4 w-4 text-gray-500" />
|
|
477
|
+
<div className="flex min-w-0 flex-col">
|
|
478
|
+
<span className="truncate">{m.filename}</span>
|
|
479
|
+
<span className="text-xs text-gray-400">{m.mimeType}</span>
|
|
480
|
+
</div>
|
|
481
|
+
</Command.Item>
|
|
482
|
+
))}
|
|
483
|
+
</Command.Group>
|
|
484
|
+
)}
|
|
485
|
+
|
|
486
|
+
{!isAiMode && hasQuery && users.length > 0 && (
|
|
487
|
+
<Command.Group heading="Users" className="mt-1 px-2 py-2">
|
|
488
|
+
<div className="mb-2 text-xs font-medium text-gray-500">USERS</div>
|
|
489
|
+
{users.map((u) => (
|
|
490
|
+
<Command.Item
|
|
491
|
+
key={`user-${u.id}`}
|
|
492
|
+
value={`${u.name} ${u.email}`}
|
|
493
|
+
onSelect={() =>
|
|
494
|
+
handleSelect(`/users?selected=${u.id}`, {
|
|
495
|
+
id: `user-${u.id}`,
|
|
496
|
+
label: u.name,
|
|
497
|
+
path: `/users?selected=${u.id}`,
|
|
498
|
+
type: 'user',
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
502
|
+
>
|
|
503
|
+
<Users className="h-4 w-4 text-gray-500" />
|
|
504
|
+
<div className="flex min-w-0 flex-col">
|
|
505
|
+
<span className="truncate">{u.name}</span>
|
|
506
|
+
<span className="text-xs text-gray-400">
|
|
507
|
+
{u.email} · {u.role}
|
|
508
|
+
</span>
|
|
509
|
+
</div>
|
|
510
|
+
</Command.Item>
|
|
511
|
+
))}
|
|
512
|
+
</Command.Group>
|
|
513
|
+
)}
|
|
514
|
+
|
|
515
|
+
{!isAiMode && (!hasQuery || !hasResults) && (
|
|
516
|
+
<Command.Group heading="Navigation" className="px-2 py-2">
|
|
517
|
+
<div className="mb-2 text-xs font-medium text-gray-500">NAVIGATION</div>
|
|
518
|
+
{navigationCommands.map((cmd) => {
|
|
519
|
+
const Icon = cmd.icon
|
|
520
|
+
return (
|
|
521
|
+
<Command.Item
|
|
522
|
+
key={cmd.id}
|
|
523
|
+
value={cmd.label}
|
|
524
|
+
onSelect={() => handleSelect(cmd.action)}
|
|
525
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
526
|
+
>
|
|
527
|
+
<Icon className="h-4 w-4 text-gray-500" />
|
|
528
|
+
<span>{cmd.label}</span>
|
|
529
|
+
</Command.Item>
|
|
530
|
+
)
|
|
531
|
+
})}
|
|
532
|
+
</Command.Group>
|
|
533
|
+
)}
|
|
534
|
+
|
|
535
|
+
{!isAiMode && (
|
|
536
|
+
<Command.Group heading="Actions" className="mt-2 px-2 py-2">
|
|
537
|
+
<div className="mb-2 text-xs font-medium text-gray-500">ACTIONS</div>
|
|
538
|
+
{staticActions.map((cmd) => {
|
|
539
|
+
const Icon = cmd.icon
|
|
540
|
+
return (
|
|
541
|
+
<Command.Item
|
|
542
|
+
key={cmd.id}
|
|
543
|
+
value={cmd.label}
|
|
544
|
+
onSelect={() =>
|
|
545
|
+
handleSelect(cmd.action, {
|
|
546
|
+
id: cmd.id,
|
|
547
|
+
label: cmd.label,
|
|
548
|
+
path: cmd.action,
|
|
549
|
+
type: 'action',
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-100 data-[selected=true]:bg-gray-100"
|
|
553
|
+
>
|
|
554
|
+
<Icon className="h-4 w-4 text-gray-500" />
|
|
555
|
+
<span>{cmd.label}</span>
|
|
556
|
+
</Command.Item>
|
|
557
|
+
)
|
|
558
|
+
})}
|
|
559
|
+
</Command.Group>
|
|
560
|
+
)}
|
|
561
|
+
</Command.List>
|
|
562
|
+
|
|
563
|
+
<div className="flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3 text-xs text-gray-500">
|
|
564
|
+
<div className="flex items-center gap-4">
|
|
565
|
+
<span className="flex items-center gap-1">
|
|
566
|
+
<kbd className="rounded border border-gray-300 bg-white px-1.5 py-0.5 text-[10px]">
|
|
567
|
+
↑
|
|
568
|
+
</kbd>
|
|
569
|
+
<kbd className="rounded border border-gray-300 bg-white px-1.5 py-0.5 text-[10px]">
|
|
570
|
+
↓
|
|
571
|
+
</kbd>
|
|
572
|
+
to navigate
|
|
573
|
+
</span>
|
|
574
|
+
<span className="flex items-center gap-1">
|
|
575
|
+
<kbd className="rounded border border-gray-300 bg-white px-1.5 py-0.5 text-[10px]">
|
|
576
|
+
↵
|
|
577
|
+
</kbd>
|
|
578
|
+
to select
|
|
579
|
+
</span>
|
|
580
|
+
</div>
|
|
394
581
|
</div>
|
|
395
|
-
</
|
|
396
|
-
</
|
|
582
|
+
</Command>
|
|
583
|
+
</div>
|
|
397
584
|
</div>
|
|
398
|
-
|
|
585
|
+
{popover}
|
|
586
|
+
</>
|
|
399
587
|
)
|
|
400
588
|
}
|