@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
191 lines (185 loc) • 9.65 kB
JavaScript
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