You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@texera.apache.org by "gupta-sahil01 (via GitHub)" <gi...@apache.org> on 2026/07/15 06:17:12 UTC

[PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

gupta-sahil01 opened a new pull request, #6437:
URL: https://github.com/apache/texera/pull/6437

   Frontend half of the ambient operator recommender (apache/texera#5240). After an operator is added to the canvas, 1-3 faded "ghost" next-operator suggestions appear on its output port; clicking one materializes the operator and wires the link as a single undoable action.
   
   - OperatorRecommendationService: calls the stateless agent-service POST /api/recommend endpoint and turns a chosen suggestion into a real operator. Fails soft — a disabled feature, an output-less operator, or a backend error simply yields no suggestions, leaving the canvas unchanged.
   - Ghost overlay in the workflow editor, reusing the chat-popover anchoring pattern (screen-coord conversion, re-anchored on move / zoom / pan); dismissed on blank click or when the anchor operator is deleted.
   - Opt-in GUI config flag `operatorRecommendationEnabled`, default off, threaded through gui.conf, GuiConfig, ConfigResource, and the frontend GuiConfig type + mock.
   
   Tested: yarn ng test (operator-recommendation.service.spec.ts, 7 passed); yarn ng build --configuration=production (succeeds, no errors).
   
   <!--
   Thanks for sending a pull request (PR)! Here are some tips for you:
     1. If this is your first time, please read our contributor guidelines: 
        [Contributing to Texera](https://github.com/apache/texera/blob/main/CONTRIBUTING.md)
     2. Ensure you have added or run the appropriate tests for your PR
     3. If the PR is work in progress, mark it a draft on GitHub.
     4. Please write your PR title to summarize what this PR proposes, we 
       are following Conventional Commits style for PR titles as well.
     5. Be sure to keep the PR description updated to reflect all changes.
   -->
   
   ### What changes were proposed in this PR?
   Frontend half (PR 2 of 2) of the ambient operator recommender, Version 1 — see #6293 (design discussion #5240). Depends on the backend `/api/recommend` endpoint added in the companion backend PR.
   
   After an operator is added to the canvas, 1–3 faded "ghost" next-operator suggestions appear on its output port; clicking one materializes that operator and wires the link as a single undoable action. Behaves like code-completion ghost text, but for dataflow graphs. **Opt-in and default-off.**
   
   - **`OperatorRecommendationService`** calls `POST /api/recommend` and materializes a chosen suggestion. Fails soft: a disabled feature, an output-less operator (e.g. a chart sink), or a backend error all yield no suggestions and leave the canvas unchanged.
   - **Ghost overlay** in the workflow editor, reusing the existing chat-popover anchoring pattern (screen-coord conversion; re-anchored on operator move / zoom / pan); dismissed on blank click or when the anchor operator is deleted.
   - **Opt-in flag** `operatorRecommendationEnabled` (default `false`), threaded through `gui.conf` → `GuiConfig` → `ConfigResource` → the frontend `GuiConfig` type + mock.
   - **Routing** for the endpoint added to the dev proxy (`frontend/proxy.config.json`) and the prod gateway (`bin/k8s/.../gateway-routes.yaml`), alongside the existing `/api/agents`.
   <!--
   Please clarify what changes you are proposing. The purpose of this section 
   is to outline the changes. Here are some tips for you:
     1. If you propose a new API, clarify the use case for a new API.
     2. If you fix a bug, you can clarify why it is a bug.
     3. If it is a refactoring, clarify what has been changed.
     3. It would be helpful to include a before-and-after comparison using 
        screenshots or GIFs.
     4. Please consider writing useful notes for better and faster reviews.
   -->
   
   
   ### Any related issues, documentation, discussions?
   - Closes #6293
   - Design discussion: #5240
   <!--
   Please use this section to link other resources if not mentioned already.
     1. If this PR fixes an issue, please include `Fixes #1234`, `Resolves #1234`
        or `Closes #1234`. If it is only related, simply mention the issue number.
     2. If there is design documentation, please add the link.
     3. If there is a discussion in the mailing list, please add the link.
   -->
   
   
   ### How was this PR tested?
   - `yarn ng test` — `operator-recommendation.service.spec.ts`, 7 passed (enabled/disabled gating, POST body, empty-list fallbacks on error and output-less operators, materialize link wiring).
   - `yarn ng build --configuration=production` — succeeds under strict templates.
   - **End-to-end (manual):** ran the full local-dev stack with `GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED=true`; verified ghost suggestions render on the output port, clicking materializes + links the operator, and `POST /api/recommend` is reached through the proxy.
   <!--
   If tests were added, say they were added here. Or simply mention that if the PR 
   is tested with existing test cases.  Make sure to include/update test cases that
   check the changes thoroughly including negative and positive cases if possible.
   If it was tested in a way different from regular unit tests, please clarify how
   you tested step by step, ideally copy and paste-able, so that other reviewers can
   test and check, and descendants can verify in the future. If tests were not added, 
   please describe why they were not added and/or why it was difficult to add. 
   -->
   
   
   ### Was this PR authored or co-authored using generative AI tooling?
   Generated-by: Anthropic Claude (Claude Code)
   <!--
   If generative AI tooling has been used in the process of authoring this PR, 
   please include the phrase: 'Generated-by: ' followed by the name of the tool 
   and its version. If no, write 'No'. 
   Please refer to the [ASF Generative Tooling Guidance](https://www.apache.org/legal/generative-tooling.html) for details.
   -->
   


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3769747861


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,194 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());

Review Comment:
   This subscription sits above the `isEnabled()` return two lines down. With the flag off it stays alive with nothing able to feed it — the subject's only producer is the `change:position` handler at 1825, registered below that return.
   
   Harmless today, but it reads as though repositioning still works when the feature is off. Moving it under the return puts the whole feature behind one gate.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,194 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+        this.repositionNextOperatorSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    const sourcePortID = operator.outputPorts[0].portID;
+    const portIsTaken = this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operator.operatorID && link.source.portID === sourcePortID);
+    if (portIsTaken) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    if (graph.hasOperator(this.nextOperatorSuggestion.operatorId)) {

Review Comment:
   The new guard covers the ask path but not this one. `portIsTaken` runs before the request (1854); by the time a chip is clicked, the port can have acquired a link.
   
   Nothing dismisses the chips when that happens — a link drag starts on a port, so it fires `element:pointerdown`, not the `blank:pointerdown` at 1809. Drop an operator, hand-draw a link out of its output port, then click a chip. You get the second successor at the fixed +100 offset you agreed was worse than noise.
   
   Re-running the same `getAllLinks()` check here is the fix I'd take — dismissing on a link-add from the anchor port is more thorough but costs a subscription for a narrow window.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -1644,3 +1649,155 @@ describe("WorkflowEditorComponent link breakpoints", () => {
     expect(show.mock.invocationCallOrder[0]).toBeLessThan(hide.mock.invocationCallOrder[0]);
   });
 });
