From 6b98c416832ab0422e2273e8ae0d747b9889af08 Mon Sep 17 00:00:00 2001 From: lijianxiong <1518062161@qq.com> Date: Thu, 15 May 2025 21:14:03 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=B3=BB=E7=BB=9F=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=A0=B7=E5=BC=8F=E8=A1=A8=E8=AE=BE=E8=AE=A1=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=96=B0=E5=A2=9EAI=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/layout-design/package.json | 1 + .../sys-css-design.controller.ts | 42 ++ .../sys-css-design/sys-css-design.tsx | 385 +++++++++++++++++- packages/model-design/package.json | 2 +- .../design-code-editor/design-code-editor.tsx | 61 +++ 5 files changed, 484 insertions(+), 7 deletions(-) diff --git a/packages/layout-design/package.json b/packages/layout-design/package.json index f0306a6e..92d37f73 100644 --- a/packages/layout-design/package.json +++ b/packages/layout-design/package.json @@ -47,6 +47,7 @@ "publish:npm": "npm run build && npm publish --access public --registry=http://172.16.240.221:8081/repository/local/" }, "dependencies": { + "@ibiz-template-plugin/ai-chat": "^0.0.28", "@ibiz-template-plugin/design-base": "^0.0.3-alpha.100", "@ibiz-template-plugin/model-design": "^0.0.3-alpha.95", "@ibiz-template/core": "0.7.39-alpha.2", diff --git a/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.controller.ts b/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.controller.ts index 46208106..b8b5a23e 100644 --- a/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.controller.ts +++ b/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.controller.ts @@ -1,10 +1,36 @@ import { + getDeACMode, EditFormController, PanelItemController, } from '@ibiz-template/runtime'; +import { IAppDEACMode, ICode } from '@ibiz/model-core'; import { SysCssDesignPanelItemState } from './sys-css-design.state'; export class SysCssDesignPanelItemController extends PanelItemController { + /** + * 编辑器模型 + * + * @type {ICode} + * @memberof SysCssDesignPanelItemController + */ + public editorModel?: ICode; + + /** + * 编辑器参数 + * + * @type {IData} + * @memberof SysCssDesignPanelItemController + */ + public editorParams: IData = {}; + + /** + * 自填模式 + * + * @type {IAppDEACMode} + * @memberof SysCssDesignPanelItemController + */ + public deACMode?: IAppDEACMode; + /** * 面板项状态 * @@ -53,6 +79,22 @@ export class SysCssDesignPanelItemController extends PanelItemController { this.panel.view.evt.on('onMounted', () => { this.subscribe(); }); + this.editorModel = (this.model as IData).editor as ICode; + if (this.editorModel) { + // TODO 找到编辑器模型 + const { appDEACModeId, appDataEntityId, editorParams } = this.editorModel; + if (editorParams) { + Object.keys(editorParams).forEach(key => { + this.editorParams[key] = editorParams![key]; + }); + } + if (appDEACModeId) + this.deACMode = await getDeACMode( + appDEACModeId, + appDataEntityId!, + this.panel.context.srfappid, + ); + } } /** diff --git a/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.tsx b/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.tsx index 4e8bb676..34da3818 100644 --- a/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.tsx +++ b/packages/layout-design/src/panel-items/sys-css-design/sys-css-design.tsx @@ -1,6 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { PropType, defineComponent } from 'vue'; -import { useNamespace } from '@ibiz-template/vue3-util'; -import { IPanelItem } from '@ibiz/model-core'; +import { useNamespace, useUIStore } from '@ibiz-template/vue3-util'; +import { + IAppDEACMode, + IAppDEUIActionGroupDetail, + IPanelItem, +} from '@ibiz/model-core'; +import { + CoreConst, + getAppCookie, + IBizContext, + IChatMessage, + IPortalAsyncAction, + StringUtil, +} from '@ibiz-template/core'; +import { AxiosProgressEvent } from 'axios'; +import { SysUIActionTag, UIActionUtil } from '@ibiz-template/runtime'; +import { createUUID } from 'qx-util'; import { SysCssDesignPanelItemController } from './sys-css-design.controller'; import './sys-css-design.scss'; @@ -16,10 +32,363 @@ export default defineComponent({ required: true, }, }, - setup() { + setup(props) { const ns = useNamespace('sys-css-design'); - return { ns }; + const c = props.controller; + + const { zIndex } = useUIStore(); + + type IAIToolbarItem = { + appId: string; + id: string | undefined; + label: string | undefined; + title: string | undefined; + icon: IData; + }; + + /** + * 通过Ac计算Ai工具栏 + * + * @export + * @param {IAppDEACMode} deACMode + * @param {IAiUIActionParams} args + * @return {*} {{ + * contentToolbarItems: IData[]; + * footerToolbarItems: IData[]; + * }} + */ + function calcAiToolbarItemsByAc(deACMode: IAppDEACMode): { + contentToolbarItems: IAIToolbarItem[]; + footerToolbarItems: IAIToolbarItem[]; + questionToolbarItems: IAIToolbarItem[]; + otherToolbarItems: IAIToolbarItem[]; + } { + const contentToolbarItems: IAIToolbarItem[] = []; + const footerToolbarItems: IAIToolbarItem[] = []; + const questionToolbarItems: IAIToolbarItem[] = []; + const otherToolbarItems: IAIToolbarItem[] = []; + deACMode.deuiactionGroup?.uiactionGroupDetails?.forEach( + (item: IAppDEUIActionGroupDetail) => { + const toolbarItem: IAIToolbarItem = { + appId: item.appId, + id: item.uiactionId, + label: item.showCaption ? item.caption : '', + title: item.tooltip, + icon: { + showIcon: item.showIcon, + cssClass: item.sysImage?.cssClass, + imagePath: item.sysImage?.imagePath, + }, + }; + // 修正图片路径 + if ( + item.sysImage && + item.sysImage.imagePath && + !item.sysImage.imagePath.startsWith('http') + ) { + toolbarItem.icon.imagePath = `${ibiz.env.assetsUrl}/images/${item.sysImage.imagePath}`; + } + if (item.uiactionId?.startsWith('msg_content_')) { + contentToolbarItems.push(toolbarItem); + } else if (item.uiactionId?.startsWith('msg_footer_')) { + footerToolbarItems.push(toolbarItem); + } else if (item.uiactionId?.startsWith('question_')) { + questionToolbarItems.push(toolbarItem); + } else { + otherToolbarItems.push(toolbarItem); + } + }, + ); + return { + contentToolbarItems, + footerToolbarItems, + questionToolbarItems, + otherToolbarItems, + }; + } + + const getCurData = (): IData => { + if (c.form) { + return c.form.state.data; + } + return c.panel.data; + }; + + /** + * 打开AI + * + */ + const openAIChat = async () => { + if (!c.deACMode || !c.editorModel?.appDataEntityId) return; + const { + contentToolbarItems, + footerToolbarItems, + questionToolbarItems, + otherToolbarItems, + } = calcAiToolbarItemsByAc(c.deACMode) as IParams; + const curData = getCurData(); + // eslint-disable-next-line import/no-extraneous-dependencies + const module = await import('@ibiz-template-plugin/ai-chat'); + const chatInstance = module.chat || module.default.chat; + let id: string = ''; + let abortController: AbortController; + chatInstance.create({ + containerOptions: { + zIndex: zIndex.increment(), + }, + chatOptions: { + caption: c.deACMode.logicName, + context: { ...c.panel.context }, + params: { ...c.panel.params, srfactag: c.deACMode.codeName }, + // 编辑器参数srfaiappendcurdata,是否传入对象参数,用于历史查询传参 + appendCurData: + c.editorParams.srfaiappendcurdata === 'true' ? curData : undefined, + // 编辑器参数srfaiappendcurcontent,传入编辑内容作为用户消息,获取历史数据后附加 + appendCurContent: c.editorParams.srfaiappendcurcontent + ? StringUtil.fill( + c.editorParams.srfaiappendcurcontent, + c.panel.context, + c.panel.params, + curData, + ) + : undefined, + appDataEntityId: c.editorModel?.appDataEntityId, + contentToolbarItems, + footerToolbarItems, + questionToolbarItems, + otherToolbarItems, + question: async ( + aiChat: any, + ctx: IContext, + param: IParams, + other: IParams, + arr: IChatMessage[], + ) => { + id = createUUID(); + abortController = new AbortController(); + const deService = await ibiz.hub + .getApp(ctx.srfappid) + .deService.getService(ctx, other.appDataEntityId); + try { + await deService.aiChatSse( + (msg: IPortalAsyncAction) => { + // 20: 持续回答中,消息会持续推送。同一个消息 id 会显示在同一个框内 + if (msg.actionstate === 20 && msg.actionresult) { + aiChat.addMessage({ + messageid: id, + state: msg.actionstate, + type: 'DEFAULT', + role: 'ASSISTANT', + content: msg.actionresult as string, + }); + } + // 30: 回答完成,包含具体所有消息内容。直接覆盖之前的临时拼接消息 + else if (msg.actionstate === 30 && msg.actionresult) { + const result = JSON.parse(msg.actionresult as string); + const choices = result.choices; + if (choices && choices.length > 0) { + aiChat.replaceMessage({ + messageid: id, + state: msg.actionstate, + type: 'DEFAULT', + role: 'ASSISTANT', + content: choices[0].content || '', + }); + } + } + // 40: 回答报错,展示错误信息 + else if (msg.actionstate === 40) { + aiChat.replaceMessage({ + messageid: id, + state: msg.actionstate, + type: 'ERROR', + role: 'ASSISTANT', + content: msg.actionresult as string, + }); + } + }, + abortController, + ctx, + param, + { + messages: arr, + }, + ); + } catch (error) { + aiChat.replaceMessage({ + messageid: id, + state: 40, + type: 'ERROR', + role: 'ASSISTANT', + content: (error as IData).message || ibiz.i18n.t('app.aiError'), + }); + abortController?.abort(); + } finally { + // 标记当前消息已经交互完成 + aiChat.completeMessage(id, true); + // eslint-disable-next-line no-unsafe-finally + return true; + } + }, + abortQuestion: async (aiChat: any) => { + abortController?.abort(); + await aiChat.stopMessage({ + messageid: id, + state: 30, + type: 'DEFAULT', + role: 'ASSISTANT', + content: '', + }); + // 标记当前消息已经交互完成 + await aiChat.completeMessage(id, true); + }, + // TODO + action: ((action: string, message: IData) => { + if (action === 'backfill') + c.state.customValue = message.realcontent; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + history: async (ctx: IContext, param: IParams, other: IParams) => { + const deService = await ibiz.hub + .getApp(ctx.srfappid) + .deService.getService(ctx, other.appDataEntityId); + const historyData = other.appendCurData ? other.appendCurData : {}; + const result = await deService.aiChatHistory( + ctx, + param, + historyData, + ); + if (result.data && Array.isArray(result.data)) { + let preMsg: IData | undefined; + result.data.forEach(item => { + if (item.role === 'TOOL') { + if (preMsg && item.content) { + chatInstance.aiChat!.updateRecommendPrompt( + preMsg as any, + item.content, + ); + } + } else { + const msg = { + messageid: createUUID(), + state: 30, + type: 'DEFAULT', + role: item.role, + content: item.content, + completed: true, + } as const; + preMsg = msg; + chatInstance.aiChat!.addMessage(msg); + } + }); + } + return true; + }, + recommendPrompt: async ( + ctx: IContext, + param: IParams, + other: IParams, + ) => { + const deService = await ibiz.hub + .getApp(ctx.srfappid) + .deService.getService(ctx, other.appDataEntityId); + const result = await deService.aiChatRecommendPrompt( + ctx, + param, + other.message, + ); + if (result.ok && result.data) { + const choices = result.data.choices; + if (choices && choices.length > 0) { + return choices[0]; + } + return null; + } + return null; + }, + uploader: { + onUpload: async ( + file: File, + reportProgress: (progress: number) => void, + options?: IData, + ) => { + const fileMeata = ibiz.util.file.calcFileUpDownUrl( + options?.context || c.panel.context, + options?.params || c.panel.params, + {}, + ); + const uploadHeaders = {}; + const token = getAppCookie(CoreConst.TOKEN); + if (token) { + Object.assign(uploadHeaders, { + [`${ibiz.env.tokenHeader}Authorization`]: `${ibiz.env.tokenPrefix}Bearer ${token}`, + }); + } + const formData = new FormData(); + formData.append('file', file); + const res = await ibiz.net.axios({ + url: fileMeata.uploadUrl, + method: 'post', + headers: uploadHeaders, + data: formData, + onUploadProgress: (progressEvent: AxiosProgressEvent) => { + const percent = + (progressEvent.loaded / progressEvent.total!) * 100; + reportProgress(percent); + }, + } as IData); + return res.data; + }, + }, + extendToolbarClick: async ( + event: MouseEvent, + source: IData, + context: IData, + params: IData, + data: IData, + ) => { + const result = await UIActionUtil.exec( + source.id, + { + view: c.panel.view, + ctrl: c.panel, + context: IBizContext.create(context), + params, + data: [data], + event, + }, + source.appId, + ); + if (result.closeView) { + // 修复编辑器失焦后,调整数据后直接点击关闭按钮导致无法触发自动保存 + // params.view.modal.ignoreDismissCheck = true; + c.panel.view.closeView({ ok: true }); + } else if (result.refresh) { + switch (result.refreshMode) { + case 1: + c.panel.view.callUIAction(SysUIActionTag.REFRESH); + break; + case 2: + c.panel.view.parentView?.callUIAction(SysUIActionTag.REFRESH); + break; + case 3: + c.panel.view + .getTopView() + ?.callUIAction(SysUIActionTag.REFRESH); + break; + default: + } + } + return result; + }, + }, + }); + + return chatInstance; + }; + + return { ns, openAIChat }; }, render() { const { formValue, customValue, cssName, sysCssName } = @@ -37,7 +406,8 @@ export default defineComponent({ 合成样式 @@ -45,8 +415,11 @@ export default defineComponent({
自定义样式 { this.controller.updateCustomValue(value); diff --git a/packages/model-design/package.json b/packages/model-design/package.json index 1b9869b9..971e402c 100644 --- a/packages/model-design/package.json +++ b/packages/model-design/package.json @@ -48,7 +48,7 @@ "dayjs": "^1.11.10", "element-plus": "^2.4.2", "lodash-es": "^4.17.21", - "monaco-editor": "^0.51.0", + "monaco-editor": "^0.45.0", "pluralize": "^8.0.0", "qs": "^6.11.2", "qx-util": "^0.4.8", diff --git a/packages/model-design/src/components/design-code-editor/design-code-editor.tsx b/packages/model-design/src/components/design-code-editor/design-code-editor.tsx index 7b9805a7..f764afe9 100644 --- a/packages/model-design/src/components/design-code-editor/design-code-editor.tsx +++ b/packages/model-design/src/components/design-code-editor/design-code-editor.tsx @@ -1,16 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-useless-escape */ /* eslint-disable import/no-extraneous-dependencies */ import { useNamespace, useUIStore } from '@ibiz-template/vue3-util'; +import { IAppDEACMode } from '@ibiz/model-core'; import { defineComponent, nextTick, onMounted, onUnmounted, + PropType, ref, watch, } from 'vue'; import loader from '@monaco-editor/loader'; import * as monaco from 'monaco-editor'; +import { createUUID } from 'qx-util'; import './design-code-editor.scss'; export default defineComponent({ @@ -41,6 +45,12 @@ export default defineComponent({ type: Boolean, default: true, }, + deACMode: { + type: Object as PropType, + }, + openAIChat: { + type: Function as PropType<() => Promise>, + }, }, emits: { 'update:modelValue': (_value: string) => true, @@ -54,8 +64,11 @@ export default defineComponent({ // 编辑器容器 const editorRef = ref(); + const UUID = createUUID(); + // 编辑器 let editor: monaco.editor.IStandaloneCodeEditor | undefined; + let codeLensProviderDisposable: monaco.IDisposable | null; let monacoEditor: typeof monaco.editor; // 语言 @@ -102,6 +115,22 @@ export default defineComponent({ { immediate: true }, ); + /** + * 检查模型是否属于当前编辑器实例 + * + * @param {monaco.editor.ITextModel} model + * @return {*} {boolean} + */ + const validate = (model: monaco.editor.ITextModel): boolean => { + const currentEditor = monacoEditor + .getEditors() + .find(e => e.getModel() === model); + + if (!currentEditor || (currentEditor as any).__instanceId !== UUID) + return false; + return true; + }; + // 编辑器初始化 const editorInit = (): void => { if (!editorRef.value) { @@ -161,6 +190,37 @@ export default defineComponent({ value: props.modelValue, wordWrap: 'on', }); + + // 为当前编辑器实例添加自定义属性 + (editor as any).__instanceId = UUID; + if (props.deACMode && props.openAIChat && ibiz.env.enableAI) { + codeLensProviderDisposable = + loaderMonaco.languages.registerCodeLensProvider( + currentLanguage.value, + { + provideCodeLenses(model, _token) { + if (!validate(model)) + return { lenses: [], dispose: () => {} }; + return { + lenses: [ + { + id: 'AI', + range: new loaderMonaco.Range(1, 1, 1, 1), + command: { + title: `${props.deACMode!.logicName}`, + id: editor!.addCommand(0, () => { + (props as IData).openAIChat(); + })!, + }, + }, + ], + dispose: () => {}, + }; + }, + resolveCodeLens: (_model, codeLens, _token) => codeLens, + }, + ); + } // 监听值的变化 editor.onDidChangeModelContent(() => { if (editor) { @@ -246,6 +306,7 @@ export default defineComponent({ handleFullScreenChange, ); resizeObserver?.disconnect(); + codeLensProviderDisposable?.dispose(); }); return { -- Gitee