545 lines
40 KiB
HTML
545 lines
40 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>SHD MCP</title>
|
||
<style>
|
||
:root{color-scheme:light dark;--bg:#f8fafc;--surface:#e8edf2;--hover:#e1e8ef;--line:rgba(31,41,55,.16);--ink:#1d1f25;--muted:#616670;--accent:#535963;--soft:rgba(75,85,99,.12);--bad:#b42318;--good:#2f855a;--button:#fff;--shadow:0 10px 26px rgba(31,41,55,.1)}
|
||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.shell{max-width:980px;margin:auto;padding:22px}.card{padding:20px;border:1px solid var(--line);border-radius:18px;background:var(--surface);box-shadow:var(--shadow)}
|
||
.eyebrow{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}h1{margin:4px 0 5px;font-size:25px;line-height:1.15}.lede{margin:0;color:var(--muted)}.summary{display:flex;gap:8px;flex-wrap:wrap;margin:18px 0 14px}.summary span{padding:5px 9px;border:1px solid var(--line);border-radius:999px;background:var(--hover);color:var(--muted)}.summary strong{color:var(--ink)}
|
||
.toolbar{display:flex;gap:8px;margin-bottom:10px}.search{min-width:0;flex:1;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:inherit;outline:0}.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--soft)}button{cursor:pointer}.refresh{padding:10px 14px;border:1px solid var(--accent);border-radius:10px;background:var(--accent);color:var(--button);font-weight:650}.refresh:disabled{opacity:.6;cursor:wait}.status{min-height:22px;color:var(--muted)}.error{color:var(--bad)}
|
||
.table-wrap{max-height:460px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--bg)}table{width:100%;border-collapse:collapse;min-width:560px}th,td{padding:11px 13px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{position:sticky;top:0;background:var(--hover);color:var(--muted);font-size:12px;font-weight:650;z-index:1}tbody tr:last-child td{border-bottom:0}.record{cursor:pointer;outline:0}.record:hover,.record:focus{background:var(--soft)}.title{font-weight:650}.sub{display:block;color:var(--muted);font-size:12px;margin-top:2px}.pill{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--soft);color:var(--accent);font-size:12px;white-space:nowrap}.pill.up,.pill.ok,.pill.active{color:var(--good);background:rgba(47,133,90,.14)}.pill.down,.pill.error,.pill.overdue{color:var(--bad);background:rgba(180,35,24,.12)}.empty{padding:24px;text-align:center;color:var(--muted)}
|
||
.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">
|
||
<div class="eyebrow">SHD MCP</div><h1 id="title">SHD</h1><p class="lede" id="desc"></p><div class="summary" id="summary"></div>
|
||
<div class="toolbar"><input class="search" id="search" type="search" autocomplete="off"><button class="refresh" id="refresh" type="button"></button></div>
|
||
<div class="status" id="status" role="status" aria-live="polite"></div>
|
||
<div class="table-wrap"><table><thead><tr><th id="head-title"></th><th id="head-status"></th><th id="head-date"></th><th id="head-value"></th></tr></thead><tbody id="rows"></tbody></table><div class="empty" id="empty" hidden></div></div>
|
||
<section class="details" id="details" hidden aria-live="polite"><div class="detail-head"><div><h2 id="detail-title"></h2><div class="sub" id="detail-sub"></div></div><button class="close" id="close" type="button">×</button></div><div class="grid" id="grid"></div></section>
|
||
</section></main>
|
||
<script>
|
||
(function(){
|
||
'use strict';
|
||
var DEFAULT_MODULE='notes';
|
||
var C={ru:{refresh:'Обновить',search:'Поиск по названию, статусу или ID',loading:'Загрузка…',empty:'Нет данных для отображения.',records:'записей',status:'Статус',date:'Дата',value:'Детали',id:'ID',details:'Детали',close:'Закрыть',load:'Не удалось загрузить данные.',detail:'Не удалось загрузить детали.',context:'Для обновления нужен контекст проекта или базы'},en:{refresh:'Refresh',search:'Search by name, status or ID',loading:'Loading…',empty:'No data to display.',records:'records',status:'Status',date:'Date',value:'Details',id:'ID',details:'Details',close:'Close',load:'Could not load data.',detail:'Could not load details.',context:'A project or base context is required to refresh.'}};
|
||
var M={
|
||
discussions:{ru:['Обсуждения','Темы обсуждений с поиском и просмотром сообщений.','Тема','Обновлено','Сообщение'],en:['Discussions','Discussion topics with search and message details.','Topic','Updated','Message']},
|
||
notifications:{ru:['Уведомления','Последние уведомления и события модулей.','Уведомление','Время','Источник'],en:['Notifications','Recent notifications and module events.','Notification','Time','Source']},
|
||
scheduling:{ru:['Расписание','Ближайшие события и записи из расписания.','Событие','Начало','Участник'],en:['Scheduling','Upcoming events and bookings from scheduling.','Event','Starts','Guest']},
|
||
inventory:{ru:['Склад','Складские позиции, остатки и статусы.','Позиция','Обновлено','Количество'],en:['Inventory','Stock items, quantities and statuses.','Item','Updated','Quantity']},
|
||
agents:{ru:['Агенты и контроллеры','Доступные агенты, контроллеры и их текущее состояние.','Агент или контроллер','Проверено','Тип'],en:['Agents & Controllers','Available agents, controllers and their current state.','Agent or controller','Checked','Type']},
|
||
'status-page':{ru:['Status Page','Состояние доступности сервисов и мониторов.','Монитор','Проверено','Доступность'],en:['Status Page','Service availability and monitor state.','Monitor','Checked','Availability']},
|
||
'project-db':{ru:['Project DB','Каталог таблиц Project DB с просмотром схемы.','Таблица','Обновлено','Записей'],en:['Project DB','Project DB table catalog with schema details.','Table','Updated','Records']},
|
||
notes:{ru:['Заметки','Страницы Notes с поиском и просмотром содержимого.','Страница','Обновлено','Пространство'],en:['Notes','Notes pages with search and content details.','Page','Updated','Space']}
|
||
};
|
||
var S={
|
||
discussions:{listTool:'shd_list_discussion_topics',listArgs:{limit:100},readTool:'shd_list_discussion_messages',detailKey:'topic_id',detailArgs:{limit:50},paths:['data'],sort:'desc'},
|
||
notifications:{listTool:'shd_list_notifications',listArgs:{limit:50,offset:0},paths:['data'],sort:'desc'},
|
||
scheduling:{listTool:'shd_get_scheduling_overview',listArgs:{limit:100},readTool:'shd_list_scheduling_bookings',detailKey:'booking_id',detailArgs:{limit:1},paths:['data.bookings','data.upcoming_bookings','data'],sort:'asc'},
|
||
inventory:{listTool:'shd_list_inventory_items',listArgs:{limit:100},readTool:'shd_get_inventory_item',detailKey:'item_id',paths:['data'],sort:'desc'},
|
||
agents:{listTool:'shd_agents_overview',listArgs:{},paths:['data.controllers','data.agents','data.user_agents','data'],sort:'asc'},
|
||
'status-page':{listTool:'shd_get_status_page_monitors',listArgs:{environment:'dev'},readTool:'shd_get_status_page_monitor_history',detailKey:'monitor_id',detailArgs:{limit:20},paths:['data'],nested:'monitors',sort:'asc'},
|
||
'project-db':{listTool:'shd_project_db_tables',listArgs:{},readTool:'shd_project_db_schema',detailKey:'table',paths:['data.tables','data'],contextRequired:true,sort:'asc'},
|
||
notes:{listTool:'shd_list_note_pages',listArgs:{limit:100},readTool:'shd_get_note_page',detailKey:'page_id',paths:['data'],sort:'desc'}
|
||
};
|
||
var pending=new Map(),nextId=1,rows=[],state={module:DEFAULT_MODULE,config:Object.assign({},S[DEFAULT_MODULE]),lang:'ru',busy:false};
|
||
var e={title:document.getElementById('title'),desc:document.getElementById('desc'),search:document.getElementById('search'),refresh:document.getElementById('refresh'),status:document.getElementById('status'),summary:document.getElementById('summary'),rows:document.getElementById('rows'),empty:document.getElementById('empty'),details:document.getElementById('details'),detailTitle:document.getElementById('detail-title'),detailSub:document.getElementById('detail-sub'),grid:document.getElementById('grid'),close:document.getElementById('close'),ht:document.getElementById('head-title'),hs:document.getElementById('head-status'),hd:document.getElementById('head-date'),hv:document.getElementById('head-value')};
|
||
function lang(){var x=typeof window!=='undefined'&&window.openai&&window.openai.locale;x=String(x||document.documentElement.lang||'ru').toLowerCase();return x.indexOf('en')===0?'en':'ru'}
|
||
function t(k){return(C[state.lang]||C.ru)[k]||k}
|
||
function obj(x){return x&&typeof x==='object'&&!Array.isArray(x)}
|
||
function merge(a,b){return Object.assign({},a||{},b||{})}
|
||
function payload(x){if(!x)return null;if(Object.prototype.hasOwnProperty.call(x,'structuredContent'))return x.structuredContent;if(Object.prototype.hasOwnProperty.call(x,'toolOutput'))return x.toolOutput;return x}
|
||
function unwrap(x){if(x&&x.isError)throw Error(t('load'));var p=payload(x)||{};if(p.error)throw Error(String(p.error.message||p.error.detail||p.error));if(p.ok===false&&p.message)throw Error(String(p.message));return p}
|
||
function applyConfig(p){p=payload(p)||{};var m=obj(p.meta)?p.meta:{},w=obj(m.widget)?m.widget:{},mod=String(w.module||m.module||state.module||DEFAULT_MODULE);state.module=M[mod]?mod:DEFAULT_MODULE;state.config=merge(S[state.module],w);if(!obj(state.config.sourceArgs)&&obj(m.sourceArgs))state.config.sourceArgs=m.sourceArgs;state.lang=lang();labels()}
|
||
function labels(){var m=(M[state.module]||M.discussions)[state.lang]||M.discussions.ru;document.documentElement.lang=state.lang;e.title.textContent=m[0];e.desc.textContent=m[1];e.search.placeholder=t('search');e.refresh.textContent=t('refresh');e.empty.textContent=t('empty');e.close.setAttribute('aria-label',t('close'));e.ht.textContent=m[2];e.hs.textContent=t('status');e.hd.textContent=m[3];e.hv.textContent=m[4]}
|
||
function req(method,params){var o=typeof window!=='undefined'&&window.openai;if(method==='tools/call'&&o&&typeof o.callTool==='function')return Promise.resolve(o.callTool(params.name,params.arguments||{}));var id=nextId++;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(Error(t('load')))},12000);pending.set(id,{resolve:resolve,reject:reject,timeout:timeout})})}
|
||
function notify(method,params){window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*')}
|
||
function path(x,p){return String(p||'').split('.').reduce(function(v,k){return v&&typeof v==='object'?v[k]:undefined},x)}
|
||
function findArray(x,d){if(Array.isArray(x))return x;if(!obj(x)||d>2)return null;var keys=['items','records','bookings','upcoming_bookings','topics','notifications','pages','spaces','controllers','agents','user_agents','tables','monitors','data'];for(var i=0;i<keys.length;i++)if(Array.isArray(x[keys[i]]))return x[keys[i]];for(var k in x)if(Object.prototype.hasOwnProperty.call(x,k)){var a=findArray(x[k],d+1);if(a)return a}return null}
|
||
function flatten(a){var n=state.config.nested||state.config.nestedCollection;if(!n)return a;var out=[];a.forEach(function(parent){if(parent&&Array.isArray(parent[n]))parent[n].forEach(function(child){var x=Object.assign({},child);if(parent.name&&!x.group_name)x.group_name=parent.name;out.push(x)});else out.push(parent)});return out}
|
||
function list(x){var p=payload(x)||{};if(Array.isArray(p.items))return p.items;var ps=Array.isArray(state.config.paths)?state.config.paths:(Array.isArray(state.config.arrayPaths)?state.config.arrayPaths:[]);for(var i=0;i<ps.length;i++){var c=path(p,ps[i]),a=Array.isArray(c)?c:findArray(c,0);if(a)return flatten(a)}var d=p.data===undefined?p:p.data;return flatten(Array.isArray(d)?d:(findArray(d,0)||[]))}
|
||
function str(x){if(x===null||x===undefined||x==='')return'';if(typeof x==='object'){try{return JSON.stringify(x)}catch(err){return''}}return String(x)}
|
||
function id(x,i){var keys=['id','key','code','uuid','page_id','topic_id','booking_id','item_id','monitor_id','table'];for(var n=0;n<keys.length;n++)if(x&&x[keys[n]]!==undefined&&x[keys[n]]!==null&&x[keys[n]]!=='')return str(x[keys[n]]);return String(i+1)}
|
||
function status(x){var v=x&&x.status;if(v&&typeof v==='object')v=v.name||v.label||v.code||v.id;return str(v||(x&&(x.state||x.phase||x.health))||'')}
|
||
function title(x,i){return str(x&&(x.title||x.name||x.subject||x.display_name||x.label||x.table_name||x.table||x.key))||(t('records')+' '+(i+1))}
|
||
function date(x){return str(x&&(x.starts_at||x.start_at||x.due_date||x.checked_at||x.updated_at||x.created_at||x.sent_at||x.published_at||x.date))}
|
||
function value(x){if(!x)return'';if(x.availability_percent!==undefined&&x.availability_percent!==null)return str(x.availability_percent)+'%';if(x.quantity!==undefined&&x.quantity!==null)return str(x.quantity);if(x.record_count!==undefined&&x.record_count!==null)return str(x.record_count);return str(x.value!==undefined?x.value:(x.guest_name||x.space_name||x.group_name||x.controller_type||x.type||x.source||x.module||''))}
|
||
function sub(x){return str(x&&(x.subtitle||x.project_code||x.space_name||x.channel_name||x.group_name||x.code||x.email))}
|
||
function normalize(x,i){var y=obj(x)?Object.assign({},x):{value:x};y.id=id(x,i);y.title=title(x,i);y.subtitle=sub(x);y.status=status(x);y.date=date(x);y.value=value(x);return y}
|
||
function dateText(x){if(!x)return'—';var n=Date.parse(x);if(Number.isNaN(n))return String(x);try{return new Intl.DateTimeFormat(state.lang,{dateStyle:'medium',timeStyle:'short'}).format(new Date(n))}catch(err){return String(x)}}
|
||
function make(tag,v,cls){var n=document.createElement(tag);if(cls)n.className=cls;if(v!==undefined)n.textContent=v;return n}
|
||
function detailText(x){return x===null||x===undefined||x===''?'—':str(x)}
|
||
function renderSummary(){e.summary.replaceChildren();var n=make('span');n.append(make('strong',String(rows.length)),' '+t('records'));e.summary.append(n);var c={};rows.forEach(function(x){if(x.status)c[x.status]=(c[x.status]||0)+1});Object.keys(c).slice(0,3).forEach(function(k){var n=make('span');n.append(make('strong',String(c[k])),' '+k);e.summary.append(n)})}
|
||
function renderRows(){var q=e.search.value.trim().toLowerCase(),a=rows.filter(function(x){return!q||[x.title,x.subtitle,x.status,x.id,x.value].map(str).join(' ').toLowerCase().indexOf(q)!==-1});e.rows.replaceChildren();e.empty.hidden=a.length!==0;a.forEach(function(x){var r=document.createElement('tr');r.className='record';r.tabIndex=0;r.setAttribute('role','button');r.setAttribute('aria-label',x.title);var n=make('td');n.append(make('span',x.title,'title'));if(x.subtitle)n.append(make('span',x.subtitle,'sub'));var s=make('span',x.status||'—','pill');if(x.status)s.classList.add(String(x.status).toLowerCase().replace(/[^a-z0-9_-]+/g,'-'));r.append(n,make('td').appendChild(s).parentNode,make('td',dateText(x.date)),make('td',x.value||'—'));r.onclick=function(){details(x)};r.onkeydown=function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();details(x)}};e.rows.append(r)});renderSummary()}
|
||
function render(x){var p=unwrap(x);applyConfig(p);rows=list(p).map(normalize);var dir=state.config.sort==='asc'?1:-1;rows.sort(function(a,b){var x=Date.parse(a.date),y=Date.parse(b.date);if(!Number.isNaN(x)&&!Number.isNaN(y)&&x!==y)return(x-y)*dir;return a.title.localeCompare(b.title,state.lang)*dir});renderRows()}
|
||
function setStatus(x,bad){e.status.textContent=x||'';e.status.className=bad?'status error':'status'}
|
||
function add(k,v){var d=make('div',undefined,'detail');d.append(make('span',k,'label'),make('span',detailText(v),'value'));e.grid.append(d)}
|
||
function show(x){e.detailTitle.textContent=x.title||'SHD';e.detailSub.textContent=x.subtitle||x.id||'';e.grid.replaceChildren();add(t('id'),x.id);if(x.status)add(t('status'),x.status);if(x.date)add(t('date'),dateText(x.date));if(x.value)add(t('value'),x.value);Object.keys(x).filter(function(k){return['id','title','subtitle','status','date','value','meta'].indexOf(k)===-1}).slice(0,8).forEach(function(k){add(k,x[k])});e.details.hidden=false}
|
||
function details(x){show(x);if(!state.config.readTool)return;var k=state.config.detailKey;if(!k)return;var v=x[k]===undefined?x.id:x[k];if(v===undefined||v===null||v==='')return;if(state.module==='status-page'&&!/^\d+$/.test(String(v)))return;var a=merge(state.config.sourceArgs,state.config.detailArgs);a[k]=k==='table'?String(v):(Number(v)||v);req('tools/call',{name:state.config.readTool,arguments:a}).then(function(result){var p=unwrap(result),d=p.data===undefined?p:p.data;e.grid.replaceChildren();if(Array.isArray(d)){add(t('records'),d.length);d.slice(0,6).forEach(function(q,i){add(String(i+1),q&&(q.message||q.body||q.content||q.title||q))})}else if(obj(d))Object.keys(d).slice(0,12).forEach(function(q){add(q,d[q])});else add(t('details'),d)}).catch(function(err){setStatus(err.message||t('detail'),true)})}
|
||
function refresh(){if(state.busy)return;var a=merge(state.config.listArgs,state.config.sourceArgs);if(state.config.contextRequired&&!a.base&&!a.project_code){setStatus(t('context'),true);return}if(!state.config.listTool)return;state.busy=true;e.refresh.disabled=true;setStatus(t('loading'),false);req('tools/call',{name:state.config.listTool,arguments:a}).then(function(x){render(x);setStatus('',false)}).catch(function(err){setStatus(err.message||t('load'),true)}).finally(function(){state.busy=false;e.refresh.disabled=false})}
|
||
function init(){return req('ui/initialize',{protocolVersion:'2025-11-25',appInfo:{name:'SHD '+(M[state.module]||M.discussions).ru[0],title:'SHD MCP',version:'1.0.0',websiteUrl:'https://shd.xyz.su'},appCapabilities:{}}).then(function(){notify('ui/notifications/initialized',{})}).catch(function(){})}
|
||
window.addEventListener('message',function(ev){if(ev.source!==window.parent)return;var m=ev.data;if(!m||m.jsonrpc!=='2.0')return;if(m.id!==undefined&&pending.has(m.id)){var q=pending.get(m.id);pending.delete(m.id);window.clearTimeout(q.timeout);if(m.error)q.reject(Error(m.error.message||t('load')));else q.resolve(m.result);return}if(m.method==='ui/notifications/tool-input'&&!rows.length){try{render(m.params||{})}catch(err){setStatus(err.message||t('load'),true)}}if(m.method==='ui/notifications/tool-result'){try{render(m.params&&m.params.structuredContent)}catch(err){setStatus(err.message||t('load'),true)}}},{passive:true});
|
||
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>
|