Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions app/scripts/devtools/panel/ui5/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,16 @@
// current selection before applying its data.
var token = ++controlSelectSeq;

var own = message.controlProperties && message.controlProperties.own;
var webcTree = frameData[frameId].controlTreeWebC;
// Prefer the per-component runtime version (multi-runtime pages can
// mix versions); fall back to the tree's primary runtime version
// when the element didn't report one. Resolved once here and used for
// both the AI context block and the enum lookup below, so the version
// shown to the assistant always matches the primed manifest.
var resolvedVersion = (own && own.meta && own.meta.version) ||
(webcTree && webcTree.versionInfo && webcTree.versionInfo.version);

var render = function () {
// A newer selection (or a frame switch) has superseded this
// one while its manifest fetch was in flight — drop the stale
Expand Down Expand Up @@ -794,33 +804,27 @@
}
}

var ownMeta = (message.controlProperties.own && message.controlProperties.own.meta) || {};

aiChat.updateContext({
control: {
type: controlType,
id: controlId,
isWebComponent: ownMeta.isWebComponent === true,
tag: ownMeta.tag,
version: resolvedVersion,
properties: message.controlProperties,
bindings: message.controlBindings,
aggregations: message.controlAggregations
aggregations: message.controlAggregations,
events: message.controlEvents
},
appInfo: frameData[frameId].applicationInformation
});
};

