diff --git a/public/assets/animated-demos.mjs b/public/assets/animated-demos.mjs index 664e7be..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'; 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/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/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 new file mode 100644 index 0000000..17d5ebe --- /dev/null +++ b/public/demos/ai-meeting/index.html @@ -0,0 +1,41 @@ + + + + + + 話して、決まったことだけ残す。 — AI Meeting 構想デモ + + + + + + + + +

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

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

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

      + +
      +
      +

      会話

      +
        +
        +
        +
        +

        +
          +
          +

          + +
          +
          + +
          + + 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/demo.css b/public/demos/demo.css new file mode 100644 index 0000000..574c2d6 --- /dev/null +++ b/public/demos/demo.css @@ -0,0 +1,50 @@ +: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} +.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{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)} +.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{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: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} +.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/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 32e2244..a77a930 100644 --- a/src/redesign-copy.mjs +++ b/src/redesign-copy.mjs @@ -31,11 +31,12 @@ 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 / 公開している根拠', 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: '実録画(無音・編集あり)', @@ -74,11 +75,12 @@ 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', 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 7fff3d1..2159d0b 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 {VISION, visionSections} from './vision-pages.mjs'; const {checkedAt: sourcesCheckedAt} = JSON.parse(await fs.readFile(new URL('../site.config.json', import.meta.url), 'utf8')); @@ -293,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(''); @@ -310,14 +312,18 @@ 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(''); + // Where the page has concept demos, the old guided-sample entry points to them instead. + const tryDemos = VISION[p.id] ? `
                      ${lang === 'ja' ? '構想デモを試す' : 'Try the concept demos'}
                      ` : 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 => 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)} ↗` : ''; @@ -330,12 +336,14 @@ function productMain(p, list, lang, {kept, figure, source, filmAnchor}) {

                      ${esc(r.whatItDoes)}

                        ${outcome}
                      ${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)}

                      +
                      ${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.scopeKicker)}

                      ${r.scopeTitle}

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

                      ${esc(r.scopeLabel)}

                      ${esc(t.scope)}

                      ${esc(r.proofLabel)}

                      ${esc(t.proof)}

                      +

                      ${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 3924a31..4ca4296 100644 --- a/src/redesign/redesign.css +++ b/src/redesign/redesign.css @@ -412,3 +412,101 @@ 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-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)} +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);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:"“" "”"} +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{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)} +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-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} +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}} +/* 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} +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 new file mode 100644 index 0000000..eb36abb --- /dev/null +++ b/src/vision-pages.mjs @@ -0,0 +1,208 @@ +/** + * 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: '構想デモ・架空データ', uiNote: '', + demos: [ + {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', 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.', 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']] + } + }, + // 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']] + } + } +}); + +// 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, e2, f], i) => { + const step = ` vp-step vp-step--${i + 1}`; + 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)}
                    • `; + return `
                    • ${esc(a)}${esc(b)}${esc(c)}${esc(e2)}
                    • `; + }).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(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)}

                      ${t.mechTitle}

                      ${esc(t.mechLead)}

                        ${mech}
                      `; +} 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件の実電話検証/);