Skip to content

Commit 5e923f6

Browse files
committed
feat(code-completion): add ai code completion
1 parent 4e93aa6 commit 5e923f6

1 file changed

Lines changed: 121 additions & 4 deletions

File tree

packages/common/js/completion.js

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
1010
*
1111
*/
12-
12+
import { ref } from 'vue'
1313
import { useCanvas, useResource } from '@opentiny/tiny-engine-meta-register'
1414

1515
const keyWords = [
@@ -172,6 +172,123 @@ const getRange = (position, words) => ({
172172
endColumn: words[words.length - 1].endColumn
173173
})
174174

175+
const fetchAiInlineCompletion = (wordContent, signal) => {
176+
const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState
177+
const { state, methods } = useCanvas().getPageSchema()
178+
const currentSchema = useCanvas().getCurrentSchema()
179+
const context =
180+
`const state=${
181+
JSON.stringify(state) || '{}'
182+
} // 请将其理解为js对象,使用方式如: this.state.xxx, 属于页面全局变量,不要作为入参或出参使用\nconst stores=${JSON.stringify(
183+
globalState
184+
)} // 请将其理解为pinia对象,使用方式如: this.stores.xxx, 属于应用全局变量,不要作为入参或出参使用\nconst dataSource=${JSON.stringify(
185+
dataSource
186+
)} // 请将其理解为js对象,使用方式如: this.dataSource.xxx, 属于页面全局变量,不要作为入参或出参使用\nconst utils=${JSON.stringify(
187+
utils
188+
)} // 请将其理解为js对象,使用方式如: this.utils.xxx, 属于页面全局变量,不要作为入参或出参使用\nconst methods=${JSON.stringify(
189+
methods
190+
)} // 请将其理解为js对象,使用方式如: this.xxx,type为JSFunction表示他的类型是function,实际方法体为其value, 属于页面全局变量,不要作为入参或出参使用\n\n当前选中组件上下文\n${JSON.stringify(
191+
currentSchema
192+
)}\n 请理解当前组件,componentName为组件名称,组件均为vue组件\nref为vue3组件的ref属性,使用方式为this.$('xxx')\nprops为其属性,是一个对象,将其理解为vue3组件的props及传递的事件\n` +
193+
`其中普通属性不以on开头,且如果其type为JSExpression表示绑定了变量,例如this.state.xxx,则从state上下文取值,其他同理\n` +
194+
`其中属性以on开头的表示为绑定事件,绑定事件如果type为JSExpression,那么其value的this.xxx表示从methods上下文中读取对应方法,如果type为JSFunction,实际方法体为其value`
195+
return fetch('https://agent.opentiny.design/api/v1/ai/chat/completions', {
196+
method: 'POST',
197+
signal,
198+
headers: {
199+
'Content-Type': 'application/json',
200+
Authorization: 'Bearer sk-trial'
201+
},
202+
body: JSON.stringify({
203+
model: 'deepseek-ai/Deepseek-V3',
204+
messages: [
205+
{
206+
role: 'user',
207+
content: `你是一个JavaScript或TypeScript代码补全器,以下是我的上下文:\n${context}\n,关键字如下:\n${wordContent}\n请帮我进行补全,紧跟着关键字进行补全,不需要多余代码,注意创建方法时,须以这种格式:function xxx() {}\n如果有多个示例,只选择其中一个,只需要返回对应的逻辑正确的纯粹代码,只返回纯粹代码,不需要返回思考过程和解释`
208+
}
209+
],
210+
stream: false
211+
})
212+
})
213+
}
214+
215+
const initInlineCompletion = (monacoInstance, editorModel) => {
216+
const controller = ref(new AbortController())
217+
const signal = ref(controller.value.signal)
218+
const requestAllowed = ref(true)
219+
const timer = ref()
220+
const inlineCompletionProvider = {
221+
provideInlineCompletions(model, position, _context, _token) {
222+
if (editorModel && model.id !== editorModel.id) {
223+
return null
224+
}
225+
226+
if (timer.value) {
227+
clearTimeout(timer.value)
228+
}
229+
230+
const words = getWords(model, position)
231+
const range = getRange(position, words)
232+
const wordContent = words.map((item) => item.word).join('')
233+
if (!wordContent || wordContent.lastIndexOf('}') === 0) {
234+
return null
235+
}
236+
// 如果是第二次请求,则中断当前未完成的请求
237+
if (controller.value && !requestAllowed.value) {
238+
controller.value.abort()
239+
}
240+
// 如果请求被中断后,重新设置AbortController
241+
if (!controller.value || signal.value.aborted) {
242+
controller.value = new AbortController()
243+
signal.value = controller.value.signal
244+
}
245+
requestAllowed.value = false
246+
return new Promise((resolve) => {
247+
// 防抖操作,延迟请求800ms
248+
timer.value = setTimeout(() => {
249+
fetchAiInlineCompletion(wordContent, signal.value)
250+
.then((response) => response.json())
251+
.then((res) => {
252+
let insertText = res.choices[0].message.content
253+
.replace(/.*\n/, '')
254+
.replaceAll('```', '')
255+
.replace('javascript', '')
256+
.replace('typescript', '')
257+
.trim()
258+
const wordContentIndex = insertText.indexOf(wordContent)
259+
if (wordContentIndex === -1) {
260+
insertText = `${wordContent}\n${insertText}`
261+
}
262+
if (wordContentIndex > 0) {
263+
insertText = insertText.slice(wordContentIndex)
264+
}
265+
requestAllowed.value = true
266+
resolve({
267+
items: [
268+
{
269+
label: wordContent,
270+
text: wordContent,
271+
insertText,
272+
range
273+
}
274+
]
275+
})
276+
})
277+
.catch(() => {
278+
requestAllowed.value = true
279+
})
280+
}, 800)
281+
})
282+
},
283+
freeInlineCompletions(completions) {
284+
completions.items = []
285+
}
286+
}
287+
return ['javascript', 'typescript'].map((lang) =>
288+
monacoInstance.languages.registerInlineCompletionsProvider(lang, inlineCompletionProvider)
289+
)
290+
}
291+
175292
export const initCompletion = (monacoInstance, editorModel, conditionFn) => {
176293
const completionItemProvider = {
177294
provideCompletionItems(model, position, _context, _token) {
@@ -199,7 +316,7 @@ export const initCompletion = (monacoInstance, editorModel, conditionFn) => {
199316
triggerCharacters: ['.']
200317
}
201318

202-
return ['javascript', 'typescript'].map((lang) =>
203-
monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider)
204-
)
319+
return ['javascript', 'typescript']
320+
.map((lang) => monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider))
321+
.concat(initInlineCompletion(monacoInstance, editorModel))
205322
}

0 commit comments

Comments
 (0)