// For UI5 Web Components, resolve enum properties to dropdown values
// from the CDN manifest before rendering. The manifest fetch is
// async (cached after the first hit per version); on any failure we
// just render with plain-string values. Classic UI5 renders
// synchronously as before.
var own = message.controlProperties && message.controlProperties.own;
if (own && own.meta && own.meta.isWebComponent) {
var webcTree = frameData[frameId].controlTreeWebC;
// Prefer the per-component runtime version (multi-runtime pages
// can mix versions); fall back to the tree's primary runtime
// version when the element didn't report one.
var version = (own.meta && own.meta.version) ||
(webcTree && webcTree.versionInfo && webcTree.versionInfo.version);
webcEnumRegistry.prime(version).then(function () {
_enrichWebcEnumTypes(own, version);
webcEnumRegistry.prime(resolvedVersion).then(function () {
_enrichWebcEnumTypes(own, resolvedVersion);
}, function () { /* keep plain strings */ }).then(render);
} else {
render();
Expand Down
135 changes: 110 additions & 25 deletions app/scripts/modules/ai/PromptBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const PROPERTIES_CAP = 800;
const BINDINGS_CAP = 800;
const AGGREGATIONS_CAP = 400;
const EVENTS_CAP = 400;
const CONSOLE_ERRORS_CAP = 400;
const BINDING_VALUE_CAP = 100;

Expand Down Expand Up @@ -47,36 +48,53 @@ function _stringifyBindingValue(value) {
/**
* @private
*/
function _renderAggregationLine(name, children) {
if (!Array.isArray(children) || children.length === 0) {
// Extract the children array from either a raw array (test fixtures) or the
// DataView envelope that both _formatAggregationValues and webcMain's
// _formatAggregations produce: { options, data: { 'content (id)': Array|DataView|null } }.
// At runtime the innermost content is an array of element-ID strings (see
// ToolsAPI._getAggregationContent and WebComponentsToolsAPI._getSlottedContent),
// not objects.
function _extractAggregationChildren(entry) {
if (Array.isArray(entry)) {
return entry;
}
if (entry && entry.data) {
const contentId = entry.data['content (id)'];
// Classic controls wrap a populated array in a nested DataView object.
if (contentId && contentId.data && Array.isArray(contentId.data)) {
return contentId.data;
}
// Web component slots store the raw array directly.
if (Array.isArray(contentId)) {
return contentId;
}
}
return [];
}

// Normalize a child entry to its display ID. Runtime data is an array of ID
// strings; the object form ({ id, type }) is tolerated for forward safety.
function _childId(child) {
if (typeof child === 'string') {
return child;
}
return (child && child.id) || '?';
}

function _renderAggregationLine(name, entry) {
const children = _extractAggregationChildren(entry);
if (children.length === 0) {
return '- ' + name + ': empty';
}

const count = children.length;

if (count <= 3) {
const ids = children.map(function (child) {
return (child && child.id) || '?';
});
const ids = children.map(_childId);
return '- ' + name + ': ' + count + ' children — ' + ids.join(', ');
}

// Preserve insertion order.
const histogram = Object.create(null);
const order = [];
for (let i = 0; i < children.length; i++) {
const type = (children[i] && children[i].type) || '?';
if (!Object.prototype.hasOwnProperty.call(histogram, type)) {
histogram[type] = 0;
order.push(type);
}
histogram[type] += 1;
}
const parts = order.map(function (type) {
return type + ' × ' + histogram[type];
});

return '- ' + name + ': ' + count + ' children (' + parts.join(', ') + ')';
return '- ' + name + ': ' + count + ' children';
}

/**
Expand All @@ -100,10 +118,11 @@ PromptBuilder.prototype.buildSystemPrompt = function (appInfo) {

const rules = 'Rules:\n' +
'1. Always reply in English, regardless of the language of the user\'s message.\n' +
'2. When a Current UI5 Control Context section is present in the user prompt, only reference property, aggregation, event, and binding names that appear in it. If asked about a name not listed there, say so plainly.\n' +
'2. When a Current UI5 Control Context or Current UI5 Web Component Context section is present, treat it as the authoritative list of that control\'s property, aggregation, slot, event, and binding names. Only reference names that appear in it. If asked about a name not listed there, do not describe it from general knowledge — say it is not in the context, or use the rule 3 uncertainty phrase.\n' +
'3. When naming a specific property, event, method, or enum value on a UI5 control, only state it if confident it exists. Otherwise use this exact phrase and add no further disclaimers:\n' +
'I\'m not certain <name> exists on <control> — verify in the API reference.\n' +
'4. Prefer runtime data (resolved binding values, console errors, application metadata shown above) over general assumptions when answering.';
'4. Prefer runtime data (resolved binding values, console errors, application metadata shown above) over general assumptions when answering.\n' +
'5. UI5 Web Components are custom elements (e.g. ui5-button). Reason about them via their tag name, attributes/properties, slots, and events — not sap.* class APIs. Slots are the web-component equivalent of aggregations, and web components have no data bindings or models.';

const style = 'Style:\n' +
'Neutral, direct, and developer-focused. Use code snippets for code. No marketing filler, no generic disclaimers.';
Expand Down Expand Up @@ -217,6 +236,10 @@ PromptBuilder.prototype.buildUserPrompt = function (userMessage, inspectionConte
* @returns {string}
*/
PromptBuilder.prototype._buildControlContextBlock = function (control) {
if (control.isWebComponent) {
return this._buildWebComponentContextBlock(control);
}

const identityLines = [
'- Type: ' + (control.type || 'Unknown'),
'- ID: ' + (control.id || 'None')
Expand All @@ -235,11 +258,49 @@ PromptBuilder.prototype._buildControlContextBlock = function (control) {
if (aggregationsSection) {
sections.push(aggregationsSection);
}
const eventsSection = this._renderEventsSection(control.events);
if (eventsSection) {
sections.push(eventsSection);
}

const contextBody = identityLines.concat(sections).join('\n');
return 'Current UI5 Control Context:\n' + contextBody;
};

/**
* Build the control-context block for a UI5 Web Component: tag/version identity,
* properties (enum types resolved from the manifest), and slots. Web components
* have no data bindings, so no Bindings section is emitted.
* @private
* @param {Object} control
* @returns {string}
*/
PromptBuilder.prototype._buildWebComponentContextBlock = function (control) {
const identityLines = [
'- Tag: ' + (control.tag || control.type || 'Unknown'),
'- Version: ' + (control.version || 'Unknown'),
'- ID: ' + (control.id || 'None'),
'- Data bindings: none (web components do not use data binding)'
];

const sections = [];
const propertiesSection = this._renderPropertiesSection(control.properties);
if (propertiesSection) {
sections.push(propertiesSection);
}
const slotsSection = this._renderAggregationsSection(control.aggregations, 'Slots:');
if (slotsSection) {
sections.push(slotsSection);
}
const eventsSection = this._renderEventsSection(control.events);
if (eventsSection) {
sections.push(eventsSection);
}

const contextBody = identityLines.concat(sections).join('\n');
return 'Current UI5 Web Component Context:\n' + contextBody;
};

/**
* Render the Recent Console Errors block, newest-first.
* @private
Expand Down Expand Up @@ -452,9 +513,12 @@ PromptBuilder.prototype._renderBindingLine = function (propertyName, binding) {
/**
* @private
* @param {Object} aggregations
* @param {string} [headerLabel] - Optional section header. Defaults to `'Aggregations:'`.
* Pass `'Slots:'` when rendering web-component slot data.
* @returns {string}
*/
PromptBuilder.prototype._renderAggregationsSection = function (aggregations) {
PromptBuilder.prototype._renderAggregationsSection = function (aggregations, headerLabel) {
headerLabel = headerLabel || 'Aggregations:';
if (!aggregations || !aggregations.own || !aggregations.own.data) {
return '';
}
Expand All @@ -468,7 +532,28 @@ PromptBuilder.prototype._renderAggregationsSection = function (aggregations) {
return _renderAggregationLine(name, data[name]);
});

return this._capSection('Aggregations:', lines.join('\n'), AGGREGATIONS_CAP);
return this._capSection(headerLabel, lines.join('\n'), AGGREGATIONS_CAP);
};

/**
* Render the Events section as a list of event names. Values are omitted — only
* the event names are useful to the assistant. Returns '' when there are none.
* @private
* @param {Object} events - `{ own: { data: { <eventName>: ... } } }`
* @returns {string}
*/
PromptBuilder.prototype._renderEventsSection = function (events) {
if (!events || !events.own || !events.own.data) {
return '';
}
const names = Object.keys(events.own.data);
if (names.length === 0) {
return '';
}
const body = names.map(function (name) {
return '- ' + name;
}).join('\n');
return this._capSection('Events:', body, EVENTS_CAP);
};

/**
Expand Down
28 changes: 28 additions & 0 deletions app/scripts/modules/webc/WebCEnumRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@ WebCEnumRegistry.storageKey = function (version) {
return 'webc_enums_' + version;
};

/**
* Normalize a runtime-reported version to its leading semver core. Page runtime
* metadata is not always a clean version — e.g. React wrappers can report
* "2.0.0/../../react@18.2.0". Extract the leading major.minor.patch(-prerelease)
* and drop any trailing path/package junk. Returns '' when the string does not
* start with a semver core, so malformed input can never reach a CDN URL or a
* storage key.
*
* @param {string} version
* @returns {string}
*/
WebCEnumRegistry.normalizeVersion = function (version) {
if (typeof version !== 'string') {
return '';
}
var match = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.+-]+)?/.exec(version.trim());
return match ? match[0] : '';
};

/**
* Parse a single attribute `type.text` into an enum value map, or null if it
* is not an enum (i.e. not a union of two-or-more quoted string literals).
Expand Down Expand Up @@ -360,6 +379,14 @@ WebCEnumRegistry.prototype.prime = function (version) {
if (!version || !this._fetch) {
return Promise.resolve(Object.create(null));
}
// Treat the page-reported version as untrusted: strip any trailing
// path/package junk down to a clean semver core before it is pattern-checked,
// fetched, memoized or stored. An unusable value normalizes to '' and falls
// into the guard below (no fetch).
version = WebCEnumRegistry.normalizeVersion(version);
if (!version) {
return Promise.resolve(Object.create(null));
}
if (!WebCEnumRegistry.VERSION_PATTERN.test(version)) {
return Promise.resolve(Object.create(null));
}
Expand Down Expand Up @@ -424,6 +451,7 @@ WebCEnumRegistry.prototype.prime = function (version) {
* @returns {Object|null} `{label: value}` map or null
*/
WebCEnumRegistry.prototype.getEnumValues = function (version, tag, property) {
version = WebCEnumRegistry.normalizeVersion(version);
var map = this._memory[version];
if (!map || !map[tag]) {
return null;
Expand Down
20 changes: 18 additions & 2 deletions app/vendor/WebComponentsToolsAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
return element && element.isUI5Element === true;
}

// Normalize a runtime-reported version to its leading semver core. Page
// runtime metadata is not always a clean version — e.g. React wrappers can
// report "2.0.0/../../react@18.2.0". Extract the leading
// major.minor.patch(-prerelease) and drop any trailing path/package junk.
// Returns '' when the string does not start with a semver core.
function _normalizeVersion(version) {
if (typeof version !== 'string') {
return '';
}
var match = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.+-]+)?/.exec(version.trim());
return match ? match[0] : '';
}

function _getVersion() {
if (cachedVersion !== null) {
return cachedVersion;
Expand All @@ -25,7 +38,7 @@
// on the meta element (an Array of runtime objects).
// See packages/base/src/Runtimes.ts.
if (meta.Runtimes && meta.Runtimes.length > 0 && meta.Runtimes[0].version) {
cachedVersion = meta.Runtimes[0].version;
cachedVersion = _normalizeVersion(meta.Runtimes[0].version);
}
} catch (e) {
// best-effort
Expand Down Expand Up @@ -259,7 +272,10 @@
_getRuntimes().forEach(function (rt) {
var rtSuffix = _safe(function () { return rt.scopingSuffix; }, '');
if (rtSuffix && rtSuffix === suffix) {
version = _safe(function () { return rt.version; }, version) || version;
var candidate = _normalizeVersion(_safe(function () { return rt.version; }, ''));
if (candidate) {
version = candidate;
}
}
});
return version;
Expand Down
Loading
Loading