← Files LuciaARCHIVED FILE
assets/playbook-editor.html
23.3 KB · Nov 11, 2026 · 18:09 UTC
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'; object-src 'none'">
<title>Lucia · Istruzioni dello studio</title>
<style>
@font-face{font-family:'Instrument Sans';src:url(data:font/ttf;base64,__FONT_DATA__) format('truetype');font-weight:400 700;font-display:swap}
:root{font-family:'Instrument Sans',sans-serif;color:var(--ink);background:#fff;line-height:1.55;font-synthesis:weight;--blue:var(--accent);--line:var(--border)}
*{box-sizing:border-box}body{margin:0}button,input,textarea,select{font:inherit;color:inherit}button,select{cursor:pointer}button{padding:10px 16px;border:1px solid var(--line);background:white;border-radius:3px}button:hover{background:var(--soft)}button.primary{background:var(--blue);border-color:var(--blue);color:white}button.primary:hover{background:var(--accent-strong)}button:disabled{opacity:.5;cursor:default}:focus-visible{outline:3px solid var(--accent);outline-offset:3px}a{color:var(--blue);text-underline-offset:4px}
.masthead{border-bottom:1px solid var(--line);padding:20px max(24px,calc((100vw - 1180px)/2));display:flex;align-items:center;justify-content:space-between;gap:20px}.brand{font-size:23px;letter-spacing:-.6px;font-weight:700}.language{display:flex;align-items:center;gap:12px;font-size:14px}.language select{padding:7px 10px;border:1px solid var(--line);border-radius:3px;background:white}
main{max-width:1180px;margin:0 auto;padding:52px 24px 100px}.intro{max-width:820px}h1{font-size:clamp(32px,4vw,48px);line-height:1.12;letter-spacing:-1.3px;margin:12px 0 22px}h2{font-size:25px;letter-spacing:-.4px;margin:0 0 12px;line-height:1.2}p{margin:0 0 16px}.eyebrow{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}.intro p{max-width:70ch;font-size:18px;color:var(--text)}.toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:26px 0 14px}.state{font-size:14px;color:var(--muted);min-height:24px}.message{max-width:820px;min-height:26px;margin:12px 0 28px;white-space:pre-wrap}.message.error{color:var(--danger)}
.work{display:grid;grid-template-columns:210px minmax(0,1fr);gap:50px;margin-top:26px}.nav{position:sticky;top:24px;align-self:start;display:grid;gap:15px;font-size:15px}.nav a{text-decoration:none}.nav a:hover{text-decoration:underline}.nav p{font-size:13px;color:var(--muted);margin-top:16px;max-width:22ch}section{border-top:1px solid var(--line);padding:28px 0 34px;scroll-margin-top:24px}.section-note{color:var(--muted);font-size:15px;max-width:65ch;margin-bottom:22px}.field{display:block;margin:0 0 24px}.field-title{display:block;font-weight:700;margin-bottom:7px}.help{display:block;font-size:14px;color:var(--muted);margin:7px 0 0;max-width:70ch}input[type=text],textarea{display:block;width:100%;border:1px solid var(--border-strong);border-radius:3px;background:#fff;padding:11px 13px}textarea{resize:vertical;min-height:90px;line-height:1.55}.radio-list{display:grid;gap:9px;border:0;padding:0;margin:0 0 24px}.radio-list legend{font-weight:700;margin-bottom:9px}.choice{display:flex;align-items:baseline;gap:10px;padding:5px 0}.choice input{accent-color:var(--blue);flex-shrink:0}.formats{display:flex;gap:24px;flex-wrap:wrap;border:0;padding:0;margin:0 0 20px}.formats legend{font-weight:700;margin-bottom:8px}.footer-note{margin-top:16px;font-size:14px;color:var(--muted)}.privacy{margin-top:18px;padding-bottom:0}.privacy p{max-width:75ch;color:var(--text)}code{font-size:13px;overflow-wrap:anywhere}.identity{color:var(--muted);font-size:13px;margin-top:24px;overflow-wrap:anywhere}[hidden]{display:none!important}
#destination{overflow-wrap:anywhere}
@media(max-width:760px){main{padding:34px 20px 70px}.masthead{padding:16px 20px}.work{display:block}.nav{position:static;display:flex;flex-wrap:wrap;gap:10px 22px;margin-bottom:24px}.nav p{display:none}.intro p{font-size:17px}.toolbar{gap:8px}.toolbar button{flex-grow:1}.language>span{display:none}h1{letter-spacing:-.8px}section{padding-top:23px}}
/* VERA_SHARED_THEME_START — generated; edit plugins/_shared/ui/vera-theme.css */
/* Canonical Vera workflow theme. Derived from the Vouching review surface.
* Run: python scripts/sync_vera_ui_theme.py after changing this file.
* Embedded at build time: no remote fonts, imports, or host CSS dependencies.
*/
:root {
color-scheme: light;
--paper: #ffffff;
--surface: #ffffff;
--soft: #fafafa;
--soft-strong: #f3f4f2;
--ink: #1f2421;
--text: #3f4541;
--muted: #596671;
--border: #e8eae7;
--border-strong: #d8dcd7;
--accent: #194b70;
--accent-strong: #123650;
--accent-soft: #eff5f9;
--danger: #7b4b44;
--danger-bg: #fbf7f6;
--warning: #6f6247;
--warning-bg: #fbfaf6;
--ok: #4b6759;
--ok-bg: #f7faf8;
--info: #52646e;
--info-bg: #f7fafb;
--radius: 8px;
--font-ui: "Instrument Sans", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-size-ui: 1rem;
--font-size-control: 0.875rem;
--font-size-title: 1.25rem;
--font-size-heading: 1.05rem;
--control-padding: 0.55rem 0.75rem;
--control-height: 2.5rem;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 0.75rem;
--space-lg: 1rem;
--space-xl: 1.5rem;
/* Compatibility aliases for existing layout-specific selectors. */
--navy: var(--accent);
--blue: var(--accent);
--cyan: var(--accent);
--line: var(--border);
--rule: var(--border);
--wash: var(--soft);
--bg: var(--paper);
--focus-border-color: var(--accent);
--focus-ring-color: var(--border-strong);
--mparanza-pill-font-family: var(--font-ui);
--white: var(--paper);
--line-strong: var(--border-strong);
--quiet: var(--muted);
--blue-wash: var(--accent-soft);
--green: var(--ok);
--green-wash: var(--ok-bg);
--amber: var(--warning);
--amber-wash: var(--warning-bg);
--red: var(--danger);
--red-wash: var(--danger-bg);
--focus: var(--accent);
--teal: var(--accent);
--teal-soft: var(--accent-soft);
--green-soft: var(--ok-bg);
--amber-soft: var(--warning-bg);
--red-soft: var(--danger-bg);
--sans: var(--font-ui);
--serif: var(--font-ui);
}
:root body {
background: var(--paper);
color: var(--text);
font-family: var(--font-ui);
font-size: var(--font-size-ui);
line-height: 1.5;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
/* Hosts may pin their body font; keep the app content on its own typeface. */
:root body > :not(script, style) {
font-family: var(--font-ui);
}
:root :is(h1, h2, h3, h4, .brand, .brand span) { color: var(--ink); font-family: var(--font-ui); }
:root h1 { font-size: var(--font-size-title); font-weight: 650; line-height: 1.25; letter-spacing: 0; }
:root :is(h2, h3) { font-size: var(--font-size-heading); font-weight: 650; line-height: 1.3; letter-spacing: 0; }
:root .brand { font-size: 1.25rem; font-weight: 650; letter-spacing: -0.04em; }
:root :is(main, .main) > button { margin-inline-end: var(--space-sm); margin-block-end: var(--space-sm); }
:root :is(a, summary, .eyebrow, .product) { color: var(--accent); }
:root :is(button, input, textarea, select, .button) {
font-family: var(--font-ui);
font-size: var(--font-size-control);
line-height: 1.4;
letter-spacing: 0;
accent-color: var(--accent);
}
:root :is(button, .button, .custom-select__button) {
border: 1px solid var(--border-strong);
border-radius: var(--radius);
background: var(--surface);
background-image: none;
color: var(--text);
box-shadow: none;
padding: var(--control-padding);
min-height: var(--control-height);
}
:root :is(button, .button, .custom-select__button):hover:not(:disabled) {
background: var(--soft-strong);
border-color: var(--border-strong);
}
/* Review controls keep their label width at every panel breakpoint. */
:root :is(.action-buttons, .tabs, .state-filters, .decision-grid) {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 0.4rem;
min-width: 0;
}
:root :is(.action-buttons, .tabs, .state-filters, .decision-grid) > button {
flex: 0 1 auto;
width: auto;
max-width: 100%;
justify-content: center;
white-space: normal;
overflow-wrap: anywhere;
}
:root :is(button.primary, .button.primary) {
background: var(--accent);
border-color: var(--accent);
color: var(--paper);
}
:root :is(button.primary, .button.primary):hover:not(:disabled) {
background: var(--accent-strong);
border-color: var(--accent-strong);
color: var(--paper);
}
:root :is(button.secondary, .button.secondary, button.quiet) { background: var(--surface); color: var(--text); }
:root :is(input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=range]), textarea, select) {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border-strong);
border-radius: var(--radius);
padding: var(--control-padding);
}
:root :is(button, input, textarea, select, a, summary):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
box-shadow: none;
}
:root :is(button[aria-current=page], button[aria-pressed=true], .custom-select__option.is-active) {
background: var(--accent-soft);
border-color: var(--border-strong);
color: var(--ink);
}
:root .custom-select__list { background: var(--surface); border-color: var(--border-strong); box-shadow: none; border-radius: var(--radius); }
:root :is(.error, #error, #status.error, .negative) { color: var(--danger); }
:root :is(.saved, .status--ok) { color: var(--ok); }
:root :is(.severity, .status--warning) { color: var(--warning); }
:root .severity.blocker { color: var(--danger); }
:root .severity.info { color: var(--info); }
/* VERA_SHARED_THEME_END */
</style>
</head>
<body>
<header class="masthead"><div class="brand">Lucia</div><label class="language"><span data-label="language"></span><select id="language" data-native-select="true" aria-label="Lingua / Language"><option value="it">Italiano</option><option value="en">English</option><option value="fr">Français</option><option value="de">Deutsch</option><option value="es">Español</option></select></label></header>
<main hidden>
<div class="intro"><div class="eyebrow" data-label="eyebrow"></div><h1 data-label="title"></h1><p data-label="intro"></p></div>
<div class="toolbar"><button type="button" id="export" class="primary" data-label="export"></button><button type="button" id="use" data-label="use" disabled></button><button type="button" id="import" data-label="import"></button><button type="button" id="duplicate" data-label="duplicate"></button><button type="button" id="reset" data-label="reset" hidden></button><input id="file" type="file" accept=".json,application/json" hidden></div>
<div id="state" class="state"></div><p id="destination" class="help"></p><div id="message" class="message" role="status" aria-live="polite"></div>
<div class="work"><nav class="nav" aria-label="Sezioni / Sections"><a href="#assignment" data-label="assignment"></a><a href="#method" data-label="method"></a><a href="#deliverables" data-label="deliverables"></a><p data-label="local_note"></p></nav>
<div>
<section id="assignment"><h2 data-label="assignment"></h2><p class="section-note" data-label="assignment_note"></p><div id="identity-fields"></div><fieldset id="workflows" class="radio-list"><legend data-label="workflow"></legend></fieldset></section>
<section id="method"><h2 data-label="method"></h2><p class="section-note" data-label="method_note"></p><div id="method-fields"></div></section>
<section id="deliverables"><h2 data-label="deliverables"></h2><p class="section-note" data-label="deliverables_note"></p><div id="template-field"></div><fieldset class="formats" id="formats"><legend data-label="outputs"></legend></fieldset><label class="choice"><input type="checkbox" id="tracked"><span data-label="tracked_changes"></span></label><p class="help" data-label="tracked_help"></p><div id="identity" class="identity"></div></section>
<section class="privacy"><h2 data-label="privacy_title"></h2><p data-label="privacy"></p></section>
</div></div>
<noscript>Abilitare JavaScript per modificare ed esportare le istruzioni. / Enable JavaScript to edit and export these instructions.</noscript>
</main>
<p id="loading" role="status">Apri le istruzioni dalla chat di Lucia.</p>
<script type="application/json" id="editor-labels">__LABEL_DATA__</script>
<script>
'use strict';
const labels = JSON.parse(document.getElementById('editor-labels').textContent);
let baseline, value, appSession, revision, saveAttempt;
let busy = false, selectedPath = null, destination = null, host = null, handedOff = false;
let dirty = false;
const workflows = ['revisione-contratti','confronto-documenti','revisione-documentale','redazione-da-modello','controllo-documento'];
const byId = id => document.getElementById(id);
const t = key => labels[value?.language || 'it'][key];
const clone = data => structuredClone(data);
const message = (text, error=false) => { byId('message').textContent = text; byId('message').classList.toggle('error',error); };
function changed() { saveAttempt = null; dirty = JSON.stringify(value) !== JSON.stringify(baseline); updateState(); message(''); }
function updateState() {
byId('destination').textContent = t('save_location') + ': ' + destination;
byId('state').textContent = t(dirty ? 'unsaved' : 'saved') + ' · ' + t('version') + ' ' + value.version;
byId('reset').hidden = !dirty;
byId('identity').textContent = value.derived_from ? t('derived') + ' · ' + t('version') + ' ' + value.derived_from.version : '';
document.querySelectorAll('button, input, textarea, select').forEach(input => input.disabled = busy);
byId('use').disabled = busy || dirty || !selectedPath || handedOff || !(host?.hostCapabilities?.message?.text || host?.hostCapabilities?.experimental?.['openai/message']);
byId('use').title = host?.hostCapabilities?.message?.text || host?.hostCapabilities?.experimental?.['openai/message'] ? '' : t('use_unavailable');
byId('tracked').disabled = !value.outputs.includes('docx');
byId('tracked').checked = value.tracked_changes;
}
function field(parent,key,rows=1,list=false) {
const label = document.createElement('label'); label.className='field';
const title = document.createElement('span'); title.className='field-title'; title.textContent=t(key); label.append(title);
const input = document.createElement(rows===1?'input':'textarea'); input.id=key;
if(rows===1) input.type='text'; else input.rows=rows;
input.value=list?value[key].join('\n'):value[key];
input.addEventListener('input',()=>{value[key]=list?input.value.split(/\r?\n/).map(s=>s.trim()).filter(Boolean):input.value;changed();});
label.append(input);
if(t(key+'_help')) { const help=document.createElement('span'); help.className='help'; help.textContent=t(key+'_help');help.id=key+'-help'; input.setAttribute('aria-describedby',help.id);label.append(help); }
byId(parent).append(label);
}
function draw() {
document.documentElement.lang=value.language;document.title='Lucia · '+t('title');
document.querySelectorAll('[data-label]').forEach(node=>node.textContent=t(node.dataset.label));
byId('language').value=value.language;
for(const id of ['identity-fields','method-fields','template-field']) byId(id).replaceChildren();
field('identity-fields','name');field('identity-fields','purpose',3);
field('method-fields','columns',5,true);field('method-fields','questions',4,true);field('method-fields','firm_positions',7);field('method-fields','source_requirements',4,true);field('template-field','template_name');
const choices=byId('workflows'); choices.querySelectorAll('label').forEach(node=>node.remove());
workflows.forEach(workflow=>{const label=document.createElement('label');label.className='choice';const input=document.createElement('input');input.type='radio';input.name='workflow';input.value=workflow;input.checked=value.workflow===workflow;input.addEventListener('change',()=>{value.workflow=workflow;changed();});const text=document.createElement('span');text.textContent=t('workflows')[workflow];label.append(input,text);choices.append(label);});
const formats=byId('formats');formats.querySelectorAll('label').forEach(node=>node.remove());
['html','xlsx','docx'].forEach(format=>{const label=document.createElement('label');label.className='choice';const input=document.createElement('input');input.type='checkbox';input.value=format;input.checked=value.outputs.includes(format);input.addEventListener('change',()=>{value.outputs=['html','xlsx','docx'].filter(f=>formats.querySelector('input[value="'+f+'"]').checked);if(!value.outputs.includes('docx'))value.tracked_changes=false;changed();});const text=document.createElement('span');text.textContent=t('formats')[format];label.append(input,text);formats.append(label);});
updateState();
}
function validate(data) {
const required=['schema_version','id','version','derived_from','name','workflow','language','purpose','columns','questions','firm_positions','source_requirements','template_name','outputs','tracked_changes'];
const str=(v,max,needed=false)=>typeof v==='string'&&v.length<=max&&(!needed||v.trim().length>0);
const strings=(v,maxItems,maxLength,needed=false)=>Array.isArray(v)&&v.length<=maxItems&&(!needed||v.length>0)&&v.every(s=>str(s,maxLength,true));
const id=v=>typeof v==='string'&&/^[a-zA-Z0-9_-]{1,80}$/.test(v);
const version=v=>Number.isSafeInteger(v)&&v>=1;
if(!data||typeof data!=='object'||Array.isArray(data)||Object.keys(data).length!==required.length||!required.every(k=>Object.hasOwn(data,k)))throw new Error(t('invalid'));
const parent=data.derived_from;
if(data.schema_version!==1||!id(data.id)||!version(data.version)||!(parent===null||(typeof parent==='object'&&!Array.isArray(parent)&&Object.keys(parent).length===2&&id(parent.id)&&version(parent.version)))||!str(data.name,300,true)||!str(data.purpose,5000,true)||!workflows.includes(data.workflow)||!Object.hasOwn(labels,data.language)||!strings(data.columns,100,300,true)||new Set(data.columns.map(s=>s.trim().toLocaleLowerCase())).size!==data.columns.length||!strings(data.questions,100,2000)||!strings(data.source_requirements,100,2000)||!str(data.firm_positions,50000)||!str(data.template_name,500)||!Array.isArray(data.outputs)||!data.outputs.length||new Set(data.outputs).size!==data.outputs.length||!data.outputs.every(s=>['html','xlsx','docx'].includes(s))||typeof data.tracked_changes!=='boolean'||(data.tracked_changes&&!data.outputs.includes('docx')))throw new Error(t('invalid'));
return data;
}
byId('language').addEventListener('change',event=>{value.language=event.target.value;changed();draw();});
byId('tracked').addEventListener('change',event=>{value.tracked_changes=event.target.checked;changed();});
byId('reset').addEventListener('click',()=>{value=clone(baseline);dirty=false;draw();message(t('reset_done'));});
byId('duplicate').addEventListener('click',()=>{value.derived_from={id:baseline.id,version:baseline.version};value.id='studio-'+crypto.randomUUID();value.version=1;value.name=value.name+' — '+t('copy');dirty=true;draw();message(t('copy_done'));byId('name').focus();});
byId('export').addEventListener('click',async()=>{
if(busy || !appSession)return;
try{
const next=clone(validate(value));
if(new TextEncoder().encode(JSON.stringify(next)).byteLength>1000000)throw new Error(t('too_large'));
saveAttempt ||= {session:appSession,revision,request_id:crypto.randomUUID(),value:next};
busy=true;updateState();
const result=await request('tools/call',{name:'lucia_method_save',arguments:saveAttempt});
accept(result);saveAttempt=null;message(t('export_done'));
}catch(error){message(error.message,true);}
finally{busy=false;if(value)updateState();}
});
byId('import').addEventListener('click',()=>{if(dirty){message(t('dirty_import'),true);return;}byId('file').click();});
byId('file').addEventListener('change',async event=>{
const file=event.target.files[0];event.target.value='';if(!file)return;
try{if(file.size>1000000)throw new Error(t('too_large'));const imported=validate(JSON.parse(await file.text()));busy=true;updateState();const result=await request('tools/call',{name:'lucia_method_import',arguments:{session:appSession,value:imported}});accept(result);message(t('import_done'));}catch(error){message(t('import_error')+' '+error.message,true);}finally{busy=false;if(value)updateState();}
});
window.addEventListener('beforeunload',event=>{if(dirty){event.preventDefault();event.returnValue='';}});
let nextId=0;
const pending=new Map();
function request(method,params){
return new Promise((resolve,reject)=>{
const id=++nextId;
const timer=setTimeout(()=>{pending.delete(id);reject(new Error(t('unconfirmed')));},30000);
pending.set(id,{resolve,reject,timer});
window.parent.postMessage({jsonrpc:'2.0',id,method,params},'*');
});
}
function accept(result){
if(result?.isError)throw new Error(result.content?.find(item=>item.type==='text')?.text || t('invalid'));
const data=result?._meta?.method;
if(!data)throw new Error(t('unconfirmed'));
value=clone(validate(data.value));baseline=clone(value);appSession=data.session;revision=data.revision;selectedPath=data.path || null;destination=data.directory;handedOff=false;dirty=false;
document.querySelector('main').hidden=false;byId('loading').hidden=true;draw();
if(data.example)message(t('example'));
}
byId('use').addEventListener('click',async()=>{
if(byId('use').disabled || !selectedPath)return;
handedOff=true;busy=true;updateState();
try{
// Recheck the saved file and revision before selecting it; no-op saves write nothing.
const verified=await request('tools/call',{name:'lucia_method_save',arguments:{session:appSession,revision,request_id:crypto.randomUUID(),value:baseline}});
if(verified?.isError || verified?._meta?.method?.path!==selectedPath)throw new Error(t('use_unconfirmed'));
const text=t('use_prompt')+'\n'+t('method_name')+': «'+baseline.name+'» · '+t('version')+' '+baseline.version+'\n'+t('method_file')+': '+selectedPath;
const extension=!!host?.hostCapabilities?.experimental?.['openai/message'];
const params={role:'user',content:extension?[{type:'text',text}]:{type:'text',text}};
if(extension)params._meta={'openai/message':{target:'active'}};
const result=await request('ui/message',params);if(result?.isError)throw new Error(t('use_unconfirmed'));message(t('use_done'));
}catch(error){message(t('use_unconfirmed'),true);}
finally{busy=false;updateState();}
});
window.addEventListener('message',event=>{
if(event.source!==window.parent || event.data?.jsonrpc!=='2.0')return;
const incoming=event.data,item=pending.get(incoming.id);
if(item){clearTimeout(item.timer);pending.delete(incoming.id);incoming.error?item.reject(new Error(incoming.error.message)):item.resolve(incoming.result);return;}
if(incoming.method==='ui/notifications/tool-result' && incoming.params?._meta?.method){
if(dirty || busy)return;
try{accept(incoming.params);}catch(error){byId('loading').textContent=error.message;}
}
if(incoming.method==='ui/resource-teardown')window.parent.postMessage({jsonrpc:'2.0',id:incoming.id,result:{}},'*');
});
request('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Lucia firm method',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
.then(result=>{host=result;if(value)updateState();window.parent.postMessage({jsonrpc:'2.0',method:'ui/notifications/initialized'},'*');})
.catch(()=>{byId('loading').textContent='Riapri le istruzioni dalla chat di Lucia.';});
</script>
</body>
</html>
SHA-256: 9a097e48112ade521054ffa5eae9a30bdf410873313ce485841c551f899c06dc