UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

191 lines (185 loc) 9.65 kB
import { WidgetConfigService, hookWidgetConfig } from '@c8y/ngx-components/context-dashboard'; import * as i0 from '@angular/core'; import { inject, Injector, Injectable } from '@angular/core'; import { PreviewService } from '@c8y/ngx-components'; import { gettext } from '@c8y/ngx-components/gettext'; import { AIService } from '@c8y/ngx-components/ai'; import { defaultWidgetIds } from '@c8y/ngx-components/widgets/definitions'; import { map, combineLatest, from, first } from 'rxjs'; import { HTML_AGENT } from '@c8y/ngx-components/ai/agents/html'; // Treat as "production" when run from Jest const mode = (typeof __MODE__ !== 'undefined' ? __MODE__ : 'production'); const HTML_WIDGET_AGENT_DEFINITIONS = { snapshot: mode === 'development', label: gettext('HTML Widget Code assistant'), definition: HTML_AGENT }; class AIHtmlWidgetConfigFactory { constructor() { this.betaPreviewService = inject(PreviewService); this.aiService = inject(AIService); /** The root injector (nb: components cannot be injected from here). */ this.injector = inject(Injector); this.codeTag = 'c8y-code-extract'; this.codeToolName = 'c8y-html-widget-code'; this.queryToolName = 'cumulocity-api-request'; this.widgetConfigService = inject(WidgetConfigService); this.aiWidgetConfigDefinition = { widgetId: defaultWidgetIds.HTML, label: gettext('AI Code Assistant'), loadComponent: () => import('@c8y/ngx-components/ai/agent-chat').then(m => m.WidgetAiChatSectionComponent), initialState: { // configuration to pass to WidgetAiChatSectionComponent agent: HTML_WIDGET_AGENT_DEFINITIONS, chatConfig: { title: gettext('I’m your AI Code Assistant, here to help you build powerful widgets for your dashboard.'), welcomeText: gettext('Describe the widget you want or select one of the options below to get started.'), showCumulativeUsage: true, showUsagePerMessage: true, showDeleteAction: true }, loadComponentConfig: this.loadWidgetAiChatComponentConfig.bind(this), variables: this.widgetConfigService.currentConfig$.pipe(map((htmlWidgetConfig) => ({ currentHtmlWidgetCode: htmlWidgetConfig?.config?.code || '', c8yContext: htmlWidgetConfig?.device || {} }))), suggestions: [ { label: gettext('Measurement widget'), prompt: gettext('Create a widget that shows the current measurement of this device.') }, { label: gettext('Device status widget'), prompt: gettext('Create a widget that shows the status of my devices.') }, { label: gettext('Critical alarm widget'), prompt: gettext('Create a widget that shows all critical alarms.') } ] }, priority: 100, injector: this.injector }; } get() { return combineLatest([ from(this.aiService.getAgentHealth()), this.betaPreviewService.getState$('ui.html-widget.v2').pipe(first()) ]).pipe(map(([aiHealthCheck, state]) => { if (state && aiHealthCheck.isProviderConfigured) { return [this.aiWidgetConfigDefinition]; } return []; })); } async loadWidgetAiChatComponentConfig(componentInjector) { const { HtmlWidgetConfigService, HtmlAiChatToolDetailsComponent } = await import('@c8y/ngx-components/widgets/implementations/html-widget'); const htmlWidgetConfigService = componentInjector.get(HtmlWidgetConfigService); return { preprocessAgentMessage: (message, changed) => this.preprocessAgentMessage(message, changed, htmlWidgetConfigService), assistantMessageDisplayConfig: { toolDetailsComponent: toolCallPart => { if (toolCallPart.toolName === this.codeToolName) { return HtmlAiChatToolDetailsComponent; } return undefined; }, toolCallConfig: { [this.queryToolName]: { executingLabel: gettext('Analyzing query…'), completedLabel: gettext('Query analyzed') }, [this.codeToolName]: { executingLabel: gettext('Creating widget…'), completedLabel: gettext('Widget created') } }, // To match current behaviour and to help with development, for now we show the JSON for all tool calls showDefaultToolDetails: 'all' } }; } applyCurrentCode(code, htmlWidgetConfigService) { const newConfig = { code: code, css: '', devMode: true, legacy: false, options: { advancedSecurity: false, cssEncapsulation: false } }; htmlWidgetConfigService.configChanged$.next(newConfig); htmlWidgetConfigService.widgetConfigService.updateConfig({ config: newConfig }); } preprocessAgentMessage(message, changedPart, htmlWidgetConfigService) { // Only apply this pre-processing to text parts, since that's what the agent will use to send the code if (!changedPart || changedPart.type !== 'text') { return message; } // Rewrite HTML content generated by the agent as text content as if it had come from a tool call // Find last tool named this.codeToolName in message.content, or undefined const codeTool = message.content.reduce((last, part) => 'toolName' in part && part.toolName === this.codeToolName ? part : last, undefined); if (codeTool && codeTool.type !== 'tool-result') { // A code update tool call is in progress - accumulate text into input const input = `${codeTool.input?.code || ''}${changedPart.text}`; codeTool.input.code = input; changedPart.text = ''; const closeIdx = input.indexOf(`</${this.codeTag}>`); if (closeIdx !== -1) { // Found closing tag - convert to result and start new step const beforeClose = input.substring(0, closeIdx); const afterClose = input.substring(closeIdx + `</${this.codeTag}>`.length); codeTool.input.code = beforeClose; codeTool.type = 'tool-result'; // Always create a new (fake) step after this (just like a real AI), since final step can't contain tool calls message.content.push({ type: 'step-start' }); if (afterClose.length > 0) { message.content.push({ type: 'text', text: afterClose }); } // Since this is a fake tool call not a real one, the tool result callback won't trigger so do this manually this.applyCurrentCode(codeTool.input.code, htmlWidgetConfigService); } else { // Replace with a new instance so that change detection works message.content[message.content.findIndex(part => part === codeTool)] = { ...codeTool }; } } else { // No in-progress code tag, so check for one const openIdx = changedPart.text.indexOf(`<${this.codeTag}>`); if (openIdx !== -1) { const afterOpen = changedPart.text.substring(openIdx + `<${this.codeTag}>`.length); changedPart.text = changedPart.text.substring(0, openIdx); // Start a new "step" to keep it separate and simple message.content.push({ type: 'step-start' }); message.content.push({ type: 'tool-input-streaming', toolName: this.codeToolName, toolCallId: this.codeToolName + message.content.length, // add part number just to ensure uniqueness input: { code: afterOpen } }); } } if (changedPart.text === '') { // Remove any empty changedPart from content message.content.splice(message.content.findIndex(part => part === changedPart), 1); } return message; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AIHtmlWidgetConfigFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AIHtmlWidgetConfigFactory, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AIHtmlWidgetConfigFactory, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const htmlWidgetAIChatProviders = [hookWidgetConfig(AIHtmlWidgetConfigFactory)]; /** * Generated bundle index. Do not edit. */ export { htmlWidgetAIChatProviders }; //# sourceMappingURL=c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map