From a0c2ac4d0735d5e04716822ad0bd4b310bef62bf Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Wed, 7 Jan 2026 22:49:31 -0800 Subject: [PATCH] Add 'Open in AI' option to item context menus (#1974) * Add 'Open in AI' option to item context menus * Add new AI-related English translations --- src/gui/src/UI/UIItem.js | 97 ++++++++++++++++++++++++++++- src/gui/src/i18n/translations/en.js | 5 +- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js index b0811e102..e644edd09 100644 --- a/src/gui/src/UI/UIItem.js +++ b/src/gui/src/UI/UIItem.js @@ -32,7 +32,82 @@ import launch_app from '../helpers/launch_app.js'; import open_item from '../helpers/open_item.js'; import mime from '../lib/mime.js'; -function UIItem (options) { +const AI_APP_NAME = 'ai'; + +const parseItemMetadataForAI = (metadata) => { + if (!metadata) { + return undefined; + } + try { + return JSON.parse(metadata); + } catch (error) { + console.warn('Failed to parse item metadata for AI payload.', error); + return undefined; + } +}; + +const buildAIPayloadFromItems = ($elements) => { + return $elements.get().map((element) => { + const $element = $(element); + return { + uid: $element.attr('data-uid'), + path: $element.attr('data-path'), + name: $element.attr('data-name'), + is_dir: $element.attr('data-is_dir') === '1', + is_shortcut: $element.attr('data-is_shortcut') === '1', + shortcut_to: $element.attr('data-shortcut_to') || undefined, + shortcut_to_path: $element.attr('data-shortcut_to_path') || undefined, + size: $element.attr('data-size') || undefined, + type: $element.attr('data-type') || undefined, + modified: $element.attr('data-modified') || undefined, + metadata: parseItemMetadataForAI($element.attr('data-metadata')), + }; + }); +}; + +const ensureAIAppIframe = async () => { + let $aiWindow = $(`.window[data-app="${AI_APP_NAME}"]`); + if ($aiWindow.length === 0) { + try { + await launch_app({ name: AI_APP_NAME }); + } catch (error) { + console.error('Failed to launch AI app.', error); + return null; + } + $aiWindow = $(`.window[data-app="${AI_APP_NAME}"]`); + } + + if ($aiWindow.length === 0) { + return null; + } + + $aiWindow.makeWindowVisible(); + const iframe = $aiWindow.find('.window-app-iframe').get(0); + return iframe ?? null; +}; + +const sendSelectionToAIApp = async ($elements) => { + const items = buildAIPayloadFromItems($elements); + if (items.length === 0) { + return; + } + + const aiIframe = await ensureAIAppIframe(); + if (!aiIframe || !aiIframe.contentWindow) { + await UIAlert({ + message: i18n('ai_app_unavailable'), + }); + return; + } + + aiIframe.contentWindow.postMessage({ + msg: 'ai:openFsEntries', + items, + source: 'desktop-context-menu', + }, '*'); +}; + +function UIItem(options){ const matching_appendto_count = $(options.appendTo).length; if ( matching_appendto_count > 1 ) { $(options.appendTo).each(function () { @@ -909,6 +984,15 @@ function UIItem (options) { }, }); // ------------------------------------------- + // Open in AI + // ------------------------------------------- + menu_items.push({ + html: i18n('open_in_ai'), + onClick: async function(){ + await sendSelectionToAIApp($selected_items); + } + }); + // ------------------------------------------- // - // ------------------------------------------- menu_items.push({ is_divider: true }); @@ -1243,6 +1327,15 @@ function UIItem (options) { UIWindowShare([{ uid: $(el_item).attr('data-uid'), path: $(el_item).attr('data-path'), name: $(el_item).attr('data-name'), icon: $(el_item_icon).find('img').attr('src') }]); }, }); + // ------------------------------------------- + // Open in AI + // ------------------------------------------- + menu_items.push({ + html: i18n('open_in_ai'), + onClick: async function(){ + await sendSelectionToAIApp($(el_item)); + } + }); } // ------------------------------------------- @@ -1837,4 +1930,4 @@ window.activate_item_name_editor = function (el_item) { } }; -export default UIItem; \ No newline at end of file +export default UIItem; diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index e59faa5f4..52cf903f8 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -27,6 +27,7 @@ const en = { account_password: 'Verify Account Password', access_granted_to: 'Access Granted To', add_existing_account: 'Add Existing Account', + ai_app_unavailable: 'AI app is not available. Please try again later.', all_fields_required: 'All fields are required.', allow: 'Allow', apply: 'Apply', @@ -194,6 +195,7 @@ const en = { ok: 'OK', open: 'Open', new_window: 'New Window', + open_in_ai: 'Open in AI', open_in_new_tab: 'Open in New Tab', open_in_new_window: 'Open in New Window', open_trash: 'Open Trash', @@ -426,7 +428,6 @@ const en = { 'billing.currently_on_free_plan': 'You are currently on the free plan.', 'billing.download_receipt': 'Download Receipt', 'billing.subscription_check_error': 'A problem occurred while checking your subscription status.', - 'billing.payment_method_updated': 'Payment method updated!', 'billing.email_confirmation_needed': 'Your email has not been confirmed. We\'ll send you a code to confirm it now.', 'billing.sub_cancelled_but_valid_until': 'You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe.', 'billing.current_plan_until_end_of_period': 'Your current plan until the end of this billing period.', @@ -537,4 +538,4 @@ const en = { }, }; -export default en; \ No newline at end of file +export default en;