mengw15 commented on code in PR #7601:
URL: https://github.com/apache/texera/pull/7601#discussion_r3778347832
##########
frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts:
##########
@@ -312,6 +322,107 @@ export class UserWorkflowComponent implements
AfterViewInit {
});
}
+ public get pythonNotebookMigrationEnabled(): boolean {
+ return this.config.env.pythonNotebookMigrationEnabled;
+ }
+
+ /** Open the AI-generate import modal, wiring its submit to
generateWorkflowFromNotebook. */
+ public openAiGenerateModal(): void {
+ this.modalService.create<NotebookImportModalComponent,
NotebookImportModalData>({
+ nzTitle: "AI Generate Workflow from Python Notebook",
+ nzContent: NotebookImportModalComponent,
+ nzWidth: 700,
+ nzFooter: null,
+ nzCentered: true,
+ nzData: {
+ requestImport: (file, model) =>
this.generateWorkflowFromNotebook(file, model),
+ },
+ });
+ }
+
+ /**
+ * Parse the notebook, generate a workflow via the LLM, save it, store the
cell mapping, and open it.
+ * Resolves true on success (modal closes), false to keep the modal open on
a bad file or a failure.
+ */
+ private async generateWorkflowFromNotebook(file: NzUploadFile, model:
string): Promise<boolean> {
+ const fileExtension = file.name.split(".").pop()?.toLowerCase();
+ if (fileExtension !== "ipynb") {
+ this.notificationService.error("Please upload a valid Jupyter Notebook
(.ipynb) file.");
+ return false;
+ }
+ let notebook: Notebook;
+ try {
+ notebook = await this.notebookMigrationService.parseAndTagNotebook(file
as unknown as File);
+ } catch (error) {
+ this.notificationService.error("Failed to read the notebook file. Please
upload a valid .ipynb file.");
+ console.error("Notebook parse failed:", error);
+ return false;
+ }
+
+ let generated: { workflowContent: WorkflowContent; mappingContent:
MappingContent };
+ try {
+ generated = await
this.notebookMigrationService.sendToAIGenerateWorkflow(notebook, model);
+ } catch (error) {
+ this.notificationService.error("Error while communicating with the LLM,
check console for details.");
+ console.error("LLM generation failed:", error);
+ return false;
+ }
+
+ // Commit point: persisting captures the expensive LLM result. On failure
nothing was created,
+ // so returning false to let the user retry is safe.
+ let wid: number;
+ try {
+ // workflow.name is VARCHAR(128); cap the base so base + suffix fits the
column.
+ const generatedSuffix = "_GENERATED_BY_LLM";
+ const generatedName = this.deriveWorkflowName(file.name).slice(0, 128 -
generatedSuffix.length);
+ const createdWorkflow = await firstValueFrom(
Review Comment:
These awaits use bare `firstValueFrom` with no `untilDestroyed`, so the
chain outlives the component. Press browser back while generating and minutes
later `router.navigate` pulls the user into the workspace from wherever they
had gone.
##########
frontend/src/app/workspace/component/notebook-import-modal/notebook-import-modal.component.ts:
##########
@@ -90,24 +84,59 @@ export class NotebookImportModalComponent {
this.modalRef.close();
}
- // Guards against a second submit while the opener callback (which may show
an
- // overwrite confirmation) is still pending, so a double-click cannot start
two imports.
+ // True while generation runs: guards against a second submit and drives the
loading overlay.
public isSubmitting = false;
+ private startTime: number | null = null;
+ private timerHandle: ReturnType<typeof setInterval> | null = null;
+
+ public ngOnDestroy(): void {
+ this.stopTimer();
+ }
+
+ public get formattedElapsedTime(): string {
+ const diffMs = this.startTime === null ? 0 : Date.now() - this.startTime;
+ const totalSeconds = Math.floor(diffMs / 1000);
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
+ }
+
+ // Empty body on purpose: the zone-patched event firing is itself what
repaints the stopwatch,
+ // so it catches up when the user returns to a backgrounded tab. Same reason
as the timer below.
+ @HostListener("document:visibilitychange")
+ public onVisibilityChange(): void {}
+
+ private startTimer(): void {
+ this.stopTimer();
+ this.startTime = Date.now();
+ // Empty body: elapsed is computed from startTime; the zone-patched tick
just triggers a repaint.
+ this.timerHandle = setInterval(() => {}, 1000);
+ }
+
+ private stopTimer(): void {
+ if (this.timerHandle !== null) {
+ clearInterval(this.timerHandle);
+ this.timerHandle = null;
+ }
+ }
public async onSubmit(): Promise<void> {
if (this.isSubmitting || !this.importForm.valid) return;
const file: NzUploadFile = this.importForm.get("file")?.value;
const model: string = this.importForm.get("model")?.value;
this.isSubmitting = true;
+ this.startTimer();
+ this.modalRef.updateConfig({ nzClosable: false, nzMaskClosable: false,
nzKeyboard: false });
Review Comment:
This closes every exit, and `generateText` (`migration-llm.ts:193`) is
called with no `abortSignal` and no timeout. If the LLM stalls instead of
erroring, the promise never settles, so `finally` never runs: X, mask and Esc
stay off, both buttons stay `[disabled]="isSubmitting"`, and since the `inert`
fix the form and footer are out of the focus tree too. Nothing is clickable,
the stopwatch just climbs, and reloading the page is the only way out.
An `abortSignal` surfacing through the existing "Error while communicating
with the LLM" path would bound it. The canvas flow this replaces had no such
trap — it closed the modal as soon as generation started.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]