From 06fa3421dd55f70013851e7f28afd0812aedd77b Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Wed, 3 Sep 2025 22:36:44 +0800 Subject: [PATCH 1/3] Fix library install check and unify confirm for missing template libs --- src/context-menu/TrayContextMenu.tsx | 11 +++++++--- src/index.tsx | 30 ++++++++++++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/context-menu/TrayContextMenu.tsx b/src/context-menu/TrayContextMenu.tsx index ac5033c7..b5dae754 100644 --- a/src/context-menu/TrayContextMenu.tsx +++ b/src/context-menu/TrayContextMenu.tsx @@ -12,13 +12,18 @@ import { normalizeLibraryName } from '../tray_library/ComponentLibraryConfig'; export async function handleInstall( app, libraryName: string, - refreshTrigger: () => void + refreshTrigger: () => void, + opts?: { silent?: boolean } + ): Promise { + const { silent = false } = opts ?? {}; const originalName = libraryName; const normalizedLibName = normalizeLibraryName(originalName); - const proceed = confirm(`Do you want to proceed with installing "${originalName}" library?`); - if (!proceed) return false; + if (!silent) { + const proceed = confirm(`Do you want to proceed with installing "${originalName}" library?`); + if (!proceed) return false; + } const installPromise = requestAPI('library/install', { method: 'POST', diff --git a/src/index.tsx b/src/index.tsx index abf8caab..a4b83127 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -583,6 +583,10 @@ const xircuits: JupyterFrontEndPlugin = { }); } + function canon(name: string): string { + return name.toLowerCase().replace(/^xai[_-]?/, ''); + } + async function getInstalledLibraries(): Promise> { try { const result = await requestAPI('library/get_config', { @@ -592,7 +596,7 @@ const xircuits: JupyterFrontEndPlugin = { return new Set( result.config.libraries .filter((lib: any) => lib.status === 'installed' && typeof lib.name === 'string') - .map((lib: any) => lib.name) + .map((lib: any) => canon(lib.name)) ); } catch (err) { console.error('Failed to load library config via API:', err); @@ -622,23 +626,27 @@ const xircuits: JupyterFrontEndPlugin = { const currentPath = browserFactory.tracker.currentWidget?.model.path ?? ''; const installedLibs = await getInstalledLibraries(); - for (const lib of libraries) { - if (installedLibs.has(lib)) { - console.log(`Library ${lib} already installed. Skipping.`); - continue; + const pairs = libraries.map(lib => ({ lib, want: canon(lib) })); + const missing = pairs.filter(p => !installedLibs.has(p.want)); + if (missing.length) { + const list = missing.map(p => p.lib).join(', '); + const ok = window.confirm(`This template requires: ${list}. Install now?`); + if (!ok) { + console.warn('User cancelled installation.'); + return; } - const ok = await handleInstall(app, lib, () => - app.commands.execute(commandIDs.refreshComponentList) - ); - + for (const { lib, want } of missing) { + const ok = await handleInstall(app, lib, () =>{}, + { silent: true } + ); if (!ok) { console.warn(`Aborted: ${lib} not installed.`); return; } - installedLibs.add(lib); + installedLibs.add(want); } - + } // Currently the templates are stored at the `examples` dir await app.commands.execute(commandIDs.fetchExamples); From 29a7a51eef61b23497d0012a702f3f5198d54623 Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Tue, 7 Oct 2025 21:12:15 +0800 Subject: [PATCH 2/3] Refactor library installation to support batch silent installs and unified notifications --- src/context-menu/TrayContextMenu.tsx | 39 ++++++++--- src/helpers/notificationEffects.ts | 96 ++++++++++++++++++++++++++++ src/index.tsx | 67 +++++++++---------- 3 files changed, 156 insertions(+), 46 deletions(-) diff --git a/src/context-menu/TrayContextMenu.tsx b/src/context-menu/TrayContextMenu.tsx index b5dae754..67d62d06 100644 --- a/src/context-menu/TrayContextMenu.tsx +++ b/src/context-menu/TrayContextMenu.tsx @@ -12,18 +12,13 @@ import { normalizeLibraryName } from '../tray_library/ComponentLibraryConfig'; export async function handleInstall( app, libraryName: string, - refreshTrigger: () => void, - opts?: { silent?: boolean } - + refreshTrigger: () => void ): Promise { - const { silent = false } = opts ?? {}; const originalName = libraryName; const normalizedLibName = normalizeLibraryName(originalName); - if (!silent) { - const proceed = confirm(`Do you want to proceed with installing "${originalName}" library?`); - if (!proceed) return false; - } + const proceed = confirm(`Do you want to proceed with installing "${originalName}" library?`); + if (!proceed) return false; const installPromise = requestAPI('library/install', { method: 'POST', @@ -60,6 +55,34 @@ export async function handleInstall( } } +export async function installLibrarySilently(app: any, libraryName: string): Promise { + const normalized = normalizeLibraryName(libraryName); + + const installPromise = requestAPI('library/install', { + method: 'POST', + body: JSON.stringify({ libraryName: normalized }) + }); + + Notification.promise(installPromise, { + pending: { message: `Installing ${libraryName} library...`, options: { autoClose: 3000 } }, + success: { message: () => `Library ${libraryName} installed successfully.`, options: { autoClose: 3000 } }, + error: { message: (err) => `Failed to install ${libraryName}: ${err}`, options: { autoClose: false } } + }); + + try { + const res = await installPromise; + if (res.status === 'OK') { + await app.commands.execute(commandIDs.refreshComponentList); + return true; + } + console.error(`Installation failed: ${res.error || 'Unknown error'}`); + return false; + } catch (e) { + console.error('Installation error:', e); + return false; + } +} + export interface TrayContextMenuProps { app: any; x: number; diff --git a/src/helpers/notificationEffects.ts b/src/helpers/notificationEffects.ts index 166c0a31..e140763a 100644 --- a/src/helpers/notificationEffects.ts +++ b/src/helpers/notificationEffects.ts @@ -1,5 +1,9 @@ import { DiagramEngine } from '@projectstorm/react-diagrams'; import { Notification } from '@jupyterlab/apputils'; +import { requestAPI } from '../server/handler'; +import { handleInstall } from '../context-menu/TrayContextMenu'; +import { commandIDs } from '../commands/CommandIDs'; +import { normalizeLibraryName } from '../tray_library/ComponentLibraryConfig'; export function showNodeCenteringNotification( message: string, @@ -50,3 +54,95 @@ export function centerNodeInView(engine: DiagramEngine, nodeId: string) { model.setOffset(offsetX, offsetY); engine.repaintCanvas(); } + +type LibraryStatus = 'installed' | 'incomplete' | 'remote' | 'unknown'; +type LibraryEntry = { library_id: string; status: string; [k: string]: any }; + +function pathToLibraryId(rawPath?: string | null): string | null { + if (!rawPath || typeof rawPath !== 'string') return null; + const m = rawPath.match(/xai_components[\/\\]([a-z0-9_-]+)[\/\\]/i); + if (!m) return null; + return normalizeLibraryName(m[1]); +} + +export async function loadLibraryIndex(): Promise> { + const res: any = await requestAPI('library/get_config', { method: 'GET' }); + const libs = res?.config?.libraries; + if (!Array.isArray(libs)) throw new Error('Invalid library response'); + + const index = new Map(); + for (const lib of libs as LibraryEntry[]) { + if (!lib?.library_id) continue; + const id = normalizeLibraryName(String(lib.library_id)); + index.set(id, lib); + } + return index; +} + +function computeStatusFromEntry(entry?: LibraryEntry, normalizedId?: string): { libId: string | null; status: LibraryStatus } { + if (!entry) return { libId: null, status: 'unknown' }; + const s = String(entry.status).toLowerCase(); + return { libId: normalizedId, status: s === 'remote' ? 'remote' : 'installed' }; +} + +export async function resolveLibraryForNode( + node: any +): Promise<{ libId: string | null; status: LibraryStatus }> { + const extras = node?.getOptions?.().extras ?? {}; + const candidateId = pathToLibraryId(extras.path); + if (!candidateId) return { libId: null, status: 'unknown' }; + + const idx = await loadLibraryIndex(); + const entry = idx.get(candidateId); + return computeStatusFromEntry(entry, candidateId); +} + +export async function showInstallForRemoteLibrary(args: { + app: any; + engine?: DiagramEngine; + nodeId: string; + libName?: string | null; + path?: string | null; + message: string; +}): Promise { + const { app, engine, nodeId, message } = args; + + const rawName = (args.libName ?? '').trim(); + if (!rawName) return false; + const candidateId = normalizeLibraryName(rawName); + const idx = await loadLibraryIndex(); + const entry = idx.get(candidateId); + const { libId, status } = computeStatusFromEntry(entry, candidateId); + + if (status !== 'remote' || !libId) return false; + + const displayName = entry.library_id; + const actions: any[] = [ + { + label: `Install ${displayName}`, + caption: `Install ${displayName} library`, + callback: async (event: MouseEvent) => { + event.preventDefault(); + await handleInstall( + app, + displayName, + () => app.commands.execute(commandIDs.refreshComponentList) + ); + } + } + ]; + + if (engine) { + actions.push({ + label: 'Show Node', + caption: 'Center this node on canvas', + callback: (event: MouseEvent) => { + event.preventDefault(); + centerNodeInView(engine, nodeId); + } + }); + } + + Notification.error(message, { autoClose: 3000, actions }); + return true; +} diff --git a/src/index.tsx b/src/index.tsx index a4b83127..bc57d7ef 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,10 +33,10 @@ import type { Signal } from "@lumino/signaling"; import { commandIDs } from "./commands/CommandIDs"; import { IEditorTracker } from '@jupyterlab/fileeditor'; import { IMainMenu } from '@jupyterlab/mainmenu'; -import { handleInstall } from './context-menu/TrayContextMenu'; -import { ComponentPreviewWidget } from './component_info_sidebar/ComponentPreviewWidget'; -import {registerPreviewResetOnCanvasChange } from './component_info_sidebar/previewHelper'; - +import { installLibrarySilently } from './context-menu/TrayContextMenu'; +import { normalizeLibraryName } from './tray_library/ComponentLibraryConfig'; +import { loadLibraryIndex } from './helpers/notificationEffects'; +import { installComponentPreview } from './component_info_sidebar/previewHelper'; const FACTORY = 'Xircuits editor'; // Export a token so other extensions can require it @@ -160,15 +160,11 @@ const xircuits: JupyterFrontEndPlugin = { restorer.add(sidebarWidget, sidebarWidget.id); app.shell.add(sidebarWidget, "left"); - - const previewWidget = new ComponentPreviewWidget(null); - previewWidget.id = 'xircuits-doc-preview'; - app.shell.add(previewWidget, 'right', { rank: 1 }); - restorer.add(previewWidget, previewWidget.id); + // === Right Sidebar + installComponentPreview(app, restorer, tracker, { rank: 0, collapseOnStart: true }); // Additional commands for node action addNodeActionCommands(app, tracker, translator); - registerPreviewResetOnCanvasChange(app, tracker); // Additional commands for chat actions addLibraryActionCommands(app, tracker, translator, widgetFactory); @@ -583,26 +579,17 @@ const xircuits: JupyterFrontEndPlugin = { }); } - function canon(name: string): string { - return name.toLowerCase().replace(/^xai[_-]?/, ''); - } - - async function getInstalledLibraries(): Promise> { - try { - const result = await requestAPI('library/get_config', { - method: 'GET' - }); - - return new Set( - result.config.libraries - .filter((lib: any) => lib.status === 'installed' && typeof lib.name === 'string') - .map((lib: any) => canon(lib.name)) - ); - } catch (err) { - console.error('Failed to load library config via API:', err); - return new Set(); + async function getInstalledIds(): Promise> { + const idx = await loadLibraryIndex(); + const set = new Set(); + for (const [id, entry] of idx) { + if (String(entry.status).toLowerCase() === 'installed') { + set.add(id); + } } + return set; } + app.commands.addCommand(commandIDs.fetchExamples, { label: 'Fetch Example Workflows', caption: 'Fetch example workflows into the examples directory', @@ -624,29 +611,33 @@ const xircuits: JupyterFrontEndPlugin = { icon: xircuitsIcon, execute: async () => { const currentPath = browserFactory.tracker.currentWidget?.model.path ?? ''; - const installedLibs = await getInstalledLibraries(); + const installedIds = await getInstalledIds(); + + const pairs = libraries.map(lib => ({ + raw: lib, + id: normalizeLibraryName(lib) + })); + + const missing = pairs.filter(p => !installedIds.has(p.id)); - const pairs = libraries.map(lib => ({ lib, want: canon(lib) })); - const missing = pairs.filter(p => !installedLibs.has(p.want)); if (missing.length) { - const list = missing.map(p => p.lib).join(', '); + const list = missing.map(p => p.raw).join(', '); const ok = window.confirm(`This template requires: ${list}. Install now?`); if (!ok) { console.warn('User cancelled installation.'); return; } - for (const { lib, want } of missing) { - const ok = await handleInstall(app, lib, () =>{}, - { silent: true } - ); + for (const { raw, id } of missing) { + const ok = await installLibrarySilently(app, raw); if (!ok) { - console.warn(`Aborted: ${lib} not installed.`); + console.warn(`Aborted: ${raw} not installed.`); return; } - installedLibs.add(want); + installedIds.add(id); } } + // Currently the templates are stored at the `examples` dir await app.commands.execute(commandIDs.fetchExamples); From 92ce9e6fe099cfd024b00211a9b61e4cc9030779 Mon Sep 17 00:00:00 2001 From: rabea-al Date: Thu, 9 Oct 2025 16:14:18 +0800 Subject: [PATCH 3/3] Update install confirmation message --- src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index bc57d7ef..25a6f769 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -622,7 +622,7 @@ const xircuits: JupyterFrontEndPlugin = { if (missing.length) { const list = missing.map(p => p.raw).join(', '); - const ok = window.confirm(`This template requires: ${list}. Install now?`); + const ok = window.confirm(`This workflow template requires the following component libraries: ${list}. Would you like to install them now?`); if (!ok) { console.warn('User cancelled installation.'); return;