Copilot commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3600302103


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,146 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
     return 
this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"]
 === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When an operator is
+   * added, ask the recommender for likely next operators and float them as
+   * faded ghost chips on the operator's output port; clicking one materializes
+   * it. The whole feature is opt-in and self-effacing: if it is disabled or 
the
+   * backend returns nothing, the canvas is untouched.
+   */
+  private handleOperatorRecommendation(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: an operator was just added to the canvas.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorAddStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.showRecommendationsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeRecommendations());
+
+    // Dismiss if the anchor operator is deleted out from under the ghosts.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.ghostSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the ghosts anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.ghostSuggestion && cell.id.toString() === 
this.ghostSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the ghosts anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.ghostSuggestion) {
+          this.repositionRecommendations();
+        }
+      });
+  }
+
+  private showRecommendationsFor(operator: OperatorPredicate): void {
+    this.closeRecommendations();
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    const sourceOutputPortID = operator.outputPorts[0].portID;
+
+    this.operatorRecommendationService
+      .getRecommendations(operator)
+      .pipe(untilDestroyed(this))
+      .subscribe(recommendations => {
+        // The operator may have been deleted while the request was in flight.
+        if (
+          recommendations.length === 0 ||
+          
!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)
+        ) {
+          return;
+        }
+        const position = this.getRecommendationPosition(operator.operatorID);
+        if (!position) {
+          return;
+        }
+        this.ghostSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });

Review Comment:
   `showRecommendationsFor()` doesn't cancel the in-flight recommendations 
request when the user dismisses ghosts (blank click) or when a newer 
operator-add event happens before the HTTP response returns. This can cause 
ghost suggestions to re-appear after dismissal or for an older operator to 
overwrite the latest suggestions. Consider cancelling the request via 
`takeUntil(...)` on dismiss/replace events.



##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { Observable, catchError, map, of } from "rxjs";
+import { OperatorLink, OperatorPredicate, Point } from 
"../../types/workflow-common.interface";
+import { WorkflowActionService } from 
"../workflow-graph/model/workflow-action.service";
+import { WorkflowUtilService } from 
"../workflow-graph/util/workflow-util.service";
+import { JointUIService } from "../joint-ui/joint-ui.service";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
+
+/**
+ * A single ghost suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+  /** Recommended operator type; a real, catalog-known type. */
+  operatorType: string;
+  /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+  score: number;
+  /** Short, human-readable rationale shown alongside the ghost operator. */
+  reason: string;
+  /** Display name from operator metadata, when available. */
+  userFriendlyName?: string;
+}
+
+interface RecommendationResponse {
+  recommendations: OperatorRecommendation[];
+  strategy: "hardcoded" | "llm";
+}
+
+/**
+ * Client for the ambient operator recommender. Asks the stateless 
agent-service
+ * endpoint what operators are likely to follow the one just added, and turns a
+ * chosen suggestion into a real operator wired onto the source's output port.
+ *
+ * The service never fails loudly: the recommender is a non-essential, ambient
+ * aid, so a backend error or a disabled feature simply yields no suggestions
+ * and the canvas behaves exactly as before.
+ */
+@Injectable({
+  providedIn: "root",
+})
+export class OperatorRecommendationService {
+  private static readonly RECOMMEND_API_URL = "/api/recommend";
+
+  // Horizontal gap between the source operator and a materialized suggestion.
+  private static readonly MATERIALIZE_GAP_X = 100;
+
+  constructor(
+    private http: HttpClient,
+    private config: GuiConfigService,
+    private workflowActionService: WorkflowActionService,
+    private workflowUtilService: WorkflowUtilService
+  ) {}
+
+  /** Whether the opt-in recommender feature is turned on for this deployment. 
*/
+  public isEnabled(): boolean {
+    return this.config.env.operatorRecommendationEnabled === true;
+  }
+
+  /**
+   * Fetch ranked next-operator suggestions for the operator just added.
+   *
+   * Returns an empty list (never errors) when the feature is disabled, the
+   * operator has no output port to suggest from, or the backend call fails.
+   *
+   * @param operator the operator that was just added to the canvas
+   * @param limit maximum number of suggestions to request
+   */
+  public getRecommendations(operator: OperatorPredicate, limit?: number): 
Observable<OperatorRecommendation[]> {
+    // A source-less/output-less operator (e.g. a chart sink) has no output 
port
+    // to hang ghost suggestions on, so don't bother the backend.
+    if (!this.isEnabled() || operator.outputPorts.length === 0) {
+      return of([]);
+    }
+
+    const existingOperatorTypes = this.workflowActionService
+      .getTexeraGraph()
+      .getAllOperators()
+      .map(op => op.operatorType);
+
+    return this.http
+      
.post<RecommendationResponse>(OperatorRecommendationService.RECOMMEND_API_URL, {
+        operatorType: operator.operatorType,
+        existingOperatorTypes,
+        ...(limit !== undefined ? { limit } : {}),
+      })
+      .pipe(
+        map(response => response.recommendations ?? []),
+        // Ambient feature: swallow failures and fall back to "no suggestions".
+        catchError(() => of([]))
+      );
+  }
+
+  /**
+   * Turn a chosen suggestion into a real operator: create it just to the right
+   * of the source operator and link the source's output port to the new
+   * operator's first input port. Added as a single undoable action.
+   *
+   * @param sourceOperator the operator the suggestion was made from
+   * @param sourceOutputPortID the output port the ghost was anchored on
+   * @param recommendedType the operator type the user clicked
+   * @returns the new operator's ID, or `undefined` if it could not be created
+   */
+  public materialize(
+    sourceOperator: OperatorPredicate,
+    sourceOutputPortID: string,
+    recommendedType: string
+  ): string | undefined {
+    const newOperator = 
this.workflowUtilService.getNewOperatorPredicate(recommendedType);
+
+    const sourcePosition = this.workflowActionService
+      .getJointGraphWrapper()
+      .getElementPosition(sourceOperator.operatorID);
+    const newPosition: Point = {
+      x: sourcePosition.x + JointUIService.DEFAULT_OPERATOR_WIDTH + 
OperatorRecommendationService.MATERIALIZE_GAP_X,
+      y: sourcePosition.y,
+    };

Review Comment:
   `materialize()` is documented to return `undefined` when the operator cannot 
be created, but `getNewOperatorPredicate(recommendedType)` throws if the 
backend returns an unknown operatorType. That would surface as a runtime error 
on click, contradicting the "fails soft" behavior expected for this feature. 
Catch creation/positioning failures and return `undefined` instead of throwing.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to