@milkdown/crepe 7.19.2 → 7.21.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/lib/cjs/builder.js +44 -1
- package/lib/cjs/builder.js.map +1 -1
- package/lib/cjs/feature/ai/index.js +1492 -0
- package/lib/cjs/feature/ai/index.js.map +1 -0
- package/lib/cjs/feature/block-edit/index.js +9 -2
- package/lib/cjs/feature/block-edit/index.js.map +1 -1
- package/lib/cjs/feature/code-mirror/index.js +2 -0
- package/lib/cjs/feature/code-mirror/index.js.map +1 -1
- package/lib/cjs/feature/cursor/index.js +2 -0
- package/lib/cjs/feature/cursor/index.js.map +1 -1
- package/lib/cjs/feature/image-block/index.js +5 -1
- package/lib/cjs/feature/image-block/index.js.map +1 -1
- package/lib/cjs/feature/latex/index.js +7 -0
- package/lib/cjs/feature/latex/index.js.map +1 -1
- package/lib/cjs/feature/link-tooltip/index.js +2 -0
- package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
- package/lib/cjs/feature/list-item/index.js +2 -0
- package/lib/cjs/feature/list-item/index.js.map +1 -1
- package/lib/cjs/feature/placeholder/index.js +2 -0
- package/lib/cjs/feature/placeholder/index.js.map +1 -1
- package/lib/cjs/feature/table/index.js +2 -0
- package/lib/cjs/feature/table/index.js.map +1 -1
- package/lib/cjs/feature/toolbar/index.js +497 -5
- package/lib/cjs/feature/toolbar/index.js.map +1 -1
- package/lib/cjs/feature/top-bar/index.js +791 -0
- package/lib/cjs/feature/top-bar/index.js.map +1 -0
- package/lib/cjs/index.js +2047 -160
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/llm-providers/anthropic/index.js +147 -0
- package/lib/cjs/llm-providers/anthropic/index.js.map +1 -0
- package/lib/cjs/llm-providers/openai/index.js +138 -0
- package/lib/cjs/llm-providers/openai/index.js.map +1 -0
- package/lib/esm/builder.js +44 -1
- package/lib/esm/builder.js.map +1 -1
- package/lib/esm/feature/ai/index.js +1487 -0
- package/lib/esm/feature/ai/index.js.map +1 -0
- package/lib/esm/feature/block-edit/index.js +9 -2
- package/lib/esm/feature/block-edit/index.js.map +1 -1
- package/lib/esm/feature/code-mirror/index.js +2 -0
- package/lib/esm/feature/code-mirror/index.js.map +1 -1
- package/lib/esm/feature/cursor/index.js +2 -0
- package/lib/esm/feature/cursor/index.js.map +1 -1
- package/lib/esm/feature/image-block/index.js +5 -1
- package/lib/esm/feature/image-block/index.js.map +1 -1
- package/lib/esm/feature/latex/index.js +7 -0
- package/lib/esm/feature/latex/index.js.map +1 -1
- package/lib/esm/feature/link-tooltip/index.js +2 -0
- package/lib/esm/feature/link-tooltip/index.js.map +1 -1
- package/lib/esm/feature/list-item/index.js +2 -0
- package/lib/esm/feature/list-item/index.js.map +1 -1
- package/lib/esm/feature/placeholder/index.js +2 -0
- package/lib/esm/feature/placeholder/index.js.map +1 -1
- package/lib/esm/feature/table/index.js +2 -0
- package/lib/esm/feature/table/index.js.map +1 -1
- package/lib/esm/feature/toolbar/index.js +499 -7
- package/lib/esm/feature/toolbar/index.js.map +1 -1
- package/lib/esm/feature/top-bar/index.js +789 -0
- package/lib/esm/feature/top-bar/index.js.map +1 -0
- package/lib/esm/index.js +2040 -153
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/llm-providers/anthropic/index.js +145 -0
- package/lib/esm/llm-providers/anthropic/index.js.map +1 -0
- package/lib/esm/llm-providers/openai/index.js +136 -0
- package/lib/esm/llm-providers/openai/index.js.map +1 -0
- package/lib/theme/common/ai.css +446 -0
- package/lib/theme/common/code-mirror.css +14 -0
- package/lib/theme/common/diff.css +177 -0
- package/lib/theme/common/style.css +3 -0
- package/lib/theme/common/top-bar.css +152 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/core/builder.d.ts +2 -1
- package/lib/types/core/builder.d.ts.map +1 -1
- package/lib/types/feature/ai/ai.spec.d.ts +2 -0
- package/lib/types/feature/ai/ai.spec.d.ts.map +1 -0
- package/lib/types/feature/ai/commands.d.ts +24 -0
- package/lib/types/feature/ai/commands.d.ts.map +1 -0
- package/lib/types/feature/ai/context.d.ts +4 -0
- package/lib/types/feature/ai/context.d.ts.map +1 -0
- package/lib/types/feature/ai/diff-actions/index.d.ts +12 -0
- package/lib/types/feature/ai/diff-actions/index.d.ts.map +1 -0
- package/lib/types/feature/ai/diff-actions/view.d.ts +21 -0
- package/lib/types/feature/ai/diff-actions/view.d.ts.map +1 -0
- package/lib/types/feature/ai/index.d.ts +7 -0
- package/lib/types/feature/ai/index.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/component.d.ts +26 -0
- package/lib/types/feature/ai/instruction-tooltip/component.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/index.d.ts +17 -0
- package/lib/types/feature/ai/instruction-tooltip/index.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/suggestions.d.ts +50 -0
- package/lib/types/feature/ai/instruction-tooltip/suggestions.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/view.d.ts +19 -0
- package/lib/types/feature/ai/instruction-tooltip/view.d.ts.map +1 -0
- package/lib/types/feature/ai/streaming-indicator.d.ts +9 -0
- package/lib/types/feature/ai/streaming-indicator.d.ts.map +1 -0
- package/lib/types/feature/ai/types.d.ts +58 -0
- package/lib/types/feature/ai/types.d.ts.map +1 -0
- package/lib/types/feature/block-edit/handle/component.d.ts.map +1 -1
- package/lib/types/feature/block-edit/menu/component.d.ts.map +1 -1
- package/lib/types/feature/image-block/index.d.ts +2 -0
- package/lib/types/feature/image-block/index.d.ts.map +1 -1
- package/lib/types/feature/index.d.ts +7 -1
- package/lib/types/feature/index.d.ts.map +1 -1
- package/lib/types/feature/latex/inline-tooltip/component.d.ts.map +1 -1
- package/lib/types/feature/latex/inline-tooltip/inline-tooltip.spec.d.ts +2 -0
- package/lib/types/feature/latex/inline-tooltip/inline-tooltip.spec.d.ts.map +1 -0
- package/lib/types/feature/latex/inline-tooltip/view.d.ts.map +1 -1
- package/lib/types/feature/loader.d.ts.map +1 -1
- package/lib/types/feature/toolbar/component.d.ts.map +1 -1
- package/lib/types/feature/toolbar/config.d.ts +1 -1
- package/lib/types/feature/toolbar/config.d.ts.map +1 -1
- package/lib/types/feature/toolbar/index.d.ts +1 -0
- package/lib/types/feature/toolbar/index.d.ts.map +1 -1
- package/lib/types/feature/top-bar/component.d.ts +11 -0
- package/lib/types/feature/top-bar/component.d.ts.map +1 -0
- package/lib/types/feature/top-bar/config.d.ts +34 -0
- package/lib/types/feature/top-bar/config.d.ts.map +1 -0
- package/lib/types/feature/top-bar/index.d.ts +26 -0
- package/lib/types/feature/top-bar/index.d.ts.map +1 -0
- package/lib/types/icons/ai.d.ts +2 -0
- package/lib/types/icons/ai.d.ts.map +1 -0
- package/lib/types/icons/chevron-left.d.ts +2 -0
- package/lib/types/icons/chevron-left.d.ts.map +1 -0
- package/lib/types/icons/chevron-right.d.ts +2 -0
- package/lib/types/icons/chevron-right.d.ts.map +1 -0
- package/lib/types/icons/code-block.d.ts +2 -0
- package/lib/types/icons/code-block.d.ts.map +1 -0
- package/lib/types/icons/enter-key.d.ts +2 -0
- package/lib/types/icons/enter-key.d.ts.map +1 -0
- package/lib/types/icons/grammar-check.d.ts +2 -0
- package/lib/types/icons/grammar-check.d.ts.map +1 -0
- package/lib/types/icons/index.d.ts +12 -0
- package/lib/types/icons/index.d.ts.map +1 -1
- package/lib/types/icons/longer.d.ts +2 -0
- package/lib/types/icons/longer.d.ts.map +1 -0
- package/lib/types/icons/retry.d.ts +2 -0
- package/lib/types/icons/retry.d.ts.map +1 -0
- package/lib/types/icons/send-prompt.d.ts +2 -0
- package/lib/types/icons/send-prompt.d.ts.map +1 -0
- package/lib/types/icons/send.d.ts +2 -0
- package/lib/types/icons/send.d.ts.map +1 -0
- package/lib/types/icons/shorter.d.ts +2 -0
- package/lib/types/icons/shorter.d.ts.map +1 -0
- package/lib/types/icons/translate.d.ts +2 -0
- package/lib/types/icons/translate.d.ts.map +1 -0
- package/lib/types/llm-providers/anthropic/index.d.ts +21 -0
- package/lib/types/llm-providers/anthropic/index.d.ts.map +1 -0
- package/lib/types/llm-providers/openai/index.d.ts +15 -0
- package/lib/types/llm-providers/openai/index.d.ts.map +1 -0
- package/lib/types/llm-providers/providers.spec.d.ts +2 -0
- package/lib/types/llm-providers/providers.spec.d.ts.map +1 -0
- package/lib/types/llm-providers/shared.d.ts +16 -0
- package/lib/types/llm-providers/shared.d.ts.map +1 -0
- package/lib/types/utils/group-builder.d.ts +1 -1
- package/lib/types/utils/group-builder.d.ts.map +1 -1
- package/lib/types/utils/keep-alive.d.ts +2 -0
- package/lib/types/utils/keep-alive.d.ts.map +1 -0
- package/package.json +34 -13
- package/src/core/builder.ts +39 -2
- package/src/feature/ai/ai.spec.ts +742 -0
- package/src/feature/ai/commands.ts +257 -0
- package/src/feature/ai/context.ts +45 -0
- package/src/feature/ai/diff-actions/index.ts +95 -0
- package/src/feature/ai/diff-actions/view.ts +237 -0
- package/src/feature/ai/index.ts +118 -0
- package/src/feature/ai/instruction-tooltip/component.tsx +414 -0
- package/src/feature/ai/instruction-tooltip/index.ts +101 -0
- package/src/feature/ai/instruction-tooltip/suggestions.ts +249 -0
- package/src/feature/ai/instruction-tooltip/view.ts +159 -0
- package/src/feature/ai/streaming-indicator.ts +183 -0
- package/src/feature/ai/types.ts +178 -0
- package/src/feature/block-edit/handle/component.tsx +3 -2
- package/src/feature/block-edit/menu/component.tsx +3 -2
- package/src/feature/block-edit/menu/config.ts +1 -1
- package/src/feature/image-block/index.ts +4 -0
- package/src/feature/index.ts +14 -2
- package/src/feature/latex/inline-tooltip/component.tsx +4 -2
- package/src/feature/latex/inline-tooltip/inline-tooltip.spec.ts +81 -0
- package/src/feature/latex/inline-tooltip/view.ts +2 -0
- package/src/feature/loader.ts +8 -0
- package/src/feature/toolbar/component.tsx +7 -5
- package/src/feature/toolbar/config.ts +27 -1
- package/src/feature/toolbar/index.ts +1 -0
- package/src/feature/top-bar/component.tsx +198 -0
- package/src/feature/top-bar/config.ts +367 -0
- package/src/feature/top-bar/index.ts +113 -0
- package/src/icons/ai.ts +14 -0
- package/src/icons/chevron-left.ts +15 -0
- package/src/icons/chevron-right.ts +15 -0
- package/src/icons/code-block.ts +12 -0
- package/src/icons/enter-key.ts +13 -0
- package/src/icons/grammar-check.ts +13 -0
- package/src/icons/index.ts +12 -0
- package/src/icons/longer.ts +13 -0
- package/src/icons/retry.ts +13 -0
- package/src/icons/send-prompt.ts +13 -0
- package/src/icons/send.ts +13 -0
- package/src/icons/shorter.ts +13 -0
- package/src/icons/translate.ts +13 -0
- package/src/llm-providers/anthropic/index.ts +132 -0
- package/src/llm-providers/openai/index.ts +109 -0
- package/src/llm-providers/providers.spec.ts +472 -0
- package/src/llm-providers/shared.ts +160 -0
- package/src/theme/common/ai.css +430 -0
- package/src/theme/common/code-mirror.css +14 -0
- package/src/theme/common/diff.css +196 -0
- package/src/theme/common/style.css +3 -0
- package/src/theme/common/top-bar.css +156 -0
- package/src/utils/group-builder.ts +1 -1
- package/src/utils/keep-alive.ts +3 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aiIcon,
|
|
3
|
+
editIcon,
|
|
4
|
+
grammarCheckIcon,
|
|
5
|
+
longerIcon,
|
|
6
|
+
shorterIcon,
|
|
7
|
+
translateIcon,
|
|
8
|
+
} from '../../../icons'
|
|
9
|
+
|
|
10
|
+
/// A single AI suggestion. `prompt` is sent to the provider; `streamingLabel`
|
|
11
|
+
/// is the active-form text shown in the streaming indicator while AI runs.
|
|
12
|
+
export interface AISuggestionItem {
|
|
13
|
+
icon: string
|
|
14
|
+
label: string
|
|
15
|
+
streamingLabel?: string
|
|
16
|
+
prompt: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// A submenu definition. `title` is shown next to the back arrow when the
|
|
20
|
+
/// submenu is open; `searchPlaceholder` replaces the input placeholder.
|
|
21
|
+
export interface AISubmenuDef {
|
|
22
|
+
icon: string
|
|
23
|
+
label: string
|
|
24
|
+
title: string
|
|
25
|
+
searchPlaceholder: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Mutates a submenu's items in place. Obtained from
|
|
29
|
+
/// `AISuggestionsBuilder.addSubmenu` (via the build callback) or
|
|
30
|
+
/// `AISuggestionsBuilder.getSubmenu(id)`.
|
|
31
|
+
export interface AISubmenuBuilder {
|
|
32
|
+
addItem: (id: string, item: AISuggestionItem) => AISubmenuBuilder
|
|
33
|
+
removeItem: (id: string) => AISubmenuBuilder
|
|
34
|
+
getItem: (id: string) => AISuggestionItem | undefined
|
|
35
|
+
clear: () => AISubmenuBuilder
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SubmenuNode {
|
|
39
|
+
def: AISubmenuDef
|
|
40
|
+
items: Map<string, AISuggestionItem>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createSubmenuBuilder(node: SubmenuNode): AISubmenuBuilder {
|
|
44
|
+
const builder: AISubmenuBuilder = {
|
|
45
|
+
addItem: (id, item) => {
|
|
46
|
+
node.items.set(id, item)
|
|
47
|
+
return builder
|
|
48
|
+
},
|
|
49
|
+
removeItem: (id) => {
|
|
50
|
+
node.items.delete(id)
|
|
51
|
+
return builder
|
|
52
|
+
},
|
|
53
|
+
getItem: (id) => node.items.get(id),
|
|
54
|
+
clear: () => {
|
|
55
|
+
node.items.clear()
|
|
56
|
+
return builder
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
return builder
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type RootNode =
|
|
63
|
+
| { kind: 'item'; id: string; item: AISuggestionItem }
|
|
64
|
+
| { kind: 'submenu'; id: string; node: SubmenuNode }
|
|
65
|
+
|
|
66
|
+
export type ResolvedItemEntry = {
|
|
67
|
+
kind: 'item'
|
|
68
|
+
id: string
|
|
69
|
+
item: AISuggestionItem
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ResolvedSubmenuEntry = {
|
|
73
|
+
kind: 'submenu'
|
|
74
|
+
id: string
|
|
75
|
+
def: AISubmenuDef
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Resolved tree consumed by the tooltip view. `submenus` is keyed by
|
|
79
|
+
/// submenu id so the view can switch between main and submenu lists.
|
|
80
|
+
export interface ResolvedSuggestions {
|
|
81
|
+
main: Array<ResolvedItemEntry | ResolvedSubmenuEntry>
|
|
82
|
+
submenus: Record<
|
|
83
|
+
string,
|
|
84
|
+
{ def: AISubmenuDef; items: Array<{ id: string; item: AISuggestionItem }> }
|
|
85
|
+
>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class AISuggestionsBuilder {
|
|
89
|
+
#nodes: RootNode[] = []
|
|
90
|
+
|
|
91
|
+
addItem = (id: string, item: AISuggestionItem) => {
|
|
92
|
+
this.#removeById(id)
|
|
93
|
+
this.#nodes.push({ kind: 'item', id, item })
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Add a submenu. Populate items via the optional `build` callback,
|
|
98
|
+
/// or call `getSubmenu(id)` afterward. Returns `this` so calls can be
|
|
99
|
+
/// chained at the parent level alongside `addItem`.
|
|
100
|
+
addSubmenu = (
|
|
101
|
+
id: string,
|
|
102
|
+
def: AISubmenuDef,
|
|
103
|
+
build?: (sub: AISubmenuBuilder) => void
|
|
104
|
+
) => {
|
|
105
|
+
this.#removeById(id)
|
|
106
|
+
const node: SubmenuNode = { def, items: new Map() }
|
|
107
|
+
if (build) build(createSubmenuBuilder(node))
|
|
108
|
+
this.#nodes.push({ kind: 'submenu', id, node })
|
|
109
|
+
return this
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
removeItem = (id: string) => {
|
|
113
|
+
this.#removeById(id)
|
|
114
|
+
return this
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getItem = (id: string) => {
|
|
118
|
+
const node = this.#nodes.find((n) => n.kind === 'item' && n.id === id)
|
|
119
|
+
return node?.kind === 'item' ? node.item : undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Return a builder that mutates the submenu's items in place.
|
|
123
|
+
/// Multiple calls return distinct builder objects backed by the same
|
|
124
|
+
/// underlying node, so changes are always visible.
|
|
125
|
+
getSubmenu = (id: string) => {
|
|
126
|
+
const node = this.#nodes.find((n) => n.kind === 'submenu' && n.id === id)
|
|
127
|
+
return node?.kind === 'submenu'
|
|
128
|
+
? createSubmenuBuilder(node.node)
|
|
129
|
+
: undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
clear = () => {
|
|
133
|
+
this.#nodes = []
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
build = (): ResolvedSuggestions => {
|
|
138
|
+
const main: ResolvedSuggestions['main'] = []
|
|
139
|
+
const submenus: ResolvedSuggestions['submenus'] = {}
|
|
140
|
+
for (const node of this.#nodes) {
|
|
141
|
+
if (node.kind === 'item') {
|
|
142
|
+
main.push({ kind: 'item', id: node.id, item: node.item })
|
|
143
|
+
} else {
|
|
144
|
+
main.push({ kind: 'submenu', id: node.id, def: node.node.def })
|
|
145
|
+
submenus[node.id] = {
|
|
146
|
+
def: node.node.def,
|
|
147
|
+
items: Array.from(node.node.items.entries()).map(([id, item]) => ({
|
|
148
|
+
id,
|
|
149
|
+
item,
|
|
150
|
+
})),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { main, submenus }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#removeById = (id: string) => {
|
|
158
|
+
this.#nodes = this.#nodes.filter((n) => n.id !== id)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Populate a builder with the built-in defaults. Called before the
|
|
163
|
+
/// user's `buildAISuggestions` callback so the user can add to,
|
|
164
|
+
/// inspect, or remove any default.
|
|
165
|
+
export function applyDefaultSuggestions(builder: AISuggestionsBuilder): void {
|
|
166
|
+
builder
|
|
167
|
+
.addItem('improve', {
|
|
168
|
+
icon: aiIcon,
|
|
169
|
+
label: 'Improve writing',
|
|
170
|
+
streamingLabel: 'Improving writing',
|
|
171
|
+
prompt: 'Improve the writing while preserving the original meaning.',
|
|
172
|
+
})
|
|
173
|
+
.addItem('grammar', {
|
|
174
|
+
icon: grammarCheckIcon,
|
|
175
|
+
label: 'Fix grammar & spelling',
|
|
176
|
+
streamingLabel: 'Fixing grammar & spelling',
|
|
177
|
+
prompt:
|
|
178
|
+
'Fix any grammar and spelling errors without changing the meaning.',
|
|
179
|
+
})
|
|
180
|
+
.addItem('shorter', {
|
|
181
|
+
icon: shorterIcon,
|
|
182
|
+
label: 'Make shorter',
|
|
183
|
+
streamingLabel: 'Making shorter',
|
|
184
|
+
prompt: 'Make this shorter while preserving the key information.',
|
|
185
|
+
})
|
|
186
|
+
.addItem('longer', {
|
|
187
|
+
icon: longerIcon,
|
|
188
|
+
label: 'Make longer',
|
|
189
|
+
streamingLabel: 'Expanding',
|
|
190
|
+
prompt: 'Expand this with more detail and examples.',
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
builder.addSubmenu(
|
|
194
|
+
'tone',
|
|
195
|
+
{
|
|
196
|
+
icon: editIcon,
|
|
197
|
+
label: 'Change tone…',
|
|
198
|
+
title: 'Change tone',
|
|
199
|
+
searchPlaceholder: 'Search tones…',
|
|
200
|
+
},
|
|
201
|
+
(sub) => {
|
|
202
|
+
const tones: Array<[string, string]> = [
|
|
203
|
+
['professional', 'Professional'],
|
|
204
|
+
['casual', 'Casual'],
|
|
205
|
+
['confident', 'Confident'],
|
|
206
|
+
['friendly', 'Friendly'],
|
|
207
|
+
['direct', 'Direct'],
|
|
208
|
+
['formal', 'Formal'],
|
|
209
|
+
]
|
|
210
|
+
for (const [id, label] of tones) {
|
|
211
|
+
sub.addItem(id, {
|
|
212
|
+
icon: editIcon,
|
|
213
|
+
label,
|
|
214
|
+
streamingLabel: 'Adjusting tone',
|
|
215
|
+
prompt: `Rewrite this in a ${label.toLowerCase()} tone.`,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
builder.addSubmenu(
|
|
222
|
+
'translate',
|
|
223
|
+
{
|
|
224
|
+
icon: translateIcon,
|
|
225
|
+
label: 'Translate…',
|
|
226
|
+
title: 'Translate',
|
|
227
|
+
searchPlaceholder: 'Search languages…',
|
|
228
|
+
},
|
|
229
|
+
(sub) => {
|
|
230
|
+
const languages: Array<[string, string, string]> = [
|
|
231
|
+
['english', 'English', 'English'],
|
|
232
|
+
['chinese', 'Chinese', 'Chinese (Simplified)'],
|
|
233
|
+
['japanese', 'Japanese', 'Japanese'],
|
|
234
|
+
['korean', 'Korean', 'Korean'],
|
|
235
|
+
['spanish', 'Spanish', 'Spanish'],
|
|
236
|
+
['french', 'French', 'French'],
|
|
237
|
+
['german', 'German', 'German'],
|
|
238
|
+
]
|
|
239
|
+
for (const [id, label, promptName] of languages) {
|
|
240
|
+
sub.addItem(id, {
|
|
241
|
+
icon: translateIcon,
|
|
242
|
+
label,
|
|
243
|
+
streamingLabel: `Translating to ${label}`,
|
|
244
|
+
prompt: `Translate this to ${promptName}.`,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Ctx } from '@milkdown/kit/ctx'
|
|
2
|
+
import type { EditorState, PluginView } from '@milkdown/kit/prose/state'
|
|
3
|
+
import type { EditorView } from '@milkdown/kit/prose/view'
|
|
4
|
+
|
|
5
|
+
import { commandsCtx, editorViewCtx } from '@milkdown/kit/core'
|
|
6
|
+
import { TooltipProvider } from '@milkdown/kit/plugin/tooltip'
|
|
7
|
+
import { posToDOMRect } from '@milkdown/kit/prose'
|
|
8
|
+
import { TextSelection } from '@milkdown/kit/prose/state'
|
|
9
|
+
import { createApp, ref, type App } from 'vue'
|
|
10
|
+
|
|
11
|
+
import type { ResolvedSuggestions } from './suggestions'
|
|
12
|
+
|
|
13
|
+
import { runAICmd } from '../commands'
|
|
14
|
+
import {
|
|
15
|
+
AIInstructionInput,
|
|
16
|
+
type AIInstructionTooltipChrome,
|
|
17
|
+
} from './component'
|
|
18
|
+
|
|
19
|
+
export interface AIInstructionTooltipViewConfig {
|
|
20
|
+
placeholder: string
|
|
21
|
+
chrome: AIInstructionTooltipChrome
|
|
22
|
+
suggestions: ResolvedSuggestions
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AIInstructionTooltipView implements PluginView {
|
|
26
|
+
#content: HTMLElement
|
|
27
|
+
#provider: TooltipProvider
|
|
28
|
+
#app: App
|
|
29
|
+
#placeholder
|
|
30
|
+
#resetSignal
|
|
31
|
+
#from = -1
|
|
32
|
+
#to = -1
|
|
33
|
+
/// Source of truth for "should the palette currently be showing".
|
|
34
|
+
/// `TooltipProvider.shouldShow` reads this so that forwarding `update()`
|
|
35
|
+
/// to the provider on every editor transition (needed to keep the
|
|
36
|
+
/// floating position in sync with layout changes) doesn't dismiss the
|
|
37
|
+
/// palette behind our back.
|
|
38
|
+
#wantsShow = false
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
readonly ctx: Ctx,
|
|
42
|
+
view: EditorView,
|
|
43
|
+
config: AIInstructionTooltipViewConfig
|
|
44
|
+
) {
|
|
45
|
+
// Wrapped in a ref so the Vue component re-renders if it ever
|
|
46
|
+
// changes; today there's no setter, but keeping it reactive lets a
|
|
47
|
+
// future API (e.g. context-sensitive placeholders) plug in cleanly.
|
|
48
|
+
this.#placeholder = ref(config.placeholder)
|
|
49
|
+
// Bumped on each `show()` so the component clears input/submenu/cursor
|
|
50
|
+
// state — the Vue app stays mounted across hide/show cycles, so its
|
|
51
|
+
// local state would otherwise persist into the next session.
|
|
52
|
+
this.#resetSignal = ref(0)
|
|
53
|
+
|
|
54
|
+
const content = document.createElement('div')
|
|
55
|
+
content.className = 'milkdown-ai-instruction'
|
|
56
|
+
|
|
57
|
+
const app = createApp(AIInstructionInput, {
|
|
58
|
+
placeholder: this.#placeholder,
|
|
59
|
+
resetSignal: this.#resetSignal,
|
|
60
|
+
suggestions: config.suggestions,
|
|
61
|
+
chrome: config.chrome,
|
|
62
|
+
onConfirm: this.#onConfirm,
|
|
63
|
+
onCancel: this.#onCancel,
|
|
64
|
+
})
|
|
65
|
+
app.mount(content)
|
|
66
|
+
this.#app = app
|
|
67
|
+
this.#content = content
|
|
68
|
+
|
|
69
|
+
this.#provider = new TooltipProvider({
|
|
70
|
+
content,
|
|
71
|
+
debounce: 0,
|
|
72
|
+
offset: 10,
|
|
73
|
+
shouldShow: () => this.#wantsShow,
|
|
74
|
+
floatingUIOptions: {
|
|
75
|
+
placement: 'bottom',
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
this.#provider.onHide = () => {
|
|
79
|
+
this.#wantsShow = false
|
|
80
|
+
requestAnimationFrame(() => {
|
|
81
|
+
try {
|
|
82
|
+
// Only restore focus to the editor when the palette itself
|
|
83
|
+
// was holding focus at hide time (Escape, Enter-to-confirm,
|
|
84
|
+
// selection-driven dismissal). If the user dismissed the
|
|
85
|
+
// palette by clicking somewhere else, focus is already on
|
|
86
|
+
// that target and stealing it back would frustrate keyboard
|
|
87
|
+
// and assistive-technology users trying to leave the editor.
|
|
88
|
+
const root = this.#content.getRootNode() as Document | ShadowRoot
|
|
89
|
+
const active = root.activeElement
|
|
90
|
+
if (!active || !this.#content.contains(active)) return
|
|
91
|
+
const v = this.ctx.get(editorViewCtx)
|
|
92
|
+
v.dom.focus({ preventScroll: true })
|
|
93
|
+
} catch {
|
|
94
|
+
// Editor may be destroyed
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
this.#provider.update(view)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#onConfirm = (instruction: string, label?: string) => {
|
|
102
|
+
if (!instruction.trim()) return
|
|
103
|
+
// Dispatch first and only dismiss the palette on success. If
|
|
104
|
+
// `runAICmd` rejects (no provider, an active streaming session, an
|
|
105
|
+
// active diff review, …) the user's typed instruction stays visible
|
|
106
|
+
// so they can retry once the editor is in a state that accepts it.
|
|
107
|
+
const commands = this.ctx.get(commandsCtx)
|
|
108
|
+
const accepted = commands.call(runAICmd.key, { instruction, label })
|
|
109
|
+
if (!accepted) return
|
|
110
|
+
this.#wantsShow = false
|
|
111
|
+
this.#provider.hide()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#onCancel = () => {
|
|
115
|
+
this.#wantsShow = false
|
|
116
|
+
this.#provider.hide()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
show = (from: number, to: number) => {
|
|
120
|
+
this.#from = from
|
|
121
|
+
this.#to = to
|
|
122
|
+
this.#resetSignal.value++
|
|
123
|
+
this.#wantsShow = true
|
|
124
|
+
const view = this.ctx.get(editorViewCtx)
|
|
125
|
+
this.#provider.show(
|
|
126
|
+
{ getBoundingClientRect: () => posToDOMRect(view, from, to) },
|
|
127
|
+
view
|
|
128
|
+
)
|
|
129
|
+
requestAnimationFrame(() => {
|
|
130
|
+
this.#content.querySelector('input')?.focus()
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
update = (view: EditorView, prevState?: EditorState) => {
|
|
135
|
+
const { selection } = view.state
|
|
136
|
+
// Hide whenever the anchor selection moves OR becomes a non-text
|
|
137
|
+
// selection (e.g. user clicks an image or table node). Otherwise the
|
|
138
|
+
// tooltip would stay orphaned on screen with no valid range.
|
|
139
|
+
const isTextSelection = selection instanceof TextSelection
|
|
140
|
+
const movedRange =
|
|
141
|
+
selection.from !== this.#from || selection.to !== this.#to
|
|
142
|
+
if (!isTextSelection || movedRange) {
|
|
143
|
+
this.#wantsShow = false
|
|
144
|
+
this.#provider.hide()
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Forward to the provider so floating UI re-runs its position
|
|
149
|
+
// calculation against the current view (anchor coordinates can shift
|
|
150
|
+
// with layout/document changes even when from/to stay the same).
|
|
151
|
+
this.#provider.update(view, prevState)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
destroy = () => {
|
|
155
|
+
this.#app.unmount()
|
|
156
|
+
this.#provider.destroy()
|
|
157
|
+
this.#content.remove()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Ctx } from '@milkdown/kit/ctx'
|
|
2
|
+
import type { Node } from '@milkdown/kit/prose/model'
|
|
3
|
+
|
|
4
|
+
import { commandsCtx } from '@milkdown/kit/core'
|
|
5
|
+
import {
|
|
6
|
+
abortStreamingCmd,
|
|
7
|
+
streamingPluginKey,
|
|
8
|
+
} from '@milkdown/kit/plugin/streaming'
|
|
9
|
+
import { Plugin, PluginKey } from '@milkdown/kit/prose/state'
|
|
10
|
+
import { Decoration, DecorationSet } from '@milkdown/kit/prose/view'
|
|
11
|
+
import { $prose } from '@milkdown/kit/utils'
|
|
12
|
+
|
|
13
|
+
import type { AIStreamingIndicatorConfig } from './types'
|
|
14
|
+
|
|
15
|
+
import { abortAICmd, aiSessionCtx } from './commands'
|
|
16
|
+
|
|
17
|
+
const CLASS_PREFIX = 'milkdown-ai-streaming'
|
|
18
|
+
const SPINNER_PERIOD_MS = 800
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_STREAMING_FALLBACK_LABEL = 'Generating'
|
|
21
|
+
export const DEFAULT_STREAMING_CANCEL_HINT = 'Esc to cancel'
|
|
22
|
+
|
|
23
|
+
const indicatorKey = new PluginKey<DecorationSet>(
|
|
24
|
+
'CREPE_AI_STREAMING_INDICATOR'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
/// One spinner per streaming session. The rotation is driven by
|
|
28
|
+
/// requestAnimationFrame instead of CSS keyframes so that DOM moves
|
|
29
|
+
/// (which happen every chunk as the indicator's position advances) do
|
|
30
|
+
/// not reset the animation back to 0deg.
|
|
31
|
+
class IndicatorWidget {
|
|
32
|
+
readonly dom: HTMLElement
|
|
33
|
+
readonly #spinner: HTMLElement
|
|
34
|
+
readonly #label: HTMLElement
|
|
35
|
+
readonly #fallbackLabel: string
|
|
36
|
+
readonly #start = performance.now()
|
|
37
|
+
#lastLabelText = ''
|
|
38
|
+
#rafId = 0
|
|
39
|
+
|
|
40
|
+
constructor(ctx: Ctx, fallbackLabel: string, cancelHint: string) {
|
|
41
|
+
this.#fallbackLabel = fallbackLabel
|
|
42
|
+
|
|
43
|
+
const dom = document.createElement('span')
|
|
44
|
+
dom.className = `${CLASS_PREFIX}-indicator`
|
|
45
|
+
dom.contentEditable = 'false'
|
|
46
|
+
// Treat the pill as a status live region so assistive technologies
|
|
47
|
+
// get notified when AI starts streaming and when the active-form
|
|
48
|
+
// label changes between sessions ("Improving writing…",
|
|
49
|
+
// "Translating to French…", etc.).
|
|
50
|
+
dom.setAttribute('role', 'status')
|
|
51
|
+
dom.setAttribute('aria-live', 'polite')
|
|
52
|
+
|
|
53
|
+
const spinner = document.createElement('span')
|
|
54
|
+
spinner.className = `${CLASS_PREFIX}-spinner`
|
|
55
|
+
// The decorative rotation has no semantic value for screen readers.
|
|
56
|
+
spinner.setAttribute('aria-hidden', 'true')
|
|
57
|
+
dom.appendChild(spinner)
|
|
58
|
+
|
|
59
|
+
const label = document.createElement('span')
|
|
60
|
+
label.className = `${CLASS_PREFIX}-label`
|
|
61
|
+
dom.appendChild(label)
|
|
62
|
+
|
|
63
|
+
const escHint = document.createElement('span')
|
|
64
|
+
escHint.className = `${CLASS_PREFIX}-esc`
|
|
65
|
+
escHint.textContent = cancelHint
|
|
66
|
+
dom.appendChild(escHint)
|
|
67
|
+
|
|
68
|
+
this.dom = dom
|
|
69
|
+
this.#spinner = spinner
|
|
70
|
+
this.#label = label
|
|
71
|
+
this.setLabel(ctx)
|
|
72
|
+
this.#tick()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setLabel(ctx: Ctx) {
|
|
76
|
+
const session = ctx.get(aiSessionCtx.key)
|
|
77
|
+
const text = `${session.label || this.#fallbackLabel}…`
|
|
78
|
+
if (text === this.#lastLabelText) return
|
|
79
|
+
this.#lastLabelText = text
|
|
80
|
+
this.#label.textContent = text
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
destroy() {
|
|
84
|
+
if (this.#rafId) cancelAnimationFrame(this.#rafId)
|
|
85
|
+
this.#rafId = 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#tick = () => {
|
|
89
|
+
const elapsed = performance.now() - this.#start
|
|
90
|
+
const angle = (elapsed / SPINNER_PERIOD_MS) * 360
|
|
91
|
+
this.#spinner.style.transform = `rotate(${angle}deg)`
|
|
92
|
+
this.#rafId = requestAnimationFrame(this.#tick)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface StreamingIndicatorPluginOptions {
|
|
97
|
+
config?: AIStreamingIndicatorConfig
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function streamingIndicatorPlugin(
|
|
101
|
+
options: StreamingIndicatorPluginOptions = {}
|
|
102
|
+
) {
|
|
103
|
+
const { config } = options
|
|
104
|
+
const fallbackLabel =
|
|
105
|
+
config?.fallbackLabel ?? DEFAULT_STREAMING_FALLBACK_LABEL
|
|
106
|
+
const cancelHint = config?.cancelHint ?? DEFAULT_STREAMING_CANCEL_HINT
|
|
107
|
+
|
|
108
|
+
return $prose((ctx) => {
|
|
109
|
+
let widget: IndicatorWidget | null = null
|
|
110
|
+
|
|
111
|
+
function ensureWidget(): IndicatorWidget {
|
|
112
|
+
if (!widget) widget = new IndicatorWidget(ctx, fallbackLabel, cancelHint)
|
|
113
|
+
else widget.setLabel(ctx)
|
|
114
|
+
return widget
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function dropWidget(): void {
|
|
118
|
+
if (widget) {
|
|
119
|
+
widget.destroy()
|
|
120
|
+
widget = null
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildSet(doc: Node, insertEndPos: number): DecorationSet {
|
|
125
|
+
const pos = Math.min(insertEndPos, doc.content.size)
|
|
126
|
+
const decoration = Decoration.widget(pos, ensureWidget().dom, {
|
|
127
|
+
side: 1,
|
|
128
|
+
key: 'ai-streaming-indicator',
|
|
129
|
+
})
|
|
130
|
+
return DecorationSet.create(doc, [decoration])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Plugin<DecorationSet>({
|
|
134
|
+
key: indicatorKey,
|
|
135
|
+
state: {
|
|
136
|
+
init: () => DecorationSet.empty,
|
|
137
|
+
apply(tr, decorations, _oldState, newState) {
|
|
138
|
+
const streaming = streamingPluginKey.getState(newState)
|
|
139
|
+
if (!streaming?.active || streaming.insertEndPos == null) {
|
|
140
|
+
dropWidget()
|
|
141
|
+
return DecorationSet.empty
|
|
142
|
+
}
|
|
143
|
+
if (tr.getMeta(streamingPluginKey) || tr.docChanged) {
|
|
144
|
+
return buildSet(newState.doc, streaming.insertEndPos)
|
|
145
|
+
}
|
|
146
|
+
return decorations.map(tr.mapping, tr.doc)
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
view() {
|
|
150
|
+
return {
|
|
151
|
+
destroy() {
|
|
152
|
+
dropWidget()
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
props: {
|
|
157
|
+
decorations(state) {
|
|
158
|
+
return indicatorKey.getState(state) ?? DecorationSet.empty
|
|
159
|
+
},
|
|
160
|
+
handleKeyDown(view, event) {
|
|
161
|
+
if (event.key !== 'Escape') return false
|
|
162
|
+
const commands = ctx.get(commandsCtx)
|
|
163
|
+
// AI-driven session: route through abortAICmd so the AI
|
|
164
|
+
// session state (abortController, label) cleans up too.
|
|
165
|
+
if (ctx.get(aiSessionCtx.key).abortController) {
|
|
166
|
+
event.preventDefault()
|
|
167
|
+
commands.call(abortAICmd.key, { keep: true })
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
// Manual streaming session (no AI session in flight) — abort
|
|
171
|
+
// the streaming plugin directly so the "Esc to cancel" hint
|
|
172
|
+
// shown in the pill isn't misleading.
|
|
173
|
+
if (streamingPluginKey.getState(view.state)?.active) {
|
|
174
|
+
event.preventDefault()
|
|
175
|
+
commands.call(abortStreamingCmd.key, { keep: true })
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
return false
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|