@milkdown/crepe 7.20.0 → 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 +1 -0
- 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 +1 -0
- package/lib/cjs/feature/block-edit/index.js.map +1 -1
- package/lib/cjs/feature/code-mirror/index.js +1 -0
- package/lib/cjs/feature/code-mirror/index.js.map +1 -1
- package/lib/cjs/feature/cursor/index.js +1 -0
- package/lib/cjs/feature/cursor/index.js.map +1 -1
- package/lib/cjs/feature/image-block/index.js +1 -0
- package/lib/cjs/feature/image-block/index.js.map +1 -1
- package/lib/cjs/feature/latex/index.js +2 -0
- package/lib/cjs/feature/latex/index.js.map +1 -1
- package/lib/cjs/feature/link-tooltip/index.js +1 -0
- package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
- package/lib/cjs/feature/list-item/index.js +1 -0
- package/lib/cjs/feature/list-item/index.js.map +1 -1
- package/lib/cjs/feature/placeholder/index.js +1 -0
- package/lib/cjs/feature/placeholder/index.js.map +1 -1
- package/lib/cjs/feature/table/index.js +1 -0
- package/lib/cjs/feature/table/index.js.map +1 -1
- package/lib/cjs/feature/toolbar/index.js +488 -3
- package/lib/cjs/feature/toolbar/index.js.map +1 -1
- package/lib/cjs/feature/top-bar/index.js +1 -0
- package/lib/cjs/feature/top-bar/index.js.map +1 -1
- package/lib/cjs/index.js +1424 -25
- 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 +1 -0
- 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 +1 -0
- package/lib/esm/feature/block-edit/index.js.map +1 -1
- package/lib/esm/feature/code-mirror/index.js +1 -0
- package/lib/esm/feature/code-mirror/index.js.map +1 -1
- package/lib/esm/feature/cursor/index.js +1 -0
- package/lib/esm/feature/cursor/index.js.map +1 -1
- package/lib/esm/feature/image-block/index.js +1 -0
- package/lib/esm/feature/image-block/index.js.map +1 -1
- package/lib/esm/feature/latex/index.js +2 -0
- package/lib/esm/feature/latex/index.js.map +1 -1
- package/lib/esm/feature/link-tooltip/index.js +1 -0
- package/lib/esm/feature/link-tooltip/index.js.map +1 -1
- package/lib/esm/feature/list-item/index.js +1 -0
- package/lib/esm/feature/list-item/index.js.map +1 -1
- package/lib/esm/feature/placeholder/index.js +1 -0
- package/lib/esm/feature/placeholder/index.js.map +1 -1
- package/lib/esm/feature/table/index.js +1 -0
- package/lib/esm/feature/table/index.js.map +1 -1
- package/lib/esm/feature/toolbar/index.js +490 -5
- package/lib/esm/feature/toolbar/index.js.map +1 -1
- package/lib/esm/feature/top-bar/index.js +1 -0
- package/lib/esm/feature/top-bar/index.js.map +1 -1
- package/lib/esm/index.js +1414 -15
- 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 +2 -0
- package/lib/tsconfig.tsbuildinfo +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/index.d.ts +4 -1
- package/lib/types/feature/index.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/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/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/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 +11 -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/package.json +18 -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/index.ts +8 -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 +4 -0
- package/src/feature/toolbar/config.ts +27 -1
- package/src/feature/toolbar/index.ts +1 -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/enter-key.ts +13 -0
- package/src/icons/grammar-check.ts +13 -0
- package/src/icons/index.ts +11 -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 +2 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
diffComponent,
|
|
3
|
+
diffComponentConfig,
|
|
4
|
+
} from '@milkdown/kit/component/diff'
|
|
5
|
+
import { diff, diffConfig } from '@milkdown/kit/plugin/diff'
|
|
6
|
+
import { streaming, streamingConfig } from '@milkdown/kit/plugin/streaming'
|
|
7
|
+
|
|
8
|
+
import type { DefineFeature } from '../shared'
|
|
9
|
+
import type { AIFeatureConfig } from './types'
|
|
10
|
+
|
|
11
|
+
import { crepeFeatureConfig } from '../../core/slice'
|
|
12
|
+
import { CrepeFeature } from '../index'
|
|
13
|
+
import {
|
|
14
|
+
aiProviderConfig,
|
|
15
|
+
aiSessionCtx,
|
|
16
|
+
abortAICmd,
|
|
17
|
+
runAICmd,
|
|
18
|
+
} from './commands'
|
|
19
|
+
import { diffActionsPanelPlugin } from './diff-actions'
|
|
20
|
+
import {
|
|
21
|
+
aiInstructionTooltip,
|
|
22
|
+
aiInstructionTooltipAPI,
|
|
23
|
+
configureAIInstructionTooltip,
|
|
24
|
+
} from './instruction-tooltip'
|
|
25
|
+
import { streamingIndicatorPlugin } from './streaming-indicator'
|
|
26
|
+
|
|
27
|
+
/// Default node types in Crepe that use custom node views.
|
|
28
|
+
const CREPE_CUSTOM_BLOCK_TYPES = ['table', 'image-block', 'code_block']
|
|
29
|
+
|
|
30
|
+
/// Default attrs to ignore in Crepe's diff and streaming features.
|
|
31
|
+
const CREPE_IGNORE_ATTRS: Record<string, string[]> = { heading: ['id'] }
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
AIDiffActionsConfig,
|
|
35
|
+
AIFeatureConfig,
|
|
36
|
+
AIPromptContext,
|
|
37
|
+
AIProvider,
|
|
38
|
+
AIStreamingIndicatorConfig,
|
|
39
|
+
AISubmenuBuilder,
|
|
40
|
+
AISubmenuDef,
|
|
41
|
+
AISuggestionItem,
|
|
42
|
+
AISuggestionsBuilder,
|
|
43
|
+
} from './types'
|
|
44
|
+
export { runAICmd, abortAICmd } from './commands'
|
|
45
|
+
export { defaultBuildContext } from './context'
|
|
46
|
+
|
|
47
|
+
export const ai: DefineFeature<AIFeatureConfig> = (editor, config) => {
|
|
48
|
+
const diffCfg = config?.diff ?? {}
|
|
49
|
+
const streamingCfg = config?.streaming ?? {}
|
|
50
|
+
|
|
51
|
+
editor
|
|
52
|
+
.config(crepeFeatureConfig(CrepeFeature.AI))
|
|
53
|
+
// -- Diff plugin + component --
|
|
54
|
+
.config((ctx) => {
|
|
55
|
+
ctx.update(diffConfig.key, (prev) => ({
|
|
56
|
+
...prev,
|
|
57
|
+
ignoreAttrs: diffCfg.ignoreAttrs ?? CREPE_IGNORE_ATTRS,
|
|
58
|
+
}))
|
|
59
|
+
const { ignoreAttrs: _, ...componentConfig } = diffCfg
|
|
60
|
+
ctx.update(diffComponentConfig.key, (prev) => ({
|
|
61
|
+
...prev,
|
|
62
|
+
customBlockTypes:
|
|
63
|
+
componentConfig.customBlockTypes ?? CREPE_CUSTOM_BLOCK_TYPES,
|
|
64
|
+
...componentConfig,
|
|
65
|
+
}))
|
|
66
|
+
})
|
|
67
|
+
.use(diff)
|
|
68
|
+
.use(diffComponent)
|
|
69
|
+
// -- Streaming plugin --
|
|
70
|
+
.config((ctx) => {
|
|
71
|
+
ctx.update(streamingConfig.key, (prev) => ({
|
|
72
|
+
...prev,
|
|
73
|
+
...streamingCfg,
|
|
74
|
+
ignoreAttrs: streamingCfg.ignoreAttrs ?? CREPE_IGNORE_ATTRS,
|
|
75
|
+
// Wire diffReviewOnEnd into the streaming plugin so manual
|
|
76
|
+
// endStreamingCmd calls (outside runAICmd) also respect it.
|
|
77
|
+
// Only override if the user explicitly set it — otherwise
|
|
78
|
+
// keep the streaming plugin's own default so tests and
|
|
79
|
+
// manual-streaming use cases aren't surprised.
|
|
80
|
+
...(config?.diffReviewOnEnd !== undefined
|
|
81
|
+
? { diffReviewOnEnd: config.diffReviewOnEnd }
|
|
82
|
+
: {}),
|
|
83
|
+
}))
|
|
84
|
+
})
|
|
85
|
+
.use(streaming)
|
|
86
|
+
// -- AI orchestration --
|
|
87
|
+
.config((ctx) => {
|
|
88
|
+
ctx.update(aiProviderConfig.key, (prev) => ({
|
|
89
|
+
...prev,
|
|
90
|
+
...(config?.provider !== undefined
|
|
91
|
+
? { provider: config.provider }
|
|
92
|
+
: {}),
|
|
93
|
+
...(config?.buildContext !== undefined
|
|
94
|
+
? { buildContext: config.buildContext }
|
|
95
|
+
: {}),
|
|
96
|
+
diffReviewOnEnd: config?.diffReviewOnEnd ?? prev.diffReviewOnEnd,
|
|
97
|
+
...(config?.onError !== undefined ? { onError: config.onError } : {}),
|
|
98
|
+
...(config?.aiIcon !== undefined ? { aiIcon: config.aiIcon } : {}),
|
|
99
|
+
}))
|
|
100
|
+
})
|
|
101
|
+
.use(aiProviderConfig)
|
|
102
|
+
.use(aiSessionCtx)
|
|
103
|
+
.use(runAICmd)
|
|
104
|
+
.use(abortAICmd)
|
|
105
|
+
// -- AI instruction tooltip --
|
|
106
|
+
.config(configureAIInstructionTooltip(config))
|
|
107
|
+
.use(aiInstructionTooltipAPI)
|
|
108
|
+
.use(aiInstructionTooltip)
|
|
109
|
+
// -- Streaming indicator --
|
|
110
|
+
.use(streamingIndicatorPlugin({ config: config?.streamingIndicator }))
|
|
111
|
+
// -- Diff actions panel --
|
|
112
|
+
.use(
|
|
113
|
+
diffActionsPanelPlugin({
|
|
114
|
+
config: config?.diffActions,
|
|
115
|
+
enterKeyIcon: config?.enterKeyIcon,
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { Icon } from '@milkdown/kit/component'
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
nextTick,
|
|
7
|
+
ref,
|
|
8
|
+
watch,
|
|
9
|
+
type Ref,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
|
|
12
|
+
import type { AISuggestionItem, ResolvedSuggestions } from './suggestions'
|
|
13
|
+
|
|
14
|
+
import { keepAlive } from '../../../utils/keep-alive'
|
|
15
|
+
|
|
16
|
+
keepAlive(h)
|
|
17
|
+
|
|
18
|
+
/// Resolved chrome (icons + labels) for the tooltip. All fields required —
|
|
19
|
+
/// the view layer fills in defaults before mounting the component.
|
|
20
|
+
export interface AIInstructionTooltipChrome {
|
|
21
|
+
aiIcon: string
|
|
22
|
+
sendIcon: string
|
|
23
|
+
sendPromptIcon: string
|
|
24
|
+
enterKeyIcon: string
|
|
25
|
+
chevronLeftIcon: string
|
|
26
|
+
chevronRightIcon: string
|
|
27
|
+
suggestionsHeaderLabel: string
|
|
28
|
+
sendAsPromptHeaderLabel: string
|
|
29
|
+
sendAsPromptLabel: string
|
|
30
|
+
submitButtonLabel: string
|
|
31
|
+
listboxLabel: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ViewMode = { kind: 'main' } | { kind: 'submenu'; id: string }
|
|
35
|
+
|
|
36
|
+
interface DisplayItem {
|
|
37
|
+
id: string
|
|
38
|
+
icon: string
|
|
39
|
+
label: string
|
|
40
|
+
hasSubmenu: boolean
|
|
41
|
+
/// undefined when the item opens a submenu; defined when it sends a prompt
|
|
42
|
+
prompt?: { text: string; streamingLabel?: string }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderHighlighted(label: string, query: string) {
|
|
46
|
+
const q = query.trim()
|
|
47
|
+
if (!q) return [label]
|
|
48
|
+
const lower = label.toLowerCase()
|
|
49
|
+
const lq = q.toLowerCase()
|
|
50
|
+
const idx = lower.indexOf(lq)
|
|
51
|
+
if (idx === -1) return [label]
|
|
52
|
+
return [
|
|
53
|
+
label.slice(0, idx),
|
|
54
|
+
h('mark', null, label.slice(idx, idx + q.length)),
|
|
55
|
+
label.slice(idx + q.length),
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type AIInstructionInputProps = {
|
|
60
|
+
placeholder: Ref<string>
|
|
61
|
+
/// Bumped by the view layer every time the tooltip is shown, so the
|
|
62
|
+
/// component can reset transient state (input value, submenu, cursor).
|
|
63
|
+
resetSignal: Ref<number>
|
|
64
|
+
suggestions: ResolvedSuggestions
|
|
65
|
+
chrome: AIInstructionTooltipChrome
|
|
66
|
+
onConfirm: (instruction: string, label?: string) => void
|
|
67
|
+
onCancel: () => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const AIInstructionInput = defineComponent<AIInstructionInputProps>({
|
|
71
|
+
props: {
|
|
72
|
+
placeholder: { type: Object, required: true },
|
|
73
|
+
resetSignal: { type: Object, required: true },
|
|
74
|
+
suggestions: { type: Object, required: true },
|
|
75
|
+
chrome: { type: Object, required: true },
|
|
76
|
+
onConfirm: { type: Function, required: true },
|
|
77
|
+
onCancel: { type: Function, required: true },
|
|
78
|
+
},
|
|
79
|
+
setup({
|
|
80
|
+
placeholder,
|
|
81
|
+
resetSignal,
|
|
82
|
+
suggestions,
|
|
83
|
+
chrome,
|
|
84
|
+
onConfirm,
|
|
85
|
+
onCancel,
|
|
86
|
+
}) {
|
|
87
|
+
const inputValue = ref('')
|
|
88
|
+
const view = ref<ViewMode>({ kind: 'main' })
|
|
89
|
+
const selectedIndex = ref(0)
|
|
90
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
91
|
+
const listRef = ref<HTMLDivElement | null>(null)
|
|
92
|
+
|
|
93
|
+
// Stable id pair used to wire the input ↔ listbox combobox so that
|
|
94
|
+
// screen readers announce the highlighted option as the user moves
|
|
95
|
+
// through the suggestions with the arrow keys.
|
|
96
|
+
const listboxId = `ai-instruction-list-${Math.random().toString(36).slice(2, 9)}`
|
|
97
|
+
const optionId = (idx: number) => `${listboxId}-opt-${idx}`
|
|
98
|
+
|
|
99
|
+
watch(resetSignal, () => {
|
|
100
|
+
inputValue.value = ''
|
|
101
|
+
view.value = { kind: 'main' }
|
|
102
|
+
selectedIndex.value = 0
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const allItems = computed<DisplayItem[]>(() => {
|
|
106
|
+
if (view.value.kind === 'submenu') {
|
|
107
|
+
const submenu = suggestions.submenus[view.value.id]
|
|
108
|
+
if (!submenu) return []
|
|
109
|
+
return submenu.items.map(({ id, item }) => ({
|
|
110
|
+
id,
|
|
111
|
+
icon: item.icon,
|
|
112
|
+
label: item.label,
|
|
113
|
+
hasSubmenu: false,
|
|
114
|
+
prompt: { text: item.prompt, streamingLabel: item.streamingLabel },
|
|
115
|
+
}))
|
|
116
|
+
}
|
|
117
|
+
return suggestions.main.map((entry) => {
|
|
118
|
+
if (entry.kind === 'item') {
|
|
119
|
+
return {
|
|
120
|
+
id: entry.id,
|
|
121
|
+
icon: entry.item.icon,
|
|
122
|
+
label: entry.item.label,
|
|
123
|
+
hasSubmenu: false,
|
|
124
|
+
prompt: {
|
|
125
|
+
text: entry.item.prompt,
|
|
126
|
+
streamingLabel: entry.item.streamingLabel,
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
id: entry.id,
|
|
132
|
+
icon: entry.def.icon,
|
|
133
|
+
label: entry.def.label,
|
|
134
|
+
hasSubmenu: true,
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const currentSubmenuDef = computed(() => {
|
|
140
|
+
if (view.value.kind !== 'submenu') return null
|
|
141
|
+
return suggestions.submenus[view.value.id]?.def ?? null
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const filteredItems = computed(() => {
|
|
145
|
+
const q = inputValue.value.trim().toLowerCase()
|
|
146
|
+
if (!q) return allItems.value
|
|
147
|
+
return allItems.value.filter((item) =>
|
|
148
|
+
item.label.toLowerCase().includes(q)
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const showSendAsPrompt = computed(() => inputValue.value.trim().length > 0)
|
|
153
|
+
|
|
154
|
+
const totalItems = computed(
|
|
155
|
+
() => filteredItems.value.length + (showSendAsPrompt.value ? 1 : 0)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
watch([filteredItems, view, showSendAsPrompt], () => {
|
|
159
|
+
// When the user has typed something, default the highlight to the
|
|
160
|
+
// "Send as prompt" row (always last in the list) so pressing Enter
|
|
161
|
+
// sends the typed text instead of running whichever built-in
|
|
162
|
+
// suggestion happens to substring-match the query.
|
|
163
|
+
selectedIndex.value = showSendAsPrompt.value
|
|
164
|
+
? filteredItems.value.length
|
|
165
|
+
: 0
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const focusInput = () => {
|
|
169
|
+
void nextTick(() => inputRef.value?.focus())
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const enterSubmenu = (id: string) => {
|
|
173
|
+
view.value = { kind: 'submenu', id }
|
|
174
|
+
inputValue.value = ''
|
|
175
|
+
selectedIndex.value = 0
|
|
176
|
+
focusInput()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const exitSubmenu = () => {
|
|
180
|
+
view.value = { kind: 'main' }
|
|
181
|
+
inputValue.value = ''
|
|
182
|
+
selectedIndex.value = 0
|
|
183
|
+
focusInput()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const runItem = (item: DisplayItem) => {
|
|
187
|
+
if (item.hasSubmenu) {
|
|
188
|
+
enterSubmenu(item.id)
|
|
189
|
+
} else if (item.prompt) {
|
|
190
|
+
onConfirm(item.prompt.text, item.prompt.streamingLabel)
|
|
191
|
+
inputValue.value = ''
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const submitRaw = () => {
|
|
196
|
+
const v = inputValue.value.trim()
|
|
197
|
+
if (!v) return
|
|
198
|
+
onConfirm(v)
|
|
199
|
+
inputValue.value = ''
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const onSelectCurrent = () => {
|
|
203
|
+
const idx = selectedIndex.value
|
|
204
|
+
const items = filteredItems.value
|
|
205
|
+
if (idx < items.length) {
|
|
206
|
+
runItem(items[idx]!)
|
|
207
|
+
} else if (showSendAsPrompt.value) {
|
|
208
|
+
submitRaw()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const scrollToSelected = () => {
|
|
213
|
+
void nextTick(() => {
|
|
214
|
+
const list = listRef.value
|
|
215
|
+
if (!list) return
|
|
216
|
+
const el = list.querySelector(
|
|
217
|
+
`[data-index="${selectedIndex.value}"]`
|
|
218
|
+
) as HTMLElement | null
|
|
219
|
+
el?.scrollIntoView({ block: 'nearest' })
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
224
|
+
e.stopPropagation()
|
|
225
|
+
|
|
226
|
+
if (e.key === 'ArrowDown') {
|
|
227
|
+
e.preventDefault()
|
|
228
|
+
if (totalItems.value === 0) return
|
|
229
|
+
selectedIndex.value = (selectedIndex.value + 1) % totalItems.value
|
|
230
|
+
scrollToSelected()
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (e.key === 'ArrowUp') {
|
|
235
|
+
e.preventDefault()
|
|
236
|
+
if (totalItems.value === 0) return
|
|
237
|
+
selectedIndex.value =
|
|
238
|
+
(selectedIndex.value - 1 + totalItems.value) % totalItems.value
|
|
239
|
+
scrollToSelected()
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (e.key === 'Enter') {
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
onSelectCurrent()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (e.key === 'Escape') {
|
|
250
|
+
e.preventDefault()
|
|
251
|
+
if (view.value.kind === 'submenu') exitSubmenu()
|
|
252
|
+
else onCancel()
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
e.key === 'Backspace' &&
|
|
258
|
+
inputValue.value === '' &&
|
|
259
|
+
view.value.kind === 'submenu'
|
|
260
|
+
) {
|
|
261
|
+
e.preventDefault()
|
|
262
|
+
exitSubmenu()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const onItemPointerDown = (e: Event) => {
|
|
267
|
+
e.preventDefault()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return () => {
|
|
271
|
+
const items = filteredItems.value
|
|
272
|
+
const showPrompt = showSendAsPrompt.value
|
|
273
|
+
const submenuDef = currentSubmenuDef.value
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div class="ai-instruction">
|
|
277
|
+
<div class="ai-instruction-input">
|
|
278
|
+
<span class="ai-instruction-input-prefix">
|
|
279
|
+
<Icon icon={chrome.aiIcon} />
|
|
280
|
+
</span>
|
|
281
|
+
<input
|
|
282
|
+
ref={inputRef}
|
|
283
|
+
class="ai-instruction-input-field"
|
|
284
|
+
role="combobox"
|
|
285
|
+
aria-expanded="true"
|
|
286
|
+
aria-autocomplete="list"
|
|
287
|
+
aria-controls={listboxId}
|
|
288
|
+
aria-activedescendant={
|
|
289
|
+
totalItems.value > 0 ? optionId(selectedIndex.value) : undefined
|
|
290
|
+
}
|
|
291
|
+
placeholder={
|
|
292
|
+
submenuDef ? submenuDef.searchPlaceholder : placeholder.value
|
|
293
|
+
}
|
|
294
|
+
value={inputValue.value}
|
|
295
|
+
onInput={(e: Event) => {
|
|
296
|
+
inputValue.value = (e.target as HTMLInputElement).value
|
|
297
|
+
}}
|
|
298
|
+
onKeydown={onKeydown}
|
|
299
|
+
/>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
class="ai-instruction-submit"
|
|
303
|
+
aria-label={chrome.submitButtonLabel}
|
|
304
|
+
disabled={!showPrompt}
|
|
305
|
+
onMousedown={onItemPointerDown}
|
|
306
|
+
onClick={submitRaw}
|
|
307
|
+
>
|
|
308
|
+
<Icon icon={chrome.sendIcon} />
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div
|
|
313
|
+
class="ai-instruction-list"
|
|
314
|
+
ref={listRef}
|
|
315
|
+
id={listboxId}
|
|
316
|
+
role="listbox"
|
|
317
|
+
aria-label={chrome.listboxLabel}
|
|
318
|
+
>
|
|
319
|
+
{submenuDef && (
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
class="ai-instruction-back"
|
|
323
|
+
onMousedown={onItemPointerDown}
|
|
324
|
+
onClick={exitSubmenu}
|
|
325
|
+
>
|
|
326
|
+
<span class="ai-instruction-back-icon" aria-hidden="true">
|
|
327
|
+
<Icon icon={chrome.chevronLeftIcon} />
|
|
328
|
+
</span>
|
|
329
|
+
<span>{submenuDef.title}</span>
|
|
330
|
+
</button>
|
|
331
|
+
)}
|
|
332
|
+
|
|
333
|
+
{items.length > 0 && (
|
|
334
|
+
<div class="ai-instruction-section">
|
|
335
|
+
<div class="ai-instruction-section-header">
|
|
336
|
+
{chrome.suggestionsHeaderLabel}
|
|
337
|
+
</div>
|
|
338
|
+
{items.map((item, idx) => (
|
|
339
|
+
<div
|
|
340
|
+
key={item.id}
|
|
341
|
+
id={optionId(idx)}
|
|
342
|
+
data-index={idx}
|
|
343
|
+
role="option"
|
|
344
|
+
aria-selected={idx === selectedIndex.value}
|
|
345
|
+
class={[
|
|
346
|
+
'ai-instruction-item',
|
|
347
|
+
idx === selectedIndex.value ? 'active' : '',
|
|
348
|
+
]}
|
|
349
|
+
onMousedown={onItemPointerDown}
|
|
350
|
+
onClick={() => runItem(item)}
|
|
351
|
+
onPointerenter={() => {
|
|
352
|
+
selectedIndex.value = idx
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
<span class="ai-instruction-item-icon">
|
|
356
|
+
<Icon icon={item.icon} />
|
|
357
|
+
</span>
|
|
358
|
+
<span class="ai-instruction-item-label">
|
|
359
|
+
{renderHighlighted(item.label, inputValue.value)}
|
|
360
|
+
</span>
|
|
361
|
+
{item.hasSubmenu && (
|
|
362
|
+
<span class="ai-instruction-item-arrow">
|
|
363
|
+
<Icon icon={chrome.chevronRightIcon} />
|
|
364
|
+
</span>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{showPrompt && (
|
|
372
|
+
<div class="ai-instruction-section">
|
|
373
|
+
<div class="ai-instruction-section-header">
|
|
374
|
+
{chrome.sendAsPromptHeaderLabel}
|
|
375
|
+
</div>
|
|
376
|
+
<div
|
|
377
|
+
id={optionId(items.length)}
|
|
378
|
+
data-index={items.length}
|
|
379
|
+
role="option"
|
|
380
|
+
aria-selected={selectedIndex.value === items.length}
|
|
381
|
+
class={[
|
|
382
|
+
'ai-instruction-item',
|
|
383
|
+
'ai-instruction-item-prompt',
|
|
384
|
+
selectedIndex.value === items.length ? 'active' : '',
|
|
385
|
+
]}
|
|
386
|
+
onMousedown={onItemPointerDown}
|
|
387
|
+
onClick={submitRaw}
|
|
388
|
+
onPointerenter={() => {
|
|
389
|
+
selectedIndex.value = items.length
|
|
390
|
+
}}
|
|
391
|
+
>
|
|
392
|
+
<span class="ai-instruction-item-icon">
|
|
393
|
+
<Icon icon={chrome.sendPromptIcon} />
|
|
394
|
+
</span>
|
|
395
|
+
<span class="ai-instruction-item-label">
|
|
396
|
+
{chrome.sendAsPromptLabel}{' '}
|
|
397
|
+
<span class="ai-instruction-item-quote">
|
|
398
|
+
"{inputValue.value}"
|
|
399
|
+
</span>
|
|
400
|
+
</span>
|
|
401
|
+
<span class="ai-instruction-item-shortcut">
|
|
402
|
+
<Icon icon={chrome.enterKeyIcon} />
|
|
403
|
+
</span>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
export type { AISuggestionItem }
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Ctx } from '@milkdown/kit/ctx'
|
|
2
|
+
|
|
3
|
+
import { tooltipFactory } from '@milkdown/kit/plugin/tooltip'
|
|
4
|
+
import { $ctx } from '@milkdown/kit/utils'
|
|
5
|
+
|
|
6
|
+
import type { AIFeatureConfig } from '../types'
|
|
7
|
+
import type { AIInstructionTooltipChrome } from './component'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
aiIcon as defaultAiIcon,
|
|
11
|
+
chevronLeftIcon as defaultChevronLeftIcon,
|
|
12
|
+
chevronRightIcon as defaultChevronRightIcon,
|
|
13
|
+
enterKeyIcon as defaultEnterKeyIcon,
|
|
14
|
+
sendIcon as defaultSendIcon,
|
|
15
|
+
sendPromptIcon as defaultSendPromptIcon,
|
|
16
|
+
} from '../../../icons'
|
|
17
|
+
import { AISuggestionsBuilder, applyDefaultSuggestions } from './suggestions'
|
|
18
|
+
import {
|
|
19
|
+
AIInstructionTooltipView,
|
|
20
|
+
type AIInstructionTooltipViewConfig,
|
|
21
|
+
} from './view'
|
|
22
|
+
|
|
23
|
+
export interface AIInstructionTooltipAPI {
|
|
24
|
+
show: (from: number, to: number) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultAPI: AIInstructionTooltipAPI = {
|
|
28
|
+
show: () => {},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const aiInstructionTooltipAPI = $ctx(
|
|
32
|
+
{ ...defaultAPI },
|
|
33
|
+
'aiInstructionTooltipAPI'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export const aiInstructionTooltip = tooltipFactory('CREPE_AI_INSTRUCTION')
|
|
37
|
+
|
|
38
|
+
/// Default strings for the instruction tooltip. Exported so the JSDoc on
|
|
39
|
+
/// `AIFeatureConfig` and consumers stay in sync with what the resolver
|
|
40
|
+
/// actually uses.
|
|
41
|
+
export const DEFAULT_SUGGESTIONS_HEADER_LABEL = 'SUGGESTIONS'
|
|
42
|
+
export const DEFAULT_SEND_AS_PROMPT_HEADER_LABEL = 'SEND AS PROMPT'
|
|
43
|
+
export const DEFAULT_SEND_AS_PROMPT_LABEL = 'Ask AI:'
|
|
44
|
+
export const DEFAULT_SUBMIT_BUTTON_LABEL = 'Send prompt'
|
|
45
|
+
export const DEFAULT_LISTBOX_LABEL = 'AI suggestions'
|
|
46
|
+
export const DEFAULT_INSTRUCTION_PLACEHOLDER =
|
|
47
|
+
'Tell AI what to do with the selection…'
|
|
48
|
+
|
|
49
|
+
function resolveChrome(config?: AIFeatureConfig): AIInstructionTooltipChrome {
|
|
50
|
+
return {
|
|
51
|
+
aiIcon: config?.aiIcon ?? defaultAiIcon,
|
|
52
|
+
sendIcon: config?.sendIcon ?? defaultSendIcon,
|
|
53
|
+
sendPromptIcon: config?.sendPromptIcon ?? defaultSendPromptIcon,
|
|
54
|
+
enterKeyIcon: config?.enterKeyIcon ?? defaultEnterKeyIcon,
|
|
55
|
+
chevronLeftIcon: config?.chevronLeftIcon ?? defaultChevronLeftIcon,
|
|
56
|
+
chevronRightIcon: config?.chevronRightIcon ?? defaultChevronRightIcon,
|
|
57
|
+
suggestionsHeaderLabel:
|
|
58
|
+
config?.suggestionsHeaderLabel ?? DEFAULT_SUGGESTIONS_HEADER_LABEL,
|
|
59
|
+
sendAsPromptHeaderLabel:
|
|
60
|
+
config?.sendAsPromptHeaderLabel ?? DEFAULT_SEND_AS_PROMPT_HEADER_LABEL,
|
|
61
|
+
sendAsPromptLabel:
|
|
62
|
+
config?.sendAsPromptLabel ?? DEFAULT_SEND_AS_PROMPT_LABEL,
|
|
63
|
+
submitButtonLabel: config?.submitButtonLabel ?? DEFAULT_SUBMIT_BUTTON_LABEL,
|
|
64
|
+
listboxLabel: config?.listboxLabel ?? DEFAULT_LISTBOX_LABEL,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveViewConfig(
|
|
69
|
+
config?: AIFeatureConfig
|
|
70
|
+
): AIInstructionTooltipViewConfig {
|
|
71
|
+
const builder = new AISuggestionsBuilder()
|
|
72
|
+
applyDefaultSuggestions(builder)
|
|
73
|
+
config?.buildAISuggestions?.(builder)
|
|
74
|
+
return {
|
|
75
|
+
placeholder:
|
|
76
|
+
config?.instructionPlaceholder ?? DEFAULT_INSTRUCTION_PLACEHOLDER,
|
|
77
|
+
chrome: resolveChrome(config),
|
|
78
|
+
suggestions: builder.build(),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function configureAIInstructionTooltip(config?: AIFeatureConfig) {
|
|
83
|
+
return (ctx: Ctx) => {
|
|
84
|
+
const viewConfig = resolveViewConfig(config)
|
|
85
|
+
let tooltipView: AIInstructionTooltipView | null = null
|
|
86
|
+
|
|
87
|
+
ctx.update(aiInstructionTooltipAPI.key, (api) => ({
|
|
88
|
+
...api,
|
|
89
|
+
show: (from, to) => {
|
|
90
|
+
tooltipView?.show(from, to)
|
|
91
|
+
},
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
ctx.set(aiInstructionTooltip.key, {
|
|
95
|
+
view: (view) => {
|
|
96
|
+
tooltipView = new AIInstructionTooltipView(ctx, view, viewConfig)
|
|
97
|
+
return tooltipView
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|