Skip to content

Commit a729d6b

Browse files
committed
Merge branch 'feat/ai-completions' of https://github.com/betterdancing/tiny-engine into feat/ai-completions
2 parents 86e5a59 + dd5b1f5 commit a729d6b

1 file changed

Lines changed: 356 additions & 0 deletions

File tree

designer-demo/completions.js

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
/**
2+
* Copyright (c) 2023 - present TinyEngine Authors.
3+
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
4+
*
5+
* Use of this source code is governed by an MIT-style license.
6+
*
7+
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
8+
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
9+
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
10+
*
11+
*/
12+
import { ref } from 'vue'
13+
import { useCanvas, useResource } from '@opentiny/tiny-engine-meta-register'
14+
15+
const keyWords = [
16+
'state',
17+
'stores',
18+
'props',
19+
'emit',
20+
'setState',
21+
'route',
22+
'i18n',
23+
'getLocale',
24+
'setLocale',
25+
'history',
26+
'utils',
27+
'bridge',
28+
'dataSourceMap'
29+
]
30+
31+
const snippets = [
32+
{
33+
lable: 'new function',
34+
type: 'Function',
35+
insertText: `function \${1:funName} (\${2}) {
36+
\${3}
37+
}`,
38+
detail: 'create new function'
39+
}
40+
]
41+
42+
const TYPES = {
43+
KeyWord: 'KeyWord',
44+
Function: 'Function',
45+
Method: 'Method',
46+
Value: 'Value',
47+
Variable: 'Variable'
48+
}
49+
50+
const getApiSuggestions = (monaco, range, wordContent) =>
51+
keyWords
52+
.map((item) => ({
53+
label: `this.${item}`,
54+
kind: monaco.languages.CompletionItemKind.Keyword,
55+
insertText: `this.${item}`,
56+
detail: `Lowcode API`,
57+
range
58+
}))
59+
.filter(({ insertText }) => insertText.indexOf(wordContent) === 0)
60+
61+
const getSnippetsSuggestions = (monaco, range, wordContent) =>
62+
snippets
63+
.map((item) => ({
64+
label: item.lable,
65+
insertText: item.insertText,
66+
detail: item.detail,
67+
kind: monaco.languages.CompletionItemKind[item.type],
68+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
69+
range
70+
}))
71+
.filter(({ insertText }) => insertText.indexOf(wordContent) === 0)
72+
73+
const getUserWords = () => {
74+
const { bridge = [], dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState
75+
76+
return {
77+
state: {
78+
type: TYPES.Variable,
79+
getInsertText: (value) => `this.state.${value}`,
80+
data: Object.keys(useCanvas().getPageSchema().state || {})
81+
},
82+
stores: {
83+
type: TYPES.Variable,
84+
getInsertText: (value) => `this.stores.${value}`,
85+
data: globalState
86+
.filter((item) => item.id)
87+
.map((item) => [
88+
item.id,
89+
...[...Object.keys(item.state), ...Object.keys(item.getters)].map((name) => `${item.id}.${name}`)
90+
])
91+
.flat()
92+
},
93+
storeFn: {
94+
type: TYPES.Method,
95+
getInsertText: (value) => `this.stores.${value}()`,
96+
data: globalState
97+
.filter((item) => item.id)
98+
.map((item) => Object.keys(item.actions).map((name) => `${item.id}.${name}`))
99+
.flat()
100+
},
101+
utils: {
102+
type: TYPES.Variable,
103+
getInsertText: (value) => `this.utils.${value}`,
104+
data: utils.map((item) => item.name)
105+
},
106+
dataSource: {
107+
type: TYPES.Method,
108+
getInsertText: (value) => `this.dataSourceMap.${value}.load()`,
109+
data: dataSource.map((item) => item.name)
110+
},
111+
bridge: {
112+
type: TYPES.Variable,
113+
getInsertText: (value) => `this.bridge.${value}`,
114+
data: bridge.map((item) => item.name)
115+
}
116+
}
117+
}
118+
119+
const getUserSuggestions = (monaco, range, wordContent) => {
120+
const userWords = getUserWords()
121+
122+
return Object.entries(userWords)
123+
.map(([_itemKey, itemContent]) =>
124+
itemContent.data.map((item) => ({
125+
kind: monaco.languages.CompletionItemKind[itemContent.type],
126+
label: itemContent.getInsertText(item),
127+
insertText: itemContent.getInsertText(item),
128+
detail: `Lowcode API`,
129+
range
130+
}))
131+
)
132+
.flat()
133+
.filter(({ insertText }) => insertText.indexOf(wordContent) === 0)
134+
}
135+
136+
const getCurrentChar = (model, position) => {
137+
const currentChar = model.getValueInRange({
138+
startLineNumber: position.lineNumber,
139+
endLineNumber: position.lineNumber,
140+
startColumn: position.column - 1,
141+
endColumn: position.column
142+
})
143+
144+
return { word: currentChar, startColumn: position.column - 1, endColumn: position.column }
145+
}
146+
147+
const getWords = (model, position) => {
148+
const words = []
149+
150+
const currentWord = model.getWordUntilPosition(position).word
151+
? model.getWordAtPosition(position)
152+
: getCurrentChar(model, position)
153+
words.push(currentWord)
154+
155+
const lastPosition = { ...position, column: currentWord.startColumn }
156+
while (lastPosition.column > 1) {
157+
const lastWord = model.getWordUntilPosition(lastPosition).word
158+
? model.getWordUntilPosition(lastPosition)
159+
: getCurrentChar(model, lastPosition)
160+
if (!/[\w.]/.test(lastWord.word)) break
161+
words.push(lastWord)
162+
lastPosition.column = lastWord.startColumn
163+
}
164+
165+
return words.reverse()
166+
}
167+
168+
const getRange = (position, words) => ({
169+
startLineNumber: position.lineNumber,
170+
endLineNumber: position.lineNumber,
171+
startColumn: words[0].startColumn,
172+
endColumn: words[words.length - 1].endColumn
173+
})
174+
175+
const generateBaseReference = () => {
176+
const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState
177+
const { state, methods } = useCanvas().getPageSchema()
178+
const currentSchema = useCanvas().getCurrentSchema()
179+
let referenceContext = `以下是一些通用的协议:
180+
\n常规属性如:{ width: '300px' }
181+
\n1.变量引用
182+
\n{ width: { type: 'JSExpression', value: 'this.state.xxx' }}
183+
\n即当type为JSExpression,取其value并将value的值当做变量调用
184+
\n2.方法引用
185+
\n{ onClickNew: { type: 'JSFunction', value: 'function onClickNew() {}' }}
186+
\n即当type为JSFunction,取其value并将value的值函数调用
187+
\n以下是一些依赖,调用均以this.开头:\n`;
188+
if (dataSource.length) {
189+
referenceContext += `数据源是定义的数据模型\nconst dataSource=${JSON.stringify(dataSource)}\n调用方式为: this.dataSource.xxx\n`
190+
}
191+
if (utils.length) {
192+
referenceContext += `工具类是通用的调用方法或npm依赖
193+
\nconst utils=${JSON.stringify(utils)}
194+
\n调用方式为: this.utils.xxx
195+
\nutils有两种类型
196+
\ntype为npm时,读取content内容,可构造如下引用,例如content中package(依赖包名)为@opentiny/vue,destructuring(解构)为true,exportName(导出组件名称)为Notify,实际引用方式是import { Notify } from '@opentiny/vue';
197+
\ntype为function时,读取content内容,当content.type为JSFunction则将value视为JS方法并调用,其他可参考通用的协议\n`
198+
}
199+
if (globalState.length) {
200+
referenceContext += `全局变量是使用pinia创建的变量\nconst stores=${JSON.stringify(globalState)}\n调用方式为: this.stores.xxx\n`
201+
}
202+
if (Object.keys(state).length) {
203+
referenceContext += `js变量\nconst state=${JSON.stringify(state)}\n调用方式为: this.state.xxx\n`
204+
}
205+
if (Object.keys(methods).length) {
206+
referenceContext += `js方法\nconst methods=${JSON.stringify(methods)}\n调用方式为: this.xxx\n`
207+
}
208+
referenceContext += `以上依赖中没有的,则不能调用,如utils中没有axios,则axios不能使用\n`
209+
if (currentSchema) {
210+
referenceContext += `以下是当前选中的组件
211+
\n${JSON.stringify(currentSchema)}
212+
\n请理解当前组件,componentName为组件名称,组件均为tinyVue组件,和基本html元素
213+
\n该对象中的ref属性为vue组件的ref属性,如ref值为testForm,使用方式为this.$('testForm')
214+
\nprops表示组件的属性,是一个对象,对应vue组件的defineProps和defineEmits中的内容
215+
\nprops中以on开头的表示其传递的是方法,如onClick,其值可以参考通用协议
216+
\nprops中没有以on开头的则是普通属性,如onClick,其值中满足type为JSExpression和JSFunction的可以参考通用协议\n`
217+
}
218+
return referenceContext;
219+
}
220+
221+
const fetchAiInlineCompletion = (codeBeforeCursor, codeAfterCursor) => {
222+
const referenceContext = generateBaseReference()
223+
return fetch('/designer/app-center/api/chat/completions', {
224+
method: 'POST',
225+
headers: {
226+
'Content-Type': 'application/json',
227+
Authorization: 'Bearer sk-1234'
228+
},
229+
body: JSON.stringify({
230+
model: 'internvl3-14b',
231+
messages: [
232+
{
233+
role: 'user',
234+
content: `你是一个JavaScript代码补全器,可以使用JS和ES的语法
235+
\n${referenceContext}
236+
\n直接上下文如下:
237+
\n${codeBeforeCursor}<cursor>${codeAfterCursor}
238+
\n请从<cursor>(光标位置)处进行补全
239+
\n返回代码不要包含上下文代码,不需要多余代码,注意如果是函数时,须以这种格式:function xxx() {}
240+
\n如果有多个示例,只选择其中一个,不需要返回思考过程和解释`
241+
}
242+
],
243+
stream: false
244+
})
245+
})
246+
}
247+
248+
const initInlineCompletion = (monacoInstance, editorModel) => {
249+
const controller = ref(new AbortController())
250+
const signal = ref(controller.value.signal)
251+
const requestAllowed = ref(true)
252+
const timer = ref()
253+
const inlineCompletionProvider = {
254+
provideInlineCompletions(model, position, _context, _token) {
255+
if (editorModel && model.id !== editorModel.id) {
256+
return new Promise((resolve) => {
257+
resolve({items: []})
258+
})
259+
}
260+
261+
if (timer.value) {
262+
clearTimeout(timer.value)
263+
}
264+
265+
const words = getWords(model, position)
266+
const range = getRange(position, words)
267+
const wordContent = words.map((item) => item.word).join('')
268+
if (!wordContent || wordContent.lastIndexOf('}') === 0 || wordContent.length < 4) {
269+
return new Promise((resolve) => {
270+
resolve({items: []})
271+
})
272+
}
273+
if (!requestAllowed.value) {
274+
return new Promise((resolve) => {
275+
resolve({items: [{
276+
insertText: '',
277+
range
278+
}]})
279+
})
280+
}
281+
return new Promise((resolve) => {
282+
// 延迟请求800ms
283+
timer.value = setTimeout(() => {
284+
// 节流操作,防止接口一直被请求
285+
requestAllowed.value = false
286+
fetchAiInlineCompletion(wordContent)
287+
.then((response) => response.json())
288+
.then((res) => {
289+
let insertText = res.choices[0].message.content
290+
.replace(/.*\n/, '')
291+
.replaceAll('```', '')
292+
.replace('javascript', '')
293+
.replace('typescript', '')
294+
.trim()
295+
const wordContentIndex = insertText.indexOf(wordContent)
296+
if (wordContentIndex === -1) {
297+
insertText = `${wordContent}${insertText}\n`
298+
}
299+
if (wordContentIndex > 0) {
300+
insertText = insertText.slice(wordContentIndex)
301+
}
302+
requestAllowed.value = true
303+
resolve({
304+
items: [
305+
{
306+
insertText,
307+
range
308+
}
309+
]
310+
})
311+
})
312+
.catch(() => {
313+
requestAllowed.value = true
314+
})
315+
}, 800)
316+
})
317+
},
318+
freeInlineCompletions() {}
319+
}
320+
return ['javascript', 'typescript'].map((lang) =>
321+
monacoInstance.languages.registerInlineCompletionsProvider(lang, inlineCompletionProvider)
322+
)
323+
}
324+
325+
export const initCompletion = (monacoInstance, editorModel, conditionFn) => {
326+
const completionItemProvider = {
327+
provideCompletionItems(model, position, _context, _token) {
328+
if (editorModel && model.id !== editorModel.id) {
329+
return {
330+
suggestions: []
331+
}
332+
}
333+
const words = getWords(model, position)
334+
const wordContent = words.map((item) => item.word).join('')
335+
const range = getRange(position, words)
336+
337+
// 内置 API 提示 e.g. this.state/props/utils/...
338+
const apiSuggestions = getApiSuggestions(monacoInstance, range, wordContent)
339+
// 代码片段提示 e.g. create new function
340+
const snippetSuggestions = getSnippetsSuggestions(monacoInstance, range, wordContent)
341+
// 用户变量数据提示 e.g. this.dataSourceMap.xxx.load()
342+
const userSuggestions = getUserSuggestions(monacoInstance, range, wordContent)
343+
return {
344+
suggestions: [...apiSuggestions, ...snippetSuggestions, ...userSuggestions].filter((item) =>
345+
conditionFn ? conditionFn(item) : true
346+
)
347+
}
348+
},
349+
triggerCharacters: ['.']
350+
}
351+
352+
return ['javascript', 'typescript']
353+
.map((lang) => monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider))
354+
.concat(initInlineCompletion(monacoInstance, editorModel))
355+
}
356+

0 commit comments

Comments
 (0)