This commit is contained in:
+3
-2
@@ -42,8 +42,9 @@ The expected flow is:
|
||||
`shd_render_acl_matrix_widget`, `shd_render_inventory_warnings_widget`,
|
||||
`shd_render_gitea_board_widget`, `shd_render_schemes_progress_widget`,
|
||||
`shd_render_notes_tree_widget` or `shd_render_project_db_preview_widget`.
|
||||
3. The selected widget renders a read-only register and can call the matching
|
||||
detail tool for one selected row.
|
||||
3. The selected widget renders a register, can call the matching detail tool for
|
||||
one selected row and exposes controlled write actions when declared by the
|
||||
render metadata.
|
||||
|
||||
The widget resources are versioned at `ui://shd/active-projects/v1.html`,
|
||||
`ui://shd/documents/v1.html`, `ui://shd/tasks/v1.html`,
|
||||
|
||||
@@ -48,12 +48,12 @@ a custom app and set the MCP URL to `https://shd.xyz.su/mcp`. Complete the SHD
|
||||
OAuth flow and refresh the app after server metadata changes.
|
||||
|
||||
The active-projects result can render an inline MCP Apps widget. It is a small
|
||||
read-only dashboard with:
|
||||
dashboard with:
|
||||
|
||||
- `Проект → Дата завершения → Статус`;
|
||||
- sorting by the nearest deadline;
|
||||
- local search by project name/code;
|
||||
- a refresh action and a read-only project-details panel.
|
||||
- a refresh action, a project-details panel and controlled project actions.
|
||||
|
||||
If the host does not support MCP Apps UI, the data tool and the plain Markdown
|
||||
table remain fully usable.
|
||||
@@ -62,7 +62,7 @@ The backend serves the checked-in widget artifact at
|
||||
`ui://shd/active-projects/v1.html`; its manifest and `SHA256SUMS` are kept next
|
||||
to the HTML resource under `plugins/shd-mcp-plugin/widgets/active-projects/v1/`.
|
||||
|
||||
The same read-only data/render contract is available for Documents, Tasks,
|
||||
The same data/render contract is available for Documents, Tasks,
|
||||
Finance, CRM, Discussions, Notifications, Scheduling, Inventory, Agents,
|
||||
Status Page, Project DB, Notes, Project overview, Files, Proposals, Terms,
|
||||
Activity, Organizations and access, Gitea Issues and Schemes:
|
||||
|
||||
@@ -34,10 +34,14 @@ modules. Skills do not grant permissions or bypass server-side ACLs.
|
||||
- safe bulk-change planning and validation rules.
|
||||
|
||||
When the connected MCP host supports MCP Apps UI, the 36 SHD render tools have
|
||||
inline read-only widgets: active projects, module registers and specialized
|
||||
inline interactive widgets: active projects, module registers and specialized
|
||||
Kanban, workload, timeline, funnel, dashboard, calendar, matrix, tree and
|
||||
Project DB views. Each widget sorts or filters normalized records, shows
|
||||
status/value fields and can load one selected record’s details where supported.
|
||||
status/value fields, can load one selected record’s details where supported and
|
||||
exposes only the write actions declared by its render metadata. A write always
|
||||
requires an explicit form submission; destructive actions also require a
|
||||
confirmation checkbox. After a successful mutation, the widget refreshes its
|
||||
source list.
|
||||
The same tools still return normal structured data for hosts that do not render
|
||||
widgets.
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
@@ -0,0 +1,429 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
@@ -1 +1 @@
|
||||
806f28ac654a7504ae5a258add4c03114dfc9fc80328d479e23b09be92c7a74d index.html
|
||||
818956bcece3e1a64a8f41f51a8bcc19ebcd0c95d1eb9bfa923166dca9660711 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="acl-matrix"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48773,
|
||||
"gzipBytes": 13973,
|
||||
"bytes": 69107,
|
||||
"gzipBytes": 18847,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/acl-matrix/v1.html",
|
||||
"sha256": "806f28ac654a7504ae5a258add4c03114dfc9fc80328d479e23b09be92c7a74d"
|
||||
"sha256": "818956bcece3e1a64a8f41f51a8bcc19ebcd0c95d1eb9bfa923166dca9660711"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
f5338c9d833ea57be5823d0f302e00ad8dc85ce5645681e71fdad134d6465ba1 index.html
|
||||
7c0ad632ddc013afd70ce80789a73c3b99aa3030d9d7b1d440ba1902aadae42d index.html
|
||||
|
||||
@@ -99,6 +99,28 @@
|
||||
.details-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app" aria-live="polite">
|
||||
@@ -492,5 +514,436 @@
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 22838,
|
||||
"gzipBytes": 6186,
|
||||
"bytes": 43172,
|
||||
"gzipBytes": 10825,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/active-projects/v1.html",
|
||||
"sha256": "f5338c9d833ea57be5823d0f302e00ad8dc85ce5645681e71fdad134d6465ba1"
|
||||
"sha256": "7c0ad632ddc013afd70ce80789a73c3b99aa3030d9d7b1d440ba1902aadae42d"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
3af72830f40d1cb3e10194f875c6e403bf5f112151399e623c858981dfcaab5b index.html
|
||||
766b830b207f2929c16635f24081bf11e9e69f2c9a4cb261157a79a6cfd4dc63 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25600,
|
||||
"gzipBytes": 8709,
|
||||
"bytes": 45934,
|
||||
"gzipBytes": 13581,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/activity/v1.html",
|
||||
"sha256": "3af72830f40d1cb3e10194f875c6e403bf5f112151399e623c858981dfcaab5b"
|
||||
"sha256": "766b830b207f2929c16635f24081bf11e9e69f2c9a4cb261157a79a6cfd4dc63"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
2795c7d06cbcc95984f34ef5f0c802b4653ec4af9239d07337ba20e795017bc0 index.html
|
||||
d7042f24d6bce4b94db2726c252def333bb99c619295c85a8fb4855788c35373 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="agents-health"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48779,
|
||||
"gzipBytes": 13974,
|
||||
"bytes": 69113,
|
||||
"gzipBytes": 18846,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/agents-health/v1.html",
|
||||
"sha256": "2795c7d06cbcc95984f34ef5f0c802b4653ec4af9239d07337ba20e795017bc0"
|
||||
"sha256": "d7042f24d6bce4b94db2726c252def333bb99c619295c85a8fb4855788c35373"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
f1576f8f4653c7178b17545c136eb3609099ebd4c581ddbff5fe52331a7b71f0 index.html
|
||||
9e502a2d867a52efbd71352cdacce359b95ade89a62b5974e67ff83d5921480b index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="agents"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21061,
|
||||
"gzipBytes": 7411,
|
||||
"bytes": 41395,
|
||||
"gzipBytes": 12426,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/agents/v1.html",
|
||||
"sha256": "f1576f8f4653c7178b17545c136eb3609099ebd4c581ddbff5fe52331a7b71f0"
|
||||
"sha256": "9e502a2d867a52efbd71352cdacce359b95ade89a62b5974e67ff83d5921480b"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
8c7aab8e966bcb14f48ba6a24993654b5083c0b85f9898aec6d4d478c56b0c22 index.html
|
||||
8aa0f61e0d1a87444c832d9de570faf4e7ffac715ca13a060379db21bac61edb index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="crm-funnel"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48773,
|
||||
"gzipBytes": 13974,
|
||||
"bytes": 69107,
|
||||
"gzipBytes": 18847,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/crm-funnel/v1.html",
|
||||
"sha256": "8c7aab8e966bcb14f48ba6a24993654b5083c0b85f9898aec6d4d478c56b0c22"
|
||||
"sha256": "8aa0f61e0d1a87444c832d9de570faf4e7ffac715ca13a060379db21bac61edb"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
64f552a54e178e55d0102a050d001751051ae4d2c50370dbdd24201263499603 index.html
|
||||
f8f91a266320065738372c23e8376c370694ee9c0ec7f8283372ebf9f67cb364 index.html
|
||||
|
||||
@@ -21,6 +21,28 @@
|
||||
.detail{padding:9px;border-radius:8px;background:var(--bg)}.detail-label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.detail-value{overflow-wrap:anywhere}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#11151d;--panel:#1b2230;--line:#303a4b;--ink:#edf2fb;--muted:#aab4c5;--accent:#82aaff;--accent-soft:#263858;--danger:#ff9a93}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="crm">
|
||||
@@ -278,5 +300,436 @@
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
"file": "index.html",
|
||||
"uri": "ui://shd/crm/v1.html",
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"bytes": 15485,
|
||||
"sha256": "64f552a54e178e55d0102a050d001751051ae4d2c50370dbdd24201263499603"
|
||||
"bytes": 35819,
|
||||
"sha256": "f8f91a266320065738372c23e8376c370694ee9c0ec7f8283372ebf9f67cb364",
|
||||
"gzipBytes": 9477
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
c9ae4958b7c0f9dc1e872811e310809b51e614ace2850e8558d6c240e612e3c0 index.html
|
||||
9a5b3fd821139066be25a8b0ef920a94b38f4e1b0f9b71a290f9fc0fc7104897 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="discussions"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21071,
|
||||
"gzipBytes": 7411,
|
||||
"bytes": 41405,
|
||||
"gzipBytes": 12425,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/discussions/v1.html",
|
||||
"sha256": "c9ae4958b7c0f9dc1e872811e310809b51e614ace2850e8558d6c240e612e3c0"
|
||||
"sha256": "9a5b3fd821139066be25a8b0ef920a94b38f4e1b0f9b71a290f9fc0fc7104897"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
553db1686496a3fa6e2097924cb33db72ca24f0a68a4a8454cad2e14ad0ea960 index.html
|
||||
c22ee23ef323e9e0b076d17b75f4d74f42670657ff787fca950334b8e882b0a7 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="documents-completeness"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48797,
|
||||
"gzipBytes": 13975,
|
||||
"bytes": 69131,
|
||||
"gzipBytes": 18846,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/documents-completeness/v1.html",
|
||||
"sha256": "553db1686496a3fa6e2097924cb33db72ca24f0a68a4a8454cad2e14ad0ea960"
|
||||
"sha256": "c22ee23ef323e9e0b076d17b75f4d74f42670657ff787fca950334b8e882b0a7"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
5959d58c47ab81813202df203cf69a568a646d61884e64802e113e752229d925 index.html
|
||||
9cf8dba408490f78756663d3593c27661fba941e63eaceb5b0576b98ae161c94 index.html
|
||||
|
||||
@@ -21,6 +21,28 @@
|
||||
.detail{padding:9px;border-radius:8px;background:var(--bg)}.detail-label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.detail-value{overflow-wrap:anywhere}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#11151d;--panel:#1b2230;--line:#303a4b;--ink:#edf2fb;--muted:#aab4c5;--accent:#82aaff;--accent-soft:#263858;--danger:#ff9a93}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="documents">
|
||||
@@ -278,5 +300,436 @@
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
"file": "index.html",
|
||||
"uri": "ui://shd/documents/v1.html",
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"bytes": 15510,
|
||||
"sha256": "5959d58c47ab81813202df203cf69a568a646d61884e64802e113e752229d925"
|
||||
"bytes": 35844,
|
||||
"sha256": "9cf8dba408490f78756663d3593c27661fba941e63eaceb5b0576b98ae161c94",
|
||||
"gzipBytes": 9468
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
09c6eed7f36baf0b214e1c9252de4f239405ae86673d06c31f6b5f66913f4144 index.html
|
||||
6091dc2361901d2f15b2cb19ef6bf5ce0c955f79a6017838cbe9b2d55da027ae index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25597,
|
||||
"gzipBytes": 8708,
|
||||
"bytes": 45931,
|
||||
"gzipBytes": 13579,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/files/v1.html",
|
||||
"sha256": "09c6eed7f36baf0b214e1c9252de4f239405ae86673d06c31f6b5f66913f4144"
|
||||
"sha256": "6091dc2361901d2f15b2cb19ef6bf5ce0c955f79a6017838cbe9b2d55da027ae"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
7e849281109ad6b9015673083b76ce73fe72b0409eded0ab381b4e48a52170b1 index.html
|
||||
7b461816e3482390408daf834faef083686f9d0fe567fc4ca9e808f56343ab67 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="finance-dashboard"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48787,
|
||||
"gzipBytes": 13972,
|
||||
"bytes": 69121,
|
||||
"gzipBytes": 18845,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/finance-dashboard/v1.html",
|
||||
"sha256": "7e849281109ad6b9015673083b76ce73fe72b0409eded0ab381b4e48a52170b1"
|
||||
"sha256": "7b461816e3482390408daf834faef083686f9d0fe567fc4ca9e808f56343ab67"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
86a8d3275d1fdfaa6bbf5d1ab308b3e19a0b153897f614edd2c23d260030094e index.html
|
||||
bcd1e3b07ccb8bdb9a28d884d01a64c473fb7ca079271eadb062446cf81174c6 index.html
|
||||
|
||||
@@ -21,6 +21,28 @@
|
||||
.detail{padding:9px;border-radius:8px;background:var(--bg)}.detail-label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.detail-value{overflow-wrap:anywhere}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#11151d;--panel:#1b2230;--line:#303a4b;--ink:#edf2fb;--muted:#aab4c5;--accent:#82aaff;--accent-soft:#263858;--danger:#ff9a93}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="finance">
|
||||
@@ -278,5 +300,436 @@
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
"file": "index.html",
|
||||
"uri": "ui://shd/finance/v1.html",
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"bytes": 15532,
|
||||
"sha256": "86a8d3275d1fdfaa6bbf5d1ab308b3e19a0b153897f614edd2c23d260030094e"
|
||||
"bytes": 35866,
|
||||
"sha256": "bcd1e3b07ccb8bdb9a28d884d01a64c473fb7ca079271eadb062446cf81174c6",
|
||||
"gzipBytes": 9488
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
e68a43f81a322387f71b947124d40a108881f9204600758a0db4ac374d3e3e04 index.html
|
||||
02b97b4308f0d7a8e3a3c2337d60b6ae0a442480851fd3926fad7e204302e81f index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="gitea-board"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48775,
|
||||
"gzipBytes": 13972,
|
||||
"bytes": 69109,
|
||||
"gzipBytes": 18846,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/gitea-board/v1.html",
|
||||
"sha256": "e68a43f81a322387f71b947124d40a108881f9204600758a0db4ac374d3e3e04"
|
||||
"sha256": "02b97b4308f0d7a8e3a3c2337d60b6ae0a442480851fd3926fad7e204302e81f"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
08a381ad106c5fa60d3c5c17c08949cd69f139c6fb5094251fa107f7c8bbaeb4 index.html
|
||||
505ea4f334275478da05b4444da93f7e7c89163eeb1880825b2c37d275678fa1 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25597,
|
||||
"gzipBytes": 8708,
|
||||
"bytes": 45931,
|
||||
"gzipBytes": 13579,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/gitea/v1.html",
|
||||
"sha256": "08a381ad106c5fa60d3c5c17c08949cd69f139c6fb5094251fa107f7c8bbaeb4"
|
||||
"sha256": "505ea4f334275478da05b4444da93f7e7c89163eeb1880825b2c37d275678fa1"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
346c27c4b13871b31b2e66116a0ca85f916127dbcb73c3f5b4c5709ec209ea81 index.html
|
||||
01c17190678fb59ec142c4c5356558d8946ee2200169557035d329fb100022d1 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="inventory-warnings"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48789,
|
||||
"gzipBytes": 13974,
|
||||
"bytes": 69123,
|
||||
"gzipBytes": 18847,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/inventory-warnings/v1.html",
|
||||
"sha256": "346c27c4b13871b31b2e66116a0ca85f916127dbcb73c3f5b4c5709ec209ea81"
|
||||
"sha256": "01c17190678fb59ec142c4c5356558d8946ee2200169557035d329fb100022d1"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
684908987a55fd77f1b3d32dcd941b82163c03c04a48785589ccbb9ceeeb7f1f index.html
|
||||
d61402f07b1f56b5e0680b783367286b60801b835aa20e190cec4e19a091ddfe index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="inventory"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21067,
|
||||
"gzipBytes": 7411,
|
||||
"bytes": 41401,
|
||||
"gzipBytes": 12425,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/inventory/v1.html",
|
||||
"sha256": "684908987a55fd77f1b3d32dcd941b82163c03c04a48785589ccbb9ceeeb7f1f"
|
||||
"sha256": "d61402f07b1f56b5e0680b783367286b60801b835aa20e190cec4e19a091ddfe"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
00e5b692dd5d28f77ea210e6ee9f1c0ad07d68c978284f5380dc1db44f6d2107 index.html
|
||||
aa953f233c4783e336f88a8680e37e0f124a2abd4d4d9ee6bd9c433ba0ee284b index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes-tree"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48773,
|
||||
"gzipBytes": 13976,
|
||||
"bytes": 69107,
|
||||
"gzipBytes": 18849,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/notes-tree/v1.html",
|
||||
"sha256": "00e5b692dd5d28f77ea210e6ee9f1c0ad07d68c978284f5380dc1db44f6d2107"
|
||||
"sha256": "aa953f233c4783e336f88a8680e37e0f124a2abd4d4d9ee6bd9c433ba0ee284b"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
3aeb485b0b27cf89fe160b6e826d83f0027c01809756ef46701668a03105d321 index.html
|
||||
f90f27e3ca3cb80aee68e1e35d37eb2d664b5055cd0d0075c3f532a811db60ff index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21059,
|
||||
"gzipBytes": 7411,
|
||||
"bytes": 41393,
|
||||
"gzipBytes": 12426,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/notes/v1.html",
|
||||
"sha256": "3aeb485b0b27cf89fe160b6e826d83f0027c01809756ef46701668a03105d321"
|
||||
"sha256": "f90f27e3ca3cb80aee68e1e35d37eb2d664b5055cd0d0075c3f532a811db60ff"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
8fd7b9cdb616836d076ae8a4883d66fcd619bf3214e7891fa0fcda4411dbf3e7 index.html
|
||||
4404502bb5202bba271baf2fbdbabcbe400d563aee46d0ebde01bafcc14ca983 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notifications"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21075,
|
||||
"gzipBytes": 7410,
|
||||
"bytes": 41409,
|
||||
"gzipBytes": 12425,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/notifications/v1.html",
|
||||
"sha256": "8fd7b9cdb616836d076ae8a4883d66fcd619bf3214e7891fa0fcda4411dbf3e7"
|
||||
"sha256": "4404502bb5202bba271baf2fbdbabcbe400d563aee46d0ebde01bafcc14ca983"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
8059ccb18d02821666416ac717cd718479cb1a3f95baa27d1edfc19be5220c90 index.html
|
||||
2afde14a7b9c2bc6148a2f630328c4573e54a53d0084acb16afe74bc5b317fae index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25605,
|
||||
"gzipBytes": 8711,
|
||||
"bytes": 45939,
|
||||
"gzipBytes": 13581,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/organizations/v1.html",
|
||||
"sha256": "8059ccb18d02821666416ac717cd718479cb1a3f95baa27d1edfc19be5220c90"
|
||||
"sha256": "2afde14a7b9c2bc6148a2f630328c4573e54a53d0084acb16afe74bc5b317fae"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
b114986b9c03d02c63c5bcf8c0697615f4c80fa853bc9cf0bb5f6529357d90a4 index.html
|
||||
91fc075555887d5084d9f1a3d5cf94cba017f0b2cf765073612a51cb099c6596 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="project-db-preview"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48777,
|
||||
"gzipBytes": 13982,
|
||||
"bytes": 69111,
|
||||
"gzipBytes": 18856,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/project-db-preview/v1.html",
|
||||
"sha256": "b114986b9c03d02c63c5bcf8c0697615f4c80fa853bc9cf0bb5f6529357d90a4"
|
||||
"sha256": "91fc075555887d5084d9f1a3d5cf94cba017f0b2cf765073612a51cb099c6596"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
bd34985f224325c18ae2427a49bf2355ee0d7009d47326000a10d9d7027effcb index.html
|
||||
1eab29c71fcd79c4e50de88fd9d3e231f845793fc507134ba393fc0b74fc6024 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="project-db"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21069,
|
||||
"gzipBytes": 7410,
|
||||
"bytes": 41403,
|
||||
"gzipBytes": 12423,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/project-db/v1.html",
|
||||
"sha256": "bd34985f224325c18ae2427a49bf2355ee0d7009d47326000a10d9d7027effcb"
|
||||
"sha256": "1eab29c71fcd79c4e50de88fd9d3e231f845793fc507134ba393fc0b74fc6024"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
adf8b6f9a96bf904cadd3eee70f4f18e870ca8e0e6286d4ad0d18f04f6174452 index.html
|
||||
a4e2aa0dfbc85fe9f8b186d47558ec6d3bd7809b8feab020601b643de0c77ad9 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25608,
|
||||
"gzipBytes": 8709,
|
||||
"bytes": 45942,
|
||||
"gzipBytes": 13579,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/project-overview/v1.html",
|
||||
"sha256": "adf8b6f9a96bf904cadd3eee70f4f18e870ca8e0e6286d4ad0d18f04f6174452"
|
||||
"sha256": "a4e2aa0dfbc85fe9f8b186d47558ec6d3bd7809b8feab020601b643de0c77ad9"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
6ab0655050f4c66631ce1a045daa3473b61f69aaafacdba7e290309c6652f7be index.html
|
||||
2f065b809eeaa8d7f5dcb28815ddba3f4121702cd7da9a073c5d49c427837646 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="project-timeline"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48785,
|
||||
"gzipBytes": 13975,
|
||||
"bytes": 69119,
|
||||
"gzipBytes": 18847,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/project-timeline/v1.html",
|
||||
"sha256": "6ab0655050f4c66631ce1a045daa3473b61f69aaafacdba7e290309c6652f7be"
|
||||
"sha256": "2f065b809eeaa8d7f5dcb28815ddba3f4121702cd7da9a073c5d49c427837646"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
118da1cb2a8b7d0f1dc1c063257e0e7d7882f79e85a54911176d0f75cbe277b1 index.html
|
||||
7add1c840a7802d6b7ffabc47e92d3556f5d21ef8e38e77bbfd40dab822d7112 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="proposal-approvals"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48789,
|
||||
"gzipBytes": 13973,
|
||||
"bytes": 69123,
|
||||
"gzipBytes": 18846,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/proposal-approvals/v1.html",
|
||||
"sha256": "118da1cb2a8b7d0f1dc1c063257e0e7d7882f79e85a54911176d0f75cbe277b1"
|
||||
"sha256": "7add1c840a7802d6b7ffabc47e92d3556f5d21ef8e38e77bbfd40dab822d7112"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
cc812d7531d1a0a512a032e8f9febbe302047daa8f45344d8e59cf46109dbec5 index.html
|
||||
f729f20f9966978f548c55f1d5e3b7af69bc78adae606ea164f0de68c7e721b4 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25601,
|
||||
"gzipBytes": 8707,
|
||||
"bytes": 45935,
|
||||
"gzipBytes": 13579,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/proposals/v1.html",
|
||||
"sha256": "cc812d7531d1a0a512a032e8f9febbe302047daa8f45344d8e59cf46109dbec5"
|
||||
"sha256": "f729f20f9966978f548c55f1d5e3b7af69bc78adae606ea164f0de68c7e721b4"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
15caaf41295414b9edefad9d112341b3ab5d6172b085c4053177f855bb307e55 index.html
|
||||
c8fa40e545beb126b55fb1834a9e7025b8b31990e2aa9bf4a73698b9ebe3714f index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="schedule-calendar"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48787,
|
||||
"gzipBytes": 13973,
|
||||
"bytes": 69121,
|
||||
"gzipBytes": 18846,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/schedule-calendar/v1.html",
|
||||
"sha256": "15caaf41295414b9edefad9d112341b3ab5d6172b085c4053177f855bb307e55"
|
||||
"sha256": "c8fa40e545beb126b55fb1834a9e7025b8b31990e2aa9bf4a73698b9ebe3714f"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
2aa242337b7a4e1976dec1b479c85ee3df718c7e3a2d98fec10b4f0c3a6f799c index.html
|
||||
e0ef3f5e588db53afe6b2617cee58853fa3b6be8cce898b7225c840e24cac1b2 index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="scheduling"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 21069,
|
||||
"gzipBytes": 7409,
|
||||
"bytes": 41403,
|
||||
"gzipBytes": 12426,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/scheduling/v1.html",
|
||||
"sha256": "2aa242337b7a4e1976dec1b479c85ee3df718c7e3a2d98fec10b4f0c3a6f799c"
|
||||
"sha256": "e0ef3f5e588db53afe6b2617cee58853fa3b6be8cce898b7225c840e24cac1b2"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
7f3769dc8cb8c4676ca3e323f7b1a84875d8b3466d086d1eb78b52f9c4e72183 index.html
|
||||
d3003fcfee7d93d5a1cf015afdd6d317a864a14425e8cbd2659eb4d90831d741 index.html
|
||||
|
||||
@@ -14,6 +14,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="schemes-progress"><section class="card">
|
||||
@@ -160,5 +182,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 48785,
|
||||
"gzipBytes": 13976,
|
||||
"bytes": 69119,
|
||||
"gzipBytes": 18849,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/schemes-progress/v1.html",
|
||||
"sha256": "7f3769dc8cb8c4676ca3e323f7b1a84875d8b3466d086d1eb78b52f9c4e72183"
|
||||
"sha256": "d3003fcfee7d93d5a1cf015afdd6d317a864a14425e8cbd2659eb4d90831d741"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
d1ceb0813f9ef03cb19becf0bec007c423fafd06a398a2d3ea64af093f540cc4 index.html
|
||||
e0cbdcef97b140b36f1f68ce17f24a4ad439130e3532b9407bbafe9d31860ccc index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="notes"><section class="card">
|
||||
@@ -103,5 +125,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"resources": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 25599,
|
||||
"gzipBytes": 8708,
|
||||
"bytes": 45933,
|
||||
"gzipBytes": 13579,
|
||||
"mediaType": "text/html;profile=mcp-app",
|
||||
"uri": "ui://shd/schemes/v1.html",
|
||||
"sha256": "d1ceb0813f9ef03cb19becf0bec007c423fafd06a398a2d3ea64af093f540cc4"
|
||||
"sha256": "e0cbdcef97b140b36f1f68ce17f24a4ad439130e3532b9407bbafe9d31860ccc"
|
||||
}
|
||||
],
|
||||
"license": "../../../../../LICENSE",
|
||||
|
||||
@@ -1 +1 @@
|
||||
f26198ae7f8dc9cbb2138d58db51e0a09529e74a469eb913c722a0e569865962 index.html
|
||||
5af408540417854819a69ccefe2e16b14ed6d04f8222f6c9e79b006de71a438d index.html
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
|
||||
</style>
|
||||
<style data-shd-widget-write-actions="css">
|
||||
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions[hidden]{display:none}
|
||||
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
|
||||
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
|
||||
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
|
||||
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
|
||||
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
|
||||
.shd-widget-actions-field:has(textarea){grid-column:span 2}
|
||||
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
|
||||
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
|
||||
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-danger{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
|
||||
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
|
||||
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
||||
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
|
||||
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
|
||||
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
|
||||
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" data-widget-module="status-page"><section class="card">
|
||||
@@ -87,5 +109,436 @@
|
||||
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
|
||||
}());
|
||||
</script>
|
||||
<script data-shd-widget-write-actions="js">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
|
||||
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
|
||||
|
||||
var copy = {
|
||||
ru: {
|
||||
actions: 'Действия',
|
||||
chooseAction: 'Выберите действие',
|
||||
selectRecord: 'Сначала выберите запись.',
|
||||
submit: 'Сохранить',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтверждаю действие',
|
||||
warning: 'Проверьте данные перед записью.',
|
||||
destructive: 'Операция может изменить или удалить данные.',
|
||||
required: 'Заполните обязательное поле.',
|
||||
invalidJson: 'Введите корректный JSON.',
|
||||
success: 'Изменения сохранены.',
|
||||
failure: 'Не удалось сохранить изменения.',
|
||||
loading: 'Сохраняю…',
|
||||
noActions: 'Для этого виджета нет доступных действий.',
|
||||
refreshed: 'Список обновлён.',
|
||||
fieldValue: 'Значение',
|
||||
},
|
||||
en: {
|
||||
actions: 'Actions',
|
||||
chooseAction: 'Choose an action',
|
||||
selectRecord: 'Select a record first.',
|
||||
submit: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'I confirm this action',
|
||||
warning: 'Review the values before writing.',
|
||||
destructive: 'This operation may change or remove data.',
|
||||
required: 'Fill in the required field.',
|
||||
invalidJson: 'Enter valid JSON.',
|
||||
success: 'Changes saved.',
|
||||
failure: 'Could not save changes.',
|
||||
loading: 'Saving…',
|
||||
noActions: 'No actions are available for this widget.',
|
||||
refreshed: 'List refreshed.',
|
||||
fieldValue: 'Value',
|
||||
},
|
||||
};
|
||||
|
||||
var state = {
|
||||
widget: {},
|
||||
records: [],
|
||||
selected: null,
|
||||
action: null,
|
||||
inputs: {},
|
||||
busy: false,
|
||||
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
|
||||
};
|
||||
var pending = new Map();
|
||||
var nextRequestId = 1;
|
||||
var elements = {};
|
||||
|
||||
function t(key) {
|
||||
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (!value) return null;
|
||||
if (value.structuredContent) return normalize(value.structuredContent);
|
||||
if (value.toolOutput) return normalize(value.toolOutput);
|
||||
if (value.toolInput) return normalize(value.toolInput);
|
||||
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
|
||||
return value;
|
||||
}
|
||||
|
||||
function widgetFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var meta = object(payload.meta) ? payload.meta : {};
|
||||
return object(meta.widget) ? meta.widget : {};
|
||||
}
|
||||
|
||||
function recordsFrom(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
|
||||
if (!Array.isArray(data)) data = payload.data;
|
||||
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
|
||||
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
|
||||
}
|
||||
|
||||
function readPath(value, path) {
|
||||
if (!value || path === undefined || path === null) return undefined;
|
||||
var parts = String(path).split('.');
|
||||
var current = value;
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) return undefined;
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function firstValue(paths) {
|
||||
var list = Array.isArray(paths) ? paths : [paths];
|
||||
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
|
||||
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
||||
var sourceObject = sources[sourceIndex];
|
||||
if (!sourceObject) continue;
|
||||
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
|
||||
var value = readPath(sourceObject, list[pathIndex]);
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function label(value) {
|
||||
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
|
||||
return String(value || t('fieldValue'));
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
return label(action && action.label) || action.id || action.tool || t('actions');
|
||||
}
|
||||
|
||||
function hasTargets(action) {
|
||||
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
|
||||
}
|
||||
|
||||
function actionList() {
|
||||
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
|
||||
return action && action.tool;
|
||||
}) : [];
|
||||
}
|
||||
|
||||
function request(method, params) {
|
||||
var openai = typeof window !== 'undefined' ? window.openai : undefined;
|
||||
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
|
||||
return openai.callTool(params.name, params.arguments || {});
|
||||
}
|
||||
var id = nextRequestId++;
|
||||
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = window.setTimeout(function () {
|
||||
pending.delete(id);
|
||||
reject(new Error(t('failure')));
|
||||
}, 15000);
|
||||
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
|
||||
});
|
||||
}
|
||||
|
||||
function unwrap(value) {
|
||||
var payload = normalize(value);
|
||||
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
|
||||
if (payload && payload.error) throw new Error(text(payload.error));
|
||||
return payload || value;
|
||||
}
|
||||
|
||||
function make(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (value !== undefined) node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (elements.panel) return;
|
||||
var root = document.querySelector('main') || document.body;
|
||||
var panel = make('section', undefined, 'shd-widget-actions');
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
var header = make('div', undefined, 'shd-widget-actions-header');
|
||||
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
|
||||
var select = document.createElement('select');
|
||||
select.className = 'shd-widget-actions-select';
|
||||
select.setAttribute('aria-label', t('actions'));
|
||||
var form = make('form', undefined, 'shd-widget-actions-form');
|
||||
var status = make('div', '', 'shd-widget-actions-status');
|
||||
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
|
||||
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
|
||||
submit.type = 'submit';
|
||||
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
|
||||
cancel.type = 'button';
|
||||
buttons.append(submit, cancel);
|
||||
panel.append(header, select, form, status, buttons);
|
||||
root.append(panel);
|
||||
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
|
||||
select.addEventListener('change', function () {
|
||||
var selected = actionList().find(function (action) { return action.id === select.value; });
|
||||
state.action = selected || null;
|
||||
renderForm();
|
||||
});
|
||||
cancel.addEventListener('click', function () {
|
||||
state.action = null;
|
||||
select.value = '';
|
||||
renderForm();
|
||||
});
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
submitAction();
|
||||
});
|
||||
}
|
||||
|
||||
function inputValue(fieldConfig) {
|
||||
var value = firstValue(fieldConfig.source || []);
|
||||
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
|
||||
return value;
|
||||
}
|
||||
|
||||
function createInput(fieldConfig) {
|
||||
var type = fieldConfig.type || 'text';
|
||||
var input;
|
||||
if (type === 'textarea' || type === 'json') {
|
||||
input = document.createElement('textarea');
|
||||
input.rows = type === 'json' ? 4 : 3;
|
||||
if (type === 'json') input.placeholder = '{ }';
|
||||
} else if (type === 'select') {
|
||||
input = document.createElement('select');
|
||||
(fieldConfig.options || []).forEach(function (option) {
|
||||
var item = document.createElement('option');
|
||||
item.value = String(option[0]);
|
||||
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
|
||||
input.append(item);
|
||||
});
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = type === 'number' || type === 'email' ? type : 'text';
|
||||
}
|
||||
input.className = 'shd-widget-actions-input';
|
||||
input.name = fieldConfig.name;
|
||||
var value = inputValue(fieldConfig);
|
||||
if (type === 'checkbox') {
|
||||
input.type = 'checkbox';
|
||||
input.checked = value === true || value === 'true' || value === 1 || value === '1';
|
||||
} else if (value !== undefined && value !== null) {
|
||||
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
}
|
||||
if (fieldConfig.required) input.required = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
ensurePanel();
|
||||
var selectedActionId = state.action ? state.action.id : '';
|
||||
elements.select.replaceChildren();
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = t('chooseAction');
|
||||
elements.select.append(placeholder);
|
||||
actionList().forEach(function (action) {
|
||||
var option = document.createElement('option');
|
||||
option.value = action.id;
|
||||
option.textContent = actionLabel(action);
|
||||
elements.select.append(option);
|
||||
});
|
||||
elements.select.value = selectedActionId;
|
||||
elements.select.hidden = actionList().length === 0;
|
||||
elements.form.replaceChildren();
|
||||
elements.status.textContent = '';
|
||||
elements.submit.disabled = state.busy;
|
||||
var action = state.action;
|
||||
if (!action) {
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = true;
|
||||
elements.cancel.hidden = true;
|
||||
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
|
||||
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
|
||||
elements.submit.hidden = false;
|
||||
elements.cancel.hidden = false;
|
||||
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var wrapper = make('label', undefined, 'shd-widget-actions-field');
|
||||
wrapper.append(make('span', label(fieldConfig.label)));
|
||||
wrapper.append(createInput(fieldConfig));
|
||||
elements.form.append(wrapper);
|
||||
});
|
||||
if (action.confirm) {
|
||||
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
|
||||
var confirm = document.createElement('input');
|
||||
confirm.type = 'checkbox';
|
||||
confirm.name = '__confirm';
|
||||
confirm.required = true;
|
||||
confirmLabel.append(confirm, make('span', t('confirm')));
|
||||
elements.form.append(confirmLabel);
|
||||
}
|
||||
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
|
||||
elements.form.append(notice);
|
||||
}
|
||||
|
||||
function coerce(value, type) {
|
||||
if (type === 'number') {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : value;
|
||||
}
|
||||
if (type === 'checkbox') return Boolean(value);
|
||||
if (type === 'json') {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function targetType(name) {
|
||||
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
|
||||
}
|
||||
|
||||
function targetList(action) {
|
||||
if (Array.isArray(action.targets)) return action.targets;
|
||||
return action.target ? [action.target] : [];
|
||||
}
|
||||
|
||||
function collectArguments(action) {
|
||||
var args = Object.assign({}, action.fixed || {});
|
||||
(action.context || []).forEach(function (name) {
|
||||
var value = firstValue([name]);
|
||||
if (value !== undefined && value !== null && value !== '') args[name] = value;
|
||||
});
|
||||
targetList(action).forEach(function (targetConfig) {
|
||||
var value = firstValue(targetConfig.source || targetConfig.name);
|
||||
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
|
||||
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
|
||||
});
|
||||
(action.fields || []).forEach(function (fieldConfig) {
|
||||
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
|
||||
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
|
||||
return node.getAttribute('name') === fieldConfig.name;
|
||||
});
|
||||
if (!input) return;
|
||||
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
|
||||
if (raw === '' && !input.checked && !fieldConfig.required) return;
|
||||
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
|
||||
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
|
||||
});
|
||||
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
if (action.confirmArg) args.confirm = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function setStatus(message, error) {
|
||||
elements.status.textContent = message || '';
|
||||
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
|
||||
}
|
||||
|
||||
function submitAction() {
|
||||
if (state.busy || !state.action) return;
|
||||
var action = state.action;
|
||||
var args;
|
||||
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
|
||||
state.busy = true;
|
||||
elements.submit.disabled = true;
|
||||
elements.cancel.disabled = true;
|
||||
setStatus(t('loading'), false);
|
||||
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
|
||||
unwrap(result);
|
||||
setStatus(t('success'), false);
|
||||
var refresh = document.getElementById('refresh');
|
||||
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
|
||||
}).catch(function (error) {
|
||||
setStatus(error && error.message ? error.message : t('failure'), true);
|
||||
}).finally(function () {
|
||||
state.busy = false;
|
||||
elements.submit.disabled = false;
|
||||
elements.cancel.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function recordFromNode(node) {
|
||||
var content = String(node.textContent || '').toLowerCase();
|
||||
var exact = state.records.find(function (item) {
|
||||
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
|
||||
return id && content.indexOf(id) !== -1;
|
||||
});
|
||||
if (exact) return exact;
|
||||
return state.records.find(function (item) {
|
||||
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
|
||||
return title && content.indexOf(title) !== -1;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function update(value) {
|
||||
var payload = normalize(value) || {};
|
||||
var widget = widgetFrom(payload);
|
||||
if (Object.keys(widget).length) state.widget = widget;
|
||||
var records = recordsFrom(payload);
|
||||
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
|
||||
ensurePanel();
|
||||
var actions = actionList();
|
||||
elements.panel.hidden = actions.length === 0;
|
||||
if (actions.length && !state.action) renderForm();
|
||||
else if (actions.length) renderForm();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
|
||||
if (!node || (elements.panel && elements.panel.contains(node))) return;
|
||||
var item = recordFromNode(node);
|
||||
if (!item) return;
|
||||
state.selected = item;
|
||||
ensurePanel();
|
||||
renderForm();
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.source !== window.parent) return;
|
||||
var message = event.data;
|
||||
if (!message || message.jsonrpc !== '2.0') return;
|
||||
if (message.id !== undefined && pending.has(message.id)) {
|
||||
var requestState = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
window.clearTimeout(requestState.timeout);
|
||||
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
|
||||
else requestState.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
|
||||
}, { passive: true });
|
||||
|
||||
ensurePanel();
|
||||
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
|
||||
if (initial) update(initial);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user