openvibers/openvibe.actor Commits
Now: the model row is designed (one row, grouped choices, real names) (#401)
Changes
@@@ -1 +1,9 @@.ay-page{max-width:1120px;margin:28px auto 80px;padding:0 20px;color:var(--text-primary);overflow-wrap:anywhere}.ay-page *{box-sizing:border-box}.ay-page h2{font-size:1.25rem;margin:0}.ay-page h3{font-size:1rem}.ay-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 0 24px;border-bottom:1px solid var(--border-light)}.ay-avatar{width:52px;height:52px;border-radius:18px;display:grid;place-items:center;background:var(--bg-secondary);font-size:24px;color:var(--accent-light);overflow:hidden}.ay-avatar img{width:100%;height:100%;object-fit:cover}.ay-name{flex:1;min-width:120px}.ay-eyebrow{display:block;color:var(--text-secondary);font-size:12px;letter-spacing:.12em}.ay-name-button{display:block;padding:0;border:0;background:none;color:inherit;font-size:1.6rem;font-weight:700;text-align:left;min-height:44px;cursor:pointer}.ay-chips{display:flex;gap:8px;flex-wrap:wrap}.ay-state{display:flex;align-items:center;gap:7px;min-height:32px}.ay-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}.ay-card{min-width:0;margin:0;padding:22px;border:1px solid var(--border-light);border-radius:18px;background:var(--bg-card,var(--bg-secondary))}.ay-section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}#ay-now,#ay-asks,#ay-tell,#ay-today,#ay-goals{grid-column:1/-1}.ay-current{font-size:1.2rem;line-height:1.6;margin:6px 0 14px}.ay-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:#4cae88;margin-right:8px;animation:ay-pulse 2s ease-in-out infinite}@keyframes ay-pulse{50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.ay-pulse{animation:none}}.ay-looking{display:flex;gap:8px;align-items:center;color:var(--text-secondary)}.ay-looking svg{width:20px;flex:none}.ay-steps,.ay-journal{list-style:none;margin:0;padding:0}.ay-steps li{display:flex;gap:14px;margin:10px 0}.ay-page time,.ay-muted,.ay-next,.ay-page small{color:var(--text-secondary)}.ay-page time{font-size:12px;white-space:nowrap}.ay-spend{display:flex;align-items:center;gap:20px;margin-top:20px;font-size:13px}.ay-spend progress{flex:1;height:5px;min-width:40px;accent-color:var(--accent)}.ay-ask{padding:16px 0;border-top:1px solid var(--border-light)}.ay-ask:first-of-type{border:0;padding-top:0}.ay-ask blockquote{margin:12px 0;padding:12px;border-left:3px solid var(--accent);background:var(--bg-primary);white-space:pre-wrap}.ay-actions{display:flex;gap:8px;flex-wrap:wrap}.ay-page .ovk-btn,.ay-page input:not([type=checkbox]):not([type=hidden]),.ay-page select{min-height:44px}.ay-page .ovk-btn{white-space:normal}.ay-page label{display:grid;gap:7px;font-size:13px;margin:10px 0}.ay-page input:not([type=checkbox]):not([type=hidden]),.ay-page select,.ay-page textarea{width:100%;max-width:100%;padding:10px 12px;border:1px solid var(--border-light);border-radius:10px;background:var(--bg-primary);color:inherit;font:inherit}.ay-page textarea{min-height:90px;resize:vertical}.ay-page .ay-check{display:flex;align-items:center;gap:10px;min-height:44px}.ay-page input[type=checkbox]{width:20px;height:20px;accent-color:var(--accent);flex:none}.ay-page summary{min-height:44px;display:flex;align-items:center;gap:8px;cursor:pointer}.ay-thread{display:block;min-height:0}.ay-thread .ovt-composer{position:static;margin:0;background:var(--bg-primary)}.ay-thread textarea{min-height:60px}.ay-thread .ovt-attach,.ay-thread .ovt-send{width:44px;height:44px;min-height:44px}.ay-thread .ovt-turn{margin-top:18px}.ay-journal li{display:grid;grid-template-columns:42px minmax(0,1fr) auto;gap:10px;margin:18px 0}.ay-journal p{margin:0;line-height:1.5}.ay-journal img{max-width:110px;max-height:70px;object-fit:cover;border-radius:7px;margin-bottom:7px}.ay-goal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-bottom:18px}.ay-goal{padding:18px;border-radius:12px;border:1px solid var(--border-light);background:var(--bg-primary)}.ay-goal header,.ay-goal footer{display:flex;gap:8px;align-items:center}.ay-goal header>svg{width:18px}.ay-goal footer{justify-content:space-between}.ay-goal .ay-switch{margin-left:auto;display:flex;align-items:center;min-height:44px}.ay-goal h3{font-size:1.1rem}.ay-goal p{font-size:13px;line-height:1.6}.ay-money-total{display:flex;gap:25px;flex-wrap:wrap}.ay-money-total span{display:block;font-size:12px;color:var(--text-secondary)}.ay-money-total strong{font-size:1.7rem}.ay-week{display:grid;grid-template-columns:minmax(0,1fr) minmax(50px,1fr) auto;gap:10px;align-items:center;margin:12px 0;font-size:12px}.ay-week meter{width:100%}.ay-page dialog{color:var(--text-primary)}.ay-page .ay-model{width:100%;display:flex;justify-content:space-between;text-align:left;flex-wrap:wrap;margin:7px 0}.ay-model small{font-weight:400}.ay-lanes{display:grid;gap:10px}.ay-lane{display:flex;align-items:center;gap:14px;width:100%;padding:16px!important}.ay-lane svg{width:24px}.ay-days{display:flex;flex-wrap:wrap;gap:6px}.ay-days label{display:flex;align-items:center;gap:4px;min-height:44px}.ay-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.ay-page fieldset{border:1px solid var(--border-light);border-radius:10px}.ay-screen{position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:12px;background:var(--bg-primary);margin-bottom:12px}.ay-screen img{width:100%;height:100%;object-fit:contain;display:block}.ay-screen[data-control=true] img{cursor:crosshair;touch-action:none}.ay-screen:fullscreen{border-radius:0}.ay-screen-note{position:absolute;bottom:6px;left:10px;font-size:12px}.ay-error{color:var(--danger,#d45f63)}#ay-notice:empty{display:none}#ay-notice{padding:12px;border:1px solid var(--border-light);border-radius:10px}.ay-page [hidden]{display:none!important}.ay-page a{color:var(--accent-light,var(--accent))}#ay-sites li{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:10px 0}#ay-sites ul{padding:0;list-style:none}@media(max-width:680px){.ay-page{padding:0 14px;margin-top:16px}.ay-layout{grid-template-columns:minmax(0,1fr);gap:14px}.ay-card{padding:18px}.ay-header{gap:8px}.ay-chips{width:100%}.ay-goal-grid{grid-template-columns:minmax(0,1fr)}.ay-spend{gap:12px}.ay-journal li{grid-template-columns:35px minmax(0,1fr) auto;gap:7px}.ay-name-button{font-size:1.35rem}.ay-screen{margin-inline:-4px}.ay-week{grid-template-columns:minmax(0,1fr) 60px auto}}++/* Now: the model for the next step. One row (the picker takes the width, the button stays one word); a quiet inset so it++ reads as a control of the run above it, not as another card. */++.ay-run-model{display:grid;gap:6px;margin:0 0 14px;padding:12px;border:1px solid var(--border);border-radius:12px;background:color-mix(in srgb,var(--text-primary) 3%,transparent)}++.ay-run-model>label{font-size:.82rem;font-weight:700;color:var(--text-secondary)}++.ay-run-model-row{display:flex;gap:8px;align-items:stretch}++.ay-run-model-row select{flex:1;min-width:0}++.ay-run-model-row .ovk-btn{flex:none;min-height:44px}++.ay-run-model-note{margin:0;font-size:.82rem;line-height:1.45;color:var(--text-secondary)}@@@ -26,7 +26,16 @@ function render(data, viewer = {}) {const rawRunBrain = activeRun?.model_override || activeRun?.brain || a.brain;const runBrain = rawRunBrain === 'own:node' ? 'own:link' : rawRunBrain;const runOption = (id,label) => '<option value="'+e(id)+'" '+(runBrain===id?'selected':'')+'>'+e(label)+'</option>';-- const runModel = modelSwitch && a.active_task ? '<form id="ay-run-model" data-task="'+e(a.active_task)+'"><label>Model for the next step<select name="model">' + runOption('auto','Auto') + runOption('openvibe','OpenVibe') + models.map(m=>runOption(m.id,m.name||m.id)).join('') + ['openai','anthropic','deepseek','google','openrouter','mistral','link'].map(p=>runOption('own:'+p,'Your '+p)).join('') + '</select></label><button class="ovk-btn ovk-btn--primary">Use for next step</button><p class="ay-muted">The run record keeps its model steps and costs. Tools this model cannot use stay unavailable.</p></form>' : '';++ // The model for the next step: one row inside Now (the label says what it changes, so the button is one word), grouped by++ // who runs it, named as the chat's model menu names them ("Your OpenAI key", "Your computer").++ const OWN_KEYS = [['openai','Your OpenAI key'],['anthropic','Your Anthropic key'],['deepseek','Your DeepSeek key'],['google','Your Google key'],['openrouter','Your OpenRouter key'],['mistral','Your Mistral key']];++ const runModel = modelSwitch && a.active_task ? '<form id="ay-run-model" class="ay-run-model" data-task="'+e(a.active_task)+'"><label for="ay-run-model-pick">Model for the next step</label>'++ + '<div class="ay-run-model-row"><select id="ay-run-model-pick" name="model">'++ + '<optgroup label="On OpenVibe">' + runOption('auto','Automatic') + runOption('openvibe','OpenVibe') + models.map(m=>runOption(m.id,m.name||m.id)).join('') + '</optgroup>'++ + '<optgroup label="Your own keys">' + OWN_KEYS.map(([p,label])=>runOption('own:'+p,label)).join('') + '</optgroup>'++ + '<optgroup label="Your own machines">' + runOption('own:link','Your computer') + '</optgroup>'++ + '</select><button class="ovk-btn ovk-btn--quiet">Switch</button></div>'++ + '<p class="ay-run-model-note">Steps already run keep the model and cost they used. A tool the new model cannot use stays off.</p></form>' : '';const nowHtml = card('ay-now','Now', screen + '<p class="ay-current">' + (state === 'Working' ? pulse : '') + e(now.current_step || state) + '</p>' + (now.looking_at ? '<p class="ay-looking">' + thread.icon('read') + e(now.looking_at) + '</p>' : '') + runModel + '<ol class="ay-steps">' + (now.last_three_steps || []).map(j => '<li>' + time(j.created_at) + '<span>' + e(j.text) + '</span></li>').join('') + '</ol>' + next + (state === 'Resting' && now.next ? button('Do it now','data-action="do-now"',true) : '') + spend);const asksHtml = asks.length ? card('ay-asks','Asks <span class="ovk-badge">' + asks.length + '</span>', asks.map(q => '<article class="ay-ask"><h3>' + e(q.text) + '</h3>' + (q.evidence?.text ? '<blockquote>' + e(q.evidence.text) + '</blockquote>' : '') + '<div class="ay-actions">' + q.options.map(o => q.kind === 'sign_in' && o !== 'Done' ? button('Sign in on its screen','data-sign-in',true) : button(o,'data-answer="' + e(q.id) + '" data-text="' + e(o) + '"',true)).join('') + button('Not now','data-decline="' + e(q.id) + '"') + '</div>' + (q.kind === 'answer_question' ? '<form data-answer-form="' + e(q.id) + '">' + input('text','Your answer','required maxlength="4000"') + '<button class="ovk-btn">Answer</button></form>' : '') + '</article>').join('')) : '<div id="ay-asks" hidden></div>';const tell = card('ay-tell','Tell it','<div class="ovt ay-thread" data-ov-thread>' + thread.composer({ id:'ay-tell-form',action:'/api/v1/me/actor/tell',placeholder:'Tell your Actor something…',maxlength:4000,attach:{name:'files',accept:'.txt,.md,.csv,.json'},note:'Small text files welcome. Your Actor reads this on its next step.' }) + '<div id="ay-replies">' + messages.slice().reverse().map(m => thread.turn({ id:m.id,you:{text:m.text},reply:m.reply ? thread.answer({html:'<p>' + e(m.reply.answer || m.reply.text || '') + '</p>'}) : '<p class="ay-muted">Kept for the next step.</p>' })).join('') + '</div></div>');@@@ -20,6 +20,15 @@ const SAME = { 'sec-fetch-site':'same-origin' };}assert.doesNotMatch(render(base),/Uncensored/);assert.doesNotMatch(render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, model_switch_enabled:false}),/ay-run-model/);++ // On: one row in Now. The label says what it changes, the button is one word, the choices are grouped by who runs++ // them and named as the chat's model menu names them.++ const sw = render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, model_switch_enabled:true});++ assert.match(sw, /<label for="ay-run-model-pick">Model for the next step<\/label>/);++ assert.match(sw, /<optgroup label="On OpenVibe"><option value="auto"[^>]*>Automatic<\/option>/);++ assert.match(sw, /<optgroup label="Your own keys"><option value="own:openai"[^>]*>Your OpenAI key<\/option>/);++ assert.match(sw, /<optgroup label="Your own machines"><option value="own:link"[^>]*>Your computer<\/option>/);++ assert.match(sw, /<button class="ovk-btn ovk-btn--quiet">Switch<\/button>/);++ assert.doesNotMatch(sw, /Your (openai|link)</, 'no raw provider ids as labels');assert.match(render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, active_run:{brain:'openvibe'}, model_switch_enabled:true}),/option value="openvibe" selected/);assert.match(render({...base,actor:{...a,adult_opt_in:true}}),/Uncensored/);assert.match(render({...base,actor:{...a,asks:[{id:'ask',kind:'sign_in',text:'Sign in to the site',options:['Sign in on its screen','Done']} ]}}),/Waiting for you/);
@@ -1 +1,9 @@
@@ -1 +1,9 @@
.ay-page{max-width:1120px;margin:28px auto 80px;padding:0 20px;color:var(--text-primary);overflow-wrap:anywhere}.ay-page *{box-sizing:border-box}.ay-page h2{font-size:1.25rem;margin:0}.ay-page h3{font-size:1rem}.ay-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 0 24px;border-bottom:1px solid var(--border-light)}.ay-avatar{width:52px;height:52px;border-radius:18px;display:grid;place-items:center;background:var(--bg-secondary);font-size:24px;color:var(--accent-light);overflow:hidden}.ay-avatar img{width:100%;height:100%;object-fit:cover}.ay-name{flex:1;min-width:120px}.ay-eyebrow{display:block;color:var(--text-secondary);font-size:12px;letter-spacing:.12em}.ay-name-button{display:block;padding:0;border:0;background:none;color:inherit;font-size:1.6rem;font-weight:700;text-align:left;min-height:44px;cursor:pointer}.ay-chips{display:flex;gap:8px;flex-wrap:wrap}.ay-state{display:flex;align-items:center;gap:7px;min-height:32px}.ay-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}.ay-card{min-width:0;margin:0;padding:22px;border:1px solid var(--border-light);border-radius:18px;background:var(--bg-card,var(--bg-secondary))}.ay-section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}#ay-now,#ay-asks,#ay-tell,#ay-today,#ay-goals{grid-column:1/-1}.ay-current{font-size:1.2rem;line-height:1.6;margin:6px 0 14px}.ay-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:#4cae88;margin-right:8px;animation:ay-pulse 2s ease-in-out infinite}@keyframes ay-pulse{50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.ay-pulse{animation:none}}.ay-looking{display:flex;gap:8px;align-items:center;color:var(--text-secondary)}.ay-looking svg{width:20px;flex:none}.ay-steps,.ay-journal{list-style:none;margin:0;padding:0}.ay-steps li{display:flex;gap:14px;margin:10px 0}.ay-page time,.ay-muted,.ay-next,.ay-page small{color:var(--text-secondary)}.ay-page time{font-size:12px;white-space:nowrap}.ay-spend{display:flex;align-items:center;gap:20px;margin-top:20px;font-size:13px}.ay-spend progress{flex:1;height:5px;min-width:40px;accent-color:var(--accent)}.ay-ask{padding:16px 0;border-top:1px solid var(--border-light)}.ay-ask:first-of-type{border:0;padding-top:0}.ay-ask blockquote{margin:12px 0;padding:12px;border-left:3px solid var(--accent);background:var(--bg-primary);white-space:pre-wrap}.ay-actions{display:flex;gap:8px;flex-wrap:wrap}.ay-page .ovk-btn,.ay-page input:not([type=checkbox]):not([type=hidden]),.ay-page select{min-height:44px}.ay-page .ovk-btn{white-space:normal}.ay-page label{display:grid;gap:7px;font-size:13px;margin:10px 0}.ay-page input:not([type=checkbox]):not([type=hidden]),.ay-page select,.ay-page textarea{width:100%;max-width:100%;padding:10px 12px;border:1px solid var(--border-light);border-radius:10px;background:var(--bg-primary);color:inherit;font:inherit}.ay-page textarea{min-height:90px;resize:vertical}.ay-page .ay-check{display:flex;align-items:center;gap:10px;min-height:44px}.ay-page input[type=checkbox]{width:20px;height:20px;accent-color:var(--accent);flex:none}.ay-page summary{min-height:44px;display:flex;align-items:center;gap:8px;cursor:pointer}.ay-thread{display:block;min-height:0}.ay-thread .ovt-composer{position:static;margin:0;background:var(--bg-primary)}.ay-thread textarea{min-height:60px}.ay-thread .ovt-attach,.ay-thread .ovt-send{width:44px;height:44px;min-height:44px}.ay-thread .ovt-turn{margin-top:18px}.ay-journal li{display:grid;grid-template-columns:42px minmax(0,1fr) auto;gap:10px;margin:18px 0}.ay-journal p{margin:0;line-height:1.5}.ay-journal img{max-width:110px;max-height:70px;object-fit:cover;border-radius:7px;margin-bottom:7px}.ay-goal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-bottom:18px}.ay-goal{padding:18px;border-radius:12px;border:1px solid var(--border-light);background:var(--bg-primary)}.ay-goal header,.ay-goal footer{display:flex;gap:8px;align-items:center}.ay-goal header>svg{width:18px}.ay-goal footer{justify-content:space-between}.ay-goal .ay-switch{margin-left:auto;display:flex;align-items:center;min-height:44px}.ay-goal h3{font-size:1.1rem}.ay-goal p{font-size:13px;line-height:1.6}.ay-money-total{display:flex;gap:25px;flex-wrap:wrap}.ay-money-total span{display:block;font-size:12px;color:var(--text-secondary)}.ay-money-total strong{font-size:1.7rem}.ay-week{display:grid;grid-template-columns:minmax(0,1fr) minmax(50px,1fr) auto;gap:10px;align-items:center;margin:12px 0;font-size:12px}.ay-week meter{width:100%}.ay-page dialog{color:var(--text-primary)}.ay-page .ay-model{width:100%;display:flex;justify-content:space-between;text-align:left;flex-wrap:wrap;margin:7px 0}.ay-model small{font-weight:400}.ay-lanes{display:grid;gap:10px}.ay-lane{display:flex;align-items:center;gap:14px;width:100%;padding:16px!important}.ay-lane svg{width:24px}.ay-days{display:flex;flex-wrap:wrap;gap:6px}.ay-days label{display:flex;align-items:center;gap:4px;min-height:44px}.ay-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.ay-page fieldset{border:1px solid var(--border-light);border-radius:10px}.ay-screen{position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:12px;background:var(--bg-primary);margin-bottom:12px}.ay-screen img{width:100%;height:100%;object-fit:contain;display:block}.ay-screen[data-control=true] img{cursor:crosshair;touch-action:none}.ay-screen:fullscreen{border-radius:0}.ay-screen-note{position:absolute;bottom:6px;left:10px;font-size:12px}.ay-error{color:var(--danger,#d45f63)}#ay-notice:empty{display:none}#ay-notice{padding:12px;border:1px solid var(--border-light);border-radius:10px}.ay-page [hidden]{display:none!important}.ay-page a{color:var(--accent-light,var(--accent))}#ay-sites li{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:10px 0}#ay-sites ul{padding:0;list-style:none}@media(max-width:680px){.ay-page{padding:0 14px;margin-top:16px}.ay-layout{grid-template-columns:minmax(0,1fr);gap:14px}.ay-card{padding:18px}.ay-header{gap:8px}.ay-chips{width:100%}.ay-goal-grid{grid-template-columns:minmax(0,1fr)}.ay-spend{gap:12px}.ay-journal li{grid-template-columns:35px minmax(0,1fr) auto;gap:7px}.ay-name-button{font-size:1.35rem}.ay-screen{margin-inline:-4px}.ay-week{grid-template-columns:minmax(0,1fr) 60px auto}}
.ay-page{max-width:1120px;margin:28px auto 80px;padding:0 20px;color:var(--text-primary);overflow-wrap:anywhere}.ay-page *{box-sizing:border-box}.ay-page h2{font-size:1.25rem;margin:0}.ay-page h3{font-size:1rem}.ay-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 0 24px;border-bottom:1px solid var(--border-light)}.ay-avatar{width:52px;height:52px;border-radius:18px;display:grid;place-items:center;background:var(--bg-secondary);font-size:24px;color:var(--accent-light);overflow:hidden}.ay-avatar img{width:100%;height:100%;object-fit:cover}.ay-name{flex:1;min-width:120px}.ay-eyebrow{display:block;color:var(--text-secondary);font-size:12px;letter-spacing:.12em}.ay-name-button{display:block;padding:0;border:0;background:none;color:inherit;font-size:1.6rem;font-weight:700;text-align:left;min-height:44px;cursor:pointer}.ay-chips{display:flex;gap:8px;flex-wrap:wrap}.ay-state{display:flex;align-items:center;gap:7px;min-height:32px}.ay-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}.ay-card{min-width:0;margin:0;padding:22px;border:1px solid var(--border-light);border-radius:18px;background:var(--bg-card,var(--bg-secondary))}.ay-section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}#ay-now,#ay-asks,#ay-tell,#ay-today,#ay-goals{grid-column:1/-1}.ay-current{font-size:1.2rem;line-height:1.6;margin:6px 0 14px}.ay-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:#4cae88;margin-right:8px;animation:ay-pulse 2s ease-in-out infinite}@keyframes ay-pulse{50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.ay-pulse{animation:none}}.ay-looking{display:flex;gap:8px;align-items:center;color:var(--text-secondary)}.ay-looking svg{width:20px;flex:none}.ay-steps,.ay-journal{list-style:none;margin:0;padding:0}.ay-steps li{display:flex;gap:14px;margin:10px 0}.ay-page time,.ay-muted,.ay-next,.ay-page small{color:var(--text-secondary)}.ay-page time{font-size:12px;white-space:nowrap}.ay-spend{display:flex;align-items:center;gap:20px;margin-top:20px;font-size:13px}.ay-spend progress{flex:1;height:5px;min-width:40px;accent-color:var(--accent)}.ay-ask{padding:16px 0;border-top:1px solid var(--border-light)}.ay-ask:first-of-type{border:0;padding-top:0}.ay-ask blockquote{margin:12px 0;padding:12px;border-left:3px solid var(--accent);background:var(--bg-primary);white-space:pre-wrap}.ay-actions{display:flex;gap:8px;flex-wrap:wrap}.ay-page .ovk-btn,.ay-page input:not([type=checkbox]):not([type=hidden]),.ay-page select{min-height:44px}.ay-page .ovk-btn{white-space:normal}.ay-page label{display:grid;gap:7px;font-size:13px;margin:10px 0}.ay-page input:not([type=checkbox]):not([type=hidden]),.ay-page select,.ay-page textarea{width:100%;max-width:100%;padding:10px 12px;border:1px solid var(--border-light);border-radius:10px;background:var(--bg-primary);color:inherit;font:inherit}.ay-page textarea{min-height:90px;resize:vertical}.ay-page .ay-check{display:flex;align-items:center;gap:10px;min-height:44px}.ay-page input[type=checkbox]{width:20px;height:20px;accent-color:var(--accent);flex:none}.ay-page summary{min-height:44px;display:flex;align-items:center;gap:8px;cursor:pointer}.ay-thread{display:block;min-height:0}.ay-thread .ovt-composer{position:static;margin:0;background:var(--bg-primary)}.ay-thread textarea{min-height:60px}.ay-thread .ovt-attach,.ay-thread .ovt-send{width:44px;height:44px;min-height:44px}.ay-thread .ovt-turn{margin-top:18px}.ay-journal li{display:grid;grid-template-columns:42px minmax(0,1fr) auto;gap:10px;margin:18px 0}.ay-journal p{margin:0;line-height:1.5}.ay-journal img{max-width:110px;max-height:70px;object-fit:cover;border-radius:7px;margin-bottom:7px}.ay-goal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-bottom:18px}.ay-goal{padding:18px;border-radius:12px;border:1px solid var(--border-light);background:var(--bg-primary)}.ay-goal header,.ay-goal footer{display:flex;gap:8px;align-items:center}.ay-goal header>svg{width:18px}.ay-goal footer{justify-content:space-between}.ay-goal .ay-switch{margin-left:auto;display:flex;align-items:center;min-height:44px}.ay-goal h3{font-size:1.1rem}.ay-goal p{font-size:13px;line-height:1.6}.ay-money-total{display:flex;gap:25px;flex-wrap:wrap}.ay-money-total span{display:block;font-size:12px;color:var(--text-secondary)}.ay-money-total strong{font-size:1.7rem}.ay-week{display:grid;grid-template-columns:minmax(0,1fr) minmax(50px,1fr) auto;gap:10px;align-items:center;margin:12px 0;font-size:12px}.ay-week meter{width:100%}.ay-page dialog{color:var(--text-primary)}.ay-page .ay-model{width:100%;display:flex;justify-content:space-between;text-align:left;flex-wrap:wrap;margin:7px 0}.ay-model small{font-weight:400}.ay-lanes{display:grid;gap:10px}.ay-lane{display:flex;align-items:center;gap:14px;width:100%;padding:16px!important}.ay-lane svg{width:24px}.ay-days{display:flex;flex-wrap:wrap;gap:6px}.ay-days label{display:flex;align-items:center;gap:4px;min-height:44px}.ay-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.ay-page fieldset{border:1px solid var(--border-light);border-radius:10px}.ay-screen{position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:12px;background:var(--bg-primary);margin-bottom:12px}.ay-screen img{width:100%;height:100%;object-fit:contain;display:block}.ay-screen[data-control=true] img{cursor:crosshair;touch-action:none}.ay-screen:fullscreen{border-radius:0}.ay-screen-note{position:absolute;bottom:6px;left:10px;font-size:12px}.ay-error{color:var(--danger,#d45f63)}#ay-notice:empty{display:none}#ay-notice{padding:12px;border:1px solid var(--border-light);border-radius:10px}.ay-page [hidden]{display:none!important}.ay-page a{color:var(--accent-light,var(--accent))}#ay-sites li{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:10px 0}#ay-sites ul{padding:0;list-style:none}@media(max-width:680px){.ay-page{padding:0 14px;margin-top:16px}.ay-layout{grid-template-columns:minmax(0,1fr);gap:14px}.ay-card{padding:18px}.ay-header{gap:8px}.ay-chips{width:100%}.ay-goal-grid{grid-template-columns:minmax(0,1fr)}.ay-spend{gap:12px}.ay-journal li{grid-template-columns:35px minmax(0,1fr) auto;gap:7px}.ay-name-button{font-size:1.35rem}.ay-screen{margin-inline:-4px}.ay-week{grid-template-columns:minmax(0,1fr) 60px auto}}
+/* Now: the model for the next step. One row (the picker takes the width, the button stays one word); a quiet inset so it
+ reads as a control of the run above it, not as another card. */
+.ay-run-model{display:grid;gap:6px;margin:0 0 14px;padding:12px;border:1px solid var(--border);border-radius:12px;background:color-mix(in srgb,var(--text-primary) 3%,transparent)}
+.ay-run-model>label{font-size:.82rem;font-weight:700;color:var(--text-secondary)}
+.ay-run-model-row{display:flex;gap:8px;align-items:stretch}
+.ay-run-model-row select{flex:1;min-width:0}
+.ay-run-model-row .ovk-btn{flex:none;min-height:44px}
+.ay-run-model-note{margin:0;font-size:.82rem;line-height:1.45;color:var(--text-secondary)}
@@ -26,7 +26,16 @@ function render(data, viewer = {}) {
@@ -26,7 +26,16 @@ function render(data, viewer = {}) {
const rawRunBrain = activeRun?.model_override || activeRun?.brain || a.brain;
const rawRunBrain = activeRun?.model_override || activeRun?.brain || a.brain;
const runBrain = rawRunBrain === 'own:node' ? 'own:link' : rawRunBrain;
const runBrain = rawRunBrain === 'own:node' ? 'own:link' : rawRunBrain;
const runOption = (id,label) => '<option value="'+e(id)+'" '+(runBrain===id?'selected':'')+'>'+e(label)+'</option>';
const runOption = (id,label) => '<option value="'+e(id)+'" '+(runBrain===id?'selected':'')+'>'+e(label)+'</option>';
- const runModel = modelSwitch && a.active_task ? '<form id="ay-run-model" data-task="'+e(a.active_task)+'"><label>Model for the next step<select name="model">' + runOption('auto','Auto') + runOption('openvibe','OpenVibe') + models.map(m=>runOption(m.id,m.name||m.id)).join('') + ['openai','anthropic','deepseek','google','openrouter','mistral','link'].map(p=>runOption('own:'+p,'Your '+p)).join('') + '</select></label><button class="ovk-btn ovk-btn--primary">Use for next step</button><p class="ay-muted">The run record keeps its model steps and costs. Tools this model cannot use stay unavailable.</p></form>' : '';
+ // The model for the next step: one row inside Now (the label says what it changes, so the button is one word), grouped by
+ // who runs it, named as the chat's model menu names them ("Your OpenAI key", "Your computer").
+ const OWN_KEYS = [['openai','Your OpenAI key'],['anthropic','Your Anthropic key'],['deepseek','Your DeepSeek key'],['google','Your Google key'],['openrouter','Your OpenRouter key'],['mistral','Your Mistral key']];
+ const runModel = modelSwitch && a.active_task ? '<form id="ay-run-model" class="ay-run-model" data-task="'+e(a.active_task)+'"><label for="ay-run-model-pick">Model for the next step</label>'
+ + '<div class="ay-run-model-row"><select id="ay-run-model-pick" name="model">'
+ + '<optgroup label="On OpenVibe">' + runOption('auto','Automatic') + runOption('openvibe','OpenVibe') + models.map(m=>runOption(m.id,m.name||m.id)).join('') + '</optgroup>'
+ + '<optgroup label="Your own keys">' + OWN_KEYS.map(([p,label])=>runOption('own:'+p,label)).join('') + '</optgroup>'
+ + '<optgroup label="Your own machines">' + runOption('own:link','Your computer') + '</optgroup>'
+ + '</select><button class="ovk-btn ovk-btn--quiet">Switch</button></div>'
+ + '<p class="ay-run-model-note">Steps already run keep the model and cost they used. A tool the new model cannot use stays off.</p></form>' : '';
const nowHtml = card('ay-now','Now', screen + '<p class="ay-current">' + (state === 'Working' ? pulse : '') + e(now.current_step || state) + '</p>' + (now.looking_at ? '<p class="ay-looking">' + thread.icon('read') + e(now.looking_at) + '</p>' : '') + runModel + '<ol class="ay-steps">' + (now.last_three_steps || []).map(j => '<li>' + time(j.created_at) + '<span>' + e(j.text) + '</span></li>').join('') + '</ol>' + next + (state === 'Resting' && now.next ? button('Do it now','data-action="do-now"',true) : '') + spend);
const nowHtml = card('ay-now','Now', screen + '<p class="ay-current">' + (state === 'Working' ? pulse : '') + e(now.current_step || state) + '</p>' + (now.looking_at ? '<p class="ay-looking">' + thread.icon('read') + e(now.looking_at) + '</p>' : '') + runModel + '<ol class="ay-steps">' + (now.last_three_steps || []).map(j => '<li>' + time(j.created_at) + '<span>' + e(j.text) + '</span></li>').join('') + '</ol>' + next + (state === 'Resting' && now.next ? button('Do it now','data-action="do-now"',true) : '') + spend);
const asksHtml = asks.length ? card('ay-asks','Asks <span class="ovk-badge">' + asks.length + '</span>', asks.map(q => '<article class="ay-ask"><h3>' + e(q.text) + '</h3>' + (q.evidence?.text ? '<blockquote>' + e(q.evidence.text) + '</blockquote>' : '') + '<div class="ay-actions">' + q.options.map(o => q.kind === 'sign_in' && o !== 'Done' ? button('Sign in on its screen','data-sign-in',true) : button(o,'data-answer="' + e(q.id) + '" data-text="' + e(o) + '"',true)).join('') + button('Not now','data-decline="' + e(q.id) + '"') + '</div>' + (q.kind === 'answer_question' ? '<form data-answer-form="' + e(q.id) + '">' + input('text','Your answer','required maxlength="4000"') + '<button class="ovk-btn">Answer</button></form>' : '') + '</article>').join('')) : '<div id="ay-asks" hidden></div>';
const asksHtml = asks.length ? card('ay-asks','Asks <span class="ovk-badge">' + asks.length + '</span>', asks.map(q => '<article class="ay-ask"><h3>' + e(q.text) + '</h3>' + (q.evidence?.text ? '<blockquote>' + e(q.evidence.text) + '</blockquote>' : '') + '<div class="ay-actions">' + q.options.map(o => q.kind === 'sign_in' && o !== 'Done' ? button('Sign in on its screen','data-sign-in',true) : button(o,'data-answer="' + e(q.id) + '" data-text="' + e(o) + '"',true)).join('') + button('Not now','data-decline="' + e(q.id) + '"') + '</div>' + (q.kind === 'answer_question' ? '<form data-answer-form="' + e(q.id) + '">' + input('text','Your answer','required maxlength="4000"') + '<button class="ovk-btn">Answer</button></form>' : '') + '</article>').join('')) : '<div id="ay-asks" hidden></div>';
const tell = card('ay-tell','Tell it','<div class="ovt ay-thread" data-ov-thread>' + thread.composer({ id:'ay-tell-form',action:'/api/v1/me/actor/tell',placeholder:'Tell your Actor something…',maxlength:4000,attach:{name:'files',accept:'.txt,.md,.csv,.json'},note:'Small text files welcome. Your Actor reads this on its next step.' }) + '<div id="ay-replies">' + messages.slice().reverse().map(m => thread.turn({ id:m.id,you:{text:m.text},reply:m.reply ? thread.answer({html:'<p>' + e(m.reply.answer || m.reply.text || '') + '</p>'}) : '<p class="ay-muted">Kept for the next step.</p>' })).join('') + '</div></div>');
const tell = card('ay-tell','Tell it','<div class="ovt ay-thread" data-ov-thread>' + thread.composer({ id:'ay-tell-form',action:'/api/v1/me/actor/tell',placeholder:'Tell your Actor something…',maxlength:4000,attach:{name:'files',accept:'.txt,.md,.csv,.json'},note:'Small text files welcome. Your Actor reads this on its next step.' }) + '<div id="ay-replies">' + messages.slice().reverse().map(m => thread.turn({ id:m.id,you:{text:m.text},reply:m.reply ? thread.answer({html:'<p>' + e(m.reply.answer || m.reply.text || '') + '</p>'}) : '<p class="ay-muted">Kept for the next step.</p>' })).join('') + '</div></div>');
@@ -20,6 +20,15 @@ const SAME = { 'sec-fetch-site':'same-origin' };
@@ -20,6 +20,15 @@ const SAME = { 'sec-fetch-site':'same-origin' };
}
}
assert.doesNotMatch(render(base),/Uncensored/);
assert.doesNotMatch(render(base),/Uncensored/);
assert.doesNotMatch(render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, model_switch_enabled:false}),/ay-run-model/);
assert.doesNotMatch(render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, model_switch_enabled:false}),/ay-run-model/);
+ // On: one row in Now. The label says what it changes, the button is one word, the choices are grouped by who runs
+ // them and named as the chat's model menu names them.
+ const sw = render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, model_switch_enabled:true});
+ assert.match(sw, /<label for="ay-run-model-pick">Model for the next step<\/label>/);
+ assert.match(sw, /<optgroup label="On OpenVibe"><option value="auto"[^>]*>Automatic<\/option>/);
+ assert.match(sw, /<optgroup label="Your own keys"><option value="own:openai"[^>]*>Your OpenAI key<\/option>/);
+ assert.match(sw, /<optgroup label="Your own machines"><option value="own:link"[^>]*>Your computer<\/option>/);
+ assert.match(sw, /<button class="ovk-btn ovk-btn--quiet">Switch<\/button>/);
+ assert.doesNotMatch(sw, /Your (openai|link)</, 'no raw provider ids as labels');
assert.match(render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, active_run:{brain:'openvibe'}, model_switch_enabled:true}),/option value="openvibe" selected/);
assert.match(render({...base, actor:{...a, active_task:'tsk_01J8Z4M2Q0R7T9YV3K6N8P1W2A'}, active_run:{brain:'openvibe'}, model_switch_enabled:true}),/option value="openvibe" selected/);
assert.match(render({...base,actor:{...a,adult_opt_in:true}}),/Uncensored/);
assert.match(render({...base,actor:{...a,adult_opt_in:true}}),/Uncensored/);
assert.match(render({...base,actor:{...a,asks:[{id:'ask',kind:'sign_in',text:'Sign in to the site',options:['Sign in on its screen','Done']} ]}}),/Waiting for you/);
assert.match(render({...base,actor:{...a,asks:[{id:'ask',kind:'sign_in',text:'Sign in to the site',options:['Sign in on its screen','Done']} ]}}),/Waiting for you/);