Skip to content

Commit 32f8368

Browse files
feat: redirect to solution view with real-time progress after 'Create Solution' (presidio-oss#252)
* refactor: Remove unused ThinkingProcess component and related logic * feat: add workflow progress component and service for tracking solution creation * chore: add changeset * feat: implement workflow progress tracking for solution creation * feat: enhance solution creation process with content generation status updates * feat: add content generation status tracking and update workflow progress handling * feat: remove 'requirement' type from content generation status methods * feat: add global listener registration for workflow progress in solution creation * feat: refactor content generation handling with new types and improved status management * feat: enhance quit confirmation dialog during active processes with detailed information and improved messaging * feat: change quit confirmation dialog type from question to warning for better user awareness * feat: remove error throw for unavailable Electron API in clearAllContentGenerationProcesses method
1 parent cab23f3 commit 32f8368

File tree

20 files changed

+1640
-217
lines changed

20 files changed

+1640
-217
lines changed

.changeset/big-cities-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"specif-ai": patch
3+
---
4+
5+
feat: redirect to solution view with real-time progress after 'Create Solution'

electron/app.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, ipcMain, BrowserWindow, shell } from "electron";
1+
import { app, ipcMain, BrowserWindow, shell, dialog } from "electron";
22
import path from "path";
33
import dotenv from "dotenv";
44
import { setupFileSystemHandlers } from "./handlers/fs-handler";
@@ -8,6 +8,11 @@ import { setupRequirementHandlers } from "./handlers/requirement-handler";
88
import { setupVisualizationHandlers } from "./handlers/visualization-handler";
99
import { setupFeatureHandlers } from "./handlers/feature-handler";
1010
import { setupSolutionHandlers } from "./handlers/solution-handler";
11+
import {
12+
setupContentGenerationHandlers,
13+
isAnyContentGenerationInProgress,
14+
getActiveContentGenerationProcessNames,
15+
} from "./handlers/content-generation-handler";
1116
import { setupJiraHandlers } from "./handlers/jira-handler";
1217
import { setupAppUpdateHandler } from "./handlers/app-update-handler";
1318
import { setupMcpHandlers } from "./handlers/mcp-handler";
@@ -77,6 +82,18 @@ function createWindow(indexPath: string, themeConfiguration: any) {
7782
});
7883
}
7984

