Skip to content

Commit a7709cc

Browse files
Merge branch 'feature/element-context-for-blocks' into dev
2 parents 51069e9 + d7684bf commit a7709cc

25 files changed

Lines changed: 472 additions & 46 deletions

File tree

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Core/Prompts/AIPromptExecutionRequest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ public class AIPromptExecutionRequest
3232
/// </summary>
3333
public required string ContentTypeAlias { get; init; }
3434

35+
/// <summary>
36+
/// The element ID when editing a block element within an entity.
37+
/// Null when editing the entity directly (e.g., a document property).
38+
/// When set, <see cref="EntityId"/> refers to the parent entity (document)
39+
/// and this refers to the block content key.
40+
/// </summary>
41+
public Guid? ElementId { get; init; }
42+
43+
/// <summary>
44+
/// The element type when editing a block element within an entity.
45+
/// Null when editing the entity directly.
46+
/// Example: "block" when editing a block element.
47+
/// </summary>
48+
public string? ElementType { get; init; }
49+
3550
/// <summary>
3651
/// The culture/language variant.
3752
/// </summary>

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Core/Prompts/AIPromptScopeValidator.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,12 @@ private async Task<ResolvedScopeContext> ResolveContextAsync(
8686
PropertyAlias = request.PropertyAlias,
8787
};
8888

89-
// Resolve the property editor UI alias from the content type
89+
// Resolve the property editor UI alias from the content type.
90+
// Use ElementType when present (e.g., "block") for correct service routing,
91+
// since ContentTypeAlias refers to the element type in that case.
9092
context.PropertyEditorUiAlias = await ResolvePropertyEditorUiAliasAsync(
9193
request.ContentTypeAlias,
92-
request.EntityType,
94+
request.ElementType ?? request.EntityType,
9395
request.PropertyAlias,
9496
cancellationToken);
9597

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Core/Prompts/AIPromptService.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,17 @@ public async Task<AIPromptExecutionResult> ExecutePromptAsync(
235235
runtimeContext.SetValue(CoreConstants.ContextKeys.EntityId, request.EntityId);
236236
}
237237

238+
// If no ElementId was set by contributors, use request.ElementId as fallback
239+
if (!runtimeContext.Data.ContainsKey(CoreConstants.ContextKeys.ElementId) && request.ElementId.HasValue && request.ElementId.Value != Guid.Empty)
240+
{
241+
runtimeContext.SetValue(CoreConstants.ContextKeys.ElementId, request.ElementId.Value);
242+
}
243+
244+
if (!runtimeContext.Data.ContainsKey(CoreConstants.ContextKeys.ElementType) && !string.IsNullOrEmpty(request.ElementType))
245+
{
246+
runtimeContext.SetValue(CoreConstants.ContextKeys.ElementType, request.ElementType);
247+
}
248+
238249
// Set prompt metadata in runtime context for auditing and telemetry
239250
runtimeContext.SetValue(Constants.MetadataKeys.PromptId, prompt.Id);
240251
runtimeContext.SetValue(Constants.MetadataKeys.PromptAlias, prompt.Alias);
@@ -448,6 +459,16 @@ void ConfigureChat(AIChatBuilder chat)
448459
["propertyAlias"] = request.PropertyAlias,
449460
};
450461

