@wakastellar/ui 3.3.3 → 3.5.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/dist/badge-BbwO7QeZ.js +1 -0
- package/dist/badge-BfiocODp.mjs +23 -0
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunk-14q5BKub.js +1 -0
- package/dist/{chunk-BH6uBOac.mjs → chunk-Cr9pTUWm.mjs} +5 -5
- package/dist/cn-DEtaFQsA.js +1 -0
- package/dist/cn-DUn6aSIQ.mjs +24 -0
- package/dist/doc.cjs.js +2 -2
- package/dist/doc.es.js +19 -19
- package/dist/editor.cjs.js +48 -0
- package/dist/editor.d.ts +1 -0
- package/dist/editor.es.js +6551 -0
- package/dist/{exceljs.min-DG9M8IZ1.mjs → exceljs.min-DL1XYDll.mjs} +1 -1
- package/dist/{exceljs.min-BuefmDRS.js → exceljs.min-qeIfSCbF.js} +1 -1
- package/dist/export.cjs.js +1 -1
- package/dist/export.es.js +1 -1
- package/dist/index.cjs.js +150 -150
- package/dist/index.es.js +26782 -27591
- package/dist/input-BfaSAGVw.js +1 -0
- package/dist/input-DVr_Qkl8.mjs +14 -0
- package/dist/rich-text.cjs.js +1 -1
- package/dist/rich-text.es.js +1 -1
- package/dist/security-CyBpuklN.mjs +122 -0
- package/dist/security-bFWwDrlg.js +1 -0
- package/dist/separator-NrkltulH.js +1 -0
- package/dist/separator-ibN2mycs.mjs +51 -0
- package/dist/src/components/editor/blocks/index.d.ts +51 -0
- package/dist/src/components/editor/blocks/waka-acceptance-criteria-block.d.ts +60 -0
- package/dist/src/components/editor/blocks/waka-ai-assist-block.d.ts +58 -0
- package/dist/src/components/editor/blocks/waka-api-endpoint-block.d.ts +63 -0
- package/dist/src/components/editor/blocks/waka-code-playground-block.d.ts +61 -0
- package/dist/src/components/editor/blocks/waka-comment-thread-block.d.ts +85 -0
- package/dist/src/components/editor/blocks/waka-diagram-block.d.ts +52 -0
- package/dist/src/components/editor/blocks/waka-embed-block.d.ts +58 -0
- package/dist/src/components/editor/blocks/waka-slash-menu-block.d.ts +67 -0
- package/dist/src/components/editor/blocks/waka-user-story-block.d.ts +79 -0
- package/dist/src/components/editor/blocks/waka-version-diff-block.d.ts +73 -0
- package/dist/src/components/editor/index.d.ts +66 -0
- package/dist/src/components/editor/waka-ai-writer.d.ts +80 -0
- package/dist/src/components/editor/waka-collaborative-editor.d.ts +93 -0
- package/dist/src/components/editor/waka-diff-viewer.d.ts +71 -0
- package/dist/src/components/editor/waka-dnd-editor.d.ts +64 -0
- package/dist/src/components/editor/waka-document-editor.d.ts +92 -0
- package/dist/src/components/editor/waka-editor-elements.d.ts +79 -0
- package/dist/src/components/editor/waka-editor-leaves.d.ts +39 -0
- package/dist/src/components/editor/waka-editor-plugins.d.ts +41 -0
- package/dist/src/components/editor/waka-editor-toolbar.d.ts +20 -0
- package/dist/src/components/editor/waka-editor.d.ts +59 -0
- package/dist/src/components/editor/waka-floating-toolbar.d.ts +47 -0
- package/dist/src/components/editor/waka-markdown-editor.d.ts +60 -0
- package/dist/src/components/editor/waka-mention-editor.d.ts +125 -0
- package/dist/src/components/editor/waka-slash-menu.d.ts +70 -0
- package/dist/src/components/editor/waka-spec-editor.d.ts +88 -0
- package/dist/src/components/index.d.ts +1 -15
- package/dist/src/editor.d.ts +26 -0
- package/dist/textarea-CdQWggYG.js +1 -0
- package/dist/textarea-DJDXJ3nd.mjs +23 -0
- package/dist/types-C2St0wOW.js +1 -0
- package/dist/{types-B6GVaSIP.mjs → types-JnqoLyuv.mjs} +214 -211
- package/dist/{useDataTableImport-BPvfo--2.mjs → useDataTableImport-BWUFesPi.mjs} +3 -3
- package/dist/{useDataTableImport-Cm_pCKnO.js → useDataTableImport-T7ddpN5k.js} +3 -3
- package/dist/waka-doc-renderer-CTxC7Trf.js +3 -0
- package/dist/{waka-doc-renderer-BkIvas3z.mjs → waka-doc-renderer-Cw-Xnyen.mjs} +264 -281
- package/dist/waka-editor-plugins-DR6tpsUC.mjs +135 -0
- package/dist/waka-editor-plugins-sGSh9hn2.js +1 -0
- package/dist/waka-rich-text-editor-BlIdtknG.js +1 -0
- package/dist/waka-rich-text-editor-D1uA3zbB.js +1 -0
- package/dist/waka-rich-text-editor-DgSWiXMW.mjs +342 -0
- package/dist/waka-rich-text-editor-DndVJuDw.mjs +2 -0
- package/package.json +87 -2
- package/src/blocks/footer/index.tsx +1 -6
- package/src/blocks/login/index.tsx +1 -7
- package/src/blocks/profile/index.tsx +3 -5
- package/src/components/editor/blocks/index.ts +182 -0
- package/src/components/editor/blocks/waka-acceptance-criteria-block.tsx +326 -0
- package/src/components/editor/blocks/waka-ai-assist-block.tsx +284 -0
- package/src/components/editor/blocks/waka-api-endpoint-block.tsx +382 -0
- package/src/components/editor/blocks/waka-code-playground-block.tsx +331 -0
- package/src/components/editor/blocks/waka-comment-thread-block.tsx +448 -0
- package/src/components/editor/blocks/waka-diagram-block.tsx +293 -0
- package/src/components/editor/blocks/waka-embed-block.tsx +416 -0
- package/src/components/editor/blocks/waka-slash-menu-block.tsx +432 -0
- package/src/components/editor/blocks/waka-user-story-block.tsx +295 -0
- package/src/components/editor/blocks/waka-version-diff-block.tsx +426 -0
- package/src/components/editor/index.ts +279 -0
- package/src/components/editor/waka-ai-writer.tsx +434 -0
- package/src/components/editor/waka-collaborative-editor.tsx +426 -0
- package/src/components/editor/waka-diff-viewer.tsx +352 -0
- package/src/components/editor/waka-dnd-editor.tsx +284 -0
- package/src/components/editor/waka-document-editor.tsx +502 -0
- package/src/components/editor/waka-editor-elements.tsx +312 -0
- package/src/components/editor/waka-editor-leaves.tsx +101 -0
- package/src/components/editor/waka-editor-plugins.ts +207 -0
- package/src/components/editor/waka-editor-toolbar.tsx +358 -0
- package/src/components/editor/waka-editor.tsx +431 -0
- package/src/components/editor/waka-floating-toolbar.tsx +268 -0
- package/src/components/editor/waka-markdown-editor.tsx +395 -0
- package/src/components/editor/waka-mention-editor.tsx +459 -0
- package/src/components/editor/waka-slash-menu.tsx +392 -0
- package/src/components/editor/waka-spec-editor.tsx +657 -0
- package/src/components/index.ts +1 -18
- package/dist/chunk-BDDJmn7V.js +0 -1
- package/dist/cn-DnPbmOCy.js +0 -1
- package/dist/cn-DpLcAzrf.mjs +0 -22
- package/dist/separator-BDReXBvI.mjs +0 -59
- package/dist/separator-BKjNl9sI.js +0 -1
- package/dist/src/components/waka-actor-badge/index.d.ts +0 -8
- package/dist/src/components/waka-actors-list/index.d.ts +0 -18
- package/dist/src/components/waka-ai-assistant-button/index.d.ts +0 -8
- package/dist/src/components/waka-document-flyover/index.d.ts +0 -10
- package/dist/src/components/waka-document-preview-popup/index.d.ts +0 -26
- package/dist/src/components/waka-hour-balance-badge/index.d.ts +0 -8
- package/dist/src/components/waka-hour-consumption-table/index.d.ts +0 -15
- package/dist/src/components/waka-hour-pack-dialog/index.d.ts +0 -8
- package/dist/src/components/waka-project-stats-header/index.d.ts +0 -15
- package/dist/src/components/waka-step-comment-bubble/index.d.ts +0 -13
- package/dist/src/components/waka-step-comment-panel/index.d.ts +0 -20
- package/dist/src/components/waka-step-permission-matrix/index.d.ts +0 -12
- package/dist/src/components/waka-time-entry-dialog/index.d.ts +0 -16
- package/dist/src/components/waka-time-tracking-flyover/index.d.ts +0 -11
- package/dist/types-BH9cQRqZ.js +0 -1
- package/dist/waka-doc-renderer-BZ2-SqyT.js +0 -3
- package/dist/waka-rich-text-editor-BJGlQgpq.js +0 -1
- package/dist/waka-rich-text-editor-BJzzxeP1.mjs +0 -361
- package/dist/waka-rich-text-editor-wnXLwvUo.js +0 -1
- package/src/components/waka-actor-badge/index.tsx +0 -34
- package/src/components/waka-actors-list/index.tsx +0 -125
- package/src/components/waka-ai-assistant-button/index.tsx +0 -31
- package/src/components/waka-document-flyover/index.tsx +0 -36
- package/src/components/waka-document-preview-popup/index.tsx +0 -103
- package/src/components/waka-hour-balance-badge/index.tsx +0 -43
- package/src/components/waka-hour-consumption-table/index.tsx +0 -72
- package/src/components/waka-hour-pack-dialog/index.tsx +0 -72
- package/src/components/waka-project-stats-header/index.tsx +0 -69
- package/src/components/waka-step-comment-bubble/index.tsx +0 -71
- package/src/components/waka-step-comment-panel/index.tsx +0 -106
- package/src/components/waka-step-permission-matrix/index.tsx +0 -65
- package/src/components/waka-time-entry-dialog/index.tsx +0 -131
- package/src/components/waka-time-tracking-flyover/index.tsx +0 -41
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../../utils/cn"
|
|
5
|
+
import type { PlateElementProps } from "../waka-editor-elements"
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export const AI_ASSIST_BLOCK_TYPE = "ai_assist" as const
|
|
12
|
+
|
|
13
|
+
/** Status of the AI request */
|
|
14
|
+
export type AiAssistStatus = "idle" | "loading" | "streaming" | "complete" | "error"
|
|
15
|
+
|
|
16
|
+
/** Slate node for AI assist blocks */
|
|
17
|
+
export interface AiAssistElement {
|
|
18
|
+
type: typeof AI_ASSIST_BLOCK_TYPE
|
|
19
|
+
/** The user's prompt/question */
|
|
20
|
+
prompt: string
|
|
21
|
+
/** The AI-generated response */
|
|
22
|
+
response: string
|
|
23
|
+
/** Current status */
|
|
24
|
+
status: AiAssistStatus
|
|
25
|
+
/** Model used for generation */
|
|
26
|
+
model?: string
|
|
27
|
+
/** Timestamp of generation */
|
|
28
|
+
generatedAt?: string
|
|
29
|
+
/** Error message if status is "error" */
|
|
30
|
+
errorMessage?: string
|
|
31
|
+
/** Token count */
|
|
32
|
+
tokenCount?: number
|
|
33
|
+
children: Array<{ text: string }>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WakaAiAssistBlockProps extends PlateElementProps {
|
|
37
|
+
element?: AiAssistElement & Record<string, unknown>
|
|
38
|
+
/** Callback to trigger AI generation. Receives the prompt, returns a response or streams it. */
|
|
39
|
+
onGenerate?: (prompt: string) => Promise<string>
|
|
40
|
+
/** Callback when the user accepts the response and wants it inserted into the document */
|
|
41
|
+
onAccept?: (response: string) => void
|
|
42
|
+
/** Callback when the user discards the response */
|
|
43
|
+
onDiscard?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Status Indicator
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
function StatusIndicator({ status }: { status: AiAssistStatus }) {
|
|
51
|
+
const config: Record<AiAssistStatus, { label: string; color: string; pulse: boolean }> = {
|
|
52
|
+
idle: { label: "Ready", color: "bg-muted-foreground/30", pulse: false },
|
|
53
|
+
loading: { label: "Thinking...", color: "bg-amber-500", pulse: true },
|
|
54
|
+
streaming: { label: "Writing...", color: "bg-blue-500", pulse: true },
|
|
55
|
+
complete: { label: "Complete", color: "bg-emerald-500", pulse: false },
|
|
56
|
+
error: { label: "Error", color: "bg-destructive", pulse: false },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const c = config[status]
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex items-center gap-1.5">
|
|
63
|
+
<div className={cn("h-2 w-2 rounded-full", c.color, c.pulse && "animate-pulse")} />
|
|
64
|
+
<span className="text-[10px] font-medium text-muted-foreground">{c.label}</span>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Element Component
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* WakaAiAssistBlock - A Plate.js block where the user poses a question or prompt,
|
|
75
|
+
* and the AI response is inserted directly into the document. Supports streaming
|
|
76
|
+
* responses, accept/discard actions, and model attribution.
|
|
77
|
+
*
|
|
78
|
+
* Designed to work with `@platejs/ai` AIChatPlugin, or standalone with a custom
|
|
79
|
+
* `onGenerate` callback.
|
|
80
|
+
*
|
|
81
|
+
* Register in Plate editor:
|
|
82
|
+
* ```ts
|
|
83
|
+
* components: {
|
|
84
|
+
* [AI_ASSIST_BLOCK_TYPE]: (props) => <WakaAiAssistBlock {...props} onGenerate={myHandler} />,
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function WakaAiAssistBlock({
|
|
89
|
+
attributes,
|
|
90
|
+
children,
|
|
91
|
+
element,
|
|
92
|
+
className,
|
|
93
|
+
onGenerate,
|
|
94
|
+
onAccept,
|
|
95
|
+
onDiscard,
|
|
96
|
+
}: WakaAiAssistBlockProps) {
|
|
97
|
+
const el = element as AiAssistElement | undefined
|
|
98
|
+
const status = el?.status || "idle"
|
|
99
|
+
const prompt = el?.prompt || ""
|
|
100
|
+
const response = el?.response || ""
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
{...attributes}
|
|
105
|
+
contentEditable={false}
|
|
106
|
+
className={cn(
|
|
107
|
+
"my-4 rounded-lg overflow-hidden",
|
|
108
|
+
"border border-border",
|
|
109
|
+
"bg-gradient-to-br from-violet-50/30 via-card to-blue-50/30",
|
|
110
|
+
"dark:from-violet-950/10 dark:via-card dark:to-blue-950/10",
|
|
111
|
+
"shadow-sm",
|
|
112
|
+
className
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{/* Header */}
|
|
116
|
+
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border/50">
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
{/* Sparkle icon */}
|
|
119
|
+
<div className="relative">
|
|
120
|
+
<svg
|
|
121
|
+
className="h-4 w-4 text-violet-600 dark:text-violet-400"
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
fill="currentColor"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
>
|
|
126
|
+
<path d="M12 2L13.09 8.26L18 6L14.74 10.91L21 12L14.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L9.26 13.09L3 12L9.26 10.91L6 6L10.91 8.26L12 2Z" />
|
|
127
|
+
</svg>
|
|
128
|
+
{(status === "loading" || status === "streaming") && (
|
|
129
|
+
<div className="absolute inset-0 animate-ping">
|
|
130
|
+
<svg className="h-4 w-4 text-violet-400/50" viewBox="0 0 24 24" fill="currentColor">
|
|
131
|
+
<path d="M12 2L13.09 8.26L18 6L14.74 10.91L21 12L14.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L9.26 13.09L3 12L9.26 10.91L6 6L10.91 8.26L12 2Z" />
|
|
132
|
+
</svg>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
<span className="text-xs font-bold uppercase tracking-wider text-violet-700 dark:text-violet-300">
|
|
137
|
+
AI Assistant
|
|
138
|
+
</span>
|
|
139
|
+
{el?.model && (
|
|
140
|
+
<span className="text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
141
|
+
{el.model}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<StatusIndicator status={status} />
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Prompt */}
|
|
150
|
+
<div className="px-4 py-3 border-b border-border/30">
|
|
151
|
+
<div className="flex items-start gap-2">
|
|
152
|
+
<div className="flex-shrink-0 mt-0.5 h-5 w-5 rounded-full bg-foreground/10 flex items-center justify-center">
|
|
153
|
+
<svg className="h-3 w-3 text-foreground/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
154
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
155
|
+
</svg>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="flex-1 min-w-0">
|
|
158
|
+
<p className="text-sm text-foreground leading-relaxed">
|
|
159
|
+
{prompt || <span className="text-muted-foreground italic">Ask the AI assistant a question...</span>}
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Response */}
|
|
166
|
+
{(response || status === "loading" || status === "streaming") && (
|
|
167
|
+
<div className="px-4 py-3">
|
|
168
|
+
<div className="flex items-start gap-2">
|
|
169
|
+
<div className="flex-shrink-0 mt-0.5 h-5 w-5 rounded-full bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">
|
|
170
|
+
<svg className="h-3 w-3 text-violet-600 dark:text-violet-400" viewBox="0 0 24 24" fill="currentColor">
|
|
171
|
+
<path d="M12 2L13.09 8.26L18 6L14.74 10.91L21 12L14.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L9.26 13.09L3 12L9.26 10.91L6 6L10.91 8.26L12 2Z" />
|
|
172
|
+
</svg>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex-1 min-w-0">
|
|
175
|
+
{status === "loading" && !response ? (
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<div className="h-3 w-3/4 bg-muted rounded animate-pulse" />
|
|
178
|
+
<div className="h-3 w-1/2 bg-muted rounded animate-pulse" />
|
|
179
|
+
<div className="h-3 w-5/6 bg-muted rounded animate-pulse" />
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<div className="text-sm text-foreground leading-relaxed whitespace-pre-wrap prose prose-sm dark:prose-invert max-w-none">
|
|
183
|
+
{response}
|
|
184
|
+
{status === "streaming" && (
|
|
185
|
+
<span className="inline-block w-1.5 h-4 bg-violet-500 ml-0.5 animate-pulse rounded-sm" />
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Error */}
|
|
195
|
+
{status === "error" && el?.errorMessage && (
|
|
196
|
+
<div className="mx-4 mb-3 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20">
|
|
197
|
+
<p className="text-xs text-destructive">{el.errorMessage}</p>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Actions */}
|
|
202
|
+
{status === "complete" && response && (
|
|
203
|
+
<div className="flex items-center justify-between px-4 py-2 border-t border-border/50 bg-muted/20">
|
|
204
|
+
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
|
205
|
+
{el?.tokenCount && <span>{el.tokenCount} tokens</span>}
|
|
206
|
+
{el?.generatedAt && (
|
|
207
|
+
<span>
|
|
208
|
+
{new Date(el.generatedAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
<div className="flex items-center gap-2">
|
|
213
|
+
{onDiscard && (
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={onDiscard}
|
|
217
|
+
className="px-3 py-1 rounded text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
218
|
+
>
|
|
219
|
+
Discard
|
|
220
|
+
</button>
|
|
221
|
+
)}
|
|
222
|
+
{onAccept && (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={() => onAccept(response)}
|
|
226
|
+
className="px-3 py-1 rounded text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm"
|
|
227
|
+
>
|
|
228
|
+
Insert into document
|
|
229
|
+
</button>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* Hidden Slate children */}
|
|
236
|
+
<div className="hidden">{children}</div>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
WakaAiAssistBlock.displayName = "WakaAiAssistBlock"
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// Node Factory
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
export function createAiAssistNodes(options?: {
|
|
248
|
+
prompt?: string
|
|
249
|
+
model?: string
|
|
250
|
+
}): AiAssistElement[] {
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
type: AI_ASSIST_BLOCK_TYPE,
|
|
254
|
+
prompt: options?.prompt || "",
|
|
255
|
+
response: "",
|
|
256
|
+
status: "idle",
|
|
257
|
+
model: options?.model || "claude-sonnet-4-20250514",
|
|
258
|
+
children: [{ text: "" }],
|
|
259
|
+
},
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function createAiAssistPlugin(
|
|
264
|
+
onGenerate?: (prompt: string) => Promise<string>,
|
|
265
|
+
onAccept?: (response: string) => void,
|
|
266
|
+
) {
|
|
267
|
+
try {
|
|
268
|
+
const { createPlatePlugin } = await import("platejs/react")
|
|
269
|
+
return createPlatePlugin({
|
|
270
|
+
key: AI_ASSIST_BLOCK_TYPE,
|
|
271
|
+
node: {
|
|
272
|
+
isElement: true,
|
|
273
|
+
isVoid: true,
|
|
274
|
+
type: AI_ASSIST_BLOCK_TYPE,
|
|
275
|
+
component: (props: WakaAiAssistBlockProps) => (
|
|
276
|
+
<WakaAiAssistBlock {...props} onGenerate={onGenerate} onAccept={onAccept} />
|
|
277
|
+
),
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
} catch {
|
|
281
|
+
console.warn("[WakaAiAssistBlock] platejs not installed")
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../../utils/cn"
|
|
5
|
+
import type { PlateElementProps } from "../waka-editor-elements"
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export const API_ENDPOINT_BLOCK_TYPE = "api_endpoint" as const
|
|
12
|
+
|
|
13
|
+
/** HTTP method type */
|
|
14
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
|
|
15
|
+
|
|
16
|
+
/** A single parameter/header definition */
|
|
17
|
+
export interface ApiParam {
|
|
18
|
+
name: string
|
|
19
|
+
type: string
|
|
20
|
+
required: boolean
|
|
21
|
+
description: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A response definition */
|
|
25
|
+
export interface ApiResponse {
|
|
26
|
+
status: number
|
|
27
|
+
description: string
|
|
28
|
+
body?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Slate node for API endpoint block */
|
|
32
|
+
export interface ApiEndpointElement {
|
|
33
|
+
type: typeof API_ENDPOINT_BLOCK_TYPE
|
|
34
|
+
/** HTTP method */
|
|
35
|
+
method: HttpMethod
|
|
36
|
+
/** Route path (e.g. "/api/v1/projects/:id") */
|
|
37
|
+
route: string
|
|
38
|
+
/** Endpoint description */
|
|
39
|
+
description: string
|
|
40
|
+
/** Authentication requirement */
|
|
41
|
+
auth: string
|
|
42
|
+
/** Required permissions/roles */
|
|
43
|
+
permissions: string[]
|
|
44
|
+
/** Rate limit description */
|
|
45
|
+
rateLimit?: string
|
|
46
|
+
/** Path/query parameters */
|
|
47
|
+
params?: ApiParam[]
|
|
48
|
+
/** Request body (JSON string) */
|
|
49
|
+
requestBody?: string
|
|
50
|
+
/** Response definitions */
|
|
51
|
+
responses: ApiResponse[]
|
|
52
|
+
children: Array<{ text: string }>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WakaApiEndpointBlockProps extends PlateElementProps {
|
|
56
|
+
element?: ApiEndpointElement & Record<string, unknown>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Method Badge
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
const METHOD_COLORS: Record<HttpMethod, { bg: string; text: string; border: string }> = {
|
|
64
|
+
GET: { bg: "bg-emerald-100 dark:bg-emerald-500/15", text: "text-emerald-700 dark:text-emerald-400", border: "border-emerald-300 dark:border-emerald-600/40" },
|
|
65
|
+
POST: { bg: "bg-blue-100 dark:bg-blue-500/15", text: "text-blue-700 dark:text-blue-400", border: "border-blue-300 dark:border-blue-600/40" },
|
|
66
|
+
PUT: { bg: "bg-amber-100 dark:bg-amber-500/15", text: "text-amber-700 dark:text-amber-400", border: "border-amber-300 dark:border-amber-600/40" },
|
|
67
|
+
PATCH: { bg: "bg-orange-100 dark:bg-orange-500/15", text: "text-orange-700 dark:text-orange-400", border: "border-orange-300 dark:border-orange-600/40" },
|
|
68
|
+
DELETE: { bg: "bg-red-100 dark:bg-red-500/15", text: "text-red-700 dark:text-red-400", border: "border-red-300 dark:border-red-600/40" },
|
|
69
|
+
HEAD: { bg: "bg-gray-100 dark:bg-gray-500/15", text: "text-gray-700 dark:text-gray-400", border: "border-gray-300 dark:border-gray-600/40" },
|
|
70
|
+
OPTIONS: { bg: "bg-purple-100 dark:bg-purple-500/15", text: "text-purple-700 dark:text-purple-400", border: "border-purple-300 dark:border-purple-600/40" },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function MethodBadge({ method }: { method: HttpMethod }) {
|
|
74
|
+
const colors = METHOD_COLORS[method] || METHOD_COLORS.GET
|
|
75
|
+
return (
|
|
76
|
+
<span className={cn(
|
|
77
|
+
"inline-flex items-center px-2 py-0.5 rounded text-[11px] font-bold font-mono tracking-wide border",
|
|
78
|
+
colors.bg, colors.text, colors.border
|
|
79
|
+
)}>
|
|
80
|
+
{method}
|
|
81
|
+
</span>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Status Badge
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
function StatusBadge({ status }: { status: number }) {
|
|
90
|
+
const color = status < 300
|
|
91
|
+
? "text-emerald-700 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10"
|
|
92
|
+
: status < 400
|
|
93
|
+
? "text-blue-700 dark:text-blue-400 bg-blue-50 dark:bg-blue-500/10"
|
|
94
|
+
: status < 500
|
|
95
|
+
? "text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10"
|
|
96
|
+
: "text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10"
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<span className={cn("text-[11px] font-mono font-bold px-1.5 py-0.5 rounded", color)}>
|
|
100
|
+
{status}
|
|
101
|
+
</span>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// JSON Code Block
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
function CodePreview({ code, label }: { code: string; label: string }) {
|
|
110
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
111
|
+
const lines = code.split("\n")
|
|
112
|
+
const isLong = lines.length > 6
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="mt-2">
|
|
116
|
+
<div className="flex items-center justify-between mb-1">
|
|
117
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
118
|
+
{label}
|
|
119
|
+
</span>
|
|
120
|
+
{isLong && (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => setExpanded(!expanded)}
|
|
124
|
+
className="text-[10px] text-primary hover:underline"
|
|
125
|
+
>
|
|
126
|
+
{expanded ? "Collapse" : "Expand"}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
<pre className={cn(
|
|
131
|
+
"text-[12px] font-mono leading-relaxed p-3 rounded-md overflow-x-auto",
|
|
132
|
+
"bg-gray-950 dark:bg-gray-900 text-gray-100",
|
|
133
|
+
"border border-gray-800/50",
|
|
134
|
+
!expanded && isLong && "max-h-[150px] overflow-hidden relative"
|
|
135
|
+
)}>
|
|
136
|
+
{!expanded && isLong && (
|
|
137
|
+
<div className="absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-gray-950 dark:from-gray-900 to-transparent" />
|
|
138
|
+
)}
|
|
139
|
+
<code>{code}</code>
|
|
140
|
+
</pre>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Element Component
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* WakaApiEndpointBlock - A Plate.js block for documenting REST API endpoints.
|
|
151
|
+
* Displays method, route, parameters, request body, and responses with
|
|
152
|
+
* syntax-colored JSON previews.
|
|
153
|
+
*
|
|
154
|
+
* Register in Plate editor:
|
|
155
|
+
* ```ts
|
|
156
|
+
* components: {
|
|
157
|
+
* [API_ENDPOINT_BLOCK_TYPE]: WakaApiEndpointBlock,
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function WakaApiEndpointBlock({
|
|
162
|
+
attributes,
|
|
163
|
+
children,
|
|
164
|
+
element,
|
|
165
|
+
className,
|
|
166
|
+
}: WakaApiEndpointBlockProps) {
|
|
167
|
+
const el = element as ApiEndpointElement | undefined
|
|
168
|
+
const method = el?.method || "GET"
|
|
169
|
+
const route = el?.route || "/api/v1/resource"
|
|
170
|
+
const methodColors = METHOD_COLORS[method] || METHOD_COLORS.GET
|
|
171
|
+
|
|
172
|
+
const [activeTab, setActiveTab] = React.useState<"params" | "body" | "responses">(
|
|
173
|
+
el?.params && el.params.length > 0 ? "params" : el?.requestBody ? "body" : "responses"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
{...attributes}
|
|
179
|
+
contentEditable={false}
|
|
180
|
+
className={cn(
|
|
181
|
+
"my-4 rounded-lg overflow-hidden",
|
|
182
|
+
"border",
|
|
183
|
+
methodColors.border,
|
|
184
|
+
"shadow-sm",
|
|
185
|
+
className
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{/* Method + Route header */}
|
|
189
|
+
<div className={cn(
|
|
190
|
+
"flex items-center gap-3 px-4 py-3",
|
|
191
|
+
methodColors.bg
|
|
192
|
+
)}>
|
|
193
|
+
<MethodBadge method={method} />
|
|
194
|
+
<code className={cn("text-sm font-mono font-semibold", methodColors.text)}>
|
|
195
|
+
{route}
|
|
196
|
+
</code>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Description + meta */}
|
|
200
|
+
<div className="px-4 py-3 border-b border-border/50 bg-card/50">
|
|
201
|
+
{el?.description && (
|
|
202
|
+
<p className="text-sm text-foreground mb-2">{el.description}</p>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<div className="flex flex-wrap gap-3 text-[11px]">
|
|
206
|
+
{el?.auth && (
|
|
207
|
+
<div className="flex items-center gap-1 text-muted-foreground">
|
|
208
|
+
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
209
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
210
|
+
</svg>
|
|
211
|
+
<span className="font-medium">{el.auth}</span>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{el?.permissions && el.permissions.length > 0 && (
|
|
216
|
+
<div className="flex items-center gap-1">
|
|
217
|
+
<svg className="h-3 w-3 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
218
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
219
|
+
</svg>
|
|
220
|
+
{el.permissions.map((perm, i) => (
|
|
221
|
+
<span key={i} className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono text-[10px]">
|
|
222
|
+
{perm}
|
|
223
|
+
</span>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{el?.rateLimit && (
|
|
229
|
+
<div className="flex items-center gap-1 text-muted-foreground">
|
|
230
|
+
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
231
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
232
|
+
</svg>
|
|
233
|
+
<span className="font-medium">{el.rateLimit}</span>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Tabs */}
|
|
240
|
+
<div className="flex border-b border-border/50">
|
|
241
|
+
{[
|
|
242
|
+
{ id: "params" as const, label: "Parameters", count: el?.params?.length },
|
|
243
|
+
{ id: "body" as const, label: "Request Body", show: !!el?.requestBody },
|
|
244
|
+
{ id: "responses" as const, label: "Responses", count: el?.responses?.length },
|
|
245
|
+
].filter((tab) => tab.show !== false && (tab.count === undefined || tab.count > 0)).map((tab) => (
|
|
246
|
+
<button
|
|
247
|
+
key={tab.id}
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => setActiveTab(tab.id)}
|
|
250
|
+
className={cn(
|
|
251
|
+
"px-4 py-2 text-xs font-medium transition-colors relative",
|
|
252
|
+
activeTab === tab.id
|
|
253
|
+
? "text-foreground"
|
|
254
|
+
: "text-muted-foreground hover:text-foreground"
|
|
255
|
+
)}
|
|
256
|
+
>
|
|
257
|
+
{tab.label}
|
|
258
|
+
{tab.count !== undefined && (
|
|
259
|
+
<span className="ml-1 text-[10px] text-muted-foreground">({tab.count})</span>
|
|
260
|
+
)}
|
|
261
|
+
{activeTab === tab.id && (
|
|
262
|
+
<div className={cn("absolute bottom-0 left-0 right-0 h-0.5", methodColors.bg.replace("bg-", "bg-").replace("/15", ""))} style={{ backgroundColor: method === "GET" ? "#10b981" : method === "POST" ? "#3b82f6" : method === "PUT" ? "#f59e0b" : method === "DELETE" ? "#ef4444" : "#6b7280" }} />
|
|
263
|
+
)}
|
|
264
|
+
</button>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Tab content */}
|
|
269
|
+
<div className="px-4 py-3">
|
|
270
|
+
{/* Parameters tab */}
|
|
271
|
+
{activeTab === "params" && el?.params && el.params.length > 0 && (
|
|
272
|
+
<div className="overflow-x-auto">
|
|
273
|
+
<table className="w-full text-xs">
|
|
274
|
+
<thead>
|
|
275
|
+
<tr className="border-b border-border">
|
|
276
|
+
<th className="text-left font-semibold text-muted-foreground py-1.5 pr-4">Name</th>
|
|
277
|
+
<th className="text-left font-semibold text-muted-foreground py-1.5 pr-4">Type</th>
|
|
278
|
+
<th className="text-left font-semibold text-muted-foreground py-1.5 pr-4">Required</th>
|
|
279
|
+
<th className="text-left font-semibold text-muted-foreground py-1.5">Description</th>
|
|
280
|
+
</tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody>
|
|
283
|
+
{el.params.map((param, i) => (
|
|
284
|
+
<tr key={i} className="border-b border-border/30">
|
|
285
|
+
<td className="py-1.5 pr-4 font-mono font-medium text-foreground">{param.name}</td>
|
|
286
|
+
<td className="py-1.5 pr-4 font-mono text-muted-foreground">{param.type}</td>
|
|
287
|
+
<td className="py-1.5 pr-4">
|
|
288
|
+
{param.required ? (
|
|
289
|
+
<span className="text-red-600 dark:text-red-400 font-semibold">required</span>
|
|
290
|
+
) : (
|
|
291
|
+
<span className="text-muted-foreground">optional</span>
|
|
292
|
+
)}
|
|
293
|
+
</td>
|
|
294
|
+
<td className="py-1.5 text-muted-foreground">{param.description}</td>
|
|
295
|
+
</tr>
|
|
296
|
+
))}
|
|
297
|
+
</tbody>
|
|
298
|
+
</table>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Request body tab */}
|
|
303
|
+
{activeTab === "body" && el?.requestBody && (
|
|
304
|
+
<CodePreview code={el.requestBody} label="Request Body" />
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Responses tab */}
|
|
308
|
+
{activeTab === "responses" && el?.responses && (
|
|
309
|
+
<div className="space-y-3">
|
|
310
|
+
{el.responses.map((resp, i) => (
|
|
311
|
+
<div key={i}>
|
|
312
|
+
<div className="flex items-center gap-2 mb-1">
|
|
313
|
+
<StatusBadge status={resp.status} />
|
|
314
|
+
<span className="text-xs text-muted-foreground">{resp.description}</span>
|
|
315
|
+
</div>
|
|
316
|
+
{resp.body && <CodePreview code={resp.body} label={`Response ${resp.status}`} />}
|
|
317
|
+
</div>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* Hidden Slate children */}
|
|
324
|
+
<div className="hidden">{children}</div>
|
|
325
|
+
</div>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
WakaApiEndpointBlock.displayName = "WakaApiEndpointBlock"
|
|
330
|
+
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Node Factory
|
|
333
|
+
// ============================================================================
|
|
334
|
+
|
|
335
|
+
export function createApiEndpointNodes(options?: Partial<Omit<ApiEndpointElement, "type" | "children">>): ApiEndpointElement[] {
|
|
336
|
+
return [
|
|
337
|
+
{
|
|
338
|
+
type: API_ENDPOINT_BLOCK_TYPE,
|
|
339
|
+
method: options?.method || "GET",
|
|
340
|
+
route: options?.route || "/api/v1/resource/:id",
|
|
341
|
+
description: options?.description || "Retrieves a resource by its unique identifier.",
|
|
342
|
+
auth: options?.auth || "Bearer JWT",
|
|
343
|
+
permissions: options?.permissions || ["ROLE_USER"],
|
|
344
|
+
rateLimit: options?.rateLimit || "100 req/min",
|
|
345
|
+
params: options?.params || [
|
|
346
|
+
{ name: "id", type: "UUID", required: true, description: "Resource unique identifier" },
|
|
347
|
+
],
|
|
348
|
+
requestBody: options?.requestBody,
|
|
349
|
+
responses: options?.responses || [
|
|
350
|
+
{
|
|
351
|
+
status: 200,
|
|
352
|
+
description: "Success",
|
|
353
|
+
body: '{\n "data": {\n "id": "uuid",\n "name": "Example",\n "createdAt": "2026-04-14T00:00:00Z"\n }\n}',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
status: 404,
|
|
357
|
+
description: "Resource not found",
|
|
358
|
+
body: '{\n "error": "NOT_FOUND",\n "message": "Resource not found"\n}',
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
children: [{ text: "" }],
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function createApiEndpointPlugin() {
|
|
367
|
+
try {
|
|
368
|
+
const { createPlatePlugin } = await import("platejs/react")
|
|
369
|
+
return createPlatePlugin({
|
|
370
|
+
key: API_ENDPOINT_BLOCK_TYPE,
|
|
371
|
+
node: {
|
|
372
|
+
isElement: true,
|
|
373
|
+
isVoid: true,
|
|
374
|
+
type: API_ENDPOINT_BLOCK_TYPE,
|
|
375
|
+
component: WakaApiEndpointBlock,
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
} catch {
|
|
379
|
+
console.warn("[WakaApiEndpointBlock] platejs not installed")
|
|
380
|
+
return null
|
|
381
|
+
}
|
|
382
|
+
}
|