From 2bb33dfd913e17320497773f137891c32f326310 Mon Sep 17 00:00:00 2001 From: FORIFOR Date: Mon, 19 Oct 2026 22:32:22 +0900 Subject: [PATCH 1/5] feat: AI Meeting page leads with its vision, like Oathra Three concept demos you can try (rephrasing, booking what was decided, turning a meeting's decisions into team tasks) and one moving diagram of the idea the product rests on: your words are quoted, held, and kept only after you confirm. Wording and states come from AI Meeting's own UI and README; data is fictional, and the scenes show their finished state without JavaScript or with reduced motion. Co-Authored-By: Claude Opus 5.5 --- public/assets/redesign.mjs | 5 ++ public/demos/ai-meeting/demo.css | 45 +++++++++++++ public/demos/ai-meeting/demo.js | 105 +++++++++++++++++++++++++++++ public/demos/ai-meeting/index.html | 41 +++++++++++ src/redesign.mjs | 2 + src/redesign/redesign.css | 59 ++++++++++++++++ src/vision-pages.mjs | 68 +++++++++++++++++++ 7 files changed, 325 insertions(+) create mode 100644 public/demos/ai-meeting/demo.css create mode 100644 public/demos/ai-meeting/demo.js create mode 100644 public/demos/ai-meeting/index.html create mode 100644 src/vision-pages.mjs diff --git a/public/assets/redesign.mjs b/public/assets/redesign.mjs index 8a0fede..8e63fe3 100644 --- a/public/assets/redesign.mjs +++ b/public/assets/redesign.mjs @@ -93,6 +93,11 @@ if (root?.dataset.rd) { }, {rootMargin: '0px 0px -6% 0px'}); pending.forEach(el => io.observe(el)); } + /* Vision scenes: the steps play only while the scene is on screen; the finished state is the default. */ + if ('IntersectionObserver' in window) { + const scenes = new IntersectionObserver(entries => entries.forEach(entry => entry.target.toggleAttribute('data-vp-play', entry.isIntersecting)), {threshold: .35}); + $$('[data-vp-scene]').forEach(el => scenes.observe(el)); + } } /* Cursor preview on the home product list: fine pointers with hover only, never under reduced motion. */ diff --git a/public/demos/ai-meeting/demo.css b/public/demos/ai-meeting/demo.css new file mode 100644 index 0000000..af91a13 --- /dev/null +++ b/public/demos/ai-meeting/demo.css @@ -0,0 +1,45 @@ +:root{--paper:#f3f2ec;--surface:#fbfaf6;--ink:#1b1e1a;--body:#4c4f48;--muted:#5f625b;--rule:#d6d7cf;--accent:#2f6fd6;--accent-ink:#2558ad;color-scheme:light} +*{box-sizing:border-box} +body{margin:0;background:var(--paper);color:var(--ink);font-family:system-ui,-apple-system,"Hiragino Sans","Noto Sans JP",sans-serif;line-height:1.7;-webkit-font-smoothing:antialiased} +.skip{position:absolute;left:-9999px}.skip:focus{left:16px;top:8px;z-index:9;background:#fff;padding:8px 12px} +.notice{margin:0;padding:8px 16px;background:var(--ink);color:var(--paper);font-size:13px;text-align:center} +.top{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px clamp(16px,3vw,40px);border-bottom:1px solid var(--rule)} +.brand{color:var(--ink);font-weight:700;text-decoration:none}.brand span{margin-left:8px;font-weight:500;font-size:13px;color:var(--muted)} +.back{color:var(--ink);font-size:14px;text-underline-offset:4px} +.stage{max-width:1180px;margin:0 auto;padding:clamp(24px,4vw,56px) clamp(16px,3vw,40px)} +h1{margin:0 0 20px;font-size:clamp(26px,3.4vw,44px);line-height:1.25;letter-spacing:-.03em} +.tabs{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 24px} +.tabs a{min-height:44px;display:inline-flex;align-items:center;padding:0 18px;border:1px solid var(--rule);border-radius:999px;color:var(--ink);text-decoration:none;font-size:14px;background:var(--surface)} +.tabs a[aria-current="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper)} +.board{display:grid;grid-template-columns:1fr 1fr;gap:20px} +.talk,.tasks{padding:20px;border:1px solid var(--rule);border-radius:18px;background:var(--surface);min-height:420px} +h2{margin:0 0 14px;font-size:13px;letter-spacing:.06em;color:var(--muted);font-weight:600} +.log{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-direction:column;gap:10px} +.msg{max-width:90%}.msg p{margin:2px 0 0;padding:10px 14px;border-radius:14px;background:#fff;border:1px solid var(--rule)} +.msg .who{font-size:12px;color:var(--muted)} +.msg--you{align-self:flex-end;text-align:right}.msg--you p{background:var(--ink);color:var(--paper);border-color:var(--ink);text-align:left} +.msg--meeting p{background:transparent;border-style:dashed} +.say-hint{margin:0 0 8px;font-size:13px;color:var(--muted)} +button{font:inherit;cursor:pointer;min-height:44px;border-radius:999px;padding:0 18px} +button:focus-visible,.tabs a:focus-visible,.back:focus-visible{outline:2px solid var(--accent-ink);outline-offset:3px} +.primary{background:var(--accent-ink);color:#fff;border:0;font-weight:600} +.primary:hover{background:#1d4a92} +.ghost{background:transparent;border:1px solid var(--rule);color:var(--ink)} +.speak{text-align:left;line-height:1.5;padding:10px 18px} +.list{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-direction:column;gap:8px} +.task{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;padding:12px 14px;border:1px solid var(--rule);border-radius:12px;background:#fff} +.task .dot{width:10px;height:10px;border-radius:50%;border:1.5px solid var(--muted)} +.task .when,.task .state{font-size:13px;color:var(--muted)} +.task.is-changed{border-color:var(--accent);background:#eef3fc}.task.is-changed .dot{background:var(--accent);border-color:var(--accent)}.task.is-changed .state{color:var(--accent-ink);font-weight:600} +.empty{color:var(--muted);font-size:14px} +.cards{display:flex;flex-direction:column;gap:10px} +.card{padding:14px 16px;border:1.5px solid var(--accent);border-radius:14px;background:#fff} +.card p{margin:0}.card-label{font-size:12px;font-weight:600;color:var(--accent-ink)}.card-main{font-weight:700} +.card-quote,.card-extra,.card-note{font-size:13px;color:var(--body)} +.card-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} +.card.is-done{border-color:var(--rule)}.card.is-dropped{opacity:.6;border-style:dashed} +.status{min-height:1.7em;margin:12px 0;font-size:14px;color:var(--accent-ink)} +.reset{background:transparent;border:1px solid var(--rule);color:var(--ink)} +.nojs{padding:16px;border:1px solid var(--rule);border-radius:12px;background:#fff} +@media (max-width:760px){.board{grid-template-columns:1fr}.talk,.tasks{min-height:0}} +@media (forced-colors:active){.task .dot,.primary{forced-color-adjust:auto}} diff --git a/public/demos/ai-meeting/demo.js b/public/demos/ai-meeting/demo.js new file mode 100644 index 0000000..f543eef --- /dev/null +++ b/public/demos/ai-meeting/demo.js @@ -0,0 +1,105 @@ +// AI Meeting concept demo. Fictional data; no microphone, model, storage or network. Wording follows the product's +// own UI (TaskList.tsx proposal card, CalendarActions.tsx states, MeetingMemory.ts candidates). +const $ = s => document.querySelector(s); +const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; }; +const button = (label, cls, on) => { const b = el('button', cls, label); b.type = 'button'; b.addEventListener('click', on); return b; }; +const wait = ms => new Promise(r => setTimeout(r, matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : ms)); + +const FLOWS = { + reschedule: { + title: 'やること', + tasks: [['メール返信', '今日', '未完了'], ['資料確認', '今日', '未完了']], + say: '資料確認は今日中に。メール返信は明日に。', + reply: 'メール返信を明日に延期する変更案を作りました。まだ反映していません。', + cards: [{label: '確認待ちの変更案', main: 'メール返信 今日 → 明日', quote: 'メール返信は明日に', ok: 'この変更を反映', + apply: list => list.map(t => t[0] === 'メール返信' ? ['メール返信', '明日', '延期'] : t), done: '反映しました。次に開いたときも、ここから続きます。'}] + }, + calendar: { + title: 'やること', + tasks: [['企画レビュー', '金曜 15:00', '未完了']], + say: 'これ、金曜の15時に入れておいて。', + reply: '登録案を作りました。まだ送信しません。', + cards: [{label: '登録案(まだ送信しません)', main: '企画レビュー 金曜 15:00–15:30', quote: '金曜の15時に入れておいて', extra: '招待・メール:送りません', ok: '承認して登録', pending: '実行中・未確認', + apply: list => list.map(t => t[0] === '企画レビュー' ? ['企画レビュー', '金曜 15:00', '登録を確認済み'] : t), done: '登録を確認済み(模擬)。招待やメールは送っていません。'}] + }, + team: { + title: 'チームのタスク', + tasks: [], + meeting: ['見積もりは来週までに出しましょう。', 'デザイン案はAとBで比べたいですね。', 'じゃあ、今日はここまで。'], + say: '会議の決定を、タスク候補にして。', + reply: '発言から候補を2件作りました。担当と期限は、言われていなければ「不明」です。', + cards: [ + {label: 'タスク候補(未確認)', main: '見積もりを出す 期限:来週 · 担当:不明', quote: '見積もりは来週までに出しましょう', ok: '確認して共有', + apply: list => [...list, ['見積もりを出す', '来週', '担当:未定']], done: '共有しました。担当は、決まったら追記できます。'}, + {label: 'タスク候補(未確認)', main: 'デザイン案AとBを比べる 期限:不明 · 担当:不明', quote: 'デザイン案はAとBで比べたいですね', ok: '確認して共有', + apply: list => [...list, ['デザイン案AとBを比べる', '期限なし', '担当:未定']], done: '共有しました。'} + ] + } +}; + +let state; +function render() { + const f = FLOWS[state.flow]; + $('[data-tasks-title]').textContent = f.title; + const list = $('[data-list]'); list.replaceChildren(); + if (!state.tasks.length) list.append(el('li', 'empty', 'まだありません')); + for (const [name, when, st] of state.tasks) { + const li = el('li', `task${st === '未完了' ? '' : ' is-changed'}`); + li.append(el('span', 'dot'), el('span', 'name', name), el('span', 'when', when), el('span', 'state', st)); + list.append(li); + } +} + +function addLog(who, text) { + const li = el('li', `msg msg--${who}`); + li.append(el('span', 'who', who === 'you' ? 'あなた' : who === 'ai' ? 'AI Meeting' : '会議'), el('p', null, text)); + $('[data-log]').append(li); +} + +async function speak() { + const f = FLOWS[state.flow]; + $('[data-say]').replaceChildren(); + addLog('you', f.say); + await wait(500); + addLog('ai', f.reply); + await wait(300); + const cards = $('[data-cards]'); + f.cards.forEach((c, i) => { + const card = el('article', 'card'); + card.append(el('p', 'card-label', c.label), el('p', 'card-main', c.main), el('p', 'card-quote', `「${c.quote}」`)); + if (c.extra) card.append(el('p', 'card-extra', c.extra)); + card.append(el('p', 'card-note', 'まだ反映していません。この内容でよいですか?')); + const actions = el('div', 'card-actions'); + actions.append(button(c.ok, 'primary', async () => { + actions.replaceChildren(); + if (c.pending) { card.querySelector('.card-note').textContent = c.pending; await wait(900); } + state.tasks = c.apply(state.tasks); render(); + card.classList.add('is-done'); card.querySelector('.card-note').textContent = c.pending ? '登録済み(模擬)' : '反映済み'; + $('[data-status]').textContent = c.done; + }), button('取り消す', 'ghost', () => { + actions.replaceChildren(); card.classList.add('is-dropped'); + card.querySelector('.card-note').textContent = '取り消しました。やることは元のままです。'; + $('[data-status]').textContent = '取り消しました。'; + })); + card.append(actions); + cards.append(card); + if (i === 0) actions.querySelector('button').focus(); + }); +} + +function start(flow) { + const f = FLOWS[flow] ? flow : 'reschedule'; + state = {flow: f, tasks: FLOWS[f].tasks.map(t => [...t])}; + document.querySelectorAll('[data-flow]').forEach(a => a.setAttribute('aria-current', String(a.dataset.flow === f))); + $('[data-log]').replaceChildren(); $('[data-cards]').replaceChildren(); $('[data-status]').textContent = ''; + (FLOWS[f].meeting || []).forEach(line => addLog('meeting', line)); + render(); + const say = $('[data-say]'); say.replaceChildren(); + say.append(el('p', 'say-hint', 'ボタンを押すと、この言葉を話したことにします。'), button(`話す:「${FLOWS[f].say}」`, 'primary speak', speak)); +} + +document.querySelectorAll('[data-flow]').forEach(a => a.addEventListener('click', e => { + e.preventDefault(); history.replaceState(null, '', `?flow=${a.dataset.flow}`); start(a.dataset.flow); +})); +$('[data-reset]').addEventListener('click', () => start(state.flow)); +start(new URLSearchParams(location.search).get('flow')); diff --git a/public/demos/ai-meeting/index.html b/public/demos/ai-meeting/index.html new file mode 100644 index 0000000..51be855 --- /dev/null +++ b/public/demos/ai-meeting/index.html @@ -0,0 +1,41 @@ + + + + + + 話して、決まったことだけ残す。 — AI Meeting 構想デモ + + + + + + + + +

構想デモ・架空データ。音声・AI・外部への送信はありません。

+
AI Meeting 構想デモ製品ページへ戻る
+
+

話して、決まったことだけ残す。

+ +
+
+

会話

+
    +
    +
    +
    +

    やること

    +
      +
      +

      + +
      +
      + +
      + + diff --git a/src/redesign.mjs b/src/redesign.mjs index 7fff3d1..c240cad 100644 --- a/src/redesign.mjs +++ b/src/redesign.mjs @@ -27,6 +27,7 @@ import {capabilitiesCheckedAt} from './products.mjs'; import {productNavigation} from './site-experience.mjs'; import {recordings} from './films.mjs'; import {oathraExperienceFilm} from './oathra-experience-films.mjs'; +import {visionSections} from './vision-pages.mjs'; const {checkedAt: sourcesCheckedAt} = JSON.parse(await fs.readFile(new URL('../site.config.json', import.meta.url), 'utf8')); @@ -330,6 +331,7 @@ function productMain(p, list, lang, {kept, figure, source, filmAnchor}) {

      ${esc(r.whatItDoes)}

        ${outcome}
      ${publicViewing(p, lang)} ${oathraExperienceFilm(p, lang)} +${visionSections(p, lang)}
      ${figure ? `
      ${figure}
      ` : filmAnchor ? `${esc(r.filmBelow)} ` : `
      ${video({...film, label: `${name} — ${kind}`}, lang)}
      `}
      ${esc(r.film(name))}${figure && film.kind === 'recording' ? (note ? `${esc(note)}` : '') : `${esc(filmAnchor ? r.stillKind : kind)}${figure || filmAnchor ? '' : ` ${esc(note)}`}${extra}`}
      ${showHighlights ? `

      ${esc(r.highlights)}

      ${highlights}
      ` : ''} ${kept.length ? `

      ${esc(r.detailKicker)}

      ${openKept}
      ` : ''}${foldKept ? `
      ${esc(r.detailMore)}${foldKept}
      ` : ''} diff --git a/src/redesign/redesign.css b/src/redesign/redesign.css index 3924a31..c5f5704 100644 --- a/src/redesign/redesign.css +++ b/src/redesign/redesign.css @@ -412,3 +412,62 @@ body[data-rd] .rd-noa-viewing h2{margin:0;font-family:var(--rd-jp);font-size:cla body[data-rd] .rd-noa-viewing>p:not(.rd-kicker){margin:0;max-width:60em;font-size:14px;line-height:1.9;color:var(--rd-muted)} body[data-rd] .rd-noa-viewing .rd-link-kind{width:170px;opacity:1;color:inherit;line-height:1.8} body[data-rd] .rd-noa-viewing .rd-noa-about{width:fit-content;min-height:44px;display:inline-flex;align-items:center;gap:12px;color:var(--rd-ink);text-decoration:underline;text-underline-offset:5px} +/* Vision sections (src/vision-pages.mjs): three concept demos as moving screens, then one moving diagram. The finished + state is the default; the steps only animate while JavaScript marks the scene as in view, and never with reduced motion. */ +body[data-rd] .vp-demos,body[data-rd] .vp-mech{padding:clamp(72px,9vw,140px) var(--rd-gutter);border-top:1px solid var(--rd-rule);scroll-margin-top:100px} +body[data-rd] .vp-head{display:grid;grid-template-columns:1fr 1fr;gap:20px 6vw;align-items:end;margin-bottom:clamp(32px,4vw,56px)} +body[data-rd] .vp-head h2{margin:12px 0 0;font-family:var(--rd-jp);font-weight:700;font-size:clamp(30px,3.6vw,56px);line-height:1.2;letter-spacing:-.04em;color:var(--rd-ink)} +body[data-rd] .vp-head>p{margin:0;max-width:34em;font-size:16px;line-height:1.9;color:var(--rd-body)} +body[data-rd] .vp-list{display:grid;gap:0} +body[data-rd] .vp-demo{display:grid;grid-template-columns:5fr 7fr;gap:28px 6vw;align-items:center;padding:clamp(32px,4vw,56px) 0;border-top:1px solid var(--rd-rule)} +body[data-rd] .vp-index{margin:0 0 14px;font-family:var(--rd-mono);font-size:12px;letter-spacing:.06em;color:var(--rd-accent-ink)} +body[data-rd] .vp-demo h3{margin:0 0 12px;font-family:var(--rd-jp);font-weight:700;font-size:clamp(22px,2.2vw,32px);line-height:1.35;letter-spacing:-.03em;color:var(--rd-ink)} +body[data-rd] .vp-demo-copy>p:not(.vp-index){margin:0 0 22px;max-width:28em;font-size:15px;line-height:1.9;color:var(--rd-body)} +body[data-rd] .vp-try{display:inline-flex;align-items:center;gap:10px;min-height:44px;padding:0 22px;border-radius:999px;background:var(--rd-ink);color:var(--rd-paper);font-size:14px;font-weight:600;text-decoration:none;transition:background .2s} +body[data-rd] .vp-try:hover{background:var(--rd-accent-ink)} +body[data-rd] .vp-try:focus-visible{outline:2px solid var(--rd-accent-ink);outline-offset:3px} +body[data-rd] .vp-screen{margin:0;padding:18px;border:1px solid var(--rd-rule);border-radius:20px;background:#fbfaf6;box-shadow:0 24px 60px -36px rgba(27,30,26,.35)} +body[data-rd] .vp-screen-bar{display:flex;justify-content:space-between;gap:12px;margin:0 0 14px;font-family:var(--rd-mono);font-size:11px;letter-spacing:.04em;color:var(--rd-muted)} +body[data-rd] .vp-screen-bar span:last-child{padding:2px 8px;border:1px solid var(--rd-rule);border-radius:999px} +body[data-rd] .vp-rows{display:flex;flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;min-height:300px} +body[data-rd] .vp-row{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--rd-rule);border-radius:12px;background:#fff;font-size:15px;line-height:1.5;color:var(--rd-ink)} +body[data-rd] .vp-row small{margin-left:auto;font-size:12px;color:var(--rd-muted);white-space:nowrap} +body[data-rd] .vp-check{flex:none;width:16px;height:16px;border:1.5px solid var(--rd-muted);border-radius:50%;display:grid;place-items:center;font-size:10px;line-height:1;color:#fff} +body[data-rd] .vp-check.is-on{border-color:var(--rd-accent);background:var(--rd-accent)} +body[data-rd] .vp-check.is-on::after{content:"✓"} +body[data-rd] .vp-row--say{align-self:flex-end;max-width:88%;border:0;border-radius:16px 16px 4px 16px;background:var(--rd-ink);color:var(--rd-paper)} +body[data-rd] .vp-row--say q{quotes:"「" "」"} +html[lang="en"] body[data-rd] .vp-row--say q{quotes:"“" "”"} +body[data-rd] .vp-mic{flex:none;width:8px;height:8px;border-radius:50%;background:var(--rd-accent)} +body[data-rd] .vp-row--card{flex-direction:column;align-items:flex-start;gap:4px;border:1.5px solid var(--rd-accent)} +body[data-rd] .vp-row--card small{margin:0;font-size:12px;font-weight:600;color:var(--rd-accent-ink)} +body[data-rd] .vp-row--card b{font-weight:700} +body[data-rd] .vp-row--card span:not(.vp-btn){font-size:13px;color:var(--rd-body)} +body[data-rd] .vp-btn{margin-top:8px;padding:6px 14px;border-radius:999px;background:var(--rd-accent-ink);color:#fff;font-size:13px;font-weight:600} +body[data-rd] .vp-row--done{border-color:var(--rd-accent);background:#fff;background:color-mix(in srgb,var(--rd-accent) 8%,#fff)} +body[data-rd] .vp-row--done small{color:var(--rd-accent-ink);font-weight:600} +body[data-rd] .vp-flow{display:grid;grid-template-columns:repeat(5,1fr);gap:0;margin:0;padding:0;list-style:none;counter-reset:vp} +body[data-rd] .vp-node{position:relative;display:flex;flex-direction:column;gap:8px;padding:22px 24px 0 0;border-top:2px solid var(--rd-accent)} +body[data-rd] .vp-node::before{content:"";position:absolute;top:-7px;left:0;width:12px;height:12px;border-radius:50%;background:var(--rd-accent)} +body[data-rd] .vp-node-n{font-family:var(--rd-mono);font-size:12px;color:var(--rd-muted)} +body[data-rd] .vp-node b{font-family:var(--rd-jp);font-size:clamp(20px,2vw,28px);letter-spacing:-.03em;color:var(--rd-ink)} +body[data-rd] .vp-node span:last-child{font-size:14px;line-height:1.7;color:var(--rd-body)} +@keyframes vp-s1{0%,2%{opacity:0;transform:translateY(10px)}8%,88%{opacity:1;transform:none}96%,100%{opacity:0}} +@keyframes vp-s2{0%,14%{opacity:0;transform:translateY(10px)}20%,88%{opacity:1;transform:none}96%,100%{opacity:0}} +@keyframes vp-s3{0%,28%{opacity:0;transform:translateY(10px)}34%,88%{opacity:1;transform:none}96%,100%{opacity:0}} +@keyframes vp-s4{0%,44%{opacity:0;transform:translateY(10px)}50%,88%{opacity:1;transform:none}96%,100%{opacity:0}} +@keyframes vp-s5{0%,60%{opacity:0;transform:translateY(10px)}66%,88%{opacity:1;transform:none}96%,100%{opacity:0}} +@keyframes vp-node{0%,100%{opacity:.28}} +body[data-rd] [data-vp-play] .vp-step--1{animation:vp-s1 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-step--2{animation:vp-s2 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-step--3{animation:vp-s3 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-step--4{animation:vp-s4 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-step--5{animation:vp-s5 11s var(--rd-ease) infinite both} +body[data-rd] .vp-flow[data-vp-play] .vp-node--1{animation:vp-s1 9s var(--rd-ease) infinite both} +body[data-rd] .vp-flow[data-vp-play] .vp-node--2{animation:vp-s2 9s var(--rd-ease) infinite both} +body[data-rd] .vp-flow[data-vp-play] .vp-node--3{animation:vp-s3 9s var(--rd-ease) infinite both} +body[data-rd] .vp-flow[data-vp-play] .vp-node--4{animation:vp-s4 9s var(--rd-ease) infinite both} +body[data-rd] .vp-flow[data-vp-play] .vp-node--5{animation:vp-s5 9s var(--rd-ease) infinite both} +@media (max-width:860px){body[data-rd] .vp-head,body[data-rd] .vp-demo{grid-template-columns:1fr}body[data-rd] .vp-flow{grid-template-columns:1fr;gap:28px}body[data-rd] .vp-node{padding-right:0}body[data-rd] .vp-rows{min-height:0}} +@media (prefers-reduced-motion:reduce){body[data-rd] [data-vp-play] *{animation:none!important}} +@media (forced-colors:active){body[data-rd] .vp-check.is-on,body[data-rd] .vp-node::before,body[data-rd] .vp-mic{forced-color-adjust:none;background:CanvasText}} diff --git a/src/vision-pages.mjs b/src/vision-pages.mjs new file mode 100644 index 0000000..af6825a --- /dev/null +++ b/src/vision-pages.mjs @@ -0,0 +1,68 @@ +/** + * Vision sections for product pages, following the Oathra page: three use-case concept demos you can try, then one + * moving diagram of the idea the product is built on. The current state stays further down the page (recording, + * status, scope), so these sections speak to what the product is for. + * + * Each scene is drawn in HTML and animated with CSS only when JavaScript marks it as in view; without JavaScript, + * or with reduced motion, the finished state is shown. The scenes and the demo pages use the product's own words + * and states, taken from its repository (cited per product below). Data is fictional; the one label says so. + */ +const esc = s => String(s).replace(/[&<>"']/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c])); + +// AI Meeting (FORIFOR/AI-meeting origin/main): README.ja.md「会話で決めたTODOを、次回へ残す。」and its spoken example; +// TaskList.tsx proposal card wording; CalendarActions.tsx draft → approve → confirmed flow; MeetingMemory.ts +// (quotes are unconfirmed candidates, owner and due date unknown unless quoted); tasks.ts (never add by guessing). +export const VISION = Object.freeze({ + 'ai-meeting': { + demo: '/demos/ai-meeting/', + ja: { + kicker: '用途別の構想デモ', title: '話して、
      決まったことだけ残す。', lead: '言い直しても、予定にしても、チームで決めても。確かめてから残ります。', + label: '構想デモ・架空データ', tryLabel: '試す', uiNote: '', + demos: [ + {id: 'reschedule', kind: '言い直し', title: '「やっぱり、明日にして。」', body: '話しながら言い直しても、反映するのは確かめてから。', + scene: [['task', 'メール返信', '今日'], ['task', '資料確認', '今日'], ['say', '資料確認は今日中に。メール返信は明日に。'], ['card', '確認待ちの変更案', 'メール返信 今日 → 明日', '「メール返信は明日に」', 'この変更を反映'], ['done', 'メール返信', '明日 · 延期']]}, + {id: 'calendar', kind: '会話から予定へ', title: '決まったことを、予定に。', body: '登録案を作り、承認したときだけ登録。招待やメールは送りません。', + scene: [['task', '企画レビュー', '金曜 15:00'], ['say', 'これ、金曜の15時に入れておいて。'], ['card', '登録案(まだ送信しません)', '企画レビュー 金曜 15:00–15:30', '招待・メール:送りません', '承認して登録'], ['done', '企画レビュー', '登録を確認済み']]}, + {id: 'team', kind: 'チームで', title: '会議の決定を、チームのタスクに。', body: '発言の引用つきで候補に。担当と期限は、言われていなければ「不明」のまま。', + scene: [['say', '見積もりは来週までに出しましょう。'], ['say', 'デザイン案はAとBで比べたいですね。'], ['card', 'タスク候補(未確認)', '見積もりを出す 期限:来週 · 担当:不明', '「見積もりは来週までに」', '確認して共有'], ['done', '見積もりを出す', 'チームで共有 · 担当は未定']]} + ], + mechKicker: 'しくみ', mechTitle: '原文が、根拠になる。', mechLead: '推測では足さない。あなたの言葉を引用した変更案だけを、確かめてから残します。', + mech: [['話す', 'いつもの言葉で'], ['引用する', '発言の原文をそのまま'], ['保留する', 'まだ反映していません'], ['確かめる', '反映か、取り消すか'], ['次回へ', '決まったことだけ']] + }, + en: { + kicker: 'Concept demos by use', title: 'Talk it through.
      Keep only what was decided.', lead: 'Changing your mind, booking it, or deciding as a team: it is kept only after you confirm.', + label: 'Concept demo · fictional data', tryLabel: 'Try it', uiNote: 'The demos run in Japanese.', + demos: [ + {id: 'reschedule', kind: 'Changing your mind', title: '“Actually, make it tomorrow.”', body: 'Rephrase as you talk; nothing changes until you confirm.', + scene: [['task', 'Reply to email', 'Today'], ['task', 'Review the deck', 'Today'], ['say', 'Review the deck today. Reply to the email tomorrow.'], ['card', 'Change waiting for you', 'Reply to email today → tomorrow', '“reply to the email tomorrow”', 'Apply this change'], ['done', 'Reply to email', 'Tomorrow · postponed']]}, + {id: 'calendar', kind: 'From talk to calendar', title: 'Put what was decided on the calendar.', body: 'A draft first, booked only when you approve. No invitations or emails are sent.', + scene: [['task', 'Plan review', 'Fri 15:00'], ['say', 'Put this in for Friday at three.'], ['card', 'Draft (not sent yet)', 'Plan review Fri 15:00–15:30', 'Invitations and email: none', 'Approve and book'], ['done', 'Plan review', 'Booking confirmed']]}, + {id: 'team', kind: 'As a team', title: 'Turn a meeting’s decisions into team tasks.', body: 'Candidates carry the quote. Owner and due date stay “unknown” unless someone said them.', + scene: [['say', 'Let’s send the estimate by next week.'], ['say', 'I’d like to compare designs A and B.'], ['card', 'Task candidate (unconfirmed)', 'Send the estimate due next week · owner unknown', '“send the estimate by next week”', 'Confirm and share'], ['done', 'Send the estimate', 'Shared with the team · owner open']]} + ], + mechKicker: 'How it works', mechTitle: 'Your words are the evidence.', mechLead: 'Nothing is added by guessing. Only changes that quote you are kept, after you confirm.', + mech: [['Talk', 'In your own words'], ['Quote', 'The exact words, kept'], ['Hold', 'Not applied yet'], ['Confirm', 'Apply or discard'], ['Next time', 'Only what was decided']] + } + } +}); + +function sceneHtml(scene) { + return scene.map(([kind, a, b, c, d], i) => { + const step = ` vp-step vp-step--${i + 1}`; + if (kind === 'task') return `
    • ${esc(a)}${esc(b)}
    • `; + if (kind === 'say') return `
    • ${esc(a)}
    • `; + if (kind === 'card') return `
    • ${esc(a)}${esc(b)}${esc(c)}
    • `; + return `
    • ${esc(a)}${esc(b)}
    • `; + }).join(''); +} + +export function visionSections(p, lang) { + const v = VISION[p.id]; + if (!v) return ''; + const t = v[lang]; + if (!t || t.demos.length !== 3 || t.mech.length !== 5) throw new Error(`Vision sections: incomplete ${p.id}/${lang}`); + const demos = t.demos.map((d, i) => `

      0${i + 1} / ${esc(d.kind)}

      ${esc(d.title)}

      ${esc(d.body)}

      ${esc(t.tryLabel)}

      ${esc(p.name)}${esc(t.label)}

        ${sceneHtml(d.scene)}
      `).join(''); + const mech = t.mech.map(([k, b], i) => `
    • 0${i + 1}${esc(k)}${esc(b)}
    • `).join(''); + return `

      ${esc(t.kicker)}

      ${t.title}

      ${esc(t.lead)}${t.uiNote ? ` ${esc(t.uiNote)}` : ''}

      ${demos}
      ` + + `

      ${esc(t.mechKicker)}

      ${esc(t.mechTitle)}

      ${esc(t.mechLead)}

        ${mech}
      `; +} From 3dba52d5ec52d01a88a2d09be0fdd038a6ffaaaf Mon Sep 17 00:00:00 2001 From: FORIFOR Date: Mon, 19 Oct 2026 22:42:51 +0900 Subject: [PATCH 2/5] fix: vision scenes change rows in place, rephrasing reads as rephrasing Review findings: the finished state no longer shows an item twice (a row switches from its old to its new value, and the card's button becomes its result), the first scene is an actual change of mind, each try button names what it opens, the demo page title and notice wrap at phrase boundaries, and the gap before the diagram is shorter. No horizontal overflow at 320 px. Co-Authored-By: Claude Opus 5.5 --- public/demos/ai-meeting/demo.css | 6 ++--- public/demos/ai-meeting/demo.js | 6 ++--- src/redesign/redesign.css | 36 ++++++++++++++++++++------ src/vision-pages.mjs | 44 ++++++++++++++++++-------------- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/public/demos/ai-meeting/demo.css b/public/demos/ai-meeting/demo.css index af91a13..f733281 100644 --- a/public/demos/ai-meeting/demo.css +++ b/public/demos/ai-meeting/demo.css @@ -2,12 +2,12 @@ *{box-sizing:border-box} body{margin:0;background:var(--paper);color:var(--ink);font-family:system-ui,-apple-system,"Hiragino Sans","Noto Sans JP",sans-serif;line-height:1.7;-webkit-font-smoothing:antialiased} .skip{position:absolute;left:-9999px}.skip:focus{left:16px;top:8px;z-index:9;background:#fff;padding:8px 12px} -.notice{margin:0;padding:8px 16px;background:var(--ink);color:var(--paper);font-size:13px;text-align:center} +.notice{word-break:keep-all;overflow-wrap:anywhere;margin:0;padding:8px 16px;background:var(--ink);color:var(--paper);font-size:13px;text-align:center} .top{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px clamp(16px,3vw,40px);border-bottom:1px solid var(--rule)} .brand{color:var(--ink);font-weight:700;text-decoration:none}.brand span{margin-left:8px;font-weight:500;font-size:13px;color:var(--muted)} .back{color:var(--ink);font-size:14px;text-underline-offset:4px} .stage{max-width:1180px;margin:0 auto;padding:clamp(24px,4vw,56px) clamp(16px,3vw,40px)} -h1{margin:0 0 20px;font-size:clamp(26px,3.4vw,44px);line-height:1.25;letter-spacing:-.03em} +h1{word-break:keep-all;overflow-wrap:anywhere;margin:0 0 20px;font-size:clamp(26px,3.4vw,44px);line-height:1.25;letter-spacing:-.03em} .tabs{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 24px} .tabs a{min-height:44px;display:inline-flex;align-items:center;padding:0 18px;border:1px solid var(--rule);border-radius:999px;color:var(--ink);text-decoration:none;font-size:14px;background:var(--surface)} .tabs a[aria-current="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper)} @@ -25,7 +25,7 @@ button:focus-visible,.tabs a:focus-visible,.back:focus-visible{outline:2px solid .primary{background:var(--accent-ink);color:#fff;border:0;font-weight:600} .primary:hover{background:#1d4a92} .ghost{background:transparent;border:1px solid var(--rule);color:var(--ink)} -.speak{text-align:left;line-height:1.5;padding:10px 18px} +.speak{text-align:left;line-height:1.5;padding:10px 18px;border-radius:18px} .list{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-direction:column;gap:8px} .task{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;padding:12px 14px;border:1px solid var(--rule);border-radius:12px;background:#fff} .task .dot{width:10px;height:10px;border-radius:50%;border:1.5px solid var(--muted)} diff --git a/public/demos/ai-meeting/demo.js b/public/demos/ai-meeting/demo.js index f543eef..ab861d4 100644 --- a/public/demos/ai-meeting/demo.js +++ b/public/demos/ai-meeting/demo.js @@ -9,14 +9,14 @@ const FLOWS = { reschedule: { title: 'やること', tasks: [['メール返信', '今日', '未完了'], ['資料確認', '今日', '未完了']], - say: '資料確認は今日中に。メール返信は明日に。', + say: 'メール返信は今日中に……いや、やっぱり明日にして。', reply: 'メール返信を明日に延期する変更案を作りました。まだ反映していません。', - cards: [{label: '確認待ちの変更案', main: 'メール返信 今日 → 明日', quote: 'メール返信は明日に', ok: 'この変更を反映', + cards: [{label: '確認待ちの変更案', main: 'メール返信 今日 → 明日', quote: 'やっぱり明日にして', ok: 'この変更を反映', apply: list => list.map(t => t[0] === 'メール返信' ? ['メール返信', '明日', '延期'] : t), done: '反映しました。次に開いたときも、ここから続きます。'}] }, calendar: { title: 'やること', - tasks: [['企画レビュー', '金曜 15:00', '未完了']], + tasks: [['企画レビュー', '日時は未定', '未完了']], say: 'これ、金曜の15時に入れておいて。', reply: '登録案を作りました。まだ送信しません。', cards: [{label: '登録案(まだ送信しません)', main: '企画レビュー 金曜 15:00–15:30', quote: '金曜の15時に入れておいて', extra: '招待・メール:送りません', ok: '承認して登録', pending: '実行中・未確認', diff --git a/src/redesign/redesign.css b/src/redesign/redesign.css index c5f5704..f2dc44b 100644 --- a/src/redesign/redesign.css +++ b/src/redesign/redesign.css @@ -415,6 +415,7 @@ body[data-rd] .rd-noa-viewing .rd-noa-about{width:fit-content;min-height:44px;di /* Vision sections (src/vision-pages.mjs): three concept demos as moving screens, then one moving diagram. The finished state is the default; the steps only animate while JavaScript marks the scene as in view, and never with reduced motion. */ body[data-rd] .vp-demos,body[data-rd] .vp-mech{padding:clamp(72px,9vw,140px) var(--rd-gutter);border-top:1px solid var(--rd-rule);scroll-margin-top:100px} +body[data-rd] .vp-demos{padding-bottom:clamp(32px,4vw,64px)} body[data-rd] .vp-head{display:grid;grid-template-columns:1fr 1fr;gap:20px 6vw;align-items:end;margin-bottom:clamp(32px,4vw,56px)} body[data-rd] .vp-head h2{margin:12px 0 0;font-family:var(--rd-jp);font-weight:700;font-size:clamp(30px,3.6vw,56px);line-height:1.2;letter-spacing:-.04em;color:var(--rd-ink)} body[data-rd] .vp-head>p{margin:0;max-width:34em;font-size:16px;line-height:1.9;color:var(--rd-body)} @@ -431,10 +432,18 @@ body[data-rd] .vp-screen-bar{display:flex;justify-content:space-between;gap:12px body[data-rd] .vp-screen-bar span:last-child{padding:2px 8px;border:1px solid var(--rd-rule);border-radius:999px} body[data-rd] .vp-rows{display:flex;flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;min-height:300px} body[data-rd] .vp-row{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--rd-rule);border-radius:12px;background:#fff;font-size:15px;line-height:1.5;color:var(--rd-ink)} -body[data-rd] .vp-row small{margin-left:auto;font-size:12px;color:var(--rd-muted);white-space:nowrap} -body[data-rd] .vp-check{flex:none;width:16px;height:16px;border:1.5px solid var(--rd-muted);border-radius:50%;display:grid;place-items:center;font-size:10px;line-height:1;color:#fff} -body[data-rd] .vp-check.is-on{border-color:var(--rd-accent);background:var(--rd-accent)} -body[data-rd] .vp-check.is-on::after{content:"✓"} +body[data-rd] .vp-row small{margin-left:auto;font-size:12px;color:var(--rd-muted);text-align:right} +body[data-rd] .vp-row>span:not([class]){min-width:0;overflow-wrap:anywhere} +body[data-rd] .vp-check{position:relative;flex:none;width:16px;height:16px;border:1.5px solid var(--rd-muted);border-radius:50%} +body[data-rd] .vp-tick{position:absolute;inset:-1.5px;display:grid;place-items:center;border-radius:50%;background:var(--rd-accent);font-size:10px;font-style:normal;line-height:1;color:#fff} +body[data-rd] .vp-tick::after{content:"✓"} +body[data-rd] .vp-swap{display:grid;margin-left:auto;text-align:right} +body[data-rd] .vp-swap>*{grid-area:1/1} +body[data-rd] .vp-swap .vp-now{color:var(--rd-accent-ink);font-weight:600} +body[data-rd] .vp-swap--action{margin:8px 0 0;text-align:left;justify-items:start} +body[data-rd] .vp-applied{align-self:center;font-size:13px;font-weight:600;color:var(--rd-accent-ink)} +body[data-rd] .vp-applied::before{content:"✓ "} +body[data-rd] [class*="vp-out--"]{opacity:0} body[data-rd] .vp-row--say{align-self:flex-end;max-width:88%;border:0;border-radius:16px 16px 4px 16px;background:var(--rd-ink);color:var(--rd-paper)} body[data-rd] .vp-row--say q{quotes:"「" "」"} html[lang="en"] body[data-rd] .vp-row--say q{quotes:"“" "”"} @@ -443,9 +452,9 @@ body[data-rd] .vp-row--card{flex-direction:column;align-items:flex-start;gap:4px body[data-rd] .vp-row--card small{margin:0;font-size:12px;font-weight:600;color:var(--rd-accent-ink)} body[data-rd] .vp-row--card b{font-weight:700} body[data-rd] .vp-row--card span:not(.vp-btn){font-size:13px;color:var(--rd-body)} -body[data-rd] .vp-btn{margin-top:8px;padding:6px 14px;border-radius:999px;background:var(--rd-accent-ink);color:#fff;font-size:13px;font-weight:600} -body[data-rd] .vp-row--done{border-color:var(--rd-accent);background:#fff;background:color-mix(in srgb,var(--rd-accent) 8%,#fff)} -body[data-rd] .vp-row--done small{color:var(--rd-accent-ink);font-weight:600} +body[data-rd] .vp-btn{padding:6px 14px;border-radius:999px;background:var(--rd-accent-ink);color:#fff;font-size:13px;font-weight:600} +body[data-rd] .vp-row--new{border-color:var(--rd-accent);background:#fff;background:color-mix(in srgb,var(--rd-accent) 8%,#fff)} +body[data-rd] .vp-row--new small{color:var(--rd-accent-ink);font-weight:600} body[data-rd] .vp-flow{display:grid;grid-template-columns:repeat(5,1fr);gap:0;margin:0;padding:0;list-style:none;counter-reset:vp} body[data-rd] .vp-node{position:relative;display:flex;flex-direction:column;gap:8px;padding:22px 24px 0 0;border-top:2px solid var(--rd-accent)} body[data-rd] .vp-node::before{content:"";position:absolute;top:-7px;left:0;width:12px;height:12px;border-radius:50%;background:var(--rd-accent)} @@ -457,12 +466,23 @@ body[data-rd] .vp-node span:last-child{font-size:14px;line-height:1.7;color:var( @keyframes vp-s3{0%,28%{opacity:0;transform:translateY(10px)}34%,88%{opacity:1;transform:none}96%,100%{opacity:0}} @keyframes vp-s4{0%,44%{opacity:0;transform:translateY(10px)}50%,88%{opacity:1;transform:none}96%,100%{opacity:0}} @keyframes vp-s5{0%,60%{opacity:0;transform:translateY(10px)}66%,88%{opacity:1;transform:none}96%,100%{opacity:0}} -@keyframes vp-node{0%,100%{opacity:.28}} +@keyframes vp-o2{0%,14%{opacity:1}20%,100%{opacity:0}} +@keyframes vp-o3{0%,28%{opacity:1}34%,100%{opacity:0}} +@keyframes vp-o4{0%,44%{opacity:1}50%,100%{opacity:0}} +@keyframes vp-o5{0%,60%{opacity:1}66%,100%{opacity:0}} body[data-rd] [data-vp-play] .vp-step--1{animation:vp-s1 11s var(--rd-ease) infinite both} body[data-rd] [data-vp-play] .vp-step--2{animation:vp-s2 11s var(--rd-ease) infinite both} body[data-rd] [data-vp-play] .vp-step--3{animation:vp-s3 11s var(--rd-ease) infinite both} body[data-rd] [data-vp-play] .vp-step--4{animation:vp-s4 11s var(--rd-ease) infinite both} body[data-rd] [data-vp-play] .vp-step--5{animation:vp-s5 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-out--2{animation:vp-o2 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-in--2{animation:vp-s2 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-out--3{animation:vp-o3 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-in--3{animation:vp-s3 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-out--4{animation:vp-o4 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-in--4{animation:vp-s4 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-out--5{animation:vp-o5 11s var(--rd-ease) infinite both} +body[data-rd] [data-vp-play] .vp-in--5{animation:vp-s5 11s var(--rd-ease) infinite both} body[data-rd] .vp-flow[data-vp-play] .vp-node--1{animation:vp-s1 9s var(--rd-ease) infinite both} body[data-rd] .vp-flow[data-vp-play] .vp-node--2{animation:vp-s2 9s var(--rd-ease) infinite both} body[data-rd] .vp-flow[data-vp-play] .vp-node--3{animation:vp-s3 9s var(--rd-ease) infinite both} diff --git a/src/vision-pages.mjs b/src/vision-pages.mjs index af6825a..e8f475f 100644 --- a/src/vision-pages.mjs +++ b/src/vision-pages.mjs @@ -17,28 +17,28 @@ export const VISION = Object.freeze({ demo: '/demos/ai-meeting/', ja: { kicker: '用途別の構想デモ', title: '話して、
      決まったことだけ残す。', lead: '言い直しても、予定にしても、チームで決めても。確かめてから残ります。', - label: '構想デモ・架空データ', tryLabel: '試す', uiNote: '', + label: '構想デモ・架空データ', uiNote: '', demos: [ - {id: 'reschedule', kind: '言い直し', title: '「やっぱり、明日にして。」', body: '話しながら言い直しても、反映するのは確かめてから。', - scene: [['task', 'メール返信', '今日'], ['task', '資料確認', '今日'], ['say', '資料確認は今日中に。メール返信は明日に。'], ['card', '確認待ちの変更案', 'メール返信 今日 → 明日', '「メール返信は明日に」', 'この変更を反映'], ['done', 'メール返信', '明日 · 延期']]}, - {id: 'calendar', kind: '会話から予定へ', title: '決まったことを、予定に。', body: '登録案を作り、承認したときだけ登録。招待やメールは送りません。', - scene: [['task', '企画レビュー', '金曜 15:00'], ['say', 'これ、金曜の15時に入れておいて。'], ['card', '登録案(まだ送信しません)', '企画レビュー 金曜 15:00–15:30', '招待・メール:送りません', '承認して登録'], ['done', '企画レビュー', '登録を確認済み']]}, - {id: 'team', kind: 'チームで', title: '会議の決定を、チームのタスクに。', body: '発言の引用つきで候補に。担当と期限は、言われていなければ「不明」のまま。', - scene: [['say', '見積もりは来週までに出しましょう。'], ['say', 'デザイン案はAとBで比べたいですね。'], ['card', 'タスク候補(未確認)', '見積もりを出す 期限:来週 · 担当:不明', '「見積もりは来週までに」', '確認して共有'], ['done', '見積もりを出す', 'チームで共有 · 担当は未定']]} + {id: 'reschedule', kind: '言い直し', title: '「やっぱり、明日にして。」', body: '話しながら言い直しても、反映するのは確かめてから。', tryLabel: '言い直しを試す', + scene: [['task', 'メール返信', '今日', '明日 · 延期', 5], ['task', '資料確認', '今日'], ['say', 'メール返信は今日中に……いや、やっぱり明日にして。'], ['card', '確認待ちの変更案', 'メール返信 今日 → 明日', '「やっぱり明日にして」', 'この変更を反映', '反映しました', 5]]}, + {id: 'calendar', kind: '会話から予定へ', title: '決まったことを、予定に。', body: '登録案を作り、承認したときだけ登録。招待やメールは送りません。', tryLabel: '予定への登録を試す', + scene: [['task', '企画レビュー', '日時は未定', '金曜 15:00 · 登録済み', 4], ['say', 'これ、金曜の15時に入れておいて。'], ['card', '登録案(まだ送信しません)', '企画レビュー 金曜 15:00–15:30', '招待・メール:送りません', '承認して登録', '登録を確認しました', 4]]}, + {id: 'team', kind: 'チームで', title: '会議の決定を、チームのタスクに。', body: '発言の引用つきで候補に。担当と期限は、言われていなければ「不明」のまま。', tryLabel: 'チームでの共有を試す', + scene: [['say', '見積もりは来週までに出しましょう。'], ['say', 'デザイン案はAとBで比べたいですね。'], ['card', 'タスク候補(未確認)', '見積もりを出す 期限:来週 · 担当:不明', '「見積もりは来週までに」', '確認して共有', '共有しました', 4], ['new', '見積もりを出す', 'チームで共有 · 担当は未定']]} ], mechKicker: 'しくみ', mechTitle: '原文が、根拠になる。', mechLead: '推測では足さない。あなたの言葉を引用した変更案だけを、確かめてから残します。', mech: [['話す', 'いつもの言葉で'], ['引用する', '発言の原文をそのまま'], ['保留する', 'まだ反映していません'], ['確かめる', '反映か、取り消すか'], ['次回へ', '決まったことだけ']] }, en: { kicker: 'Concept demos by use', title: 'Talk it through.
      Keep only what was decided.', lead: 'Changing your mind, booking it, or deciding as a team: it is kept only after you confirm.', - label: 'Concept demo · fictional data', tryLabel: 'Try it', uiNote: 'The demos run in Japanese.', + label: 'Concept demo · fictional data', uiNote: 'The demos run in Japanese.', demos: [ - {id: 'reschedule', kind: 'Changing your mind', title: '“Actually, make it tomorrow.”', body: 'Rephrase as you talk; nothing changes until you confirm.', - scene: [['task', 'Reply to email', 'Today'], ['task', 'Review the deck', 'Today'], ['say', 'Review the deck today. Reply to the email tomorrow.'], ['card', 'Change waiting for you', 'Reply to email today → tomorrow', '“reply to the email tomorrow”', 'Apply this change'], ['done', 'Reply to email', 'Tomorrow · postponed']]}, - {id: 'calendar', kind: 'From talk to calendar', title: 'Put what was decided on the calendar.', body: 'A draft first, booked only when you approve. No invitations or emails are sent.', - scene: [['task', 'Plan review', 'Fri 15:00'], ['say', 'Put this in for Friday at three.'], ['card', 'Draft (not sent yet)', 'Plan review Fri 15:00–15:30', 'Invitations and email: none', 'Approve and book'], ['done', 'Plan review', 'Booking confirmed']]}, - {id: 'team', kind: 'As a team', title: 'Turn a meeting’s decisions into team tasks.', body: 'Candidates carry the quote. Owner and due date stay “unknown” unless someone said them.', - scene: [['say', 'Let’s send the estimate by next week.'], ['say', 'I’d like to compare designs A and B.'], ['card', 'Task candidate (unconfirmed)', 'Send the estimate due next week · owner unknown', '“send the estimate by next week”', 'Confirm and share'], ['done', 'Send the estimate', 'Shared with the team · owner open']]} + {id: 'reschedule', kind: 'Changing your mind', title: '“Actually, make it tomorrow.”', body: 'Rephrase as you talk; nothing changes until you confirm.', tryLabel: 'Try rephrasing', + scene: [['task', 'Reply to email', 'Today', 'Tomorrow · postponed', 5], ['task', 'Review the deck', 'Today'], ['say', 'Reply to the email today… no, actually, make it tomorrow.'], ['card', 'Change waiting for you', 'Reply to email today → tomorrow', '“actually, make it tomorrow”', 'Apply this change', 'Applied', 5]]}, + {id: 'calendar', kind: 'From talk to calendar', title: 'Put what was decided on the calendar.', body: 'A draft first, booked only when you approve. No invitations or emails are sent.', tryLabel: 'Try booking it', + scene: [['task', 'Plan review', 'No time yet', 'Fri 15:00 · booked', 4], ['say', 'Put this in for Friday at three.'], ['card', 'Draft (not sent yet)', 'Plan review Fri 15:00–15:30', 'Invitations and email: none', 'Approve and book', 'Booking confirmed', 4]]}, + {id: 'team', kind: 'As a team', title: 'Turn a meeting’s decisions into team tasks.', body: 'Candidates carry the quote. Owner and due date stay “unknown” unless someone said them.', tryLabel: 'Try sharing with the team', + scene: [['say', 'Let’s send the estimate by next week.'], ['say', 'I’d like to compare designs A and B.'], ['card', 'Task candidate (unconfirmed)', 'Send the estimate due next week · owner unknown', '“send the estimate by next week”', 'Confirm and share', 'Shared', 4], ['new', 'Send the estimate', 'Shared with the team · owner open']]} ], mechKicker: 'How it works', mechTitle: 'Your words are the evidence.', mechLead: 'Nothing is added by guessing. Only changes that quote you are kept, after you confirm.', mech: [['Talk', 'In your own words'], ['Quote', 'The exact words, kept'], ['Hold', 'Not applied yet'], ['Confirm', 'Apply or discard'], ['Next time', 'Only what was decided']] @@ -46,13 +46,19 @@ export const VISION = Object.freeze({ } }); +// A scene is a list of rows that appear one step at a time. A task or card may name the step at which the change is +// applied (`at`): the row then switches in place from its "was" value to its "now" value, so the finished state never +// shows the same item twice. Without animation the finished state is what shows. function sceneHtml(scene) { - return scene.map(([kind, a, b, c, d], i) => { + return scene.map(([kind, a, b, c, d, e2, f], i) => { const step = ` vp-step vp-step--${i + 1}`; - if (kind === 'task') return `
    • ${esc(a)}${esc(b)}
    • `; + const swap = (was, now, at) => `${esc(was)}${esc(now)}`; + if (kind === 'task') return c == null + ? `
    • ${esc(a)}${esc(b)}
    • ` + : `
    • ${esc(a)}${swap(b, c, d)}
    • `; + if (kind === 'new') return `
    • ${esc(a)}${esc(b)}
    • `; if (kind === 'say') return `
    • ${esc(a)}
    • `; - if (kind === 'card') return `
    • ${esc(a)}${esc(b)}${esc(c)}
    • `; - return `
    • ${esc(a)}${esc(b)}
    • `; + return `
    • ${esc(a)}${esc(b)}${esc(c)}${esc(e2)}
    • `; }).join(''); } @@ -61,7 +67,7 @@ export function visionSections(p, lang) { if (!v) return ''; const t = v[lang]; if (!t || t.demos.length !== 3 || t.mech.length !== 5) throw new Error(`Vision sections: incomplete ${p.id}/${lang}`); - const demos = t.demos.map((d, i) => `

      0${i + 1} / ${esc(d.kind)}

      ${esc(d.title)}

      ${esc(d.body)}

      ${esc(t.tryLabel)}

      ${esc(p.name)}${esc(t.label)}

        ${sceneHtml(d.scene)}
      `).join(''); + const demos = t.demos.map((d, i) => `

      0${i + 1} / ${esc(d.kind)}

      ${esc(d.title)}

      ${esc(d.body)}

      ${esc(d.tryLabel)}

      ${esc(p.name)}${esc(t.label)}

        ${sceneHtml(d.scene)}
      `).join(''); const mech = t.mech.map(([k, b], i) => `
    • 0${i + 1}${esc(k)}${esc(b)}
    • `).join(''); return `

      ${esc(t.kicker)}

      ${t.title}

      ${esc(t.lead)}${t.uiNote ? ` ${esc(t.uiNote)}` : ''}

      ${demos}
      ` + `

      ${esc(t.mechKicker)}

      ${esc(t.mechTitle)}

      ${esc(t.mechLead)}

        ${mech}
      `; From 459d62b0df7ff1d062e8ed68ced1763146036cda Mon Sep 17 00:00:00 2001 From: FORIFOR Date: Mon, 19 Oct 2026 22:50:12 +0900 Subject: [PATCH 3/5] fix: product films open on the real recording, not the illustrative story On AI Secure, AI Meeting and Launchloom the film section opened on the animated UI story, so the page looked as if it had no video. Inside a product page's film section the player now opens on the recording (poster and play button); the story stays one tab away. Produced films keep their own labels and default. Co-Authored-By: Claude Opus 5.5 --- public/assets/animated-demos.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/assets/animated-demos.mjs b/public/assets/animated-demos.mjs index 664e7be..5e10d8b 100644 --- a/public/assets/animated-demos.mjs +++ b/public/assets/animated-demos.mjs @@ -131,7 +131,8 @@ export function mountDemo(figure, initialId, lang = 'ja') { visual.innerHTML = renderStory(id,lang); chapters.innerHTML = copy.steps.map((label,i) => ``).join(''); root.querySelector('.rm-demo-disclosure').textContent = `${t('説明用の再現UI・サンプルデータ。実行速度を示すものではありません。','Illustrative UI · fictional data. Timing is not product performance.')} ${copy.scope}`; - draw(); setMode('story'); + // On a product page's film section the real footage is the point, so it opens on the recording; the story stays a tab. + draw(); setMode(figure.closest('[data-rd-reel]') && !produced ? 'recording' : 'story'); } on(toolbar,'click',event => { const b = event.target.closest('button[data-mode]'); if (b) setMode(b.dataset.mode); }); on(pause,'click',() => { paused = allowed(); userPlay = !paused; reconcile(); }); From e6977317d5672579e6943bc0d458163281fd27d1 Mon Sep 17 00:00:00 2001 From: FORIFOR Date: Mon, 19 Oct 2026 23:06:58 +0900 Subject: [PATCH 4/5] feat: product pages lead with the vision; the rest folds into one place Each product page now reads: name and one line, one action, what it does, the use cases and the idea (when the product has them), the film itself without tabs or notes around it, then a short status band (stage, licence, try/source/evidence). Capabilities, limits, access, records and the original sections move into a single "In detail" fold; nothing is deleted. Oathra keeps its use cases, ring and future screens open, and its brief order inside the fold. Co-Authored-By: Claude Opus 5.5 --- public/assets/animated-demos.mjs | 5 +++-- src/redesign-copy.mjs | 2 ++ src/redesign.mjs | 25 ++++++++++++++----------- src/redesign/redesign.css | 17 +++++++++++++++++ tests/noa-public-viewing.test.mjs | 3 ++- tests/oathra-lp.test.mjs | 12 +++++++----- 6 files changed, 45 insertions(+), 19 deletions(-) diff --git a/public/assets/animated-demos.mjs b/public/assets/animated-demos.mjs index 5e10d8b..4f7cb48 100644 --- a/public/assets/animated-demos.mjs +++ b/public/assets/animated-demos.mjs @@ -58,6 +58,8 @@ export function renderStory(id, lang = 'ja') { /** One clock per mounted player. User pause is independent from visibility and motion preferences. */ export function mountDemo(figure, initialId, lang = 'ja') { if (!PRODUCTS.includes(initialId) || figure.dataset.demoVersion) return null; + // A product page's film section shows the footage itself, with no story overlay, tabs or notes around it. + if (figure.closest('[data-rd-reel]')) return null; const screen = figure.querySelector('.studio-player-screen, .owned-film__screen'); if (!screen) return null; const doc = figure.ownerDocument, win = doc.defaultView, ja = lang === 'ja'; @@ -131,8 +133,7 @@ export function mountDemo(figure, initialId, lang = 'ja') { visual.innerHTML = renderStory(id,lang); chapters.innerHTML = copy.steps.map((label,i) => ``).join(''); root.querySelector('.rm-demo-disclosure').textContent = `${t('説明用の再現UI・サンプルデータ。実行速度を示すものではありません。','Illustrative UI · fictional data. Timing is not product performance.')} ${copy.scope}`; - // On a product page's film section the real footage is the point, so it opens on the recording; the story stays a tab. - draw(); setMode(figure.closest('[data-rd-reel]') && !produced ? 'recording' : 'story'); + draw(); setMode('story'); } on(toolbar,'click',event => { const b = event.target.closest('button[data-mode]'); if (b) setMode(b.dataset.mode); }); on(pause,'click',() => { paused = allowed(); userPlay = !paused; reconcile(); }); diff --git a/src/redesign-copy.mjs b/src/redesign-copy.mjs index 32e2244..f96c14f 100644 --- a/src/redesign-copy.mjs +++ b/src/redesign-copy.mjs @@ -31,6 +31,7 @@ export const redesignCopy = { whatItDoes: 'What it does', film: name => `(03) ${name} — Film`, highlights: '(04) 要点', features: '(05) できること', featuresNote: d => `機能は各リポジトリと照合済み(${d})`, detailKicker: '(+) くわしく見る', detailMore: 'さらにくわしく', + nowKicker: 'いまの状態', moreSummary: 'くわしく', moreHint: 'できること・できないこと・利用条件・記録', scopeKicker: '(06) 試せる範囲と根拠', scopeTitle: 'できないことも、
      先に書いておく。', scopeNote: () => '最新の利用条件は、各リポジトリでご確認ください。', scopeLabel: 'SCOPE / 範囲', proofLabel: 'PROOF / 公開している根拠', @@ -74,6 +75,7 @@ export const redesignCopy = { whatItDoes: 'What it does', film: name => `(03) ${name} — Film`, highlights: '(04) Highlights', features: '(05) What it can do', featuresNote: d => `Features checked against each repository (${d})`, detailKicker: '(+) In detail', detailMore: 'More detail', + nowKicker: 'Where it stands', moreSummary: 'In detail', moreHint: 'Capabilities, limits, access and records', scopeKicker: '(06) Scope and evidence', scopeTitle: 'What it cannot do,
      stated up front.', scopeNote: () => 'Current terms live in each repository.', scopeLabel: 'SCOPE', proofLabel: 'PROOF / PUBLISHED EVIDENCE', diff --git a/src/redesign.mjs b/src/redesign.mjs index c240cad..720c8b5 100644 --- a/src/redesign.mjs +++ b/src/redesign.mjs @@ -294,11 +294,12 @@ function productMain(p, list, lang, {kept, figure, source, filmAnchor}) { const long = name.length > 8 || p.id === 'noa'; const conceptEntry = p.id === 'oathra' && lang === 'ja'; const viewing = p.id === 'noa' ? p.publicViewing?.[lang] : null; + // One action in the first view; everything else is further down the page. const heroActions = viewing - ? `${esc(viewing.primaryLabel)}${esc(viewing.secondaryLabel)}` + ? `${esc(viewing.primaryLabel)}` : conceptEntry - ? `用途別デモを試す紹介映像へ` - : `${esc(nav.demoLabel)}${esc(lang === 'ja' ? '紹介映像へ' : 'To the film')}`; + ? `用途別デモを試す` + : `${esc(nav.demoLabel)}`; const steps = t.outcome.split(' → '); const outcome = steps.map((s, i) => `
    • ${esc(s)}${i < steps.length - 1 ? `` : ''}
    • `).join(''); const highlights = t.highlights.map((h, i) => `
      0${i + 1}

      ${esc(h.value)}

      ${esc(h.label)}

      `).join(''); @@ -311,14 +312,15 @@ function productMain(p, list, lang, {kept, figure, source, filmAnchor}) { // A moved figure carries its own labels (a recording block may open on its reconstruction view), so the // "recording" label is not repeated beside it. Non-recording kinds are always stated. const kind = r.filmKinds[film.kind]; - const keptHtml = kept.join(''); // The original hero (its own try link and the real-recording block) stays in view: evidence sits next to the claim. // Only the long-form sections after it fold away; a page whose film links down into them keeps everything open. - const lastEntry = Math.max(kept.findIndex(k => k.includes('class="owned-primary"')), kept.findIndex(k => k.includes('class="owned-film'))); - const split = filmAnchor ? kept.length : lastEntry + 1; - const openKept = kept.slice(0, split).join(''), foldKept = kept.slice(split).join(''); + // Vision first: the owner-composed use cases and ideas (Oathra's demos, ring and screens to come; Genie's examples and + // take-home files) stay on the page. Every other kept section, the capabilities and the scope go into one fold. + const VISION_KEPT = ['olp-demos', 'olp-ring', 'olp-steps', 'glp-demos', 'glp-artifacts']; + const isVision = k => VISION_KEPT.some(c => new RegExp(`^]*class="[^"]*\\b${c}\\b`).test(k.trimStart())); + const visionKept = kept.filter(isVision).join(''), moreKept = kept.filter(k => !isVision(k)).join(''); // Oathra's status table already shows every highlight; the ledger values must not appear twice. - const showHighlights = !t.highlights.every(h => keptHtml.includes(esc(h.label))); + const showHighlights = !t.highlights.every(h => kept.join('').includes(esc(h.label))); // The proof line lives once, in #scope. const note = r.filmNotes[p.id] || ''; const extra = p.id === 'agent-team' ? ` ${esc(r.agentTeamRecording)} ↗` : ''; @@ -332,11 +334,12 @@ function productMain(p, list, lang, {kept, figure, source, filmAnchor}) { ${publicViewing(p, lang)} ${oathraExperienceFilm(p, lang)} ${visionSections(p, lang)} +${visionKept}
      ${figure ? `
      ${figure}
      ` : filmAnchor ? `${esc(r.filmBelow)} ` : `
      ${video({...film, label: `${name} — ${kind}`}, lang)}
      `}
      ${esc(r.film(name))}${figure && film.kind === 'recording' ? (note ? `${esc(note)}` : '') : `${esc(filmAnchor ? r.stillKind : kind)}${figure || filmAnchor ? '' : ` ${esc(note)}`}${extra}`}
      -${showHighlights ? `

      ${esc(r.highlights)}

      ${highlights}
      ` : ''} -${kept.length ? `

      ${esc(r.detailKicker)}

      ${openKept}
      ` : ''}${foldKept ? `
      ${esc(r.detailMore)}${foldKept}
      ` : ''} +

      ${esc(r.nowKicker)}

      ${esc(t.status)} · ${esc(t.license)}

      ${showHighlights ? `
      ${highlights}
      ` : ''}
      +
      ${esc(r.moreSummary)}${esc(r.moreHint)}${moreKept ? `
      ${moreKept}
      ` : ''}

      ${esc(r.features)}

      ${esc(t.short)}

      ${source ? source : `

      ${esc(r.featuresNote(capabilitiesCheckedAt))}

      `}
      ${features}
      -

      ${esc(r.scopeKicker)}

      ${r.scopeTitle}

      ${esc(r.scopeNote(sourcesCheckedAt))}

      ${esc(r.scopeLabel)}

      ${esc(t.scope)}

      ${esc(r.proofLabel)}

      ${esc(t.proof)}

      +

      ${esc(r.scopeKicker)}

      ${r.scopeTitle}

      ${esc(r.scopeNote(sourcesCheckedAt))}

      ${esc(r.scopeLabel)}

      ${esc(t.scope)}

      ${esc(r.proofLabel)}

      ${esc(t.proof)}

      ${esc(r.consultKicker)}

      ${esc(t.consult)}

      ${p.id === 'launchloom' ? `${esc(r.useLaunchloom)}` : ''}${esc(r.consultCta)}
      `; } diff --git a/src/redesign/redesign.css b/src/redesign/redesign.css index f2dc44b..219660c 100644 --- a/src/redesign/redesign.css +++ b/src/redesign/redesign.css @@ -491,3 +491,20 @@ body[data-rd] .vp-flow[data-vp-play] .vp-node--5{animation:vp-s5 9s var(--rd-eas @media (max-width:860px){body[data-rd] .vp-head,body[data-rd] .vp-demo{grid-template-columns:1fr}body[data-rd] .vp-flow{grid-template-columns:1fr;gap:28px}body[data-rd] .vp-node{padding-right:0}body[data-rd] .vp-rows{min-height:0}} @media (prefers-reduced-motion:reduce){body[data-rd] [data-vp-play] *{animation:none!important}} @media (forced-colors:active){body[data-rd] .vp-check.is-on,body[data-rd] .vp-node::before,body[data-rd] .vp-mic{forced-color-adjust:none;background:CanvasText}} +/* Product page, vision first: a short status band, then one fold for everything else. */ +body[data-rd] .rd-now{padding:clamp(72px,9vw,140px) var(--rd-gutter) clamp(56px,7vw,100px);border-top:1px solid var(--rd-rule)} +body[data-rd] .rd-now-head{display:flex;justify-content:space-between;align-items:baseline;gap:16px;margin-bottom:24px} +body[data-rd] .rd-now-head .rd-kicker{margin:0} +body[data-rd] .rd-now-status{display:inline-flex;align-items:center;gap:10px;margin:0;font-family:var(--rd-mono);font-size:12px;letter-spacing:.04em;color:var(--rd-ink)} +body[data-rd] .rd-now-status i{width:8px;height:8px;border-radius:50%;background:var(--rd-accent)} +body[data-rd] .rd-now .rd-hl{min-height:0} +body[data-rd] .rd-now .rd-links{margin-top:clamp(32px,4vw,56px)} +body[data-rd] .rd-more{border-top:1px solid var(--rd-rule)} +body[data-rd] .rd-more>summary{list-style:none;cursor:pointer;display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 20px;min-height:88px;padding:28px var(--rd-gutter);color:var(--rd-ink)} +body[data-rd] .rd-more>summary::-webkit-details-marker{display:none} +body[data-rd] .rd-more>summary span{font-family:var(--rd-jp);font-weight:700;font-size:clamp(20px,2vw,28px);letter-spacing:-.02em} +body[data-rd] .rd-more>summary span::after{content:" +";font-weight:400} +body[data-rd] .rd-more[open]>summary span::after{content:" −"} +body[data-rd] .rd-more>summary small{font-size:14px;color:var(--rd-muted)} +body[data-rd] .rd-more>summary:hover span{color:var(--rd-accent-ink)} +body[data-rd] .rd-more>summary:focus-visible{outline:2px solid var(--rd-accent-ink);outline-offset:-4px} diff --git a/tests/noa-public-viewing.test.mjs b/tests/noa-public-viewing.test.mjs index c09caba..864cfd4 100644 --- a/tests/noa-public-viewing.test.mjs +++ b/tests/noa-public-viewing.test.mjs @@ -13,7 +13,8 @@ test('Noa JA/EN first action opens an actual stream edit before the recreated fi const firstAction = hero.match(/]*>[\s\S]*?<\/a>/)?.[0]; assert.ok(firstAction?.includes(`href="${noaVideos.liveEdit}"`)); assert.match(firstAction, prefix ? /Watch gameplay \(4:12\)/ : /実況を見る(4分12秒)/); - assert.match(hero, /href="#noa-viewing"/); + // One action in the first view (2026-10-19); the viewing list follows directly after it. + assert.ok(html.indexOf('id="noa-viewing"') - html.indexOf('', html.indexOf('
      ]/g) || []).length, 1); const at = s => { const i = html.indexOf(s); assert.ok(i >= 0, s); return i; }; - const order = ['class="container olp-hero"', 'id="ring"', 'id="steps"', 'id="trust"', 'id="recordings"', 'id="status"', 'id="start"', 'id="consult"', 'class="rd-next'].map(at); // 2026-10 redesign: consult and next-product sections + // 2026-10-19, vision first (owner): the use cases, the ring and the screens to come lead; the brief's first view, the + // delegation levels, the recordings and the status follow inside the detail fold, still in the brief's order. + const order = ['id="concept-demos"', 'id="ring"', 'id="steps"', 'class="container olp-hero"', 'id="trust"', 'id="recordings"', 'id="status"', 'id="start"', 'id="consult"', 'class="rd-next'].map(at); assert.deepEqual([...order].sort((a, b) => a - b), order, 'sections follow the brief'); // First view: one primary button, one-line note, the 30-second image film labelled as such, never autoplaying. - const hero = html.slice(order[0], order[1]); + const hero = html.slice(order[3], order[4]); assert.equal((hero.match(/class="owned-primary"/g) || []).length, 1); assert.match(hero, /構想デモ・実際の発信\/予約は行いません。合成データだけで、承認から結果までを試せます。/); assert.match(hero, /イメージ映像(演出を含む)/); @@ -28,11 +30,11 @@ test('the Japanese Oathra page follows the brief order and states the status of const steps = html.slice(order[2], order[3]); assert.equal(steps.split('開発中の画面 · 未リリース').length - 1, STEPS.length); assert.match(steps, /現行のOathra(CLIとシミュレーター)には、この画面はまだありません/); - const trust = html.slice(order[3], order[4]); + const trust = html.slice(order[4], order[5]); assert.match(trust, /設計 · 実電話フローには未実装/); assert.match(trust, /この3段階の指定、発信前の承認、費用の上限による打ち切り、着信の応対はまだありません/); // 4: the existing 13-second recording and the 15-second film, moved not rewritten. - const rec = html.slice(order[4], order[5]); + const rec = html.slice(order[5], order[6]); assert.match(rec, /data-recording-src="\/media\/products\/oathra\.mp4"/); assert.match(rec, /data-film15="oathra"/); assert.match(rec, /最初に出るのは、判定の流れを説明する再現です/); @@ -41,7 +43,7 @@ test('the Japanese Oathra page follows the brief order and states the status of assert.equal(html.split(license + ' — ').length - 1 + html.split('>' + license + ' · ').length - 1, 1, 'the licence is stated once, in the table'); assert.match(rec, /公開シミュレーター.*再生速度は変えていません/); // 5: the ledger highlights appear once, in the status table, next to the unclaimed items. - const status = html.slice(order[5], order[6]); + const status = html.slice(order[6], order[7]); for (const h of products.find(p => p.id === 'oathra').ja.highlights) assert.ok(status.includes(h.label), h.value); assert.match(status, /店舗システムへの登録/); assert.match(status, /100件の実電話検証/); From d3d3ee080cb871483be300560b568f59635c57de Mon Sep 17 00:00:00 2001 From: FORIFOR Date: Tue, 20 Oct 2026 00:33:20 +0900 Subject: [PATCH 5/5] feat: every product page leads with its vision, with concept demos to try - AI Secure, Launchloom, Agent Team and Noa get three concept use cases and a five-step idea diagram, like AI Meeting, each from its product's own words and UI terms (cited in src/vision-pages.mjs). Each use case opens a demo page under /demos// that runs on fictional data with one shared engine. - The product film section shows the footage only; the guided-sample link in the detail fold points to the concept demos instead. - The status band keeps stage, licence and the three links; highlights and the consult scope move into the fold, and the consult band has one short line. - Agent Team's cut starts after the page settles; Launchloom's lab link points to #proof (#kit does not exist). - Browser checks follow the new layout without loosening: the first action is checked in the first view, and the original primary action, story player and links are checked after opening the fold as a visitor would. Co-Authored-By: Claude Opus 5.5 --- public/demos/agent-team/flows.js | 127 +++++++++++++++++++++ public/demos/agent-team/index.html | 41 +++++++ public/demos/ai-meeting/demo.js | 105 ----------------- public/demos/ai-meeting/flows.js | 145 ++++++++++++++++++++++++ public/demos/ai-meeting/index.html | 12 +- public/demos/aisecure/flows.js | 150 ++++++++++++++++++++++++ public/demos/aisecure/index.html | 41 +++++++ public/demos/{ai-meeting => }/demo.css | 11 +- public/demos/engine.js | 73 ++++++++++++ public/demos/launchloom/flows.js | 127 +++++++++++++++++++++ public/demos/launchloom/index.html | 41 +++++++ public/demos/noa/flows.js | 151 +++++++++++++++++++++++++ public/demos/noa/index.html | 41 +++++++ scripts/ui-capture.mjs | 26 ++--- scripts/verify-outcome-first.py | 17 +++ scripts/verify_showcase.py | 34 ++++-- src/premium-film-cuts.mjs | 6 +- src/redesign-copy.mjs | 4 +- src/redesign.mjs | 13 ++- src/redesign/redesign.css | 2 + src/site-content.mjs | 2 +- src/vision-pages.mjs | 136 +++++++++++++++++++++- 22 files changed, 1158 insertions(+), 147 deletions(-) create mode 100644 public/demos/agent-team/flows.js create mode 100644 public/demos/agent-team/index.html delete mode 100644 public/demos/ai-meeting/demo.js create mode 100644 public/demos/ai-meeting/flows.js create mode 100644 public/demos/aisecure/flows.js create mode 100644 public/demos/aisecure/index.html rename public/demos/{ai-meeting => }/demo.css (85%) create mode 100644 public/demos/engine.js create mode 100644 public/demos/launchloom/flows.js create mode 100644 public/demos/launchloom/index.html create mode 100644 public/demos/noa/flows.js create mode 100644 public/demos/noa/index.html diff --git a/public/demos/agent-team/flows.js b/public/demos/agent-team/flows.js new file mode 100644 index 0000000..94b057b --- /dev/null +++ b/public/demos/agent-team/flows.js @@ -0,0 +1,127 @@ +// Agent Team concept demo. Fictional data; wording from the product's own UI and README. See ../engine.js. +import {run} from '../engine.js'; + +run({ + "ask": "まだ反映していません。この内容でよいですか?", + "doneShort": "反映済み", + "cancel": "取り消す", + "cancelled": "取り消しました。元のままです。", + "cancelledShort": "取り消しました。", + "empty": "まだありません", + "hint": "ボタンを押すと、この言葉を入れたことにします。", + "act": "チームにお願いする", + "who": { + "you": "あなた", + "ai": "まとめ役", + "other": "チーム" + }, + "flows": { + "tool": { + "tab": "01 小さな道具", + "title": "版と確認の記録", + "rows": [ + [ + "版1 · aggregate.py", + "つくる係", + "要修正" + ] + ], + "before": [ + [ + "other", + "確かめる係:月の境目で集計がずれます。版1は要修正です。" + ] + ], + "input": "このCSVを年月別に集計するツールを作って。使い方とテストも付けて。", + "reply": "つくる係が版2を作りました。確かめる係が、この版を確かめます。", + "cards": [ + { + "label": "確かめる係", + "main": "版2 · aggregate.py テスト:通過", + "lines": [ + "確認は、この版の中身に結びつきます。" + ], + "ask": "この版を採用しますか?", + "ok": "この版を採用", + "doneShort": "採用版になりました", + "add": [ + "版2 · aggregate.py", + "確かめる係", + "通過 · 採用版" + ], + "done": "版2を採用しました(模擬)。確認の記録も一緒に残ります。" + } + ] + }, + "research": { + "tab": "02 調べて比べる", + "title": "版と確認の記録", + "rows": [ + [ + "出典メモ", + "調べる係", + "12件" + ] + ], + "input": "3社の公開資料を比べて、違いをまとめて。", + "reply": "つくる係が比較メモを作りました。確かめる係の指摘は3件です。", + "cards": [ + { + "label": "確かめる係", + "main": "比較メモ · 版2 指摘3件 → 修正済み", + "lines": [ + "出典の照合:1件は未確認のまま" + ], + "ask": "未確認を残したまま受け取りますか?", + "ok": "確認の記録を見て受け取る", + "doneShort": "受け取りました(未確認 1件)", + "add": [ + "比較メモ · 版2", + "確かめる係", + "通過 · 未確認 1件" + ], + "done": "受け取りました(模擬)。未確認の点は、そのまま記録に残っています。" + } + ] + }, + "docs": { + "tab": "03 記録つきで受け取る", + "title": "版と確認の記録", + "rows": [ + [ + "版1 · guide.md", + "つくる係", + "確認の記録なし" + ], + [ + "版2 · guide.md", + "確かめる係", + "通過" + ] + ], + "input": "採用した版だけ、まとめて受け取りたい。", + "reply": "版2を採用候補にしました。版1には確認の記録がありません。", + "cards": [ + { + "label": "まとめて取得", + "main": "採用版 · 確認の記録 · 作業記録", + "lines": [ + "ZIPで受け取ります" + ], + "ask": "この内容でまとめて取得しますか?", + "ok": "まとめて取得", + "doneShort": "受け取りました(模擬)", + "set": [ + 1, + [ + "版2 · guide.md", + "確かめる係", + "採用版" + ] + ], + "done": "採用版を、確認の記録ごと受け取りました(模擬)。" + } + ] + } + } +}); diff --git a/public/demos/agent-team/index.html b/public/demos/agent-team/index.html new file mode 100644 index 0000000..0d6e7d6 --- /dev/null +++ b/public/demos/agent-team/index.html @@ -0,0 +1,41 @@ + + + + + + ひとりのAIに、全部は任せない。 — Agent Team 構想デモ + + + + + + + + +

      構想デモ・架空データ。AIは動かしていません。

      +
      Agent Team 構想デモ製品ページへ戻る
      +
      +

      ひとりのAIに、全部は任せない。

      + +
      +
      +

      お願い

      +
        +
        +
        +
        +

        +
          +
          +

          + +
          +
          + +
          + + diff --git a/public/demos/ai-meeting/demo.js b/public/demos/ai-meeting/demo.js deleted file mode 100644 index ab861d4..0000000 --- a/public/demos/ai-meeting/demo.js +++ /dev/null @@ -1,105 +0,0 @@ -// AI Meeting concept demo. Fictional data; no microphone, model, storage or network. Wording follows the product's -// own UI (TaskList.tsx proposal card, CalendarActions.tsx states, MeetingMemory.ts candidates). -const $ = s => document.querySelector(s); -const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; }; -const button = (label, cls, on) => { const b = el('button', cls, label); b.type = 'button'; b.addEventListener('click', on); return b; }; -const wait = ms => new Promise(r => setTimeout(r, matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : ms)); - -const FLOWS = { - reschedule: { - title: 'やること', - tasks: [['メール返信', '今日', '未完了'], ['資料確認', '今日', '未完了']], - say: 'メール返信は今日中に……いや、やっぱり明日にして。', - reply: 'メール返信を明日に延期する変更案を作りました。まだ反映していません。', - cards: [{label: '確認待ちの変更案', main: 'メール返信 今日 → 明日', quote: 'やっぱり明日にして', ok: 'この変更を反映', - apply: list => list.map(t => t[0] === 'メール返信' ? ['メール返信', '明日', '延期'] : t), done: '反映しました。次に開いたときも、ここから続きます。'}] - }, - calendar: { - title: 'やること', - tasks: [['企画レビュー', '日時は未定', '未完了']], - say: 'これ、金曜の15時に入れておいて。', - reply: '登録案を作りました。まだ送信しません。', - cards: [{label: '登録案(まだ送信しません)', main: '企画レビュー 金曜 15:00–15:30', quote: '金曜の15時に入れておいて', extra: '招待・メール:送りません', ok: '承認して登録', pending: '実行中・未確認', - apply: list => list.map(t => t[0] === '企画レビュー' ? ['企画レビュー', '金曜 15:00', '登録を確認済み'] : t), done: '登録を確認済み(模擬)。招待やメールは送っていません。'}] - }, - team: { - title: 'チームのタスク', - tasks: [], - meeting: ['見積もりは来週までに出しましょう。', 'デザイン案はAとBで比べたいですね。', 'じゃあ、今日はここまで。'], - say: '会議の決定を、タスク候補にして。', - reply: '発言から候補を2件作りました。担当と期限は、言われていなければ「不明」です。', - cards: [ - {label: 'タスク候補(未確認)', main: '見積もりを出す 期限:来週 · 担当:不明', quote: '見積もりは来週までに出しましょう', ok: '確認して共有', - apply: list => [...list, ['見積もりを出す', '来週', '担当:未定']], done: '共有しました。担当は、決まったら追記できます。'}, - {label: 'タスク候補(未確認)', main: 'デザイン案AとBを比べる 期限:不明 · 担当:不明', quote: 'デザイン案はAとBで比べたいですね', ok: '確認して共有', - apply: list => [...list, ['デザイン案AとBを比べる', '期限なし', '担当:未定']], done: '共有しました。'} - ] - } -}; - -let state; -function render() { - const f = FLOWS[state.flow]; - $('[data-tasks-title]').textContent = f.title; - const list = $('[data-list]'); list.replaceChildren(); - if (!state.tasks.length) list.append(el('li', 'empty', 'まだありません')); - for (const [name, when, st] of state.tasks) { - const li = el('li', `task${st === '未完了' ? '' : ' is-changed'}`); - li.append(el('span', 'dot'), el('span', 'name', name), el('span', 'when', when), el('span', 'state', st)); - list.append(li); - } -} - -function addLog(who, text) { - const li = el('li', `msg msg--${who}`); - li.append(el('span', 'who', who === 'you' ? 'あなた' : who === 'ai' ? 'AI Meeting' : '会議'), el('p', null, text)); - $('[data-log]').append(li); -} - -async function speak() { - const f = FLOWS[state.flow]; - $('[data-say]').replaceChildren(); - addLog('you', f.say); - await wait(500); - addLog('ai', f.reply); - await wait(300); - const cards = $('[data-cards]'); - f.cards.forEach((c, i) => { - const card = el('article', 'card'); - card.append(el('p', 'card-label', c.label), el('p', 'card-main', c.main), el('p', 'card-quote', `「${c.quote}」`)); - if (c.extra) card.append(el('p', 'card-extra', c.extra)); - card.append(el('p', 'card-note', 'まだ反映していません。この内容でよいですか?')); - const actions = el('div', 'card-actions'); - actions.append(button(c.ok, 'primary', async () => { - actions.replaceChildren(); - if (c.pending) { card.querySelector('.card-note').textContent = c.pending; await wait(900); } - state.tasks = c.apply(state.tasks); render(); - card.classList.add('is-done'); card.querySelector('.card-note').textContent = c.pending ? '登録済み(模擬)' : '反映済み'; - $('[data-status]').textContent = c.done; - }), button('取り消す', 'ghost', () => { - actions.replaceChildren(); card.classList.add('is-dropped'); - card.querySelector('.card-note').textContent = '取り消しました。やることは元のままです。'; - $('[data-status]').textContent = '取り消しました。'; - })); - card.append(actions); - cards.append(card); - if (i === 0) actions.querySelector('button').focus(); - }); -} - -function start(flow) { - const f = FLOWS[flow] ? flow : 'reschedule'; - state = {flow: f, tasks: FLOWS[f].tasks.map(t => [...t])}; - document.querySelectorAll('[data-flow]').forEach(a => a.setAttribute('aria-current', String(a.dataset.flow === f))); - $('[data-log]').replaceChildren(); $('[data-cards]').replaceChildren(); $('[data-status]').textContent = ''; - (FLOWS[f].meeting || []).forEach(line => addLog('meeting', line)); - render(); - const say = $('[data-say]'); say.replaceChildren(); - say.append(el('p', 'say-hint', 'ボタンを押すと、この言葉を話したことにします。'), button(`話す:「${FLOWS[f].say}」`, 'primary speak', speak)); -} - -document.querySelectorAll('[data-flow]').forEach(a => a.addEventListener('click', e => { - e.preventDefault(); history.replaceState(null, '', `?flow=${a.dataset.flow}`); start(a.dataset.flow); -})); -$('[data-reset]').addEventListener('click', () => start(state.flow)); -start(new URLSearchParams(location.search).get('flow')); diff --git a/public/demos/ai-meeting/flows.js b/public/demos/ai-meeting/flows.js new file mode 100644 index 0000000..60b51e7 --- /dev/null +++ b/public/demos/ai-meeting/flows.js @@ -0,0 +1,145 @@ +// AI Meeting concept demo. Fictional data; wording from the product's own UI and README. See ../engine.js. +import {run} from '../engine.js'; + +run({ + "ask": "まだ反映していません。この内容でよいですか?", + "doneShort": "反映済み", + "cancel": "取り消す", + "cancelled": "取り消しました。元のままです。", + "cancelledShort": "取り消しました。", + "empty": "まだありません", + "hint": "ボタンを押すと、この言葉を入れたことにします。", + "act": "話す", + "who": { + "you": "あなた", + "ai": "AI Meeting", + "other": "会議" + }, + "flows": { + "reschedule": { + "tab": "01 言い直し", + "title": "やること", + "rows": [ + [ + "メール返信", + "今日", + "未完了" + ], + [ + "資料確認", + "今日", + "未完了" + ] + ], + "input": "メール返信は今日中に……いや、やっぱり明日にして。", + "reply": "メール返信を明日に延期する変更案を作りました。まだ反映していません。", + "cards": [ + { + "label": "確認待ちの変更案", + "main": "メール返信 今日 → 明日", + "lines": [ + "「やっぱり明日にして」" + ], + "ok": "この変更を反映", + "set": [ + 0, + [ + "メール返信", + "明日", + "延期" + ] + ], + "done": "反映しました。次に開いたときも、ここから続きます。" + } + ] + }, + "calendar": { + "tab": "02 会話から予定へ", + "title": "やること", + "rows": [ + [ + "企画レビュー", + "日時は未定", + "未完了" + ] + ], + "input": "これ、金曜の15時に入れておいて。", + "reply": "登録案を作りました。まだ送信しません。", + "cards": [ + { + "label": "登録案(まだ送信しません)", + "main": "企画レビュー 金曜 15:00–15:30", + "lines": [ + "「金曜の15時に入れておいて」", + "招待・メール:送りません" + ], + "ok": "承認して登録", + "pending": "実行中・未確認", + "doneShort": "登録済み(模擬)", + "set": [ + 0, + [ + "企画レビュー", + "金曜 15:00", + "登録を確認済み" + ] + ], + "done": "登録を確認済み(模擬)。招待やメールは送っていません。" + } + ] + }, + "team": { + "tab": "03 チームで", + "title": "チームのタスク", + "rows": [], + "before": [ + [ + "other", + "見積もりは来週までに出しましょう。" + ], + [ + "other", + "デザイン案はAとBで比べたいですね。" + ], + [ + "other", + "じゃあ、今日はここまで。" + ] + ], + "input": "会議の決定を、タスク候補にして。", + "reply": "発言から候補を2件作りました。担当と期限は、言われていなければ「不明」です。", + "cards": [ + { + "label": "タスク候補(未確認)", + "main": "見積もりを出す 期限:来週 · 担当:不明", + "lines": [ + "「見積もりは来週までに出しましょう」" + ], + "ok": "確認して共有", + "doneShort": "共有済み", + "add": [ + "見積もりを出す", + "期限:来週", + "担当:不明" + ], + "done": "共有しました。担当は、決まったら追記できます。" + }, + { + "label": "タスク候補(未確認)", + "main": "デザイン案AとBを比べる 期限:不明 · 担当:不明", + "lines": [ + "「デザイン案はAとBで比べたいですね」" + ], + "ok": "確認して共有", + "doneShort": "共有済み", + "add": [ + "デザイン案AとBを比べる", + "期限:不明", + "担当:不明" + ], + "done": "共有しました。" + } + ] + } + } +}); diff --git a/public/demos/ai-meeting/index.html b/public/demos/ai-meeting/index.html index 51be855..17d5ebe 100644 --- a/public/demos/ai-meeting/index.html +++ b/public/demos/ai-meeting/index.html @@ -4,13 +4,13 @@ 話して、決まったことだけ残す。 — AI Meeting 構想デモ - + - - + + - +

          構想デモ・架空データ。音声・AI・外部への送信はありません。

          AI Meeting 構想デモ製品ページへ戻る
          @@ -27,8 +27,8 @@

          会話

            -
            -

            やること

            +
            +

              diff --git a/public/demos/aisecure/flows.js b/public/demos/aisecure/flows.js new file mode 100644 index 0000000..acfb14f --- /dev/null +++ b/public/demos/aisecure/flows.js @@ -0,0 +1,150 @@ +// AI Secure concept demo. Fictional data; wording from the product's own UI and README. See ../engine.js. +import {run} from '../engine.js'; + +run({ + "ask": "まだ反映していません。この内容でよいですか?", + "doneShort": "反映済み", + "cancel": "取り消す", + "cancelled": "取り消しました。元のままです。", + "cancelledShort": "取り消しました。", + "empty": "まだありません", + "hint": "ボタンを押すと、この言葉を入れたことにします。", + "act": "送る前に検査", + "who": { + "you": "あなた", + "ai": "AI Secure", + "other": "資料" + }, + "flows": { + "sheet": { + "tab": "01 社外秘の資料", + "title": "検査する資料", + "rows": [ + [ + "見積もり_次期モデル.xlsx", + "Excel", + "未検査" + ] + ], + "before": [ + [ + "other", + "セルA1:「社外秘:次期モデルの原価内訳と取引先別の掛け率」" + ] + ], + "input": "この資料の要点を確認してください。", + "reply": "送信せずに検査しました。外部送信していません。", + "cards": [ + { + "label": "判定:保留", + "main": "DOC-CLASS 社外秘などの機密表記があります", + "lines": [ + "管理者に分類を確認してください。", + "検査範囲:対応する文字範囲を検査済み(画像・意味の安全性は対象外)" + ], + "ask": "送るかどうかは、あなたが決めます。", + "ok": "検査結果をJSONで保存", + "doneShort": "保存しました(模擬)", + "cancel": "送らずに閉じる", + "cancelled": "送らずに閉じました。", + "set": [ + 0, + [ + "見積もり_次期モデル.xlsx", + "Excel", + "保留" + ] + ], + "done": "判定・理由・検査範囲を保存しました(模擬)。AIには何も送っていません。" + } + ] + }, + "personal": { + "tab": "02 個人情報", + "title": "検査する資料", + "rows": [ + [ + "顧客メモ.txt", + "テキスト", + "未検査" + ] + ], + "before": [ + [ + "other", + "「山田 花子(hanako@example.com)から、見積もりの再送依頼あり」" + ] + ], + "input": "この顧客メモを短くまとめて。", + "reply": "送信せずに検査しました。外部送信していません。", + "cards": [ + { + "label": "判定:保留", + "main": "DOC-PII 個人情報らしい文字列があります", + "lines": [ + "取り扱いを確認してください。", + "検査範囲:対応する文字範囲を検査済み(画像・意味の安全性は対象外)" + ], + "ask": "送るかどうかは、あなたが決めます。", + "ok": "検査結果をJSONで保存", + "doneShort": "保存しました(模擬)", + "cancel": "送らずに閉じる", + "cancelled": "送らずに閉じました。", + "set": [ + 0, + [ + "顧客メモ.txt", + "テキスト", + "保留" + ] + ], + "done": "保存しました(模擬)。名前やメールアドレスは、AIに送っていません。" + } + ] + }, + "secret": { + "tab": "03 認証情報", + "title": "検査する資料", + "rows": [ + [ + "提案資料.pptx", + "PowerPoint", + "未検査" + ] + ], + "before": [ + [ + "other", + "ノート欄:「api_key = sk-test-xxxxxxxx」" + ] + ], + "input": "このスライドの構成を見直して。", + "reply": "送信せずに検査しました。外部送信していません。", + "cards": [ + { + "label": "判定:拒否", + "main": "DOC-SECRET 認証情報らしい文字列があります", + "lines": [ + "原本から削除し、検査し直してください。", + "検査範囲:対応する文字範囲を検査済み(画像・意味の安全性は対象外)" + ], + "ask": "このままでは送れません。", + "ok": "原本を直して検査し直す", + "pending": "検査しています…", + "doneShort": "検査通過(模擬)", + "cancel": "閉じる", + "cancelled": "閉じました。原本はそのままです。", + "set": [ + 0, + [ + "提案資料.pptx", + "PowerPoint", + "検査通過" + ] + ], + "done": "ノート欄のキーを消して検査し直しました(模擬)。検査通過は、安全の証明ではありません。" + } + ] + } + } +}); diff --git a/public/demos/aisecure/index.html b/public/demos/aisecure/index.html new file mode 100644 index 0000000..c21d88e --- /dev/null +++ b/public/demos/aisecure/index.html @@ -0,0 +1,41 @@ + + + + + + AIに貼る前に、端末で止める。 — AI Secure 構想デモ + + + + + + + + +

              構想デモ・架空データ。検査も送信も行いません。

              +
              AI Secure 構想デモ製品ページへ戻る
              +
              +

              AIに貼る前に、端末で止める。

              + +
              +
              +

              送る予定のもの

              +
                +
                +
                +
                +

                +
                  +
                  +

                  + +
                  +
                  + +
                  + + diff --git a/public/demos/ai-meeting/demo.css b/public/demos/demo.css similarity index 85% rename from public/demos/ai-meeting/demo.css rename to public/demos/demo.css index f733281..574c2d6 100644 --- a/public/demos/ai-meeting/demo.css +++ b/public/demos/demo.css @@ -1,4 +1,9 @@ -:root{--paper:#f3f2ec;--surface:#fbfaf6;--ink:#1b1e1a;--body:#4c4f48;--muted:#5f625b;--rule:#d6d7cf;--accent:#2f6fd6;--accent-ink:#2558ad;color-scheme:light} +:root{--paper:#f3f2ec;--surface:#fbfaf6;--ink:#1b1e1a;--body:#4c4f48;--muted:#5f625b;--rule:#d6d7cf;--accent:#2f6fd6;--accent-ink:#2558ad;--tint:#eef3fc;color-scheme:light} +/* Product accents, as on the product pages. */ +body[data-accent="aisecure"]{--accent:#6b4fd1;--accent-ink:#5a40b5;--tint:#f1eefb} +body[data-accent="agent-team"]{--accent:#d98a1f;--accent-ink:#8f560b;--tint:#fbf3e8} +body[data-accent="launchloom"]{--accent:#c2417a;--accent-ink:#a33463;--tint:#fbeef4} +body[data-accent="noa"]{--accent:#3a4fb8;--accent-ink:#3a4fb8;--tint:#eef0fa} *{box-sizing:border-box} body{margin:0;background:var(--paper);color:var(--ink);font-family:system-ui,-apple-system,"Hiragino Sans","Noto Sans JP",sans-serif;line-height:1.7;-webkit-font-smoothing:antialiased} .skip{position:absolute;left:-9999px}.skip:focus{left:16px;top:8px;z-index:9;background:#fff;padding:8px 12px} @@ -23,14 +28,14 @@ h2{margin:0 0 14px;font-size:13px;letter-spacing:.06em;color:var(--muted);font-w button{font:inherit;cursor:pointer;min-height:44px;border-radius:999px;padding:0 18px} button:focus-visible,.tabs a:focus-visible,.back:focus-visible{outline:2px solid var(--accent-ink);outline-offset:3px} .primary{background:var(--accent-ink);color:#fff;border:0;font-weight:600} -.primary:hover{background:#1d4a92} +.primary:hover{filter:brightness(.88)} .ghost{background:transparent;border:1px solid var(--rule);color:var(--ink)} .speak{text-align:left;line-height:1.5;padding:10px 18px;border-radius:18px} .list{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-direction:column;gap:8px} .task{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;padding:12px 14px;border:1px solid var(--rule);border-radius:12px;background:#fff} .task .dot{width:10px;height:10px;border-radius:50%;border:1.5px solid var(--muted)} .task .when,.task .state{font-size:13px;color:var(--muted)} -.task.is-changed{border-color:var(--accent);background:#eef3fc}.task.is-changed .dot{background:var(--accent);border-color:var(--accent)}.task.is-changed .state{color:var(--accent-ink);font-weight:600} +.task.is-changed{border-color:var(--accent);background:var(--tint)}.task.is-changed .dot{background:var(--accent);border-color:var(--accent)}.task.is-changed .state{color:var(--accent-ink);font-weight:600} .empty{color:var(--muted);font-size:14px} .cards{display:flex;flex-direction:column;gap:10px} .card{padding:14px 16px;border:1.5px solid var(--accent);border-radius:14px;background:#fff} diff --git a/public/demos/engine.js b/public/demos/engine.js new file mode 100644 index 0000000..1dd0f8d --- /dev/null +++ b/public/demos/engine.js @@ -0,0 +1,73 @@ +// Shared engine for the product concept demos. Fictional data only: no microphone, model, storage or network. +// Each product page imports this with its own flows (wording taken from that product's own UI and README). +const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; }; +const button = (label, cls, on) => { const b = el('button', cls, label); b.type = 'button'; b.addEventListener('click', on); return b; }; +const wait = ms => new Promise(r => setTimeout(r, matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : ms)); + +export function run(config) { + const $ = s => document.querySelector(s); + let state; + const flows = config.flows; + function render() { + const f = flows[state.flow]; + $('[data-rows-title]').textContent = f.title; + const list = $('[data-list]'); list.replaceChildren(); + if (!state.rows.length) list.append(el('li', 'empty', config.empty)); + state.rows.forEach(([name, meta, st], i) => { + const li = el('li', `task${state.changed.has(i) ? ' is-changed' : ''}`); + li.append(el('span', 'dot'), el('span', 'name', name), el('span', 'when', meta), el('span', 'state', st || '')); + list.append(li); + }); + } + function log(who, text) { + const li = el('li', `msg msg--${who}`); + li.append(el('span', 'who', config.who[who]), el('p', null, text)); + $('[data-log]').append(li); + } + async function act() { + const f = flows[state.flow]; + $('[data-say]').replaceChildren(); + log('you', f.input); + await wait(500); + log('ai', f.reply); + await wait(300); + f.cards.forEach((c, i) => { + const card = el('article', 'card'); + card.append(el('p', 'card-label', c.label), el('p', 'card-main', c.main)); + for (const line of c.lines || []) card.append(el('p', 'card-quote', line)); + const note = el('p', 'card-note', c.ask || config.ask); card.append(note); + const actions = el('div', 'card-actions'); + actions.append(button(c.ok, 'primary', async () => { + actions.replaceChildren(); + if (c.pending) { note.textContent = c.pending; await wait(900); } + if (c.set) { state.rows[c.set[0]] = c.set[1]; state.changed.add(c.set[0]); } + if (c.add) { state.rows.push(c.add); state.changed.add(state.rows.length - 1); } + render(); + card.classList.add('is-done'); note.textContent = c.doneShort || config.doneShort; + $('[data-status]').textContent = c.done; + }), button(c.cancel || config.cancel, 'ghost', () => { + actions.replaceChildren(); card.classList.add('is-dropped'); + note.textContent = c.cancelled || config.cancelled; + $('[data-status]').textContent = config.cancelledShort; + })); + card.append(actions); + $('[data-cards]').append(card); + if (i === 0) actions.querySelector('button').focus(); + }); + } + function start(flow) { + const id = flows[flow] ? flow : Object.keys(flows)[0]; + state = {flow: id, rows: flows[id].rows.map(r => [...r]), changed: new Set()}; + document.querySelectorAll('[data-flow]').forEach(a => a.setAttribute('aria-current', String(a.dataset.flow === id))); + $('[data-log]').replaceChildren(); $('[data-cards]').replaceChildren(); $('[data-status]').textContent = ''; + (flows[id].before || []).forEach(([who, text]) => log(who, text)); + render(); + const say = $('[data-say]'); say.replaceChildren(); + say.append(el('p', 'say-hint', config.hint), button(`${config.act}:「${flows[id].input}」`, 'primary speak', act)); + } + document.querySelectorAll('[data-flow]').forEach(a => a.addEventListener('click', e => { + e.preventDefault(); history.replaceState(null, '', `?flow=${a.dataset.flow}`); start(a.dataset.flow); + })); + $('[data-reset]').addEventListener('click', () => start(state.flow)); + start(new URLSearchParams(location.search).get('flow')); +} diff --git a/public/demos/launchloom/flows.js b/public/demos/launchloom/flows.js new file mode 100644 index 0000000..f357efd --- /dev/null +++ b/public/demos/launchloom/flows.js @@ -0,0 +1,127 @@ +// Launchloom concept demo. Fictional data; wording from the product's own UI and README. See ../engine.js. +import {run} from '../engine.js'; + +run({ + "ask": "まだ反映していません。この内容でよいですか?", + "doneShort": "反映済み", + "cancel": "取り消す", + "cancelled": "取り消しました。元のままです。", + "cancelledShort": "取り消しました。", + "empty": "まだありません", + "hint": "ボタンを押すと、この言葉を入れたことにします。", + "act": "伝える", + "who": { + "you": "あなた", + "ai": "Launchloom", + "other": "素材" + }, + "flows": { + "feature": { + "tab": "01 新機能の告知", + "title": "制作キット", + "rows": [ + [ + "操作録画.mp4", + "実画面 · 0:30", + "取り込み済み" + ] + ], + "input": "新しい書き出し機能を、使う人に届けたい。", + "reply": "構成を作りました。構成の確認待ちです。読んでから動画を作ります。", + "cards": [ + { + "label": "構成の確認待ち", + "main": "見出し:書き出しが、1クリックに。", + "lines": [ + "字幕 4枚 · 横長 16:9 / 縦長 9:16" + ], + "ask": "動画に入れる言葉を確認してください。", + "ok": "保存して動画を作る", + "pending": "書き出しています…", + "doneShort": "制作キットを書き出しました(模擬)", + "add": [ + "制作キット.zip", + "動画 · LP · 投稿案", + "下書き" + ], + "done": "横長・縦長の動画、LP、投稿案を書き出しました(模擬)。投稿はしていません。" + } + ] + }, + "revise": { + "tab": "02 LPの改訂", + "title": "紹介ページ", + "rows": [ + [ + "紹介ページ", + "CTA:今すぐ試す", + "書き出し済み" + ] + ], + "input": "ボタンの言葉を「無料ではじめる」に変えて。", + "reply": "改訂案を作りました。録り直しはいりません。", + "cards": [ + { + "label": "改訂の確認", + "main": "CTA 今すぐ試す → 無料ではじめる", + "lines": [ + "録り直し:なし" + ], + "ask": "この内容で書き出し直しますか?", + "ok": "改訂して書き出す", + "pending": "書き出しています…", + "doneShort": "書き出しました(模擬)", + "set": [ + 0, + [ + "紹介ページ", + "CTA:無料ではじめる", + "改訂済み" + ] + ], + "done": "変えたところだけ書き出し直しました(模擬)。公開先への反映はしていません。" + } + ] + }, + "publish": { + "tab": "03 公開はあなた", + "title": "投稿案", + "rows": [ + [ + "X", + "「書き出しが、1クリックに。」", + "下書き" + ], + [ + "LinkedIn", + "新機能のお知らせ", + "下書き" + ] + ], + "input": "Xの投稿だけ、明日の朝に出したい。", + "reply": "承認待ちにしました。承認は、文面・画像・日時に結びつきます。", + "cards": [ + { + "label": "承認待ち", + "main": "X 「書き出しが、1クリックに。」", + "lines": [ + "予約:明日 9:00", + "承認したものだけが予約されます" + ], + "ask": "この投稿を承認しますか?", + "ok": "承認して予約", + "doneShort": "承認済み(模擬)", + "set": [ + 0, + [ + "X", + "明日 9:00", + "承認済み" + ] + ], + "done": "承認しました(模擬)。実際の投稿や予約はしていません。" + } + ] + } + } +}); diff --git a/public/demos/launchloom/index.html b/public/demos/launchloom/index.html new file mode 100644 index 0000000..de816a9 --- /dev/null +++ b/public/demos/launchloom/index.html @@ -0,0 +1,41 @@ + + + + + + 作った、その先まで。 — Launchloom 構想デモ + + + + + + + + +

                  構想デモ・架空データ。書き出しも投稿も行いません。

                  +
                  Launchloom 構想デモ製品ページへ戻る
                  +
                  +

                  作った、その先まで。

                  + +
                  +
                  +

                  制作

                  +
                    +
                    +
                    +
                    +

                    +
                      +
                      +

                      + +
                      +
                      + +
                      + + diff --git a/public/demos/noa/flows.js b/public/demos/noa/flows.js new file mode 100644 index 0000000..0bddf61 --- /dev/null +++ b/public/demos/noa/flows.js @@ -0,0 +1,151 @@ +// 星藍ノア concept demo. Fictional data; wording from the product's own UI and README. See ../engine.js. +import {run} from '../engine.js'; + +run({ + "ask": "まだ反映していません。この内容でよいですか?", + "doneShort": "反映済み", + "cancel": "取り消す", + "cancelled": "取り消しました。元のままです。", + "cancelledShort": "取り消しました。", + "empty": "まだありません", + "hint": "ボタンを押すと、この言葉を入れたことにします。", + "act": "コメントする", + "who": { + "you": "視聴者", + "ai": "ノア", + "other": "配信" + }, + "flows": { + "comment": { + "tab": "01 コメントに返す", + "title": "いまの配信", + "rows": [ + [ + "雑談", + "家具の話", + "話している" + ] + ], + "before": [ + [ + "other", + "ノア:「机と椅子とベッドを選んだけど、服の置き場所がないね。」" + ] + ], + "input": "どの家具が好き?", + "reply": "話の切れ目で返します。出力ゲートで確かめてから声にします。", + "cards": [ + { + "label": "返事の候補", + "main": "「椅子かな。座ったまま考えごとができるから。」", + "lines": [ + "表情:照れ(あたたかい光)" + ], + "ask": "この返事を声にしますか?", + "ok": "声にする", + "pending": "出力ゲートで確かめています…", + "doneShort": "声と表情で返しました(模擬)", + "cancel": "話さない", + "cancelled": "話さずに次へ。通らない返事は読み上げません。", + "set": [ + 0, + [ + "雑談", + "コメントに返事", + "返した" + ] + ], + "done": "コハクの声と表情で返しました(模擬)。" + } + ] + }, + "game": { + "tab": "02 ゲーム実況", + "title": "いまの配信", + "rows": [ + [ + "スマブラSP", + "観測:試合中", + "実況中" + ] + ], + "before": [ + [ + "other", + "観測:相手のストック 0。試合終了。" + ] + ], + "input": "勝った?", + "reply": "観測した状態に合う言葉かを確かめてから話します。", + "cards": [ + { + "label": "実況の候補", + "main": "「やったー、勝利!いい試合だったね。」", + "lines": [ + "観測:勝利 と照合", + "演出:喜び(きらきら)" + ], + "ask": "この実況を声にしますか?", + "ok": "声にする", + "pending": "出力ゲートで確かめています…", + "doneShort": "喜びの演出で返しました(模擬)", + "cancel": "話さない", + "cancelled": "話さずに次へ。", + "set": [ + 0, + [ + "スマブラSP", + "観測:勝利", + "実況した" + ] + ], + "done": "勝利を、喜びの演出といっしょに実況しました(模擬)。" + } + ] + }, + "blocks": { + "tab": "03 NOA BLOCKS", + "title": "いまの配信", + "rows": [ + [ + "NOA BLOCKS", + "危険ゾーン", + "プレイ中" + ] + ], + "before": [ + [ + "other", + "ノア:「ちょっと積みすぎたかも…」(汗)" + ] + ], + "input": "がんばれ!", + "reply": "4ライン消えました。自己ベストを更新しました。", + "cards": [ + { + "label": "実況の候補", + "main": "「よし、4ライン!」", + "lines": [ + "自己ベスト更新", + "演出:紙吹雪とチャイム" + ], + "ask": "この実況を声にしますか?", + "ok": "声にする", + "pending": "出力ゲートで確かめています…", + "doneShort": "記録更新の演出で返しました(模擬)", + "cancel": "話さない", + "cancelled": "話さずに次へ。", + "set": [ + 0, + [ + "NOA BLOCKS", + "自己ベスト更新", + "記録" + ] + ], + "done": "記録更新を、紙吹雪とチャイムで祝いました(模擬)。" + } + ] + } + } +}); diff --git a/public/demos/noa/index.html b/public/demos/noa/index.html new file mode 100644 index 0000000..7040c27 --- /dev/null +++ b/public/demos/noa/index.html @@ -0,0 +1,41 @@ + + + + + + コメントがなくても、話が続く。 — 星藍ノア 構想デモ + + + + + + + + +

                      構想デモ・架空のコメント。声・AI・配信は動かしていません。

                      +
                      星藍ノア 構想デモ製品ページへ戻る
                      +
                      +

                      コメントがなくても、話が続く。

                      + +
                      +
                      +

                      配信

                      +
                        +
                        +
                        +
                        +

                        +
                          +
                          +

                          + +
                          +
                          + +
                          + + diff --git a/scripts/ui-capture.mjs b/scripts/ui-capture.mjs index 6eef18c..3555b70 100644 --- a/scripts/ui-capture.mjs +++ b/scripts/ui-capture.mjs @@ -72,15 +72,15 @@ export const SHOTS = Object.freeze([ { file: 'noa-viewing-en-desktop.png', route: '/en/products/noa/', width: 1440, height: 1200, scrollTo: '#noa-viewing', assertVisible: '#noa-viewing', state: 'Noa en public viewing viewing, desktop, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'noa-viewing-en-mobile.png', route: '/en/products/noa/', width: 390, height: 1400, scrollTo: '#noa-viewing', assertVisible: '#noa-viewing', state: 'Noa en public viewing viewing, mobile, reduced motion, real time', reducedMotion: true, realtime: true }, // Public Noa links: inspect the actual changed watch cards and localized proof destinations. - { file: 'noa-watch-desktop.png', route: '/products/noa/', width: 1440, height: 1200, openDetails: '.rd-kept--fold', scrollTo: '#watch', assertVisible: '#watch .nlp-watch', state: 'Noa individual public video links, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'noa-watch-mobile.png', route: '/products/noa/', width: 390, height: 1800, openDetails: '.rd-kept--fold', scrollTo: '#watch', assertVisible: '#watch .nlp-watch', state: 'phone, Noa individual public video links, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'noa-scope-en-desktop.png', route: '/en/products/noa/', width: 1440, height: 1200, scrollTo: '#scope', assertVisible: '#scope .rd-link-row', state: 'English Noa edited-test and explainer destinations, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'noa-scope-en-mobile.png', route: '/en/products/noa/', width: 390, height: 1600, scrollTo: '#scope', assertVisible: '#scope .rd-link-row', state: 'phone, English Noa edited-test and explainer destinations, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'noa-watch-desktop.png', route: '/products/noa/', width: 1440, height: 1200, openDetails: '.rd-more', scrollTo: '#watch', assertVisible: '#watch .nlp-watch', state: 'Noa individual public video links, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'noa-watch-mobile.png', route: '/products/noa/', width: 390, height: 1800, openDetails: '.rd-more', scrollTo: '#watch', assertVisible: '#watch .nlp-watch', state: 'phone, Noa individual public video links, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'noa-scope-en-desktop.png', route: '/en/products/noa/', width: 1440, height: 1200, scrollTo: '#now', assertVisible: '#now .rd-link-row', state: 'English Noa edited-test and explainer destinations, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'noa-scope-en-mobile.png', route: '/en/products/noa/', width: 390, height: 1600, scrollTo: '#now', assertVisible: '#now .rd-link-row', state: 'phone, English Noa edited-test and explainer destinations, reduced motion, real time', reducedMotion: true, realtime: true }, // Japanese Genie page (LP re-composition): its sections at a readable size, real time. // Japanese Oathra page (2026-09-29 brief): each section, real time. // Agent Team page (2026-09-29 patch): hero, capabilities and the next-UI design with its film. { file: 'agent-team-hero.png', route: '/products/agent-team/', width: 1440, height: 1000, state: 'Agent Team hero, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'agent-team-features.png', route: '/products/agent-team/', width: 1440, height: 1000, scrollTo: '#capabilities', state: 'Agent Team capabilities, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'agent-team-features.png', route: '/products/agent-team/', width: 1440, height: 1000, openDetails: '.rd-more', scrollTo: '#capabilities', state: 'Agent Team capabilities, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'agent-team-next-ui.png', route: '/products/agent-team/', width: 1440, height: 1400, scrollTo: '#next-ui', state: 'Agent Team next-UI design film and frames, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'agent-team-next-ui-en.png', route: '/en/products/agent-team/', width: 1440, height: 1200, scrollTo: '#next-ui', state: 'Agent Team next-UI frames, English, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'agent-team-m-next-ui.png', route: '/products/agent-team/', width: 390, height: 1400, scrollTo: '#next-ui', state: 'phone, Agent Team next-UI, reduced motion, real time', reducedMotion: true, realtime: true }, @@ -97,19 +97,19 @@ export const SHOTS = Object.freeze([ { file: 'oathra-lp-tall.png', route: '/products/oathra/', width: 1440, height: 1000, state: 'Oathra whole page, reduced motion, real time', reducedMotion: true, realtime: true, fullPage: true }, { file: 'oathra-lp-nojs-390.png', route: '/products/oathra/', width: 390, height: 1400, scrollTo: '#trust', state: 'phone, JavaScript disabled, Oathra delegation and recordings, real time', realtime: true, noScript: true }, { file: 'genie-lp-hero.png', route: '/products/genie/', width: 1440, height: 1000, state: 'Genie LP hero, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'genie-lp-features.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '#features', state: 'Genie LP features with stills, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'genie-lp-features.png', route: '/products/genie/', width: 1440, height: 1000, openDetails: '.rd-more', scrollTo: '#features', state: 'Genie LP features with stills, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'genie-lp-demos.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '#demos', state: 'Genie LP three demos, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'genie-lp-artifacts.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '#artifacts', state: 'Genie LP saved artifacts, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'genie-lp-trust.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '#trust', state: 'Genie LP scope, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'genie-lp-start.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '#start', state: 'Genie LP first task, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'genie-lp-start.png', route: '/products/genie/', width: 1440, height: 1000, openDetails: '.rd-more', scrollTo: '#start', state: 'Genie LP first task, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'genie-lp-m-hero.png', route: '/products/genie/', width: 390, height: 844, state: 'phone, Genie LP hero, reduced motion, real time', reducedMotion: true, realtime: true }, // JavaScript disabled, at phone width, section by section (a whole-page shot is too small to read). { file: 'genie-lp-nojs-demos.png', route: '/products/genie/', width: 390, height: 1400, scrollTo: '#demos', state: 'phone, JavaScript disabled, three demos, real time', realtime: true, noScript: true }, { file: 'genie-lp-nojs-artifacts.png', route: '/products/genie/', width: 390, height: 1400, scrollTo: '#artifacts', state: 'phone, JavaScript disabled, saved artifacts, real time', realtime: true, noScript: true }, { file: 'genie-lp-nojs-next-ui.png', route: '/products/genie/', width: 390, height: 1400, scrollTo: '#next-ui', state: 'phone, JavaScript disabled, next-UI frames, real time', realtime: true, noScript: true }, { file: 'home-nojs-chapter.png', route: '/', width: 390, height: 1400, scrollTo: 'a.rd-row[href="/products/launchloom/"]', assertVisible: 'a.rd-row[href="/products/launchloom/"]', state: 'phone, JavaScript disabled, product list (redesign)', realtime: true, noScript: true }, - { file: 'genie-lp-m-features.png', route: '/products/genie/', width: 390, height: 1900, scrollTo: '#features', state: 'phone, Genie LP 01 with the next-TaskDock figure (4:3), reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'genie-lp-nojs-features.png', route: '/products/genie/', width: 390, height: 1900, scrollTo: '#features', state: 'phone, JavaScript disabled, Genie LP 01, real time', realtime: true, noScript: true }, + { file: 'genie-lp-m-features.png', route: '/products/genie/', width: 390, height: 1900, openDetails: '.rd-more', scrollTo: '#features', state: 'phone, Genie LP 01 with the next-TaskDock figure (4:3), reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'genie-lp-nojs-features.png', route: '/products/genie/', width: 390, height: 1900, openDetails: '.rd-more', scrollTo: '#features', state: 'phone, JavaScript disabled, Genie LP 01, real time', realtime: true, noScript: true }, { file: 'genie-lp-return-motion.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '[data-glp-return]', state: 'Genie LP closing line, normal motion, after it ran once, real time', realtime: true }, { file: 'genie-lp-return.png', route: '/products/genie/', width: 1440, height: 1000, scrollTo: '[data-glp-return]', state: 'Genie LP closing line returning to the mark, real time', realtime: true }, { file: 'genie-lp-m-demos.png', route: '/products/genie/', width: 390, height: 844, scrollTo: '#demos', state: 'phone, Genie LP demos, reduced motion, real time', reducedMotion: true, realtime: true }, @@ -164,14 +164,14 @@ export const SHOTS = Object.freeze([ { file: 'header-solid-desktop.png', route: '/', width: 1440, height: 1000, scrollTo: '#products', scrollBack: 240, state: 'home, scrolled down then up 240px: solid header over the product list, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'header-solid-mobile.png', route: '/products/launchloom/', width: 390, height: 844, scrollTo: '#film', scrollBack: 240, state: 'phone, Launchloom scrolled down then up 240px: solid header, reduced motion, real time', reducedMotion: true, realtime: true }, // Product long-form sections folded behind one summary row, closed and opened. - { file: 'product-fold-mobile.png', route: '/products/launchloom/', width: 390, height: 844, scrollTo: '.rd-kept--fold', state: 'phone, Launchloom folded detail row, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'product-fold-open.png', route: '/products/launchloom/', width: 1440, height: 1000, openDetails: '.rd-kept--fold', scrollTo: '.rd-kept--fold', state: 'Launchloom detail row opened by a click, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'product-fold-mobile.png', route: '/products/launchloom/', width: 390, height: 844, scrollTo: '.rd-more', state: 'phone, Launchloom folded detail row, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'product-fold-open.png', route: '/products/launchloom/', width: 1440, height: 1000, openDetails: '.rd-more', scrollTo: '.rd-more', state: 'Launchloom detail row opened by a click, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'product-consult-launchloom-mobile.png', route: '/products/launchloom/', width: 390, height: 844, scrollTo: '#consult', state: 'phone, Launchloom consult band with the use-it entry, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'product-launchloom-top.png', route: '/products/launchloom/', width: 1440, height: 1000, state: 'Launchloom hero: try link + film link, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'product-launchloom-top-mobile.png', route: '/products/launchloom/', width: 390, height: 844, state: 'phone, Launchloom hero: try link + film link, reduced motion, real time', reducedMotion: true, realtime: true }, // 2026-10-19: English Genie and Oathra follow the Japanese composition. - { file: 'genie-en-demos.png', route: '/en/products/genie/', width: 1440, height: 1000, openDetails: '.rd-kept--fold', scrollTo: '#demos', state: 'English Genie 02 requests, reduced motion, real time', reducedMotion: true, realtime: true }, - { file: 'genie-en-artifacts-mobile.png', route: '/en/products/genie/', width: 390, height: 1400, openDetails: '.rd-kept--fold', scrollTo: '#artifacts', state: 'phone, English Genie 03 files, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'genie-en-demos.png', route: '/en/products/genie/', width: 1440, height: 1000, openDetails: '.rd-more', scrollTo: '#demos', state: 'English Genie 02 requests, reduced motion, real time', reducedMotion: true, realtime: true }, + { file: 'genie-en-artifacts-mobile.png', route: '/en/products/genie/', width: 390, height: 1400, openDetails: '.rd-more', scrollTo: '#artifacts', state: 'phone, English Genie 03 files, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'oathra-en-demos.png', route: '/en/products/oathra/', width: 1440, height: 1000, scrollTo: '#concept-demos', state: 'English Oathra concept demos, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'oathra-en-ring-mobile.png', route: '/en/products/oathra/', width: 390, height: 1400, scrollTo: '#ring', state: 'phone, English Oathra ring, reduced motion, real time', reducedMotion: true, realtime: true }, { file: 'oathra-en-status.png', route: '/en/products/oathra/', width: 1440, height: 1000, scrollTo: '#status', state: 'English Oathra where it stands, reduced motion, real time', reducedMotion: true, realtime: true }, diff --git a/scripts/verify-outcome-first.py b/scripts/verify-outcome-first.py index c6709af..55d7cd2 100644 --- a/scripts/verify-outcome-first.py +++ b/scripts/verify-outcome-first.py @@ -64,6 +64,14 @@ def source_contract(): def product_checks(page, lang, product, width): contract = source_contract() + # 2026-10-19, vision first: the one action in the first view is visible on entry and a usable touch target. + first = page.locator('.rd-phero .rd-ctas a.rd-btn--ink').first + expect(first).to_be_visible() + assert first.bounding_box()['height'] >= 44, 'The first action remains a usable touch target' + # The original sections, with the landing's own primary action, sit in the one detail fold; open it as a visitor would. + more = page.locator('details.rd-more > summary') + if more.count(): + more.first.click() primary = page.locator('.owned-primary').first expect(primary).to_be_visible() assert primary.bounding_box()['height'] >= 44, 'Primary action remains a usable touch target' @@ -98,6 +106,15 @@ def product_checks(page, lang, product, width): if width == 390: page.locator('a.nlp-secondary[href="#watch"]').click() expect(page.locator('#watch')).to_be_visible() + elif page.locator('#concept-demos .vp-try').count(): + # Pages that lead with concept demos: the hands-on sample is the demo page each use case opens. + try_link = page.locator('#concept-demos .vp-try').first + expect(try_link).to_be_visible() + assert try_link.get_attribute('href').startswith(f'/demos/{product}/?flow=') + if width == 390: + try_link.click() + expect(page.locator('.speak')).to_be_visible() + page.go_back() else: sample = page.locator('[data-outcome-sample]:not([hidden])') expect(sample).to_be_visible() diff --git a/scripts/verify_showcase.py b/scripts/verify_showcase.py index 525521b..6c40e54 100644 --- a/scripts/verify_showcase.py +++ b/scripts/verify_showcase.py @@ -142,7 +142,8 @@ def native_noa(page, failed=False): assert failed_request.value.failure failures.append(failed_request.value.failure) expect(video).to_be_visible() - expect(page.locator('.nlp-actions .owned-primary')).to_be_visible() + # The stream stays reachable when the film fails: the first action opens the actual YouTube edit. + expect(page.locator('.rd-phero .rd-ctas a.rd-btn--ink[href*="youtube.com"]')).to_be_visible() return {'native_error': True, 'same_source_retry': True, 'channel_accessible': True, 'failed_requests': failures, 'decoded_frames': 0} video.evaluate('v=>v.play()') @@ -176,9 +177,16 @@ def native_noa(page, failed=False): elif lang == 'ja' and product == 'noa': info = native_noa(page) else: - page.wait_for_selector(selector+' [data-mode="recording"]') - check_story(page, selector, product) - enter_recording(page, selector) + # A product page's film section shows the footage itself (no story overlay or tabs). + # Elsewhere the player sits in the detail fold; open it as a visitor would. + if not page.locator('[data-rd-reel] '+selector).count() and page.locator('details.rd-more > summary').count(): + page.locator('details.rd-more > summary').first.click() + if page.locator('[data-rd-reel] '+selector).count(): + assert page.locator('[data-rd-reel] .rm-demo-toolbar, [data-rd-reel] .rm-live-demo').count() == 0 + else: + page.wait_for_selector(selector+' [data-mode="recording"]') + check_story(page, selector, product) + enter_recording(page, selector) video = page.locator(selector+' video') assert video.evaluate('(v)=>v.paused && v.currentTime===0') assert video.get_attribute('src') is None, 'A recording must not be fetched before play' @@ -217,8 +225,11 @@ def native_noa(page, failed=False): elif lang == 'ja' and product == 'noa': details = native_noa(page, failed=True) else: - page.wait_for_selector(selector+' [data-mode="recording"]') - enter_recording(page, selector) + if not page.locator('[data-rd-reel] '+selector).count(): + if page.locator('details.rd-more > summary').count(): + page.locator('details.rd-more > summary').first.click() + page.wait_for_selector(selector+' [data-mode="recording"]') + enter_recording(page, selector) button = page.locator(selector).locator('.owned-film__play') button.click() page.wait_for_function('s=>document.querySelector(s).dataset.mediaState==="unavailable"',arg=selector) @@ -230,8 +241,11 @@ def native_noa(page, failed=False): with page.expect_request('**'+PAGE_FILMS[(lang, product)][0] if (lang, product) in PAGE_FILMS else '**/media/products/*.mp4'): button.click() page.wait_for_function('s=>document.querySelector(s).dataset.mediaState==="unavailable"',arg=selector) - check_story(page, selector, product) - details = {'network_aborted':True,'retry':True,'return_to_story':True} + # Outside a product film section the player still offers its story after a failure. + in_reel = page.locator('[data-rd-reel] '+selector).count() > 0 + if not in_reel: + check_story(page, selector, product) + details = {'network_aborted':True,'retry':True,'return_to_story':not in_reel} report['failure_recovery'].append({'case':key,**details}) except Exception as exc: report['errors'].append({'case':'failure-'+key,'error':str(exc)}) @@ -254,6 +268,10 @@ def native_noa(page, failed=False): page = context.new_page() try: page.goto(origin+'/products/'+product+'/',wait_until='networkidle') + # Without JavaScript the first action is visible, and the detail fold opens to the original sections. + assert page.locator('.rd-phero .rd-ctas a.rd-btn--ink').first.is_visible() + if page.locator('details.rd-more > summary').count(): + page.locator('details.rd-more > summary').first.click() assert page.locator('.owned-primary').first.is_visible() if product == 'noa': expect(page.locator('.nlp-film video')).to_be_visible() diff --git a/src/premium-film-cuts.mjs b/src/premium-film-cuts.mjs index 34c4e3f..bc13cff 100644 --- a/src/premium-film-cuts.mjs +++ b/src/premium-film-cuts.mjs @@ -25,10 +25,10 @@ export const premiumFilmCuts = Object.freeze({ clips: [[3, 9], [14, 20]], label: { ja: '入力から判定へ', en: 'Input to decision' }, }, - // The teammates (3.5 s), then the run's result and its check record (20.5 s). + // The teammates (4 s, after the page has settled), then the run's result and its check record (20.5 s). 'agent-team': { - sourceWindow: [3.5, 27.5], - clips: [[3.5, 8.5], [20.5, 27.5]], + sourceWindow: [4, 27.5], + clips: [[4, 9], [20.5, 27.5]], label: { ja: 'チームから確認済みの版へ', en: 'Team to a checked version' }, }, launchloom: { diff --git a/src/redesign-copy.mjs b/src/redesign-copy.mjs index f96c14f..a77a930 100644 --- a/src/redesign-copy.mjs +++ b/src/redesign-copy.mjs @@ -36,7 +36,7 @@ export const redesignCopy = { scopeNote: () => '最新の利用条件は、各リポジトリでご確認ください。', scopeLabel: 'SCOPE / 範囲', proofLabel: 'PROOF / 公開している根拠', linkKinds: { demo: 'DEMO', source: 'SOURCE', evidence: 'EVIDENCE' }, sourceLabel: 'コードをGitHubで読む', evidenceLabel: '検証資料を読む', - consultKicker: '(07) この技術で相談する', consultCta: '開発を相談する', useLaunchloom: '自分の製品で使いたい', + consultKicker: '(07) この技術で相談する', consultTitle: 'この仕組みを、あなたの仕事にも。', consultCta: '開発を相談する', useLaunchloom: '自分の製品で使いたい', next: 'NEXT', fig: 'FIG.', filmKinds: { recording: '実録画(無音・編集あり)', @@ -80,7 +80,7 @@ export const redesignCopy = { scopeNote: () => 'Current terms live in each repository.', scopeLabel: 'SCOPE', proofLabel: 'PROOF / PUBLISHED EVIDENCE', linkKinds: { demo: 'DEMO', source: 'SOURCE', evidence: 'EVIDENCE' }, sourceLabel: 'Read the code on GitHub', evidenceLabel: 'Read the evaluation record', - consultKicker: '(07) Build with it', consultCta: 'Discuss a project', useLaunchloom: 'Use it for my product', + consultKicker: '(07) Build with it', consultTitle: 'Bring this to your work.', consultCta: 'Discuss a project', useLaunchloom: 'Use it for my product', next: 'NEXT', fig: 'FIG.', filmKinds: { recording: 'Real recording (silent · edited)', diff --git a/src/redesign.mjs b/src/redesign.mjs index 720c8b5..2159d0b 100644 --- a/src/redesign.mjs +++ b/src/redesign.mjs @@ -27,7 +27,7 @@ import {capabilitiesCheckedAt} from './products.mjs'; import {productNavigation} from './site-experience.mjs'; import {recordings} from './films.mjs'; import {oathraExperienceFilm} from './oathra-experience-films.mjs'; -import {visionSections} from './vision-pages.mjs'; +import {VISION, visionSections} from './vision-pages.mjs'; const {checkedAt: sourcesCheckedAt} = JSON.parse(await fs.readFile(new URL('../site.config.json', import.meta.url), 'utf8')); @@ -318,7 +318,10 @@ function productMain(p, list, lang, {kept, figure, source, filmAnchor}) { // take-home files) stay on the page. Every other kept section, the capabilities and the scope go into one fold. const VISION_KEPT = ['olp-demos', 'olp-ring', 'olp-steps', 'glp-demos', 'glp-artifacts']; const isVision = k => VISION_KEPT.some(c => new RegExp(`^]*class="[^"]*\\b${c}\\b`).test(k.trimStart())); - const visionKept = kept.filter(isVision).join(''), moreKept = kept.filter(k => !isVision(k)).join(''); + const visionKept = kept.filter(isVision).join(''); + // Where the page has concept demos, the old guided-sample entry points to them instead. + const tryDemos = VISION[p.id] ? `` : null; + const moreKept = kept.filter(k => !isVision(k)).join('').replace(/
                          [\s\S]*?<\/a><\/div>/, m => tryDemos || m); // Oathra's status table already shows every highlight; the ledger values must not appear twice. const showHighlights = !t.highlights.every(h => kept.join('').includes(esc(h.label))); // The proof line lives once, in #scope. @@ -336,11 +339,11 @@ ${oathraExperienceFilm(p, lang)} ${visionSections(p, lang)} ${visionKept}
                          ${figure ? `
                          ${figure}
                          ` : filmAnchor ? `${esc(r.filmBelow)} ` : `
                          ${video({...film, label: `${name} — ${kind}`}, lang)}
                          `}
                          ${esc(r.film(name))}${figure && film.kind === 'recording' ? (note ? `${esc(note)}` : '') : `${esc(filmAnchor ? r.stillKind : kind)}${figure || filmAnchor ? '' : ` ${esc(note)}`}${extra}`}
                          -

                          ${esc(r.nowKicker)}

                          ${esc(t.status)} · ${esc(t.license)}

                          ${showHighlights ? `
                          ${highlights}
                          ` : ''}
                          -
                          ${esc(r.moreSummary)}${esc(r.moreHint)}${moreKept ? `
                          ${moreKept}
                          ` : ''} +

                          ${esc(r.nowKicker)}

                          ${esc(t.status)} · ${esc(t.license)}

                          +
                          ${esc(r.moreSummary)}${esc(r.moreHint)}${showHighlights ? `

                          ${esc(r.highlights)}

                          ${highlights}
                          ` : ''}

                          ${esc(r.consultKicker)}

                          ${esc(t.consult)}

                          ${moreKept ? `
                          ${moreKept}
                          ` : ''}

                          ${esc(r.features)}

                          ${esc(t.short)}

                          ${source ? source : `

                          ${esc(r.featuresNote(capabilitiesCheckedAt))}

                          `}
                          ${features}

                          ${esc(r.scopeKicker)}

                          ${r.scopeTitle}

                          ${esc(r.scopeNote(sourcesCheckedAt))}

                          ${esc(r.scopeLabel)}

                          ${esc(t.scope)}

                          ${esc(r.proofLabel)}

                          ${esc(t.proof)}

                          -

                          ${esc(r.consultKicker)}

                          ${esc(t.consult)}

                          ${p.id === 'launchloom' ? `${esc(r.useLaunchloom)}` : ''}${esc(r.consultCta)}
                          +

                          ${esc(r.consultKicker)}

                          ${esc(r.consultTitle)}

                          ${p.id === 'launchloom' ? `${esc(r.useLaunchloom)}` : ''}${esc(r.consultCta)}
                          `; } diff --git a/src/redesign/redesign.css b/src/redesign/redesign.css index 219660c..4ca4296 100644 --- a/src/redesign/redesign.css +++ b/src/redesign/redesign.css @@ -508,3 +508,5 @@ body[data-rd] .rd-more[open]>summary span::after{content:" −"} body[data-rd] .rd-more>summary small{font-size:14px;color:var(--rd-muted)} body[data-rd] .rd-more>summary:hover span{color:var(--rd-accent-ink)} body[data-rd] .rd-more>summary:focus-visible{outline:2px solid var(--rd-accent-ink);outline-offset:-4px} +body[data-rd] .rd-consult-scope{padding:clamp(48px,6vw,88px) var(--rd-gutter);border-top:1px solid var(--rd-rule)} +body[data-rd] .rd-consult-scope>p:not(.rd-kicker){max-width:44em;margin:16px 0 0;font-size:16px;line-height:1.9;color:var(--rd-body)} diff --git a/src/site-content.mjs b/src/site-content.mjs index 7262836..95a0418 100644 --- a/src/site-content.mjs +++ b/src/site-content.mjs @@ -7,7 +7,7 @@ const entries = { oathra: { ja: ['https://oathra.reachmade.com/', 'https://reachmade.com/demos/oathra/', '用途別の構想デモを試す'], en: ['https://oathra.reachmade.com/en/', 'https://oathra.reachmade.com/en/#sim', 'Try the evidence engine'] }, aisecure: { ja: ['https://aisecure.reachmade.com/index.ja.html', 'https://aisecure.reachmade.com/index.ja.html#start', 'デモの起動手順を見る'], en: ['https://aisecure.reachmade.com/', 'https://aisecure.reachmade.com/#start', 'See how to run the demo'] }, 'agent-team': { ja: ['https://multibot.reachmade.com/ja/', 'https://multibot.reachmade.com/ja/#guide', '3ステップの使い方を見る'], en: ['https://multibot.reachmade.com/', 'https://multibot.reachmade.com/#workflow', 'See the three-step workflow'] }, - launchloom: { ja: ['https://launchloom.reachmade.com/ja/', 'https://launchloom.reachmade.com/ja/#kit', '完成した公開素材を見る'], en: ['https://launchloom.reachmade.com/', 'https://launchloom.reachmade.com/#kit', 'Explore the launch kit'] }, + launchloom: { ja: ['https://launchloom.reachmade.com/ja/', 'https://launchloom.reachmade.com/ja/#proof', '完成した公開素材を見る'], en: ['https://launchloom.reachmade.com/', 'https://launchloom.reachmade.com/#proof', 'Explore the launch kit'] }, }; export function productNavigation(product, language) { if (!['ja', 'en'].includes(language)) throw new TypeError('Unsupported language'); diff --git a/src/vision-pages.mjs b/src/vision-pages.mjs index e8f475f..eb36abb 100644 --- a/src/vision-pages.mjs +++ b/src/vision-pages.mjs @@ -43,6 +43,140 @@ export const VISION = Object.freeze({ mechKicker: 'How it works', mechTitle: 'Your words are the evidence.', mechLead: 'Nothing is added by guessing. Only changes that quote you are kept, after you confirm.', mech: [['Talk', 'In your own words'], ['Quote', 'The exact words, kept'], ['Hold', 'Not applied yet'], ['Confirm', 'Apply or discard'], ['Next time', 'Only what was decided']] } + }, + // AI Secure (FORIFOR/AISecure origin/main): docs/index.ja.html「AIに貼る前に、端末で止める。」; decisions, rule texts and + // coverage from aisecure/control/web/app.js and documents.py; the 入力 → 検査 → 判定 flow from control/web/index.html. + aisecure: { + demo: '/demos/aisecure/', + ja: { + kicker: '用途別の構想デモ', title: 'AIに貼る前に、
                          端末で止める。', lead: '判定・理由・検査範囲が、その場で出ます。送るかどうかは、あなたが決めます。', + label: '構想デモ・架空データ', uiNote: '', + demos: [ + {id: 'sheet', kind: '社外秘の資料', title: '見積もりのExcelを、AIに貼る前に。', body: '「社外秘」の表記を見つけて保留。理由と検査範囲が、その場で出ます。', tryLabel: '資料の検査を試す', + scene: [['task', '見積もり_次期モデル.xlsx', '未検査', '保留', 4], ['say', 'この資料の要点を確認してください。'], ['card', '判定', '保留 DOC-CLASS', '社外秘などの機密表記があります', '送信せずに検査', '外部送信していません', 4]]}, + {id: 'personal', kind: '個人情報', title: '顧客メモを、要約に出す前に。', body: '名前やメールアドレスらしい文字列を見つけて保留。取り扱いを確かめてから。', tryLabel: '個人情報の検査を試す', + scene: [['task', '顧客メモ.txt', '未検査', '保留', 4], ['say', 'この顧客メモを短くまとめて。'], ['card', '判定', '保留 DOC-PII', '個人情報らしい文字列があります', '送信せずに検査', '外部送信していません', 4]]}, + {id: 'secret', kind: '認証情報', title: 'スライドのメモに、キーが残っていた。', body: '認証情報らしい文字列は拒否。原本から消して、検査し直します。', tryLabel: '拒否になる例を試す', + scene: [['task', '提案資料.pptx', '未検査', '拒否', 4], ['say', 'このスライドの構成を見直して。'], ['card', '判定', '拒否 DOC-SECRET', '原本から削除し、検査し直してください', '送信せずに検査', '外部送信していません', 4]]} + ], + mechKicker: 'しくみ', mechTitle: 'その場で止まる。
                          送るのは、あなた。', mechLead: '検査はこの端末で。判定には、いつも理由と検査範囲が付きます。', + mech: [['入れる', '資料と、送る予定の指示'], ['手元で検査', '外部には送らない'], ['判定', '検査通過・保留・拒否'], ['理由と範囲', '根拠ルールと検査範囲'], ['送るかはあなた', '決めるのは人']] + }, + en: { + kicker: 'Concept demos by use', title: 'Before you paste it into AI,
                          it stops on your machine.', lead: 'The decision, its reasons and the coverage appear on the spot. Whether to send is up to you.', + label: 'Concept demo · fictional data', uiNote: 'The demos run in Japanese.', + demos: [ + {id: 'sheet', kind: 'Confidential file', title: 'A quote spreadsheet, before it goes to AI.', body: 'A “confidential” marking puts it on hold, with the reason and coverage on the spot.', tryLabel: 'Try checking a file', + scene: [['task', 'quote_next_model.xlsx', 'Not checked', 'Hold', 4], ['say', 'Summarise the key points of this file.'], ['card', 'Decision', 'Hold DOC-CLASS', 'A confidentiality marking was found', 'Check without sending', 'Nothing sent out', 4]]}, + {id: 'personal', kind: 'Personal data', title: 'Customer notes, before you ask for a summary.', body: 'Something that looks like a name or email address puts it on hold until you check.', tryLabel: 'Try a personal-data check', + scene: [['task', 'customer_notes.txt', 'Not checked', 'Hold', 4], ['say', 'Make these customer notes shorter.'], ['card', 'Decision', 'Hold DOC-PII', 'Something that looks like personal data', 'Check without sending', 'Nothing sent out', 4]]}, + {id: 'secret', kind: 'Credentials', title: 'A key left in the speaker notes.', body: 'Something that looks like a credential is rejected: remove it from the original and check again.', tryLabel: 'Try a rejected file', + scene: [['task', 'proposal.pptx', 'Not checked', 'Rejected', 4], ['say', 'Rework the structure of these slides.'], ['card', 'Decision', 'Rejected DOC-SECRET', 'Remove it from the original and check again', 'Check without sending', 'Nothing sent out', 4]]} + ], + mechKicker: 'How it works', mechTitle: 'It stops on the spot.
                          You decide to send.', mechLead: 'Checks run on this machine. Every decision comes with its reasons and coverage.', + mech: [['Add', 'The file and the prompt'], ['Check locally', 'Nothing leaves the machine'], ['Decide', 'Passed, hold or rejected'], ['Reasons and coverage', 'Rule IDs and what was checked'], ['You send', 'A person decides']] + } + }, + // Launchloom (FORIFOR/Launchloom origin/main): README.ja.md「作った、その先まで。」, homepage/ja「公開の判断は最後まで自分の + // 手に。」; stage and button names from launchloom/web/i18n.js and index.html; drafts only, publishing needs approval. + launchloom: { + demo: '/demos/launchloom/', + ja: { + kicker: '用途別の構想デモ', title: '作った、
                          その先まで。', lead: '収録1本が、動画と紹介ページと投稿文に。公開の判断は、最後まで自分の手に。', + label: '構想デモ・架空データ', uiNote: '', + demos: [ + {id: 'feature', kind: '新機能の告知', title: '30秒の録画を、告知一式に。', body: '構成を読んでから書き出し。横長・縦長の動画、LP、投稿案がそろいます。', tryLabel: '告知づくりを試す', + scene: [['task', '操作録画.mp4', '実画面 · 0:30'], ['say', '新しい書き出し機能を、使う人に届けたい。'], ['card', '構成の確認待ち', '見出し:書き出しが、1クリックに。', '字幕 4枚 · 横長 16:9 / 縦長 9:16', '保存して動画を作る', '制作キットを書き出しました', 4], ['new', '制作キット.zip', '動画 · LP · 投稿案(下書き)']]}, + {id: 'revise', kind: 'LPの改訂', title: 'ボタンひとつ分だけ、作り直す。', body: '録り直さずに改訂。変えたところだけ、書き出し直します。', tryLabel: 'LPの改訂を試す', + scene: [['task', '紹介ページ', 'CTA:今すぐ試す', 'CTA:無料ではじめる', 4], ['say', 'ボタンの言葉を「無料ではじめる」に変えて。'], ['card', '改訂の確認', 'CTA 今すぐ試す → 無料ではじめる', '録り直し:なし', '改訂して書き出す', '書き出しました', 4]]}, + {id: 'publish', kind: '公開はあなた', title: '投稿は、承認したものだけ。', body: '投稿案は下書きのまま。承認した投稿だけが予約されます。', tryLabel: '承認の流れを試す', + scene: [['task', 'X', '下書き', '承認済み · 明日 9:00', 4], ['task', 'LinkedIn', '下書き'], ['card', '承認待ち', 'X 「書き出しが、1クリックに。」', '文面・画像・日時に結びつけて承認', '承認して予約', '承認しました', 4]]} + ], + mechKicker: 'しくみ', mechTitle: '公開の判断は、
                          最後まで自分の手に。', mechLead: '読んでから書き出す。承認したものだけが外に出ます。', + mech: [['録画', '実際の画面から'], ['構成', '見出しと字幕'], ['確認', '読んでから書き出す'], ['書き出し', '動画・LP・投稿案'], ['公開はあなた', '承認したものだけ']] + }, + en: { + kicker: 'Concept demos by use', title: 'You built it.
                          Now take it further.', lead: 'One recording becomes a film, a page and posts. The decision to publish stays with you.', + label: 'Concept demo · fictional data', uiNote: 'The demos run in Japanese.', + demos: [ + {id: 'feature', kind: 'Feature launch', title: 'From a 30-second recording to a launch kit.', body: 'Read the plan, then render: landscape and vertical films, a page and post drafts.', tryLabel: 'Try a launch kit', + scene: [['task', 'screen_recording.mp4', 'Real screen · 0:30'], ['say', 'I want people to know about the new export.'], ['card', 'Plan waiting for review', 'Headline: Export, in one click.', '4 captions · 16:9 / 9:16', 'Save and render', 'Launch kit exported', 4], ['new', 'launch_kit.zip', 'Film · page · post drafts']]}, + {id: 'revise', kind: 'Page update', title: 'Rebuild just the one button.', body: 'Revise without re-recording; only what changed is rendered again.', tryLabel: 'Try a page revision', + scene: [['task', 'Product page', 'CTA: Try it now', 'CTA: Start free', 4], ['say', 'Change the button to “Start free”.'], ['card', 'Check the revision', 'CTA Try it now → Start free', 'Re-recording: none', 'Revise and export', 'Exported', 4]]}, + {id: 'publish', kind: 'You publish', title: 'Only approved posts go out.', body: 'Post drafts stay drafts. Only the posts you approve are scheduled.', tryLabel: 'Try the approval step', + scene: [['task', 'X', 'Draft', 'Approved · tomorrow 9:00', 4], ['task', 'LinkedIn', 'Draft'], ['card', 'Waiting for approval', 'X “Export, in one click.”', 'Approval is tied to the text, media and time', 'Approve and schedule', 'Approved', 4]]} + ], + mechKicker: 'How it works', mechTitle: 'The decision to publish
                          stays with you.', mechLead: 'Read it, then render. Only what you approve goes out.', + mech: [['Record', 'From the real screen'], ['Shape', 'Headlines and captions'], ['Review', 'Read before rendering'], ['Export', 'Film · page · posts'], ['You publish', 'Only what you approve']] + } + }, + // Agent Team (FORIFOR/Multibot origin/main): docs/site/build_site.py「頼むのは一度。作った版を、別のAIが確かめる。」and + // 「ひとりのAIに、全部は任せない。」; roles from frontend/src/lib/journey.ts; 通過 / 要修正 / 未確認, 「この版を採用」, + // 「まとめて取得」from Workroom.tsx and RunView.tsx. Checks are tied to a version's bytes (README). + 'agent-team': { + demo: '/demos/agent-team/', + ja: { + kicker: '用途別の構想デモ', title: 'ひとりのAIに、
                          全部は任せない。', lead: '頼むのは一度。作った版を、別のAIが確かめます。', + label: '構想デモ・架空データ', uiNote: '', + demos: [ + {id: 'tool', kind: '小さな道具', title: 'CSVの集計ツールを、テストつきで。', body: 'つくる係が作り、確かめる係が差し戻す。2版目で通過しました。', tryLabel: 'ツールづくりを試す', + scene: [['say', 'このCSVを年月別に集計するツールを作って。テストも付けて。'], ['task', '版1 · aggregate.py', '要修正'], ['task', '版2 · aggregate.py', '確認中', '通過', 4], ['card', '確かめる係', '版2 テスト:通過', '確認は、この版の中身に結びつきます', 'この版を採用', '採用版になりました', 4]]}, + {id: 'research', kind: '調べて比べる', title: '公開資料を、出典つきで比べる。', body: '調べる係が出典を集め、つくる係がメモに。照合できない点は「未確認」のまま。', tryLabel: '比較メモを試す', + scene: [['say', '3社の公開資料を比べて、違いをまとめて。'], ['task', '出典メモ', '調べる係 · 12件'], ['task', '比較メモ · 版2', '確認中', '通過 · 未確認 1件', 4], ['card', '確かめる係', '指摘3件 → 修正済み', '出典の照合:1件は未確認のまま', '確認の記録を見る', '未確認を残して届けます', 4]]}, + {id: 'docs', kind: '記録つきで受け取る', title: '採用した版だけ、まとめて受け取る。', body: '確認の記録がない版には警告。選んだ版を、記録ごと受け取ります。', tryLabel: '受け取りを試す', + scene: [['task', '版1 · guide.md', '確認の記録なし'], ['task', '版2 · guide.md', '通過', '採用版', 4], ['say', '採用した版だけ、まとめて受け取りたい。'], ['card', 'まとめて取得', '採用版 · 確認の記録 · 作業記録', 'ZIPで受け取ります', 'まとめて取得', '受け取りました', 4]]} + ], + mechKicker: 'しくみ', mechTitle: '作る係と、
                          確かめる係を分ける。', mechLead: '作った本人は、自分の間違いに気づきにくい。だから、別のAIがその版を確かめます。', + mech: [['お願い', '頼むのは一度'], ['分担', 'まとめ役が割り振る'], ['つくる', '版ごとに残る'], ['確かめる', '別のAIが、その版を'], ['記録つきで届く', '選ぶのはあなた']] + }, + en: { + kicker: 'Concept demos by use', title: 'Don’t leave everything
                          to one AI.', lead: 'Ask once. One AI makes the file; another checks that exact version.', + label: 'Concept demo · fictional data', uiNote: 'The demos run in Japanese.', + demos: [ + {id: 'tool', kind: 'A small tool', title: 'A CSV summary tool, with tests.', body: 'The maker builds it, the checker sends it back, and version 2 passes.', tryLabel: 'Try building a tool', + scene: [['say', 'Make a tool that totals this CSV by month, with tests.'], ['task', 'v1 · aggregate.py', 'Needs changes'], ['task', 'v2 · aggregate.py', 'Checking', 'Passed', 4], ['card', 'Checker', 'v2 tests: passed', 'The check is tied to this exact version', 'Use this version', 'Now the adopted version', 4]]}, + {id: 'research', kind: 'Research and compare', title: 'Compare public documents, with sources.', body: 'The researcher gathers sources, the maker writes the memo, and anything unmatched stays “unverified”.', tryLabel: 'Try a comparison memo', + scene: [['say', 'Compare these three companies’ public documents.'], ['task', 'Source notes', 'Researcher · 12'], ['task', 'Comparison memo · v2', 'Checking', 'Passed · 1 unverified', 4], ['card', 'Checker', '3 findings → fixed', 'Source check: 1 still unverified', 'See the check record', 'Delivered with the unverified point', 4]]}, + {id: 'docs', kind: 'Delivered with records', title: 'Take only the version you adopted.', body: 'A version without a check record is flagged. Take the one you chose, with its records.', tryLabel: 'Try taking delivery', + scene: [['task', 'v1 · guide.md', 'No check record'], ['task', 'v2 · guide.md', 'Passed', 'Adopted', 4], ['say', 'I only want the adopted version.'], ['card', 'Download all', 'Adopted version · check record · run log', 'As a ZIP', 'Download all', 'Received', 4]]} + ], + mechKicker: 'How it works', mechTitle: 'Separate the maker
                          from the checker.', mechLead: 'Whoever made it is the last to spot its mistakes, so another AI checks that version.', + mech: [['Ask', 'Once'], ['Share out', 'The lead assigns roles'], ['Make', 'Every version is kept'], ['Check', 'Another AI, that version'], ['Delivered with records', 'You choose']] + } + }, + // 星藍ノア (private repository; README「コメントがなくても話が続く。コメントが来たら自然な切れ目で返す。ゲームも実況 + // する。」): the output gate stays silent when a check fails; game lines from games/smash-ultimate/show.mjs and + // src/blocks-show.mjs; effects from public/effect-director.mjs; voice AivisSpeech コハク (docs/VOICE.md). + noa: { + demo: '/demos/noa/', + ja: { + kicker: '配信の構想デモ', title: 'コメントがなくても、
                          話が続く。', lead: 'コメントが来たら、自然な切れ目で返す。ゲームも実況する。', + label: '構想デモ・架空のコメント', uiNote: '', + demos: [ + {id: 'comment', kind: 'コメントに返す', title: '「どの家具が好き?」', body: '話の切れ目で、短く返事。確かめた言葉だけを、声と表情に。', tryLabel: '返事の流れを試す', + scene: [['task', '雑談', '家具の話', 'コメントに返事', 4], ['say', 'どの家具が好き?'], ['card', '返事の候補', '「椅子かな。座ったまま考えごとができるから。」', '表情:照れ', '声にする', '声と表情で返しました', 4]]}, + {id: 'game', kind: 'ゲーム実況', title: '勝ったら、いっしょに喜ぶ。', body: '試合の様子は観測した結果から。言葉は、その様子に合うかを確かめてから。', tryLabel: '実況の流れを試す', + scene: [['task', 'スマブラSP', '試合中', '観測:勝利', 4], ['say', '勝った?'], ['card', '実況の候補', '「やったー、勝利!いい試合だったね。」', '演出:きらきら', '声にする', '喜びの演出で返しました', 4]]}, + {id: 'blocks', kind: 'NOA BLOCKS', title: '積みすぎたら、ちゃんと焦る。', body: '危ない場面では汗、助かったらきらきら。自己ベストは紙吹雪とチャイムで。', tryLabel: 'NOA BLOCKSを試す', + scene: [['task', 'NOA BLOCKS', '危険ゾーン', '自己ベスト更新', 4], ['say', 'がんばれ!'], ['card', '実況の候補', '「よし、4ライン!」', '演出:紙吹雪とチャイム', '声にする', '記録更新を祝いました', 4]]} + ], + mechKicker: 'しくみ', mechTitle: '話す前に、
                          確かめる。', mechLead: '返事はすべて出力ゲートを通ります。通らなければ、話しません。', + mech: [['コメント', '見て、選ぶ'], ['返事を考える', '短い1〜2文'], ['出力ゲート', '通らなければ話さない'], ['声と表情', 'コハクの声で'], ['配信', '自然な切れ目で']] + }, + en: { + kicker: 'Stream concept demos', title: 'The talk keeps going,
                          even without comments.', lead: 'When a comment comes in, she answers at a natural break. She commentates games too.', + label: 'Concept demo · fictional comments', uiNote: 'The demos run in Japanese.', + demos: [ + {id: 'comment', kind: 'Answering a comment', title: '“Which furniture do you like?”', body: 'A short answer at a natural break. Only checked words become voice and expression.', tryLabel: 'Try a reply', + scene: [['task', 'Chat', 'Talking about furniture', 'Answering a comment', 4], ['say', 'Which furniture do you like?'], ['card', 'Reply candidate', '“The chair. I can think while I sit.”', 'Expression: shy', 'Say it', 'Answered with voice and expression', 4]]}, + {id: 'game', kind: 'Game commentary', title: 'When the match is won, she celebrates.', body: 'What happened comes from the observed game; the words are checked against it first.', tryLabel: 'Try commentary', + scene: [['task', 'Smash Bros. Ultimate', 'In the match', 'Observed: win', 4], ['say', 'Did you win?'], ['card', 'Commentary candidate', '“Yes, a win! That was a good match.”', 'Effect: sparkle', 'Say it', 'Celebrated', 4]]}, + {id: 'blocks', kind: 'NOA BLOCKS', title: 'Stack too high, and she gets nervous.', body: 'Sweat when it is risky, sparkle when she is saved, confetti and a chime for a personal best.', tryLabel: 'Try NOA BLOCKS', + scene: [['task', 'NOA BLOCKS', 'Danger zone', 'Personal best', 4], ['say', 'You can do it!'], ['card', 'Commentary candidate', '“Yes, four lines!”', 'Effect: confetti and chime', 'Say it', 'Personal best celebrated', 4]]} + ], + mechKicker: 'How it works', mechTitle: 'Check first,
                          then speak.', mechLead: 'Every line goes through an output gate. If it does not pass, she stays silent.', + mech: [['Comment', 'Read and pick'], ['Think of a reply', 'One or two short lines'], ['Output gate', 'No pass, no speech'], ['Voice and face', 'In the Kohaku voice'], ['Stream', 'At a natural break']] + } } }); @@ -70,5 +204,5 @@ export function visionSections(p, lang) { const demos = t.demos.map((d, i) => `

                          0${i + 1} / ${esc(d.kind)}

                          ${esc(d.title)}

                          ${esc(d.body)}

                          ${esc(d.tryLabel)}

                          ${esc(p.name)}${esc(t.label)}

                            ${sceneHtml(d.scene)}
                          `).join(''); const mech = t.mech.map(([k, b], i) => `
                        • 0${i + 1}${esc(k)}${esc(b)}
                        • `).join(''); return `

                          ${esc(t.kicker)}

                          ${t.title}

                          ${esc(t.lead)}${t.uiNote ? ` ${esc(t.uiNote)}` : ''}

                          ${demos}
                          ` - + `

                          ${esc(t.mechKicker)}

                          ${esc(t.mechTitle)}

                          ${esc(t.mechLead)}

                            ${mech}
                          `; + + `

                          ${esc(t.mechKicker)}

                          ${t.mechTitle}

                          ${esc(t.mechLead)}

                            ${mech}
                          `; }