+
+/**
+ * Ambient operator recommender (apache/texera#5240).
+ *
+ * The feature is opt-in and `handleNextOperatorSuggestions` reads the flag once
+ * during ngAfterViewInit, returning early when it is off — so the flag has to
+ * be set before the first change detection, not inside the individual tests.
+ */
+describe("Ambient Operator Recommender", () => {
+  let component: WorkflowEditorComponent;
+  let fixture: ComponentFixture<WorkflowEditorComponent>;
+  let workflowActionService: WorkflowActionService;
+  let dragDropService: DragDropService;
+  let getRecommendationsSpy: MockInstance;
+
+  const mockRecommendations = [{ operatorType: "NlpSentiment", score: 0.9, reason: "Analyze the text" }];
+
+  // The recommender listens to the drag-drop service's drop stream. Push the
+  // dropped operator through directly rather than driving a real drag gesture,
+  // which would need a live flying-operator paper.
+  function emitOperatorDrop(operator: OperatorPredicate): void {
+    (dragDropService as any).operatorDroppedSubject.next(operator);
+  }
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: workflowEditorTestImports,
+      providers: workflowEditorTestProviders,
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(WorkflowEditorComponent);
+    component = fixture.componentInstance;
+    workflowActionService = TestBed.inject(WorkflowActionService);
+    dragDropService = TestBed.inject(DragDropService);
+
+    (TestBed.inject(GuiConfigService).env as any).operatorRecommendationEnabled = true;
+    getRecommendationsSpy = vi
+      .spyOn(TestBed.inject(OperatorRecommendationService), "getRecommendations")
+      .mockReturnValue(of(mockRecommendations));
+
+    // detect changes to run ngAfterViewInit and wire up the recommender
+    fixture.detectChanges();
+  });
+
+  it("requests suggestions when the user drops an operator onto the canvas", () => {
+    workflowActionService.addOperator(mockScanPredicate, mockPoint);
+    emitOperatorDrop(mockScanPredicate);
+
+    expect(getRecommendationsSpy).toHaveBeenCalledTimes(1);
+    expect(component.nextOperatorSuggestion?.operatorId).toEqual(mockScanPredicate.operatorID);
+    expect(component.nextOperatorSuggestion?.recommendations).toEqual(mockRecommendations);
+  });
+
+  it("does not request suggestions for operators that arrive without a drop", () => {
+    // Workflow load, undo/redo, paste and remote co-editor edits all reach the
+    // graph through addOperator with no drop event, and must stay silent.
+    workflowActionService.addOperator(mockScanPredicate, mockPoint);
+    workflowActionService.addOperator(mockSentimentPredicate, mockPoint);
+
+    expect(getRecommendationsSpy).not.toHaveBeenCalled();
+    expect(component.nextOperatorSuggestion).toBeNull();
+  });
+
+  it("does not suggest when the drop already wired the output port", () => {
+    // dragDropped links on both branches — dropping onto an existing edge, and

Review Comment:
   `links` is doing verb duty with no object, so the sentence never says what `dragDropped` links.
   ```suggestion
       // dragDropped adds links on both branches — dropping onto an existing edge, and
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762175426


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {
+    const jointCell = this.paper.getModelById(operatorId);
+    if (!jointCell) {
+      return null;
+    }
+    const bbox = jointCell.getBBox();
+    const scale = this.paper.scale();
+    const translate = this.paper.translate();
+    const screenX = (bbox.x + bbox.width) * scale.sx + translate.tx + 20;

Review Comment:
   Fixed a while back with the early return, and it now has a regression test: repositioning to an unchanged position asserts detectChanges is not called. I verified the test by removing the early return and confirming it fails.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "sahil-endee (via GitHub)" <gi...@apache.org>.
sahil-endee commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3790566173


##########
frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts:
##########
@@ -1080,3 +1080,20 @@ export function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = k
     (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler
   );
 }
+
+/**
+ * Observable of a JointJS graph's cell events (`change:position`, `add`, ...),
+ * emitting the cell the event fired on.
+ *
+ * The graph belongs to `JointGraphWrapper`, which is root-provided, so it
+ * outlives the components listening to it. Going through an Observable lets a
+ * listener be torn down with `untilDestroyed(this)` like any other subscription,
+ * where a raw `graph.on(...)` closure would leak.
+ */
+export function fromJointGraphCellEvent(graph: joint.dia.Graph, eventName: string): Observable<joint.dia.Cell> {

Review Comment:
   Done



##########
frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts:
##########
@@ -1080,3 +1080,20 @@ export function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = k
     (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler
   );
 }
+
+/**
+ * Observable of a JointJS graph's cell events (`change:position`, `add`, ...),
+ * emitting the cell the event fired on.
+ *
+ * The graph belongs to `JointGraphWrapper`, which is root-provided, so it
+ * outlives the components listening to it. Going through an Observable lets a
+ * listener be torn down with `untilDestroyed(this)` like any other subscription,
+ * where a raw `graph.on(...)` closure would leak.
+ */
+export function fromJointGraphCellEvent(graph: joint.dia.Graph, eventName: string): Observable<joint.dia.Cell> {

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3649277015


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();

Review Comment:
   I've fixed that issue with an early return. Please verify



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5308740786

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "chenlica (via GitHub)" <gi...@apache.org>.
chenlica commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5009835138

   @xuang7 and @zuozhiw : can you review it?


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762202280


##########
common/config/src/main/resources/gui.conf:
##########
@@ -108,6 +108,10 @@ gui {
     copilot-enabled = false
     copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
 
+    # whether the ambient operator recommender (ghost next-operator suggestions) is enabled
+    operator-recommendation-enabled = false
+    operator-recommendation-enabled = ${?GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED}

Review Comment:
   Fixed — added to all three, set to `false.` Listing it is what makes it reachable (config-service renders pod env strictly from `texeraEnvVars)`, and `false` keeps the default-off posture professor @chenlica asked for in #5240 while making it a one-line flip to enable.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5498353523

   @Yicong-Huang I feel like since #6436 has been marked as stale, we shouldn't wait any longer.


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3901203750


##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,166 @@
+/**
+ * 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 operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+  /** Recommended operator type (validated against the live catalog when available). */
+  operatorType: string;
+  /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+  score: number;
+  /** Short, human-readable rationale shown alongside the suggested 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.
+   *
+   * How many suggestions come back is the backend's call: it defaults to three and
+   * clamps any larger requested limit, so there is nothing useful to send from here.
+   *
+   * @param operator the operator that was just added to the canvas
+   */
+  public getRecommendations(operator: OperatorPredicate): Observable<OperatorRecommendation[]> {
+    // An operator with no output ports (e.g. a chart sink) has no port to anchor
+    // suggestions on, so we skip the backend call.

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5493445866

   @gupta-sahil01 shall we wait for #6436?


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3787428196


##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,166 @@
+/**
+ * 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 operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+  /** Recommended operator type (validated against the live catalog when available). */
+  operatorType: string;
+  /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+  score: number;
+  /** Short, human-readable rationale shown alongside the suggested 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.
+   *
+   * How many suggestions come back is the backend's call: it defaults to three
+   * and clamps anything larger, so there is nothing useful to send from here.

Review Comment:
   Done



##########
frontend/src/app/workspace/service/drag-drop/drag-drop.service.ts:
##########
@@ -75,10 +75,16 @@ export class DragDropService {
 
     this.workflowActionService.addOperatorsAndLinks([{ op: this.op, pos: coordinates }], newLinks);
     this.resetSuggestions();
-    this.operatorDroppedSubject.next();
+    this.operatorDroppedSubject.next(this.op);
   }
 
-  get operatorDropStream() {
+  /**
+   * Emits the operator a user just dropped onto the canvas, after it has been
+   * added to the graph. Unlike the graph's own operator-add stream, this fires
+   * only for interactive drag-drop placement — not for workflow load, undo/redo,
+   * paste, or a remote co-editor's edits.
+   */
+  get operatorDropStream(): Observable<OperatorPredicate> {

Review Comment:
   Added. expect(droppedOperator).toBe(newOp) — identity rather than toEqual, since a stale this.op would be a structurally similar predicate with a different ID and could slip past structural equality. Verified by mutating dragDropped to emit a fresh predicate of the same type: the test fails.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,205 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {

Review Comment:
   Fixed ours. There was a fromJointPaperEvent helper but no graph-model equivalent, so I added fromJointGraphCellEvent next to it and routed the listener through untilDestroyed(this) like its neighbours. Note it takes an explicit result selector — Backbone passes (cell, position, opt) and fromEventPattern would otherwise emit the arg array, which is why the paper helper is typed as Parameters<...>.
   
   Left 496 and 1673 alone since they predate this PR, but the helper is there for them — happy to convert both here if you'd rather it land together, or as a follow-up.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5009929032

   > Am I allowed to update the branches because I made some changes with respect to eliminating the word "ghost" from everywhere?
   
   of course. thought out the review please update the code as requested, or as you see fit.


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3761879171


##########
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 operator 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 suggested 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 suggestions on, so we skip the backend call.
+    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 suggestion 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

Review Comment:
   Left the caller discarding it deliberately. On failure there's nothing useful for the component to do, and dismissing the chips beats leaving dead ones on the canvas. The return isn't dead API either: the spec asserts on newId, including the new undefined case.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762182589


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {
+    const jointCell = this.paper.getModelById(operatorId);
+    if (!jointCell) {
+      return null;
+    }
+    const bbox = jointCell.getBBox();
+    const scale = this.paper.scale();
+    const translate = this.paper.translate();
+    const screenX = (bbox.x + bbox.width) * scale.sx + translate.tx + 20;

Review Comment:
    It's now `SUGGESTION_GAP_X,` declared with the other component fields. I named the chat popover's `40` as `CHAT_POPOVER_GAP_Y` too, since extracting the shared anchor helper put both next to each other.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762190695


##########
frontend/proxy.config.json:
##########
@@ -10,6 +10,11 @@
     "secure": false,
     "changeOrigin": true
   },
+  "/api/recommend": {

Review Comment:
   Fixed — /api/recommend now has a location pointing at agent-service:3001. I used the plain four-line REST form rather than copying /api/agents' WebSocket block, since this is a plain POST with no upgrade and no need for 1d timeouts.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3647468475


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();

Review Comment:
   Yes, thank you so much for the clarification. I'll get right on it!



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3628078570


##########
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
+   * suggestion 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));

Review Comment:
   You're right, thanks for catching this. getOperatorAddStream() is driven by the shared-model change handler, so it fires not only on interactive placement but also on workflow load, undo/redo, paste, and remote co-editor adds — each triggering a /api/recommend call and a suggestion popup. Not intended. So I'll gate the recommender the same way — switching the trigger to the drag-drop drop event (local, single, interactive) so load/undo/redo/paste/remote no longer fire it, plus a small debounce. One question: should a single-operator paste also surface suggestions, or only drag-drop placement?



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "xuang7 (via GitHub)" <gi...@apache.org>.
xuang7 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3648428339


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();

Review Comment:
   Thanks for helping clarify—yes, that’s what I meant.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3790567381


##########
frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts:
##########
@@ -1080,3 +1080,20 @@ export function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = k
     (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler
   );
 }
+
+/**
+ * Observable of a JointJS graph's cell events (`change:position`, `add`, ...),
+ * emitting the cell the event fired on.
+ *
+ * The graph belongs to `JointGraphWrapper`, which is root-provided, so it
+ * outlives the components listening to it. Going through an Observable lets a
+ * listener be torn down with `untilDestroyed(this)` like any other subscription,
+ * where a raw `graph.on(...)` closure would leak.
+ */
+export function fromJointGraphCellEvent(graph: joint.dia.Graph, eventName: string): Observable<joint.dia.Cell> {

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3787852640


##########
frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts:
##########
@@ -1080,3 +1080,20 @@ export function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = k
     (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler
   );
 }
+
+/**
+ * Observable of a JointJS graph's cell events (`change:position`, `add`, ...),
+ * emitting the cell the event fired on.
+ *
+ * The graph belongs to `JointGraphWrapper`, which is root-provided, so it
+ * outlives the components listening to it. Going through an Observable lets a
+ * listener be torn down with `untilDestroyed(this)` like any other subscription,
+ * where a raw `graph.on(...)` closure would leak.
+ */
+export function fromJointGraphCellEvent(graph: joint.dia.Graph, eventName: string): Observable<joint.dia.Cell> {

Review Comment:
   `fromJointPaperEvent` two lines up constrains its event name to `keyof joint.dia.Paper.EventMap`, so a wrong name is a compile error. This one takes a bare `string` while asserting a `Cell` payload. Backbone also emits graph-level events (`change`, `reset`, `sort`) that hand you the Graph, and the signature does not refuse them.
   
   No caller hits that today. It matters because the two raw listeners at `workflow-editor.component.ts:501` and `1678` are the obvious next ones.
   
   ```suggestion
   export function fromJointGraphCellEvent(
     graph: joint.dia.Graph,
     eventName: "add" | "remove" | "change:position"
   ): Observable<joint.dia.Cell> {
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,221 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here would be unrepealable; the chips must

Review Comment:
   `repeal` applies to statutes, not to a canvas edit. The point the clause before it makes is that `undoAction()` refuses while the lock is on, so the write cannot be taken back.
   
   ```suggestion
       // while the lock is on, so a click here could not be undone; the chips must
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,221 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here would be unrepealable; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Re-check the port: nothing dismisses the chips when the user hand-draws a
+    // link out of the anchor port, because that gesture starts on the port and
+    // fires element:pointerdown rather than the blank:pointerdown we listen for.
+    // Without this the click would add a second successor on top of the first.

Review Comment:
   The event name is wrong, and the comment no longer covers the whole condition.
   
   A port drag never fires `element:pointerdown`. In jointjs 3.5.4 `ElementView.onmagnet` calls `dragMagnetStart`, which stops propagation and hands off to `dragLinkStart`. `notifyPointerdown`, the only emitter of that event, is never reached, and there is no `element:magnet:pointerdown` either. Your conclusion holds and the guard is right — only the stated mechanism is off.
   
   The condition also leads with `checkWorkflowModificationEnabled()` now, which this comment does not mention.
   
   ```suggestion
       // Both conditions re-check state the chips cannot see. The lock can engage
       // between render and click; and nothing dismisses the chips when the user
       // hand-draws a link out of the anchor port, because that gesture is a magnet
       // drag that stops propagation before any pointerdown notification, so the
       // blank:pointerdown we listen for never fires. Without the port check the
       // click would add a second successor on top of the first.
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762211546


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ 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
+   * suggestion 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 readonly repositionSuggestion$ = new Subject<void>();

Review Comment:
   Fixed — `repositionSuggestion$` moved into the header block next to `chatPopoverOperator,` and the feature TSDoc now attaches to `handleOperatorRecommendation.` I also updated its first line, since the drag-drop gating made "when an operator is added" wrong.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3768889941


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream

Review Comment:
   No — suggesting after an auto-link isn't intended, and you're right that the fixed +100 offset makes it worse than noise. Added a `getAllLinks()` check in the trigger: if the dropped operator's anchor port already has an outgoing link, no request is made. Covers both `dragDropped` branches, since it tests the resulting graph rather than which branch ran.
   
   Two notes: it checks the anchor port specifically rather than the operator, so a multi-output operator with one port wired still gets suggestions on a free one; and it sits at the trigger next to the output-port guard rather than in the service, keeping the "should we ask" conditions together. Chaining is unaffected — the new operator's output is free.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -109,6 +113,22 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     position: { x: number; y: number };
   } | null = null;
 
+  // Ambient operator recommender state (apache/texera#5240). Holds the faded
+  // next-operator suggestions anchored on the output port of the
+  // operator that was just added; null when nothing is being suggested.
+  public operatorSuggestion: {

Review Comment:
   Renamed the component-side symbols to `nextOperatorSuggestion*` as suggested — fields, methods, and the CSS classes, which carried the same ambiguous prefix. `OperatorRecommendationService`, the `OperatorRecommendation` wire type and its methods stay as-is, aligned with the backend. `DragDropService` is untouched, so it's now the sole owner of `operatorSuggestion*` / `SUGGESTION_DISTANCE_THRESHOLD` / `handleOperatorRecommendationOnDrag`.
   
   One thing I left: the `operatorRecommendationEnabled` config flag and its `GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED` env var. That's deployment-facing config matching the backend feature name rather than a component symbol, and renaming it would touch `gui.conf`, `GuiConfig.scala`, `ConfigResource.scala`, the frontend type and mock, and all three values/env files. Happy to include it if you'd rather it move too.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3769696258


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream

Review Comment:
   The request-time half is verified — `portIsTaken` at 1854-1861, with a test at spec:1717. Leaving this open for the other half, and for one correction.
   
   The click path has no equivalent check. `materializeNextOperatorSuggestion` re-reads the source operator and re-checks `hasOperator` (1902) but never re-checks the port, and nothing dismisses the chips when a link appears on it — link drags start on a port, so they fire `element:pointerdown`, not the `blank:pointerdown` at 1809 that dismisses. Draw a link out of the anchor port while the chips are up, then click a chip, and you get the second successor at the same fixed offset. Separate comment on 1902.
   
   The correction is to your second note: the code only ever looks at `outputPorts[0]` — at 1854 for the guard and 1883 for the rendered state — so a multi-output operator whose *first* port is wired gets no suggestions at all, rather than suggestions on a free one. Conservative and fine as behaviour; just not what the note describes.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5826860776

   Any updates here? @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-6330086141

   @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "xuang7 (via GitHub)" <gi...@apache.org>.
xuang7 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3641142695


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();

Review Comment:
   repositionRecommendations() calls detectChanges() unconditionally, and it is driven by the change:position listener (line 1767), which fires on every frame while an operator is dragged. So change detection runs per frame whenever suggestions are showing. Consider early-returning when the position is unchanged, or debouncing.



##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {
+    const jointCell = this.paper.getModelById(operatorId);
+    if (!jointCell) {
+      return null;
+    }
+    const bbox = jointCell.getBBox();
+    const scale = this.paper.scale();
+    const translate = this.paper.translate();
+    const screenX = (bbox.x + bbox.width) * scale.sx + translate.tx + 20;

Review Comment:
   The chip offset here uses a bare + 20 (screen px). We could pull it into a named constant for clarity.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3787837123


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,205 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {

Review Comment:
   Verified: 1844-1846 goes through the helper with `untilDestroyed(this)`, and the result selector is right — `change:position` emits `(cell, position, opt)`, so without it `fromEventPattern` would hand you the arg array.
   
   Follow-up for 501 and 1678, not here. Both are in `main` and neither is what this PR is about; converting them would put an unrelated lifecycle fix inside a feature diff that is already five rounds deep.
   
   One note on the helper's signature, left as a separate comment on `joint-ui.service.ts`.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3892025193


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,223 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here could not be undone; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Both conditions re-check state the chips cannot see. The lock can engage
+    // between render and click; and nothing dismisses the chips when the user
+    // hand-draws a link out of the anchor port, because that gesture is a magnet
+    // drag that stops propagation before any pointerdown notification, so the
+    // blank:pointerdown we listen for never fires. Without the port check the
+    // click would add a second successor on top of the first.

Review Comment:
   The `if` below has three conjuncts, so "Both" undercounts and `hasOperator` goes unnamed. Singling out the lock and the port is right: they are the state no dismissal covers, while `hasOperator` only backs up `getOperatorDeleteStream()` at :1835. The comment never says that, so a reader counting conjuncts stalls on the sentence meant to explain them.
   
   This was my wording in the round-2 thread, so the miss is mine.
   
   ```suggestion
       // The lock check and the port check re-check state the chips cannot see. The
       // lock can engage between render and click; and nothing dismisses the chips
       // when the user hand-draws a link out of the anchor port, because that gesture
       // is a magnet drag that stops propagation before any pointerdown notification,
       // so the blank:pointerdown we listen for never fires. Without the port check
       // the click would add a second successor on top of the first. The hasOperator
       // check is belt-and-braces: the delete stream above already dismisses.
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3896940896


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,223 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here could not be undone; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Both conditions re-check state the chips cannot see. The lock can engage
+    // between render and click; and nothing dismisses the chips when the user
+    // hand-draws a link out of the anchor port, because that gesture is a magnet
+    // drag that stops propagation before any pointerdown notification, so the
+    // blank:pointerdown we listen for never fires. Without the port check the
+    // click would add a second successor on top of the first.

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5482249652

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762189100


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ 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
+   * suggestion 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 readonly repositionSuggestion$ = new Subject<void>();
+
+  private handleOperatorRecommendation(): void {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    const prev = this.operatorSuggestion.position;
+    if (!position || (position.x === prev.x && position.y === prev.y)) {
+      return;
+    }
+    this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {

Review Comment:
   Extracted. Both overlays now go through getOperatorAnchorPosition(operatorId, anchor), which takes the anchor point as box-relative fractions plus screen-pixel offsets; the chat popover is {0.5, 1, offsetY: 40} and the suggestions are {1, 0.5, offsetX: 20}. The transform and the null-cell guard exist once.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762170687


##########
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
+   * suggestion 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));

Review Comment:
   Fixed. The trigger is now the drag-drop drop event rather than getOperatorAddStream(), so workflow load, undo/redo, paste, and remote co-editor adds no longer fire requests. On your open question: paste does not surface suggestions — #5240 frames this as code-completion for canvas authoring, and completions don't fire on paste. No debounce in the end; drops are discrete gestures with nothing to coalesce. Guarded by a test that adds two operators via addOperator and asserts zero requests.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762208044


##########
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 operator 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 suggested 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[]> {

Review Comment:
   Dropped. The backend defaults to 3 and clamps to 5, which is exactly the 1–3 the design calls for, so there's nothing useful to send. `RecommendationRequest.limit` stays in the contract, so re-adding it later is non-breaking.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-4982898860

   <!-- texera-benchmarks-comment -->
   ## ⚠️ Benchmark changes need a look
   
   🟢 2 better · 🔴 3 worse · ⚪ 10 noise (<±5%) · 0 without baseline
   
   > Compared against main `181e38b` benchmarked on **this same runner**, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.
   
   [Dashboard](https://apache.github.io/texera/dev/bench/) · [Run](https://github.com/apache/texera/actions/runs/29393770072)
   
   | | config | throughput | MB/s | latency | max Δ latest / 7d |
   |---|---|---:|---:|---:|---:|
   | 🔴 | bs=10 sw=10 sl=64 | 373 | 0.228 | 26,923/32,355/32,355 us | 🔴 +13.4% / 🔴 +131.7% |
   | ⚪ | bs=100 sw=10 sl=64 | 793 | 0.484 | 125,476/134,482/134,482 us | ⚪ within ±5% / 🔴 +37.5% |
   | ⚪ | bs=1000 sw=10 sl=64 | 913 | 0.557 | 1,100,195/1,126,374/1,126,374 us | ⚪ within ±5% / 🔴 +21.9% |
   
   <details><summary>Baseline details</summary>
   
   Latest main `181e38b` from same runner
   
   | config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
   |---|---|---:|---:|---:|---:|---:|
   | bs=10 sw=10 sl=64 | throughput | 373 tuples/sec | 411 tuples/sec | 847.3 tuples/sec | -9.2% | -56.0% |
   | bs=10 sw=10 sl=64 | MB/s | 0.228 MB/s | 0.251 MB/s | 0.517 MB/s | -9.2% | -55.9% |
   | bs=10 sw=10 sl=64 | p50 | 26,923 us | 23,740 us | 11,621 us | +13.4% | +131.7% |
   | bs=10 sw=10 sl=64 | p95 | 32,355 us | 35,239 us | 14,154 us | -8.2% | +128.6% |
   | bs=10 sw=10 sl=64 | p99 | 32,355 us | 35,239 us | 19,069 us | -8.2% | +69.7% |
   | bs=100 sw=10 sl=64 | throughput | 793 tuples/sec | 831 tuples/sec | 1,104 tuples/sec | -4.6% | -28.2% |
   | bs=100 sw=10 sl=64 | MB/s | 0.484 MB/s | 0.507 MB/s | 0.674 MB/s | -4.5% | -28.2% |
   | bs=100 sw=10 sl=64 | p50 | 125,476 us | 120,718 us | 91,279 us | +3.9% | +37.5% |
   | bs=100 sw=10 sl=64 | p95 | 134,482 us | 131,993 us | 97,771 us | +1.9% | +37.5% |
   | bs=100 sw=10 sl=64 | p99 | 134,482 us | 131,993 us | 107,798 us | +1.9% | +24.8% |
   | bs=1000 sw=10 sl=64 | throughput | 913 tuples/sec | 932 tuples/sec | 1,127 tuples/sec | -2.0% | -19.0% |
   | bs=1000 sw=10 sl=64 | MB/s | 0.557 MB/s | 0.569 MB/s | 0.688 MB/s | -2.1% | -19.0% |
   | bs=1000 sw=10 sl=64 | p50 | 1,100,195 us | 1,075,607 us | 902,553 us | +2.3% | +21.9% |
   | bs=1000 sw=10 sl=64 | p95 | 1,126,374 us | 1,101,228 us | 941,491 us | +2.3% | +19.6% |
   | bs=1000 sw=10 sl=64 | p99 | 1,126,374 us | 1,101,228 us | 979,025 us | +2.3% | +15.1% |
   
   </details>
   
   <details><summary>Raw CSV</summary>
   
   ```csv
   config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
   0,10,10,64,20,536.36,200,128000,373,0.228,26923.17,32355.08,32355.08
   1,100,10,64,20,2522.41,2000,1280000,793,0.484,125475.72,134482.36,134482.36
   2,1000,10,64,20,21901.91,20000,12800000,913,0.557,1100194.50,1126374.01,1126374.01
   ```
   
   </details>
   


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "Ma77Ball (via GitHub)" <gi...@apache.org>.
Ma77Ball commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3610274873


##########
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
+   * suggestion 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));

Review Comment:
   Optional performance fix: getOperatorAddStream triggers a POST /api/recommend on every add, including loading a saved workflow, pasting, and undo/redo. I recommend gating this to interactive ads or suppressing while a workflow is being restored.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3761879171


##########
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 operator 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 suggested 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 suggestions on, so we skip the backend call.
+    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 suggestion 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

Review Comment:
   Left the caller discarding it deliberately. On failure there's nothing useful for the component to do, and dismissing the chips beats leaving dead ones on the canvas. The return isn't dead API either: the spec asserts on newId, including the new undefined case.



##########
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 operator 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 suggested 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 suggestions on, so we skip the backend call.
+    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 suggestion 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

Review Comment:
   The return value is now consumed: materializeRecommendation uses it to chain a fresh round of suggestions onto the operator it just created, and skips chaining when materializing failed.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762213833


##########
common/config/src/main/resources/gui.conf:
##########
@@ -108,6 +108,10 @@ gui {
     copilot-enabled = false
     copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
 
+    # whether the ambient operator recommender (ghost next-operator suggestions) is enabled

Review Comment:
   Done



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ 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
+   * suggestion 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 readonly repositionSuggestion$ = new Subject<void>();
+
+  private handleOperatorRecommendation(): void {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3764522889


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream

Review Comment:
   `dragDropped` links the dropped operator before it emits, on both branches — `createEdgeReconnectionLinks` for a drop onto an existing edge (drag-drop.service.ts:71), and `getNewOperatorLinks(..., suggestionOutputs)` for proximity auto-linking (73).
   
   So the chips can anchor on a port that already has a successor, and `materialize`'s fixed +100 offset can land on top of it. Is suggesting after an auto-link intended? If not, a `getAllLinks()` check on the source port skips it cheaply.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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)

Review Comment:
   Reinforcing Copilot's cancellation point (r3600302103) — that thread was resolved without a code change.
   
   This subscription is never cancelled. Drop A then drop B: response A can land last and show A's chips. Blank-click to dismiss: a late response re-opens the overlay, because `closeRecommendations` (1885) only nulls the state.
   
   I'd route the drop stream and `materializeRecommendation`'s chaining call through one subject and `switchMap` it — that buys cancellation and ordering together. A request token fixes only the first case.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -109,6 +113,22 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     position: { x: number; y: number };
   } | null = null;
 
+  // Ambient operator recommender state (apache/texera#5240). Holds the faded
+  // next-operator suggestions anchored on the output port of the
+  // operator that was just added; null when nothing is being suggested.
+  public operatorSuggestion: {

Review Comment:
   `DragDropService` already owns this vocabulary for a different feature: `handleOperatorRecommendationOnDrag` (drag-drop.service.ts:114), `operatorSuggestionHighlightStream`, `resetSuggestions`, `SUGGESTION_DISTANCE_THRESHOLD` — proximity *link* suggestions. The new emit sits three lines below that handler's call site.
   
   Grepping either term now returns both features, indistinguishable. I'd rename the component-side symbols to `nextOperatorSuggestion*`, keeping the service and the wire types aligned with the backend's `OperatorRecommendation`.
   
   Partly downstream of the ghost→suggestion rename, so a consequence of review rather than an oversight.



##########
bin/single-node/.env:
##########
@@ -86,6 +86,7 @@ FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT=http://file-service:9092/api/da
 
 # Toggles the texera agent panel; set false to hide it in the GUI.
 GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED=true
+GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED=false

Review Comment:
   This lands directly under the copilot comment with no separator, so `# Toggles the texera agent panel` now reads as documenting both flags. Every other group in the file gets its own comment block.
   ```suggestion
   
   # Toggles the ambient operator recommender (next-operator suggestions on the canvas).
   GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED=false
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html:
##########
@@ -46,4 +46,24 @@
       </div>
     </div>
   </div>
+
+  <!-- Ambient operator recommender: operator suggestions on the output port -->
+  @if (operatorSuggestion) {
+  <div
+    class="operator-recommendation-container"
+    [style.left.px]="operatorSuggestion.position.x"
+    [style.top.px]="operatorSuggestion.position.y">
+    @for (recommendation of operatorSuggestion.recommendations; track recommendation.operatorType) {

Review Comment:
   `operatorType` comes from the backend, so it is not a guaranteed-unique track key, and duplicates raise NG0955 — a hard render failure inside a feature that swallows errors everywhere else. I can't confirm from this branch whether the ranker can emit duplicates, but `$index` is free.
   ```suggestion
       @for (recommendation of operatorSuggestion.recommendations; track $index) {
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      newOperatorID = this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+
+    // Chaining has to be explicit: a materialized operator reaches the graph
+    // through addOperatorsAndLinks, not through a drop, so the drop-gated
+    // trigger above will never fire for it.
+    if (newOperatorID !== undefined && graph.hasOperator(newOperatorID)) {
+      this.showRecommendationsFor(graph.getOperator(newOperatorID));
+    }
+  }
+
+  closeRecommendations(): void {

Review Comment:
   No template binding for this one — the markup only calls `materializeRecommendation`.
   ```suggestion
     private closeRecommendations(): void {
   ```



##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -159,6 +159,35 @@ spec:
           port: 3001
 {{- end }}
 ---
+# Operator Recommender Route (apache/texera#5240). Served by the agent-service,
+# but deliberately kept out of the agent-service HTTPRoute: that route is the
+# target of the agent BackendTrafficPolicy, which pins requests to a replica by
+# consistent-hashing X-Agent-Workflow-Id. The recommender is stateless and sends
+# no such header, so it must not inherit that affinity. Longest-prefix matching

Review Comment:
   The recommender *receives* these requests; it is the frontend client that omits the header. Worth being exact, since this comment is the entire justification for splitting the route out.
   ```suggestion
   # consistent-hashing X-Agent-Workflow-Id. The recommender is stateless and its
   # requests carry no such header, so it must not inherit that affinity. Longest-prefix matching
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5297249951

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3891580008


##########
frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts:
##########
@@ -1080,3 +1080,27 @@ export function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = k
     (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler
   );
 }
+
+/**
+ * Observable of a JointJS graph event that reports a cell, emitting that cell.
+ *
+ * The event name is restricted to the cell-reporting events: graph-level ones
+ * (`change`, `reset`, `sort`) hand the handler the Graph itself, which would
+ * make the `Cell` payload a lie. Widen the union only alongside a check that
+ * the new event reports a cell.

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-6333836769

   please resolve the conflicts and we can merge this


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5009611199

   > How about just call them "suggested operators"? On the UI, the suggested operators are rendered with faded, blurred effect, but they are real suggestion instances. "ghost" the word has a negative semantic and I'd like to avoid it, and it is bundled with its UI effect: if we were to change such operators UI effect to be not blurred, the naming will be obsolete.
   
   Well, the word "ghost" doesn't appear directly in the UI. When we drag an operator from the panel, the 3 suggestions will show up with it.


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3647413155


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();

Review Comment:
   for angular, it uses `this.changeDetectorRef.detectChanges();` and when that happens, it recalculates all the pixels and re-renders the frontend components. 
   
   I think @xuang7 's point is that such re-render might be too frequent. it will be laggy or blinky on the UX. She suggested for 
   - early return (do not invoke `detectChanges()`) if position has not changed: so that no re-render if everything would stay the same.
   - debouncing: do not rerender right away, after you get a few frames of changes, render them all at once. the debouncing could be controlled by number of invocations here (i.e., when entering this function 30 times), or by time (i.e., every 200/300 ms). 
   
   I think both suggestions are valid, if the UX is really bad. @gupta-sahil01 does that answer your question?



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "chenlica (via GitHub)" <gi...@apache.org>.
chenlica commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-4989160298

   
   @xuang7 @zuozhiw : Per the discussion https://github.com/apache/texera/discussions/5240, can you review it?


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5000096473

   Hi @gupta-sahil01 a high level comment: let's not name it `ghost` suggestions. Let's find a better name for it?


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3770352168


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -109,6 +113,22 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     position: { x: number; y: number };
   } | null = null;
 
+  // Ambient operator recommender state (apache/texera#5240). Holds the faded
+  // next-operator suggestions anchored on the output port of the
+  // operator that was just added; null when nothing is being suggested.
+  public operatorSuggestion: {

Review Comment:
   Got it.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3769693256


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -109,6 +113,22 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     position: { x: number; y: number };
   } | null = null;
 
+  // Ambient operator recommender state (apache/texera#5240). Holds the faded
+  // next-operator suggestions anchored on the output port of the
+  // operator that was just added; null when nothing is being suggested.
+  public operatorSuggestion: {

Review Comment:
   Verified: the component-side symbols are all `nextOperatorSuggestion*` now (119, 1774-1953) and the SCSS classes match, so `DragDropService` is the sole owner of `operatorSuggestion*` / `SUGGESTION_DISTANCE_THRESHOLD`. Resolving.
   
   On your open question — leave the config flag alone. `operatorRecommendationEnabled` and its env var are deployment-facing names that should track the backend feature, and the collision I was worried about was between two sets of *component* symbols. Renaming five files to fix a problem that no longer exists isn't worth it.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3741382850


##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -154,6 +154,11 @@ spec:
         - path:
             type: PathPrefix
             value: /api/agents
+        # Ambient operator recommender endpoint (apache/texera#5240) is served

Review Comment:
   Worth settling the auth posture now rather than after V2. For V1 leaving it open is defensible: it matches the `/api/agents` sibling, #6293 specifies no user token, and the payload is operator-type strings with no user-owned entity.
   
   But #6293's V2 puts a LiteLLM-backed model behind this same path, and an anonymous LLM endpoint is a cost and abuse surface. Is the plan to add auth when V2 lands?



##########
common/config/src/main/resources/gui.conf:
##########
@@ -108,6 +108,10 @@ gui {
     copilot-enabled = false
     copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
 
+    # whether the ambient operator recommender (ghost next-operator suggestions) is enabled
+    operator-recommendation-enabled = false
+    operator-recommendation-enabled = ${?GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED}

Review Comment:
   `GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED` is absent from `bin/k8s/values.yaml`, `values-development.yaml`, and `bin/single-node/.env`. config-service's pod env renders strictly from `.Values.texeraEnvVars` (`config-service-deployment.yaml:41-52`), so an unlisted var never reaches the container and the flag keeps its `false` default.
   
   Every sibling GUI flag is listed — `COPILOT_ENABLED` at `values.yaml:344`, `values-development.yaml:341`, `.env:88` — so this reads as a gap rather than a deliberate omission.



##########
frontend/proxy.config.json:
##########
@@ -10,6 +10,11 @@
     "secure": false,
     "changeOrigin": true
   },
+  "/api/recommend": {

Review Comment:
   The dev proxy is covered, but `bin/single-node/nginx.conf` is not. It routes `/api/agents` to `agent-service:3001` (line 73) and has no `/api/recommend` location, so longest-prefix matching hands the call to `location /api/` and `dashboard-service:8080` (line 91).
   
   With `catchError(() => of([]))` swallowing the result, the feature is silently inert in single-node docker with the flag on — no console error to explain it.



##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -154,6 +154,11 @@ spec:
         - path:
             type: PathPrefix
             value: /api/agents
+        # Ambient operator recommender endpoint (apache/texera#5240) is served
+        # by the agent-service alongside /api/agents.
+        - path:
+            type: PathPrefix
+            value: /api/recommend

Review Comment:
   This route is not a neutral container. Its own comment says it is split out so the `BackendTrafficPolicy` can target it, and `gateway-agent-traffic-policy.yaml:28-37` pins the whole route to `ConsistentHash` on `X-Agent-Workflow-Id`. That policy's comment states the client stamps the header on every agent request. `AgentService` does (`agent.service.ts:238`); `OperatorRecommendationService` sends none.
   
   #6293 specifies the recommender as stateless, so it should not want affinity at all. Its own HTTPRoute, outside the policy's `targetRefs`, sidesteps this. Could you confirm what Envoy does when the hash header is absent?



##########
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 operator 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 suggested 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 suggestions on, so we skip the backend call.
+    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 suggestion 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

Review Comment:
   Reinforcing Copilot's r3600302078 rather than re-raising it. The documented `undefined` return is not merely unused, it is unreachable: the body has exactly one return (`return newOperator.operatorID`, line 150) and no early return.
   
   The failure that can happen — `getNewOperatorPredicate` throwing on an unknown operator type (`workflow-util.service.ts:116-119`) — is neither caught nor documented, and the sole caller ignores the return value. So the fix is not only to catch the throw: the declared `string | undefined` contract describes a branch that does not exist.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ 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
+   * suggestion 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 readonly repositionSuggestion$ = new Subject<void>();
+
+  private handleOperatorRecommendation(): void {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    const prev = this.operatorSuggestion.position;
+    if (!position || (position.x === prev.x && position.y === prev.y)) {
+      return;
+    }
+    this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {

Review Comment:
   This is `getOperatorChatPopoverPosition` (line 1693) with different anchor offsets: same `getModelById` to `getBBox()` to `scale()` to `translate()` sequence, same null-cell guard, nothing shared, and neither helper mentions the other.
   
   Nothing is wrong today. The cost is the next fix to that transform landing on one copy and silently mis-anchoring the other. Suggest extracting one helper that takes the anchor offsets — the description's "reusing the chat-popover anchoring pattern" makes the drift likelier, not less.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ 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
+   * suggestion 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 readonly repositionSuggestion$ = new Subject<void>();

Review Comment:
   The feature-overview TSDoc block above this line binds to `repositionSuggestion$`, so tooling shows the whole "Ambient operator recommender..." description on a `Subject`.
   
   The field also sits in the method-body region, while every other field in the class — including its direct sibling `chatPopoverOperator` at line 109 — is declared in the header block at 99-123.



##########
common/config/src/main/resources/gui.conf:
##########
@@ -108,6 +108,10 @@ gui {
     copilot-enabled = false
     copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
 
+    # whether the ambient operator recommender (ghost next-operator suggestions) is enabled

Review Comment:
   Last PR-introduced occurrence of "ghost" in the tree — the rename reached every other site.
   
   ```suggestion
       # whether the ambient operator recommender (faded next-operator suggestions) is enabled
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ 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
+   * suggestion 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 readonly repositionSuggestion$ = new Subject<void>();
+
+  private handleOperatorRecommendation(): void {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.

Review Comment:
   The subscribed stream is `getWorkflowEditorZoomStream()`, whose only producer is `setZoomProperty` (`joint-graph-wrapper.ts:575`). Pan is `handlePaperPan`'s `blank:pointerdown` plus `mousemove` to `paper.translate` (lines 551-562) and emits nothing there. Harmless, since the same `blank:pointerdown` dismisses the overlay, but the comment claims behaviour the code does not implement.
   
   ```suggestion
       // Keep the suggestions anchored on zoom.
   ```



##########
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 operator 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 suggested 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

Review Comment:
   In Texera a *source* operator is defined by having no **input** ports, so "source-less" reads as "has inputs" — the opposite axis from the guard on the next line, which tests output ports.
   
   ```suggestion
       // An operator with no output ports (e.g. a chart sink) has no port to anchor on
   ```



##########
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 operator 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 suggested 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[]> {

Review Comment:
   `limit` has no caller: the spread at line 104 omits it, and `editor.ts:1802` plus all five spec calls pass nothing. Suggest dropping it until a second use arrives. As it stands it is an untested parameter reaching a wire payload whose backend contract is not on this branch yet.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5298600791

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3786771925


##########
frontend/src/app/workspace/service/drag-drop/drag-drop.service.ts:
##########
@@ -75,10 +75,16 @@ export class DragDropService {
 
     this.workflowActionService.addOperatorsAndLinks([{ op: this.op, pos: coordinates }], newLinks);
     this.resetSuggestions();
-    this.operatorDroppedSubject.next();
+    this.operatorDroppedSubject.next(this.op);
   }
 
-  get operatorDropStream() {
+  /**
+   * Emits the operator a user just dropped onto the canvas, after it has been
+   * added to the graph. Unlike the graph's own operator-add stream, this fires
+   * only for interactive drag-drop placement — not for workflow load, undo/redo,
+   * paste, or a remote co-editor's edits.
+   */
+  get operatorDropStream(): Observable<OperatorPredicate> {

Review Comment:
   The payload is the point of this change, and nothing asserts it. `drag-drop.service.spec.ts:397` does `subscribe(() => (dropped = true))`. A `dragDropped` that emitted the wrong operator — or a stale `this.op` — would still pass, and the recommender specs stub this producer entirely.
   
   One line in that existing test closes it: capture the emission and assert it is the operator that was dropped. Optional, and my miss.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,205 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {

Review Comment:
   This one listener isn't torn down. `attachMainJointPaper` points the paper at `JointGraphWrapper`'s own graph (`joint-graph-wrapper.ts:201`), which is root-provided. So the closure outlives the component, while the six subscriptions around it all use `untilDestroyed(this)`.
   
   It matches lines 496 and 1673, so this is a consistency note rather than something this PR broke. One shared helper would fix all three. Also my miss.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,205 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+        this.repositionNextOperatorSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Re-check the port: nothing dismisses the chips when the user hand-draws a
+    // link out of the anchor port, because that gesture starts on the port and
+    // fires element:pointerdown rather than the blank:pointerdown we listen for.
+    // Without this the click would add a second successor on top of the first.
+    if (
+      graph.hasOperator(this.nextOperatorSuggestion.operatorId) &&

Review Comment:
   This guard covers the anchor operator and its port, but not the workflow-modification lock. The chips are the only authoring affordance in the workspace that skips it.
   
   Drop an operator, click Run, then click a chip. `execute-workflow.service.ts:400` has already called `disableWorkflowModification()`. Nothing here dismisses the chips — only `blank:pointerdown`, an anchor delete, or a new request do. And `addOperatorsAndLinks` checks nothing itself, so the operator and link land in the shared model mid-execution. `undoRedoService.undoAction()` then refuses while the lock is on, so the user can't take it back. Version preview (`workflow-version.service.ts:109`) and read-only hub views reach it the same way.
   
   I'd subscribe to `getWorkflowModificationEnabledStream()` here and close the suggestions when it emits false. That also stops the chips looking clickable while the canvas is frozen, which is what the paper does at 307-321. Adding the check to this condition is smaller but leaves dead chips on screen.
   
   My miss from earlier rounds.



##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,166 @@
+/**
+ * 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 operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+  /** Recommended operator type (validated against the live catalog when available). */
+  operatorType: string;
+  /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+  score: number;
+  /** Short, human-readable rationale shown alongside the suggested 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.
+   *
+   * How many suggestions come back is the backend's call: it defaults to three
+   * and clamps anything larger, so there is nothing useful to send from here.

Review Comment:
   "anything larger" has no antecedent here — the method sends no count, so a reader can't tell what is being clamped.
   ```suggestion
      * How many suggestions come back is the backend's call: it defaults to three and
      * clamps any larger requested limit, so there is nothing useful to send from here.
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-6357639132

   > please resolve the conflicts and we can merge this
   
   Done


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5009623593

   Not only about UI, but also for code comments, variable names as well as GitHub discussion, pr titles and description, etc. can we avoid this naming in general? Thanks


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5005515726

   How about just call them "suggested operators"? On the UI, the suggested operators are rendered with faded, blurred effect, but they are real suggestion instances. "ghost" the word has a negative semantic and I'd like to avoid it, and it is bundled with its UI effect: if we were to  change such operators UI effect to be not blurred, the naming will be obsolete. 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "xuang7 (via GitHub)" <gi...@apache.org>.
xuang7 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-4997146358

   > @xuang7 @zuozhiw : Per the discussion #5240, can you review it?
   
   I'm not very familiar with this part of the codebase, so I'll leave general comments where I can. It would probably be helpful to assign someone more familiar with this area for a thorough review.


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3617681428


##########
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
+   * suggestion 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));

Review Comment:
   Hey, could you be a little more specific about the requirement?



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3770472905


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream

Review Comment:
   Correction accepted — my note was wrong. Only outputPorts[0] is considered, at the guard and in the rendered state, so a multi-output operator whose first port is wired gets no suggestions rather than suggestions on a free port. Conservative and intentional for V1; multi-port anchoring would need the overlay to render per-port.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3768891573


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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)

Review Comment:
   Fixed



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762175426


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {
+    const jointCell = this.paper.getModelById(operatorId);
+    if (!jointCell) {
+      return null;
+    }
+    const bbox = jointCell.getBBox();
+    const scale = this.paper.scale();
+    const translate = this.paper.translate();
+    const screenX = (bbox.x + bbox.width) * scale.sx + translate.tx + 20;

Review Comment:
   Fixed a while back with the early return, and it now has a regression test: repositioning to an unchanged position asserts detectChanges is not called. I verified the test by removing the early return and confirming it fails.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5260106296

   ## [Codecov](https://app.codecov.io/gh/apache/texera/pull/6437?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :white_check_mark: All modified and coverable lines are covered by tests.
   :white_check_mark: Project coverage is 87.88%. Comparing base ([`e878df3`](https://app.codecov.io/gh/apache/texera/commit/e878df3eee7ca3baee7b0c34eceb63548c59e68a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`e797a9b`](https://app.codecov.io/gh/apache/texera/commit/e797a9b6f0cc01e32f8d82ff402fbbd67f39610f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   :warning: Report is 1 commits behind head on main.
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   ```diff
   @@             Coverage Diff              @@
   ##               main    #6437      +/-   ##
   ============================================
   - Coverage     87.88%   87.88%   -0.01%     
     Complexity     4276     4276              
   ============================================
     Files          1176     1176              
     Lines         47018    47013       -5     
     Branches       5245     5245              
   ============================================
   - Hits          41323    41318       -5     
     Misses         3972     3972              
     Partials       1723     1723              
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | | *Carryforward flag |
   |---|---|---|---|
   | [access-control-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `70.00% <ø> (ø)` | | |
   | [agent-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `98.62% <ø> (ø)` | | Carriedforward from [e878df3](https://app.codecov.io/gh/apache/texera/commit/e878df3eee7ca3baee7b0c34eceb63548c59e68a?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [amber](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `82.88% <ø> (ø)` | | Carriedforward from [e878df3](https://app.codecov.io/gh/apache/texera/commit/e878df3eee7ca3baee7b0c34eceb63548c59e68a?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [computing-unit-managing-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `60.38% <ø> (ø)` | | |
   | [config-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `66.32% <100.00%> (+0.34%)` | :arrow_up: | |
   | [file-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `69.05% <ø> (ø)` | | |
   | [frontend](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `89.94% <ø> (-0.01%)` | :arrow_down: | Carriedforward from [e878df3](https://app.codecov.io/gh/apache/texera/commit/e878df3eee7ca3baee7b0c34eceb63548c59e68a?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [notebook-migration-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `78.89% <ø> (ø)` | | |
   | [pyamber](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `97.52% <ø> (ø)` | | Carriedforward from [e878df3](https://app.codecov.io/gh/apache/texera/commit/e878df3eee7ca3baee7b0c34eceb63548c59e68a?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [workflow-compiling-service](https://app.codecov.io/gh/apache/texera/pull/6437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `26.31% <ø> (ø)` | | |
   
   *This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to find out more.
   </details>
   
   [:umbrella: View full report in Codecov by Harness](https://app.codecov.io/gh/apache/texera/pull/6437?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
   :loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3900062289


##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,166 @@
+/**
+ * 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 operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+  /** Recommended operator type (validated against the live catalog when available). */
+  operatorType: string;
+  /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+  score: number;
+  /** Short, human-readable rationale shown alongside the suggested 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.
+   *
+   * How many suggestions come back is the backend's call: it defaults to three and
+   * clamps any larger requested limit, so there is nothing useful to send from here.
+   *
+   * @param operator the operator that was just added to the canvas
+   */
+  public getRecommendations(operator: OperatorPredicate): Observable<OperatorRecommendation[]> {
+    // An operator with no output ports (e.g. a chart sink) has no port to anchor
+    // suggestions on, so we skip the backend call.

Review Comment:
   The sentence and the guard are right, but the example names a shape no operator has. `OperatorInfo.forVisualization` hardcodes `outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))` (`OperatorMetadataGenerator.scala:50-61`), which `BarChartOpDesc` calls — so a chart has an output port, and dropping one *does* produce chips. The catalog has no zero-output operator outside test stubs, so there is nothing to substitute: dropping the parenthetical is the fix.
   
   This is my own round-1 wording, so the miss is mine.
   
   ```suggestion
       // An operator with no output ports has no port to anchor suggestions on,
       // so we skip the backend call.
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,224 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here could not be undone; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.

Review Comment:
   Same sentence as the one on `operator-recommendation.service.ts:91`, same fix — one finding, two places.
   
   ```suggestion
       // An operator with no output ports has no port to anchor suggestions on,
       // so there is nothing to ask for.
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3887775622


##########
frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts:
##########
@@ -1080,3 +1080,27 @@ export function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = k
     (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler
   );
 }
+
+/**
+ * Observable of a JointJS graph event that reports a cell, emitting that cell.
+ *
+ * The event name is restricted to the cell-reporting events: graph-level ones
+ * (`change`, `reset`, `sort`) hand the handler the Graph itself, which would
+ * make the `Cell` payload a lie. Widen the union only alongside a check that
+ * the new event reports a cell.

Review Comment:
   The union is right; two of the three examples are not. Backbone 1.4.1 fires `reset` and `sort` as `(collection, options)` (`backbone.js:965`, `:1045`); the graph re-triggers them verbatim, so a handler gets the cells collection, not the Graph. And `change` is re-triggered per cell by `_onModelEvent` (`:1210-1222`), so it usually does deliver a Cell.
   
   The next line tells a maintainer to widen the union only for cell-reporting events, and `change` is the name they would wrongly rule out. The slip is mine — it was my wording in the thread that asked for the narrowing.
   
   ```suggestion
    * The event name is restricted to events that report a cell: `reset` and `sort`
    * hand the handler the cells collection instead, and `change` reports a cell only
    * when a cell changed — setting a graph attribute delivers the Graph. Widen the
    * union only alongside a check that the new event reports a cell.
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5473465576

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3901206227


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,224 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here could not be undone; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.

Review Comment:
   It was actually in 3 places. The PR description had that wording too but I changed it.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-4982730347

   great feature, could you please include screenshots/gifs in the PR description?


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3642031197


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();

Review Comment:
   Not exactly understanding the requirement here. Maybe a screenshot will help if you could please?



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3790567106


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,221 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here would be unrepealable; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Re-check the port: nothing dismisses the chips when the user hand-draws a
+    // link out of the anchor port, because that gesture starts on the port and
+    // fires element:pointerdown rather than the blank:pointerdown we listen for.
+    // Without this the click would add a second successor on top of the first.

Review Comment:
   Done



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,221 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here would be unrepealable; the chips must

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3786736699


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1762,158 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 {
+    this.repositionSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionRecommendations());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream

Review Comment:
   Verified: the click-path half is in now. `materializeNextOperatorSuggestion` re-checks the anchor port at 1912 through the same `isOutputPortLinked` helper the trigger uses (1863), so both paths test the resulting graph rather than which drop branch ran. The `outputPorts[0]` correction you accepted stands as documented V1 behaviour. Resolving.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3753725476


##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -154,6 +154,11 @@ spec:
         - path:
             type: PathPrefix
             value: /api/agents
+        # Ambient operator recommender endpoint (apache/texera#5240) is served
+        # by the agent-service alongside /api/agents.
+        - path:
+            type: PathPrefix
+            value: /api/recommend

Review Comment:
   A missing hash header doesn't error. Envoy falls back to unhashed host selection



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3753863605


##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -154,6 +154,11 @@ spec:
         - path:
             type: PathPrefix
             value: /api/agents
+        # Ambient operator recommender endpoint (apache/texera#5240) is served

Review Comment:
   Yes



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3770437006


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -1644,3 +1649,155 @@ describe("WorkflowEditorComponent link breakpoints", () => {
     expect(show.mock.invocationCallOrder[0]).toBeLessThan(hide.mock.invocationCallOrder[0]);
   });
 });
+
+/**
+ * Ambient operator recommender (apache/texera#5240).
+ *
+ * The feature is opt-in and `handleNextOperatorSuggestions` reads the flag once
+ * during ngAfterViewInit, returning early when it is off — so the flag has to
+ * be set before the first change detection, not inside the individual tests.
+ */
+describe("Ambient Operator Recommender", () => {
+  let component: WorkflowEditorComponent;
+  let fixture: ComponentFixture<WorkflowEditorComponent>;
+  let workflowActionService: WorkflowActionService;
+  let dragDropService: DragDropService;
+  let getRecommendationsSpy: MockInstance;
+
+  const mockRecommendations = [{ operatorType: "NlpSentiment", score: 0.9, reason: "Analyze the text" }];
+
+  // The recommender listens to the drag-drop service's drop stream. Push the
+  // dropped operator through directly rather than driving a real drag gesture,
+  // which would need a live flying-operator paper.
+  function emitOperatorDrop(operator: OperatorPredicate): void {
+    (dragDropService as any).operatorDroppedSubject.next(operator);
+  }
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: workflowEditorTestImports,
+      providers: workflowEditorTestProviders,
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(WorkflowEditorComponent);
+    component = fixture.componentInstance;
+    workflowActionService = TestBed.inject(WorkflowActionService);
+    dragDropService = TestBed.inject(DragDropService);
+
+    (TestBed.inject(GuiConfigService).env as any).operatorRecommendationEnabled = true;
+    getRecommendationsSpy = vi
+      .spyOn(TestBed.inject(OperatorRecommendationService), "getRecommendations")
+      .mockReturnValue(of(mockRecommendations));
+
+    // detect changes to run ngAfterViewInit and wire up the recommender
+    fixture.detectChanges();
+  });
+
+  it("requests suggestions when the user drops an operator onto the canvas", () => {
+    workflowActionService.addOperator(mockScanPredicate, mockPoint);
+    emitOperatorDrop(mockScanPredicate);
+
+    expect(getRecommendationsSpy).toHaveBeenCalledTimes(1);
+    expect(component.nextOperatorSuggestion?.operatorId).toEqual(mockScanPredicate.operatorID);
+    expect(component.nextOperatorSuggestion?.recommendations).toEqual(mockRecommendations);
+  });
+
+  it("does not request suggestions for operators that arrive without a drop", () => {
+    // Workflow load, undo/redo, paste and remote co-editor edits all reach the
+    // graph through addOperator with no drop event, and must stay silent.
+    workflowActionService.addOperator(mockScanPredicate, mockPoint);
+    workflowActionService.addOperator(mockSentimentPredicate, mockPoint);
+
+    expect(getRecommendationsSpy).not.toHaveBeenCalled();
+    expect(component.nextOperatorSuggestion).toBeNull();
+  });
+
+  it("does not suggest when the drop already wired the output port", () => {
+    // dragDropped links on both branches — dropping onto an existing edge, and

Review Comment:
   Done.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,194 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());

Review Comment:
   Done



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,194 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+        this.repositionNextOperatorSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    const sourcePortID = operator.outputPorts[0].portID;
+    const portIsTaken = this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operator.operatorID && link.source.portID === sourcePortID);
+    if (portIsTaken) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    if (graph.hasOperator(this.nextOperatorSuggestion.operatorId)) {

Review Comment:
   Fixed.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762215029


##########
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 operator 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 suggested 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

Review Comment:
   Done



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-4977497920

   <!-- texera-reviewer-suggestion -->
   ### Automated Reviewer Suggestions
   
   Based on the `git blame` history of the changed files, we recommend the following reviewers:
   
   - **Contributors with relevant context:** `@Ma77Ball`, `@xuang7`, `@bobbai00`
     You can notify them by mentioning `@Ma77Ball`, `@xuang7`, `@bobbai00` in a comment.
   


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-4983578844

   > great feature, could you please include screenshots/gifs in the PR description?
   
   added screenshots


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5009921561

   Am I allowed to update the branches because I made some changes with respect to eliminating the word "ghost" from everywhere?


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3762190695


##########
frontend/proxy.config.json:
##########
@@ -10,6 +10,11 @@
     "secure": false,
     "changeOrigin": true
   },
+  "/api/recommend": {

Review Comment:
   Fixed — `/api/recommend` now has a location pointing at `agent-service:3001.` I used the plain four-line REST form rather than copying `/api/agents`' WebSocket block, since this is a plain POST with no upgrade and no need for 1d timeouts.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3787357645


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1764,205 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+        this.repositionNextOperatorSuggestion$.next();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Re-check the port: nothing dismisses the chips when the user hand-draws a
+    // link out of the anchor port, because that gesture starts on the port and
+    // fires element:pointerdown rather than the blank:pointerdown we listen for.
+    // Without this the click would add a second successor on top of the first.
+    if (
+      graph.hasOperator(this.nextOperatorSuggestion.operatorId) &&

Review Comment:
   Good catch, and worth more than the port one — this lands in the shared model and `undoAction()` refuses while the lock is on, so it's unrepealable.
   
   Took the stream subscription as you suggested; it sits with the other dismissals and mirrors `handleDisableJointPaperInteractiveness` , so the chips now freeze with the rest of the canvas. I also added `checkWorkflowModificationEnabled()` to the click condition rather than treating them as either/or: dismissal is the visible fix, but it can't be the safety one — same window the port check had, and here the failure isn't undoable.
   
   Two tests, one per mechanism, mutation-checked separately so neither covers for the other.



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5460383573

   This PR currently conflicts with its base branch and needs a rebase before it can be merged.


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5464755434

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "Yicong-Huang (via GitHub)" <gi...@apache.org>.
Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3891993558


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1717,6 +1769,223 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy
     return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true;
   }
 
+  /**
+   * Ambient operator recommender (apache/texera#5240). When the user drops an
+   * operator onto the canvas, ask the recommender for likely next operators and
+   * float them as suggestion 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 handleNextOperatorSuggestions(): void {
+    if (!this.operatorRecommendationService.isEnabled()) {
+      return;
+    }
+
+    // Repositioning is throttled: change:position fires once per drag frame.
+    this.repositionNextOperatorSuggestion$
+      .pipe(auditTime(100), untilDestroyed(this))
+      .subscribe(() => this.repositionNextOperatorSuggestions());
+
+    // Every suggestion request — from a drop or from chaining after a click —
+    // goes through this one pipeline. switchMap unsubscribes the previous
+    // request, so a slow response can neither overwrite newer suggestions nor
+    // re-open the overlay after the user dismissed it; `null` means "cancel".
+    this.nextOperatorSuggestionRequest$
+      .pipe(
+        switchMap(operator =>
+          operator === null
+            ? of(null)
+            : this.operatorRecommendationService
+                .getRecommendations(operator)
+                .pipe(map(recommendations => ({ operator, recommendations })))
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(result => this.showNextOperatorSuggestions(result));
+
+    // Trigger: the user interactively dropped an operator onto the canvas.
+    // Deliberately not the graph's operator-add stream, which also fires on
+    // workflow load, undo/redo, paste, and remote co-editor edits — none of
+    // which are a user authoring a next step.
+    this.dragDropService.operatorDropStream
+      .pipe(untilDestroyed(this))
+      .subscribe(operator => this.requestNextOperatorSuggestionsFor(operator));
+
+    // Dismiss when the user clicks on blank canvas.
+    fromJointPaperEvent(this.paper, "blank:pointerdown")
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.closeNextOperatorSuggestions());
+
+    // Dismiss when the canvas is frozen — during execution, version preview, or a
+    // read-only view. Materializing writes to the shared model, and undo refuses
+    // while the lock is on, so a click here could not be undone; the chips must
+    // not stay on screen looking clickable either.
+    this.workflowActionService
+      .getWorkflowModificationEnabledStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(enabled => {
+        if (!enabled) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Dismiss if the anchor operator is deleted out from under the suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.nextOperatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeNextOperatorSuggestions();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    fromJointGraphCellEvent(this.paper.model, "change:position")
+      .pipe(untilDestroyed(this))
+      .subscribe(cell => {
+        if (this.nextOperatorSuggestion && cell.id.toString() === this.nextOperatorSuggestion.operatorId) {
+          this.repositionNextOperatorSuggestion$.next();
+        }
+      });
+
+    // Keep the suggestions anchored on zoom.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.nextOperatorSuggestion) {
+          this.repositionNextOperatorSuggestions();
+        }
+      });
+  }
+
+  /** Ask for suggestions on `operator`, cancelling whatever was in flight. */
+  private requestNextOperatorSuggestionsFor(operator: OperatorPredicate): void {
+    this.closeNextOperatorSuggestions();
+    // An operator with no output ports (e.g. a chart sink) has no port to
+    // anchor suggestions on, so there is nothing to ask for.
+    if (operator.outputPorts.length === 0) {
+      return;
+    }
+    // A drop can arrive already wired: onto an existing edge, or auto-linked to
+    // a nearby operator. The next step is chosen in that case, so suggesting
+    // another one would both be noise and risk placing it on top of the
+    // successor the drop just created.
+    if (this.isOutputPortLinked(operator.operatorID, operator.outputPorts[0].portID)) {
+      return;
+    }
+    this.nextOperatorSuggestionRequest$.next(operator);
+  }
+
+  /** Whether `portID` on `operatorID` already has a link leaving it. */
+  private isOutputPortLinked(operatorID: string, portID: string): boolean {
+    return this.workflowActionService
+      .getTexeraGraph()
+      .getAllLinks()
+      .some(link => link.source.operatorID === operatorID && link.source.portID === portID);
+  }
+
+  /** Render the result of the most recent, uncancelled suggestion request. */
+  private showNextOperatorSuggestions(
+    result: { operator: OperatorPredicate; recommendations: OperatorRecommendation[] } | null
+  ): void {
+    if (result === null || result.recommendations.length === 0) {
+      return;
+    }
+    const { operator, recommendations } = result;
+    // The operator may have been deleted while the request was in flight.
+    if (!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)) {
+      return;
+    }
+    const position = this.getNextOperatorSuggestionPosition(operator.operatorID);
+    if (!position) {
+      return;
+    }
+    this.nextOperatorSuggestion = {
+      operatorId: operator.operatorID,
+      sourceOutputPortID: operator.outputPorts[0].portID,
+      position,
+      recommendations,
+    };
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port, then suggest what could follow that new
+   * operator in turn — accepting a suggestion leaves the canvas ready for the
+   * next one, the way accepting a code completion does.
+   */
+  materializeNextOperatorSuggestion(recommendation: OperatorRecommendation): void {
+    if (!this.nextOperatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    let newOperatorID: string | undefined;
+    // Both conditions re-check state the chips cannot see. The lock can engage
+    // between render and click; and nothing dismisses the chips when the user
+    // hand-draws a link out of the anchor port, because that gesture is a magnet
+    // drag that stops propagation before any pointerdown notification, so the
+    // blank:pointerdown we listen for never fires. Without the port check the
+    // click would add a second successor on top of the first.

Review Comment:
   `Both` counts two, but the `if` below has three conjuncts — the lock, `graph.hasOperator(...)`, and the port check. The unnamed one guards the same kind of invisible state the sentence is about: the anchor can be deleted between render and click. Naming the checks instead of counting them also keeps the comment true if a fourth is ever added.
   
   ```suggestion
       // All three conditions re-check state the chips cannot see. The lock can
       // engage between render and click, and the anchor can be deleted in that same
       // window; and nothing dismisses the chips when the user hand-draws a link out
       // of the anchor port, because that gesture is a magnet drag that stops
       // propagation before any pointerdown notification, so the blank:pointerdown we
       // listen for never fires. Without the port check the click would add a second
       // successor on top of the first.
   ```



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): suggest next operators on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5489610647

   /request-review @Yicong-Huang 


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3646864078


##########
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
+   * suggestion 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));

Review Comment:
   @Ma77Ball can I get an update?



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender suggestions on the canvas [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3653928240


##########
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
+   * suggestion 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 suggestions.
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorDeleteStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(({ deletedOperatorID }) => {
+        if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+          this.closeRecommendations();
+        }
+      });
+
+    // Keep the suggestions anchored to the operator's output port as it moves.
+    this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+      if (this.operatorSuggestion && cell.id.toString() === this.operatorSuggestion.operatorId) {
+        this.repositionRecommendations();
+      }
+    });
+
+    // Keep the suggestions anchored on zoom / pan.
+    this.wrapper
+      .getWorkflowEditorZoomStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        if (this.operatorSuggestion) {
+          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.operatorSuggestion = {
+          operatorId: operator.operatorID,
+          sourceOutputPortID,
+          position,
+          recommendations,
+        };
+        this.changeDetectorRef.detectChanges();
+      });
+  }
+
+  /**
+   * Materialize a clicked suggestion into a real operator wired onto the
+   * source operator's output port.
+   */
+  materializeRecommendation(recommendation: OperatorRecommendation): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+      const sourceOperator = graph.getOperator(this.operatorSuggestion.operatorId);
+      this.operatorRecommendationService.materialize(
+        sourceOperator,
+        this.operatorSuggestion.sourceOutputPortID,
+        recommendation.operatorType
+      );
+    }
+    this.closeRecommendations();
+  }
+
+  closeRecommendations(): void {
+    if (this.operatorSuggestion) {
+      this.operatorSuggestion = null;
+      this.changeDetectorRef.detectChanges();
+    }
+  }
+
+  private repositionRecommendations(): void {
+    if (!this.operatorSuggestion) {
+      return;
+    }
+    const position = this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+    if (position) {
+      this.operatorSuggestion = { ...this.operatorSuggestion, position };
+    }
+    this.changeDetectorRef.detectChanges();
+  }
+
+  /**
+   * Screen position for the suggestions: just off the right edge of the
+   * operator, vertically centered — the direction its output port faces.
+   */
+  private getRecommendationPosition(operatorId: string): { x: number; y: number } | null {
+    const jointCell = this.paper.getModelById(operatorId);
+    if (!jointCell) {
+      return null;
+    }
+    const bbox = jointCell.getBBox();
+    const scale = this.paper.scale();
+    const translate = this.paper.translate();
+    const screenX = (bbox.x + bbox.width) * scale.sx + translate.tx + 20;

Review Comment:
   Do you mean give it a variable name?



-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(frontend): ambient operator recommender ghost suggestions [texera]

Posted by "gupta-sahil01 (via GitHub)" <gi...@apache.org>.
gupta-sahil01 commented on PR #6437:
URL: https://github.com/apache/texera/pull/6437#issuecomment-5000117533

   > Hi @gupta-sahil01 a high level comment: let's not name it `ghost` suggestions. Let's find a better name for it?
   
   I thought since they were blurred like a "ghost". If you have a better name for it, I'm open to changing it.


-- 
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: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org