462+
if (request.ElementId.HasValue)
463+
{
464+
context["elementId"] = request.ElementId.Value.ToString();
465+
}
466+
467+
if (!string.IsNullOrEmpty(request.ElementType))
468+
{
469+
context["elementType"] = request.ElementType;
470+
}
471+
451472
if (!string.IsNullOrEmpty(request.Culture))
452473
{
453474
context["culture"] = request.Culture;

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Web.StaticAssets/Client/src/prompt/controllers/prompt.controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export interface UaiPromptExecuteOptions {
2020
propertyAlias: string;
2121
/** The content type alias for scope validation. For blocks, this is the element type alias. */
2222
contentTypeAlias: string;
23+
/** The element ID when editing a block element within an entity. */
24+
elementId?: string;
25+
/** The element type when editing a block element (e.g., "block"). */
26+
elementType?: string;
2327
/** The culture variant. */
2428
culture?: string;
2529
/** The segment variant. */
@@ -104,6 +108,8 @@ export class UaiPromptController extends UmbControllerBase {
104108
entityType: options.entityType,
105109
propertyAlias: options.propertyAlias,
106110
contentTypeAlias: options.contentTypeAlias,
111+
elementId: options.elementId,
112+
elementType: options.elementType,
107113
culture: options.culture,
108114
segment: options.segment,
109115
context: options.context,

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Web.StaticAssets/Client/src/prompt/property-actions/prompt-insert.property-action.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { UmbPropertyActionBase, type UmbPropertyActionArgs } from "@umbraco-cms/
33
import { UMB_PROPERTY_CONTEXT } from "@umbraco-cms/backoffice/property";
44
import { UMB_CONTENT_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/content";
55
import { UMB_BLOCK_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/block";
6+
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/document";
67
import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/content-type";
78
import { umbOpenModal } from "@umbraco-cms/backoffice/modal";
8-
import { createEntityContextItem, resolveEntityAdapterByType, type UaiEntityAdapterApi } from "@umbraco-ai/core";
9+
import { createEntityContextItem, createElementContextItem, resolveEntityAdapterByType, type UaiEntityAdapterApi } from "@umbraco-ai/core";
910
import { UAI_PROMPT_PREVIEW_MODAL, UAI_PROMPT_PREVIEW_SIDEBAR } from "./prompt-preview-modal.token.js";
1011
import type { UaiPromptPropertyActionMeta, UaiPromptContextItem, UaiPromptPreviewModalData } from "./types.js";
1112

@@ -30,6 +31,8 @@ interface WorkspaceContextLike {
3031
export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiPromptPropertyActionMeta> {
3132
#propertyContext?: typeof UMB_PROPERTY_CONTEXT.TYPE;
3233
#workspaceContext?: WorkspaceContextLike;
34+
#parentDocumentContext?: WorkspaceContextLike;
35+
#isBlockWorkspace = false;
3336
#contentTypeAlias?: string;
3437
#init: Promise<unknown>;
3538
#workspaceAdapter?: UaiEntityAdapterApi;
@@ -49,6 +52,7 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
4952
this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (ctx) => {
5053
if (!this.#workspaceContext) {
5154
this.#workspaceContext = ctx;
55+
this.#isBlockWorkspace = true;
5256
// For blocks, get content type alias from the content element manager's structure
5357
if (ctx) {
5458
this.observe(
@@ -63,6 +67,13 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
6367
});
6468
});
6569

70+
// Observe the parent document context for blocks.
71+
// Uses passContextAliasMatches() to skip the block workspace alias match
72+
// and find the document workspace higher in the DOM tree.
73+
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (ctx) => {
74+
this.#parentDocumentContext = ctx as unknown as WorkspaceContextLike;
75+
}).passContextAliasMatches();
76+
6677
this.#init = Promise.all([
6778
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
6879
this.#propertyContext = context;
@@ -100,10 +111,39 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
100111
throw new Error("Property action meta is not available");
101112
}
102113

103-
// Resolve required entity context for prompt execution
104-
const entityId = this.#workspaceContext.getUnique();
105-
const entityType = this.#workspaceContext.getEntityType();
114+
// Resolve required context for prompt execution
106115
const propertyAlias = this.#propertyContext.getAlias();
116+
if (!propertyAlias) {
117+
throw new Error("Property alias is not available");
118+
}
119+
120+
// For blocks: entity = parent document, element = block
121+
// For documents: entity = document, no element
122+
let entityId: string | null | undefined;
123+
let entityType: string;
124+
let elementId: string | undefined;
125+
let elementType: string | undefined;
126+
127+
if (this.#isBlockWorkspace) {
128+
try {
129+
elementId = this.#workspaceContext.getUnique() ?? undefined;
130+
} catch {
131+
// getUnique() can throw for blocks if contentKey is not yet available
132+
}
133+
elementType = this.#workspaceContext.getEntityType();
134+
135+
if (this.#parentDocumentContext) {
136+
entityId = this.#parentDocumentContext.getUnique();
137+
entityType = this.#parentDocumentContext.getEntityType();
138+
} else {
139+
// Fallback: if parent document couldn't be resolved, use block as entity
140+
entityId = elementId;
141+
entityType = elementType;
142+
}
143+
} else {
144+
entityId = this.#workspaceContext.getUnique();
145+
entityType = this.#workspaceContext.getEntityType();
146+
}
107147

108148
if (!entityId) {
109149
throw new Error("Entity ID is not available");
@@ -113,11 +153,7 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
113153
throw new Error("Entity type is not available");
114154
}
115155

116-
if (!propertyAlias) {
117-
throw new Error("Property alias is not available");
118-
}
119-
120-
// Serialize document context for AI operations
156+
// Serialize entity and element context for AI operations
121157
const context = await this.#serializeEntityContext();
122158

123159
// Get maxChars from property editor config (if available)
@@ -134,6 +170,8 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
134170
entityType,
135171
propertyAlias,
136172
contentTypeAlias: this.#contentTypeAlias ?? "",
173+
elementId,
174+
elementType,
137175
culture: this.#propertyContext.getVariantId?.()?.culture ?? undefined,
138176
segment: this.#propertyContext.getVariantId?.()?.segment ?? undefined,
139177
context,
@@ -187,8 +225,9 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
187225
}
188226

189227
/**
190-
* Serialize the current entity for AI context injection.
191-
* Resolves the appropriate adapter based on the workspace entity type.
228+
* Serialize the current entity (and element, if editing a block) for AI context injection.
229+
* For blocks: sends both the parent document (entity context) and the block (element context).
230+
* For documents: sends only the document (entity context).
192231
*/
193232
async #serializeEntityContext(): Promise<UaiPromptContextItem[] | undefined> {
194233
if (!this.#workspaceContext) {
@@ -200,13 +239,33 @@ export class UaiPromptInsertPropertyAction extends UmbPropertyActionBase<UaiProm
200239
return undefined;
201240
}
202241

242+
const contextItems: UaiPromptContextItem[] = [];
243+
203244
try {
204-
const serializedEntity = await adapter.serializeForLlm(this.#workspaceContext);
205-
return [createEntityContextItem(serializedEntity)];
245+
if (this.#isBlockWorkspace) {
246+
// Block: serialize block as element context
247+
const serializedElement = await adapter.serializeForLlm(this.#workspaceContext);
248+
contextItems.push(createElementContextItem(serializedElement));
249+
250+
// Serialize parent document as entity context
251+
if (this.#parentDocumentContext) {
252+
const docAdapter = await resolveEntityAdapterByType("document");
253+
if (docAdapter?.canHandle(this.#parentDocumentContext)) {
254+
const serializedEntity = await docAdapter.serializeForLlm(this.#parentDocumentContext);
255+
contextItems.push(createEntityContextItem(serializedEntity));
256+
}
257+
}
258+
} else {
259+
// Document/media: serialize as entity context (as before)
260+
const serializedEntity = await adapter.serializeForLlm(this.#workspaceContext);
261+
contextItems.push(createEntityContextItem(serializedEntity));
262+
}
206263
} catch {
207264
// Serialization failed - continue without context
208265
return undefined;
209266
}
267+
268+
return contextItems.length > 0 ? contextItems : undefined;
210269
}
211270
}
212271

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Web.StaticAssets/Client/src/prompt/property-actions/prompt-preview-modal.element.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export class UaiPromptPreviewModalElement extends UmbModalBaseElement<
6262
entityType: this.data.entityType,
6363
propertyAlias: this.data.propertyAlias,
6464
contentTypeAlias: this.data.contentTypeAlias,
65+
elementId: this.data.elementId,
66+
elementType: this.data.elementType,
6567
culture: this.data.culture,
6668
segment: this.data.segment,
6769
// Pass serialized entity context for AI processing

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Web.StaticAssets/Client/src/prompt/property-actions/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ export interface UaiPromptPreviewModalData {
100100
propertyAlias: string;
101101
/** Content type alias for scope validation. For blocks, this is the element type alias. */
102102
contentTypeAlias: string;
103+
/** The element ID when editing a block element within an entity. */
104+
elementId?: string;
105+
/** The element type when editing a block element (e.g., "block"). */
106+
elementType?: string;
103107
culture?: string;
104108
segment?: string;
105109
/** Serialized entity context for AI operations */

Umbraco.AI.Prompt/src/Umbraco.AI.Prompt.Web.StaticAssets/Client/src/prompt/repository/execution/prompt-execution.server.data-source.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@ export interface UaiPromptValueChange {
3434
export interface UaiPromptExecutionRequest {
3535
/** The entity ID for context. Required for scope validation. */
3636
entityId: string;
37-
/** The entity type (e.g., "document", "media", "block"). Required for scope validation. */
37+
/** The entity type (e.g., "document", "media"). Required for scope validation. */
3838
entityType: string;
3939
/** The property alias being edited. Required for scope validation. */
4040
propertyAlias: string;
4141
/** The content type alias for scope validation. For blocks, this is the element type alias. */
4242
contentTypeAlias: string;
43+
/** The element ID when editing a block element within an entity. */
44+
elementId?: string;
45+
/** The element type when editing a block element (e.g., "block"). */
46+
elementType?: string;
4347
/** The culture variant. */
4448
culture?: string;
4549
/** The segment variant. */
@@ -106,6 +110,8 @@ export class UaiPromptExecutionServerDataSource {
106110
entityType: request.entityType,
107111
propertyAlias: request.propertyAlias,
108112
contentTypeAlias: request.contentTypeAlias,
113+
elementId: request.elementId,
114+
elementType: request.elementType,
109115
culture: request.culture,
110116
segment: request.segment,
111117
context: request.context,

0 commit comments

Comments
 (0)