85+
mainWindow.on("close", async (event) => {
86+
if (isAnyContentGenerationInProgress()) {
87+
event.preventDefault();
88+
89+
const shouldQuit = await confirmQuitDuringActiveProcesses();
90+
if (shouldQuit) {
91+
mainWindow = null;
92+
app.exit(0);
93+
}
94+
}
95+
});
96+
8097
mainWindow.on("closed", () => {
8198
mainWindow = null;
8299
app.quit();
@@ -190,6 +207,57 @@ function isValidUrl(url: string): boolean {
190207
}
191208
}
192209

210+
async function confirmQuitDuringActiveProcesses(): Promise<boolean> {
211+
const activeProcesses = getActiveContentGenerationProcessNames();
212+
try {
213+
const processCount = activeProcesses.length;
214+
const isPlural = processCount > 1;
215+
216+
const displayProcesses = activeProcesses.map((name) =>
217+
name.length > 30 ? `${name.substring(0, 27)}...` : name
218+
);
219+
220+
const processListText =
221+
processCount <= 3
222+
? displayProcesses.join(", ")
223+
: `${displayProcesses.slice(0, 2).join(", ")} and ${
224+
processCount - 2
225+
} more`;
226+
227+
const choice = await dialog.showMessageBox(mainWindow!, {
228+
type: "warning",
229+
buttons: ["Wait for Completion", "Quit Anyway"],
230+
defaultId: 0,
231+
cancelId: 0,
232+
title: `${processCount} Active ${
233+
isPlural ? "Processes" : "Process"
234+
} Running`,
235+
message: `You have ${
236+
isPlural ? "processes" : "a process"
237+
} currently running that ${
238+
isPlural ? "haven't" : "hasn't"
239+
} finished yet.`,
240+
detail: [
241+
`📋 Active ${isPlural ? "processes" : "process"}: ${processListText}`,
242+
"",
243+
"💡 What happens if you quit now:",
244+
"• Your work in progress may be lost",
245+
"• Generated content might be incomplete",
246+
"• You may need to restart these tasks",
247+
"",
248+
"🔒 Recommended: Let the processes finish, then quit safely.",
249+
].join("\n"),
250+
noLink: true,
251+
normalizeAccessKeys: false,
252+
});
253+
254+
return choice.response === 1;
255+
} catch (error) {
256+
console.error("Error showing quit dialog:", error);
257+
return false;
258+
}
259+
}
260+
193261
// ========================
194262
// MAIN APPLICATION LOGIC
195263
// ========================
@@ -213,6 +281,19 @@ app.whenReady().then(async () => {
213281

214282
app.on("window-all-closed", () => app.quit());
215283

284+
app.on("before-quit", async (event) => {
285+
console.log("App before quit...", isAnyContentGenerationInProgress());
286+
287+
if (isAnyContentGenerationInProgress()) {
288+
event.preventDefault();
289+
290+
const shouldQuit = await confirmQuitDuringActiveProcesses();
291+
if (shouldQuit) {
292+
app.exit(0);
293+
}
294+
}
295+
});
296+
216297
if (mainWindow) {
217298
// Setup window event handlers
218299
setupWindowHandlers(mainWindow, indexPath);
@@ -227,6 +308,7 @@ app.whenReady().then(async () => {
227308
setupVisualizationHandlers();
228309
setupFeatureHandlers();
229310
setupSolutionHandlers();
311+
setupContentGenerationHandlers();
230312
setupMcpHandlers();
231313

232314
// start mcp servers in the background
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { ipcMain } from "electron";
2+
import { contentGenerationManager } from "../services/content-generation/content-generation.service";
3+
import {
4+
ContentGenerationProcess,
5+
ContentGenerationType,
6+
} from "../types/content-generation.types";
7+
8+
export function setupContentGenerationHandlers() {
9+
ipcMain.handle(
10+
"content-generation:setStatus",
11+
async (
12+
_event,
13+
solutionId: string,
14+
type: ContentGenerationType,
15+
isInProgress: boolean
16+
) => {
17+
contentGenerationManager.setContentGenerationStatus(
18+
solutionId,
19+
type,
20+
isInProgress
21+
);
22+
return { success: true };
23+
}
24+
);
25+
26+
ipcMain.handle(
27+
"content-generation:getStatus",
28+
async (_event, type: ContentGenerationType) => {
29+
return contentGenerationManager
30+
.getActiveContentGenerationProcesses()
31+
.some((p: ContentGenerationProcess) => p.type === type);
32+
}
33+
);
34+
35+
ipcMain.handle("content-generation:getActiveProcesses", async () => {
36+
return contentGenerationManager.getActiveContentGenerationProcesses();
37+
});
38+
39+
ipcMain.handle("content-generation:isAnyInProgress", async () => {
40+
return contentGenerationManager.isAnyContentGenerationInProgress();
41+
});
42+
43+
ipcMain.handle("content-generation:getActiveProcessNames", async () => {
44+
return contentGenerationManager.getActiveContentGenerationProcessNames();
45+
});
46+
47+
ipcMain.handle("content-generation:clearAll", async () => {
48+
contentGenerationManager.clearAllContentGenerationProcesses();
49+
return { success: true };
50+
});
51+
}
52+
53+
export function isAnyContentGenerationInProgress(): boolean {
54+
return contentGenerationManager.isAnyContentGenerationInProgress();
55+
}
56+
57+
export function getActiveContentGenerationProcessNames(): string[] {
58+
return contentGenerationManager.getActiveContentGenerationProcessNames();
59+
}

electron/preload.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
2+
import { ContentGenerationType } from "./types/content-generation.types";
23

34
type IpcListener = (event: IpcRendererEvent, ...args: any[]) => void;
45

@@ -75,6 +76,30 @@ const solutionListeners = {
7576
ipcRenderer.invoke("solution:createSolution", data),
7677
};
7778

79+
const contentGenerationListeners = {
80+
setContentGenerationStatus: (
81+
solutionId: string,
82+
type: ContentGenerationType,
83+
isInProgress: boolean
84+
) =>
85+
ipcRenderer.invoke(
86+
"content-generation:setStatus",
87+
solutionId,
88+
type,
89+
isInProgress
90+
),
91+
getContentGenerationStatus: (type: ContentGenerationType) =>
92+
ipcRenderer.invoke("content-generation:getStatus", type),
93+
getActiveContentGenerationProcesses: () =>
94+
ipcRenderer.invoke("content-generation:getActiveProcesses"),
95+
isAnyContentGenerationInProgress: () =>
96+
ipcRenderer.invoke("content-generation:isAnyInProgress"),
97+
getActiveContentGenerationProcessNames: () =>
98+
ipcRenderer.invoke("content-generation:getActiveProcessNames"),
99+
clearAllContentGenerationProcesses: () =>
100+
ipcRenderer.invoke("content-generation:clearAll"),
101+
};
102+
78103
const appAutoUpdaterListeners = {
79104
checkForUpdates: () =>
80105
ipcRenderer.invoke("app-updater:check-for-updates"),
@@ -105,6 +130,7 @@ const electronAPI = {
105130
...coreListeners,
106131
...requirementListeners,
107132
...solutionListeners,
133+
...contentGenerationListeners,
108134
...visualizationListeners,
109135
...featureListeners,
110136
...appAutoUpdaterListeners,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
ContentGenerationProcess,
3+
ContentGenerationType,
4+
} from "../../types/content-generation.types";
5+
6+
class ContentGenerationManager {
7+
private processes: Map<string, ContentGenerationProcess> = new Map();
8+
9+
setContentGenerationStatus(
10+
solutionId: string,
11+
type: ContentGenerationType,
12+
isInProgress: boolean
13+
): void {
14+
const displayNames: Record<ContentGenerationType, string> = {
15+
[ContentGenerationType.Solution]: "Solution Generation",
16+
[ContentGenerationType.Story]: "Story Generation",
17+
[ContentGenerationType.Task]: "Task Generation",
18+
};
19+
20+
const key = `${solutionId}-${type}`;
21+
this.processes.set(key, {
22+
solutionId,
23+
type,
24+
displayName: displayNames[type],
25+
isInProgress,
26+
});
27+
}
28+
29+
isAnyContentGenerationInProgress(): boolean {
30+
return Array.from(this.processes.values()).some(
31+
(process) => process.isInProgress
32+
);
33+
}
34+
35+
getActiveContentGenerationProcesses(): ContentGenerationProcess[] {
36+
return Array.from(this.processes.values()).filter(
37+
(process) => process.isInProgress
38+
);
39+
}
40+
41+
getActiveContentGenerationProcessNames(): string[] {
42+
return this.getActiveContentGenerationProcesses().map(
43+
(process) => process.displayName
44+
);
45+
}
46+
47+
clearAllContentGenerationProcesses(): void {
48+
this.processes.clear();
49+
}
50+
}
51+
52+
export const contentGenerationManager = new ContentGenerationManager();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export enum ContentGenerationType {
2+
Solution = 'solution',
3+
Story = 'story',
4+
Task = 'task',
5+
}
6+
7+
export interface ContentGenerationProcess {
8+
solutionId: string;
9+
type: ContentGenerationType;
10+
displayName: string;
11+
isInProgress: boolean;
12+
}

ui/package-lock.json

Lines changed: 0 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"@tiptap/extension-paragraph": "^2.11.5",
6666
"@tiptap/extension-text": "^2.11.5",
6767
"@tiptap/pm": "^2.11.5",
68-
"canvas-confetti": "^1.9.3",
6968
"dotenv": "^16.4.5",
7069
"exceljs": "^4.4.0",
7170
"file-saver": "^2.0.5",
@@ -98,7 +97,6 @@
9897
"@angular/compiler-cli": "^16.2.0",
9998
"@rrweb/types": "^2.0.0-alpha.16",
10099
"@tailwindcss/typography": "^0.5.16",
101-
"@types/canvas-confetti": "^1.9.0",
102100
"@types/d3": "^7.4.3",
103101
"@types/dompurify": "~3.0.5",
104102
"@types/file-saver": "^2.0.7",

0 commit comments

Comments
 (0)