mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-25 16:21:54 +00:00
feat(gui): "Open with AI" in the dashboard Files menu, opening like Open With (#4289)
"Open with AI" now launches the `ai` app with the item the same way
Open With launches any app: launch_app with file_path/file_uid, so the
app gets a signed launch item via puter.ui.onLaunchedWithItems. It
replaces the custom `ai:openFsEntries` postMessage, which went to an
existing AI window.
- The helper moved from UIItem into helpers/openWithAI.js, shared by the
desktop item menu and the dashboard Files menu (generate_file_context_menu).
- A shortcut opens its target.
- Dropped from multi-select menus, since a launch opens a single item.
- Renamed open_in_ai -> open_with_ai ("Open with AI") and removed the
unused ai_app_unavailable string.
This commit is contained in:
1 parent
ed531e3992
commit
66093e78f3
8 files changed
+143
-107
No files matched your search
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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('-');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<unknown>} 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;
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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',
|
||||
}));
|
||||
});
|
||||
@@ -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ář.",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '資料夾',
|
||||
|
||||
Reference in new issue
Block a user