diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js
index 139788e06..0b504eaf1 100644
--- a/src/gui/src/UI/UIItem.js
+++ b/src/gui/src/UI/UIItem.js
@@ -35,81 +35,7 @@ import { isWeblinkName, weblinkChangeIconMenuItem } from '../helpers/weblink.js'
import { is_owned_by_me } from '../helpers/pathOwner.js';
import { can_rename, can_restructure, invalidate_shared_roots, shared_mode_for } from '../helpers/sharedAccess.js';
import { is_default_app } from '../helpers/defaultApp.js';
-
-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',
- }, '*');
-};
+import openWithAI from '../helpers/openWithAI.js';
async function UIItem (options) {
const matching_appendto_count = $(options.appendTo).length;
@@ -972,20 +898,6 @@ async function UIItem (options) {
menu_items.push('-');
}
if ( ! are_trashed ) {
- // -------------------------------------------
- // Open in AI
- // -------------------------------------------
- menu_items.push({
- html: i18n('open_in_ai'),
- onClick: async function () {
- await sendSelectionToAIApp($selected_items);
- },
- });
- // -------------------------------------------
- // -
- // -------------------------------------------
- menu_items.push({ is_divider: true });
-
// -------------------------------------------
// Donwload
// -------------------------------------------
@@ -1305,13 +1217,13 @@ async function UIItem (options) {
}
}
// -------------------------------------------
- // Open in AI
+ // Open with AI
// -------------------------------------------
if ( !is_trashed && !is_trash ) {
menu_items.push({
- html: i18n('open_in_ai'),
- onClick: async function () {
- await sendSelectionToAIApp($(el_item));
+ html: i18n('open_with_ai'),
+ onClick: function () {
+ openWithAI(el_item);
},
});
}
diff --git a/src/gui/src/helpers/generateFileContextMenu.js b/src/gui/src/helpers/generateFileContextMenu.js
index da14c6609..c3c2570d4 100644
--- a/src/gui/src/helpers/generateFileContextMenu.js
+++ b/src/gui/src/helpers/generateFileContextMenu.js
@@ -26,6 +26,7 @@ import UIWindowPublishWorker from '../UI/UIWindowPublishWorker.js';
import UIWindowShare from '../UI/UIWindowShare.js';
import publish_as_website from './publishAsWebsite.js';
import open_item from './openItem.js';
+import openWithAI from './openWithAI.js';
import launch_app from './launchApp.js';
import path from '../lib/path.js';
import { isWeblinkName, weblinkChangeIconMenuItem } from './weblink.js';
@@ -96,13 +97,6 @@ const generate_file_context_menu = async function (options) {
}
},
});
-
- // -------------------------------------------
- // Separator
- // -------------------------------------------
- if ( options.associated_app_name || is_trash ) {
- menu_items.push('-');
- }
}
// -------------------------------------------
@@ -114,7 +108,24 @@ const generate_file_context_menu = async function (options) {
html: i18n('open_with'),
items: openWithItems,
});
+ }
+ // -------------------------------------------
+ // Open with AI
+ // -------------------------------------------
+ if ( !is_trashed && !is_trash ) {
+ menu_items.push({
+ html: i18n('open_with_ai'),
+ onClick: () => {
+ openWithAI(el_item);
+ },
+ });
+ }
+
+ // -------------------------------------------
+ // Separator
+ // -------------------------------------------
+ if ( ! is_trashed ) {
menu_items.push('-');
}
diff --git a/src/gui/src/helpers/openWithAI.js b/src/gui/src/helpers/openWithAI.js
new file mode 100644
index 000000000..90f19cb60
--- /dev/null
+++ b/src/gui/src/helpers/openWithAI.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import launch_app from './launchApp.js';
+import path from '../lib/path.js';
+
+const AI_APP_NAME = 'ai';
+
+/**
+ * Opens a file or folder row (desktop item or dashboard row) in the AI app the
+ * same way "Open With" opens it in any other app. A shortcut opens its target.
+ *
+ * @param {Element} el_item
+ * @returns {Promise} the launch_app result
+ */
+const openWithAI = (el_item) => {
+ const $item = $(el_item);
+ const file_path = $item.attr('data-shortcut_to_path') || $item.attr('data-path');
+ const file_uid = $item.attr('data-shortcut_to') || $item.attr('data-uid');
+ return launch_app({
+ name: AI_APP_NAME,
+ file_path,
+ file_uid,
+ window_title: path.basename(file_path),
+ });
+};
+
+export default openWithAI;
diff --git a/src/gui/src/helpers/openWithAI.test.js b/src/gui/src/helpers/openWithAI.test.js
new file mode 100644
index 000000000..170209a4f
--- /dev/null
+++ b/src/gui/src/helpers/openWithAI.test.js
@@ -0,0 +1,73 @@
+// @vitest-environment jsdom
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { afterEach, expect, it, vi } from 'vitest';
+import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
+
+vi.mock('./launchApp.js', () => ({ default: vi.fn() }));
+
+globalThis.$ = globalThis.jQuery = jQuery;
+
+const { default: launch_app } = await import('./launchApp.js');
+const { default: openWithAI } = await import('./openWithAI.js');
+
+const fileRow = (attrs = {}) => {
+ const row = document.createElement('div');
+ const all = {
+ 'data-uid': 'uid-1',
+ 'data-path': '/user/Documents/notes.txt',
+ 'data-name': 'notes.txt',
+ 'data-shortcut_to': '',
+ 'data-shortcut_to_path': '',
+ ...attrs,
+ };
+ for ( const [name, value] of Object.entries(all) ) row.setAttribute(name, value);
+ return row;
+};
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+it('launches the AI app with the file, as Open With does', () => {
+ openWithAI(fileRow());
+
+ expect(launch_app).toHaveBeenCalledWith({
+ name: 'ai',
+ file_path: '/user/Documents/notes.txt',
+ file_uid: 'uid-1',
+ window_title: 'notes.txt',
+ });
+});
+
+it('opens a shortcut\'s target rather than the shortcut', () => {
+ openWithAI(fileRow({
+ 'data-uid': 'shortcut-uid',
+ 'data-path': '/user/Desktop/notes.txt',
+ 'data-shortcut_to': 'target-uid',
+ 'data-shortcut_to_path': '/user/Documents/report.txt',
+ }));
+
+ expect(launch_app).toHaveBeenCalledWith(expect.objectContaining({
+ file_path: '/user/Documents/report.txt',
+ file_uid: 'target-uid',
+ window_title: 'report.txt',
+ }));
+});
diff --git a/src/gui/src/i18n/translations/cs.js b/src/gui/src/i18n/translations/cs.js
index d215aeda0..aaffb7dec 100644
--- a/src/gui/src/i18n/translations/cs.js
+++ b/src/gui/src/i18n/translations/cs.js
@@ -42,7 +42,6 @@ const cs = {
"app_feedback_placeholder": "Co funguje dobře? Co by mohlo být lepší?",
"add_another_account": "Přidat další účet",
"add_to_desktop": "Přidat na plochu",
- "ai_app_unavailable": "Aplikace AI není k dispozici. Zkuste to znovu později.",
"app_feedback_c2a": "Vaše zpětná vazba bude odeslána přímo vývojáři této aplikace.",
"app_feedback_privacy_note": "Vaše uživatelské jméno a e-mailová adresa budou sdíleny s vývojářem, aby vám mohl odpovědět.",
"add_app_request_sent": "Děkujeme – váš požadavek patří našemu týmu. Pokud váš účet obsahuje e-mail, můžeme na něj navázat.",
@@ -238,7 +237,7 @@ const cs = {
"new_email": "Nový e-mail",
"open_desktop": "Otevřít plochu",
"new_username": "Nové uživatelské jméno",
- "open_in_ai": "Otevřít v AI",
+ "open_with_ai": "Otevřít pomocí AI",
"os": "Operační systém",
"name_must_be_string": "Název může být pouze řetězec.",
"no_dir_associated_with_site": "K této adrese není přidružen žádný adresář.",
diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js
index 3da041f24..1bc50b6da 100644
--- a/src/gui/src/i18n/translations/en.js
+++ b/src/gui/src/i18n/translations/en.js
@@ -42,7 +42,6 @@ const en = {
add_app_request_sent: 'Thanks — your request is with our team. If your account has an email, we may follow up on it.',
add_to_desktop: 'Add to Desktop',
adjust_profile_picture: 'Adjust your photo',
- ai_app_unavailable: 'AI app is not available. Please try again later.',
all_fields_required: 'All fields are required.',
allow: 'Allow',
app_feedback_c2a: 'Your feedback will be sent directly to the developer of this app.',
@@ -302,7 +301,7 @@ const en = {
open: 'Open',
new_window: 'New Window',
open_desktop: 'Open Desktop',
- open_in_ai: 'Open in AI',
+ open_with_ai: 'Open with AI',
open_in_new_tab: 'Open in New Tab',
app_pages: 'App pages',
apps_load_failed: 'Failed to load apps',
diff --git a/src/gui/src/i18n/translations/fr.js b/src/gui/src/i18n/translations/fr.js
index fd9fa3859..07979e138 100644
--- a/src/gui/src/i18n/translations/fr.js
+++ b/src/gui/src/i18n/translations/fr.js
@@ -27,7 +27,6 @@ const fr = {
account_password: 'Vérifier le mot de passe du compte',
access_granted_to: 'Accès accordé à',
add_another_account: 'Ajouter un autre compte',
- ai_app_unavailable: 'L\'application IA n\'est pas disponible. Veuillez réessayer plus tard.',
all_fields_required: 'Tous les champs sont requis.',
allow: 'Autoriser',
apply: 'Appliquer',
@@ -198,7 +197,7 @@ const fr = {
ok: 'OK',
open: 'Ouvrir',
new_window: 'Nouvelle fenêtre',
- open_in_ai: 'Ouvrir dans l\'IA',
+ open_with_ai: 'Ouvrir avec l\'IA',
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
open_in_new_window: 'Ouvrir dans une nouvelle fenêtre',
open_trash: 'Ouvrir la corbeille',
diff --git a/src/gui/src/i18n/translations/zhtw.js b/src/gui/src/i18n/translations/zhtw.js
index c7e2012f8..954f3bbee 100644
--- a/src/gui/src/i18n/translations/zhtw.js
+++ b/src/gui/src/i18n/translations/zhtw.js
@@ -39,7 +39,6 @@ const zhtw = {
add_app_request_sent: '謝謝,您的需求已送達我們的團隊。如果您的帳戶設有電子郵件,我們可能會透過該信箱與您聯絡。',
add_another_account: '新增其他帳戶',
add_to_desktop: '新增至桌面',
- ai_app_unavailable: 'AI 應用程式目前無法使用,請稍後再試。',
all_fields_required: '所有欄位都是必填的。',
allow: '允許',
app_group_default_name: '資料夾',