@takagaki/cortex-decisions-viewer 0.4.25 → 0.4.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/render.js +170 -5
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -19,6 +19,8 @@ function renderSite(site) {
19
19
  const safeTitle = escapeHtml(site.title);
20
20
  // 接続マップ(操作タブ最上部のSVG図)。ビルド時に生成し<template>で埋め込む。fleet情報が無ければ出さない
21
21
  const connMap = renderConnectionMap(site);
22
+ // チュートリアル(「はじめに」タブ)。ボタンURLは既存の codeUrl を流用し、案件データ由来の値だけビルド時に埋める
23
+ const tutorialJson = JSON.stringify(buildTutorialData(site, codeUrl, repoSlug)).replace(/</g, "\\u003c");
22
24
  const githubBtn = site.repoBaseUrl
23
25
  ? `<a class="gh-btn" href="${escapeHtml(site.repoBaseUrl)}" target="_blank" rel="noopener" title="コンテキストリポジトリをGitHubで開く">${GITHUB_ICON}</a>`
24
26
  : "";
@@ -47,7 +49,8 @@ function renderSite(site) {
47
49
  <footer class="site-footer">
48
50
  <span id="footer-meta"></span>
49
51
  </footer>
50
- ${connMap ? `<template id="conn-map">${connMap}</template>\n` : ""}<script type="application/json" id="site-data">${dataJson}</script>
52
+ ${connMap ? `<template id="conn-map">${connMap}</template>\n` : ""}<script type="application/json" id="tutorial-data">${tutorialJson}</script>
53
+ <script type="application/json" id="site-data">${dataJson}</script>
51
54
  <script>${CLIENT_JS}</script>
52
55
  </body>
53
56
  </html>`;
@@ -160,6 +163,69 @@ function renderConnectionMap(site) {
160
163
  repo +
161
164
  `</svg>`);
162
165
  }
166
+ /**
167
+ * 「はじめに」タブのチュートリアルデータをビルド時に生成する。
168
+ * 各チャレンジのプロンプトは固定文(ビューアコードに実案件名は残さない)。
169
+ * チャレンジ4だけは site.decisions から根拠付きの最新決定を選び、そのtitle/idを埋め込む。
170
+ */
171
+ function buildTutorialData(site, codeUrl, repoSlug) {
172
+ const c4 = tutorialChallenge4Prompt(site.decisions);
173
+ const specs = [
174
+ {
175
+ title: "全体像を知る",
176
+ desc: "プロジェクトの目的と現在地を掴む",
177
+ prompt: "このプロジェクトの全体像——目的・現状・主要な論点——を、蓄積されたコンテキストをもとに教えてください。",
178
+ },
179
+ {
180
+ title: "関係者を知る",
181
+ desc: "誰が何を担っているか",
182
+ prompt: "このプロジェクトの関係者(社内・顧客・関係会社)と、それぞれの役割を教えてください。",
183
+ },
184
+ {
185
+ title: "ドメインを可視化する",
186
+ desc: "案件の概念と関係を図にする",
187
+ prompt: "このプロジェクトの主要なドメイン概念(用語集)と、それらの関係性を mermaid 図で可視化してください。",
188
+ },
189
+ {
190
+ title: "決定の経緯を辿る",
191
+ desc: "なぜそう決まったかを根拠まで遡る",
192
+ prompt: c4,
193
+ },
194
+ {
195
+ title: "最近の動きを掴む",
196
+ desc: "直近のキャッチアップ",
197
+ prompt: "直近2週間の主要な動き・論点・未解決事項を、議事録や決定記録を横断してまとめてください。",
198
+ },
199
+ ];
200
+ return {
201
+ slug: repoSlug,
202
+ challenges: specs.map((s, i) => ({
203
+ n: i + 1,
204
+ title: s.title,
205
+ desc: s.desc,
206
+ url: codeUrl(s.prompt),
207
+ })),
208
+ };
209
+ }
210
+ /**
211
+ * チャレンジ4のプロンプトを決める。
212
+ * based_on を1件以上持つ決定のうちID最新 → 無ければ最新決定 → 決定ゼロなら汎用文。
213
+ * IDは YYYYMMDD-NNN 形式で文字列降順が新しい順。
214
+ */
215
+ function tutorialChallenge4Prompt(decisions) {
216
+ const generic = "このプロジェクトの重要な決定を1つ選び、なぜそう決まったのか、根拠になった会議や課題まで遡って経緯を説明してください。";
217
+ if (!decisions || !decisions.length)
218
+ return generic;
219
+ const byIdDesc = (a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0);
220
+ const withBasedOn = decisions
221
+ .filter((d) => (d.relations ?? []).some((r) => r.rel === "based_on"))
222
+ .sort(byIdDesc);
223
+ const picked = withBasedOn[0] ?? [...decisions].sort(byIdDesc)[0];
224
+ if (!picked)
225
+ return generic;
226
+ return (`このプロジェクトの決定「${picked.title}」(${picked.id})について、なぜこの結論に至ったのか、` +
227
+ "根拠になった会議や課題まで遡って経緯を説明してください。関連する未解決の論点があれば併せて教えてください。");
228
+ }
163
229
  const CSS = `
164
230
  :root {
165
231
  --bg: #f6f7f9;
@@ -426,6 +492,38 @@ a:hover { text-decoration: underline; }
426
492
  }
427
493
  .edit-btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
428
494
  .section-label { font-size: 12px; font-weight: 700; color: var(--muted); letter-spacing: .04em; text-transform: uppercase; margin: 24px 0 8px; }
495
+ /* ---- はじめに(チュートリアル) ---- */
496
+ .tut-intro { color: var(--muted); font-size: 14px; line-height: 1.8; margin: 2px 0 6px; }
497
+ .tut-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; margin: 8px 0 16px; }
498
+ .tut-progress { font-size: 13px; font-weight: 700; color: var(--text); }
499
+ .tut-progress-bar { flex: 1 1 180px; height: 8px; border-radius: 999px; background: #e6eaf0; overflow: hidden; min-width: 120px; }
500
+ .tut-progress-fill { height: 100%; background: var(--accent); border-radius: 999px; transition: width .25s ease; }
501
+ .tut-celebrate { font-size: 13.5px; font-weight: 700; color: #1f7a45; }
502
+ .tut-list { display: flex; flex-direction: column; gap: 12px; }
503
+ .tut-card {
504
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
505
+ padding: 16px 18px; box-shadow: var(--shadow); display: flex; align-items: flex-start; gap: 14px;
506
+ }
507
+ .tut-card.tut-done { border-color: #a7f3d0; background: #f4fdf8; }
508
+ .tut-num {
509
+ flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%; background: var(--accent-weak);
510
+ color: var(--accent); font-weight: 700; font-size: 14px; display: flex; align-items: center; justify-content: center;
511
+ }
512
+ .tut-card.tut-done .tut-num { background: #d1fae5; color: #047857; }
513
+ .tut-body { flex: 1 1 auto; min-width: 0; }
514
+ .tut-title { font-size: 15.5px; font-weight: 700; color: var(--text); margin: 0 0 3px; }
515
+ .tut-desc { font-size: 13px; color: var(--muted); line-height: 1.7; margin: 0 0 12px; }
516
+ .tut-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 16px; }
517
+ .tut-btn {
518
+ display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 999px;
519
+ border: 1px solid var(--accent); background: var(--accent); color: #fff; font-size: 13px; font-weight: 600; white-space: nowrap;
520
+ }
521
+ .tut-btn:hover { background: #1d4ed8; text-decoration: none; }
522
+ .tut-check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); cursor: pointer; user-select: none; }
523
+ .tut-check input { width: 15px; height: 15px; cursor: pointer; }
524
+ @media (max-width: 600px) {
525
+ .tut-actions { flex-direction: column; align-items: flex-start; }
526
+ }
429
527
  /* ---- データソースと自動化 ---- */
430
528
  .datasources { margin-top: 34px; }
431
529
  .datasources-head { font-size: 18px; font-weight: 700; color: var(--text); margin: 0 0 4px; }
@@ -607,6 +705,11 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
607
705
  const CLIENT_JS = `
608
706
  (function () {
609
707
  var SITE = JSON.parse(document.getElementById("site-data").textContent);
708
+ // 「はじめに」タブのチュートリアルデータ(ビルド時に埋め込み。ボタンURL・チャレンジ4の決定は解決済み)
709
+ var TUTORIAL = (function () {
710
+ var elx = document.getElementById("tutorial-data");
711
+ try { return elx ? JSON.parse(elx.textContent) : null; } catch (e) { return null; }
712
+ })();
610
713
 
611
714
  // ---------- インデックス構築 ----------
612
715
  var byId = {};
@@ -632,8 +735,10 @@ const CLIENT_JS = `
632
735
  // 本文は bodies/<i>.html に外出しされ詳細表示時に fetch されるため、linkify は loadBody 側で適用する
633
736
  // (ここで inline bodyHtml に適用すると検索テキストにHTMLが混ざるため適用しない)。
634
737
 
635
- // タブ: ホーム → Decisions → records/ を持つ各ディレクトリ(自動発見)
738
+ // タブ: はじめに → ホーム → Decisions → records/ を持つ各ディレクトリ(自動発見)
636
739
  var TABS = [];
740
+ // 新メンバー向けオンボーディング。最初に目に入るようタブ列の先頭に置く
741
+ if (TUTORIAL && TUTORIAL.challenges && TUTORIAL.challenges.length) TABS.push({ key: "tutorial", label: "はじめに" });
637
742
  if (SITE.home) TABS.push({ key: "home", label: "Home" });
638
743
  TABS.push({ key: "decisions", label: "Decisions", items: SITE.decisions });
639
744
  (SITE.sections || []).forEach(function (sec) {
@@ -650,6 +755,11 @@ const CLIENT_JS = `
650
755
  TABS.push({ key: "ops", label: "操作" });
651
756
  }
652
757
 
758
+ // 既定の着地タブ: 「はじめに」はタブ列の先頭に置くが、初回着地は Home にする
759
+ // (毎回開く顧客・リピーターに自然。新規は先頭タブで気づける)。home が無い案件は先頭タブに着地。
760
+ // URLに ?tab= があれば route() がそれを優先するので直リンクは不変。
761
+ var DEFAULT_TAB = SITE.home ? "home" : TABS[0].key;
762
+
653
763
  var app = document.getElementById("app");
654
764
  // ウィキリンク(本文中の [[用語]])クリックでSPA内遷移する
655
765
  app.addEventListener("click", function (ev) {
@@ -693,7 +803,7 @@ const CLIENT_JS = `
693
803
  return new URLSearchParams(location.search).get(name);
694
804
  }
695
805
  function tabUrl(tabKey) {
696
- return tabKey === TABS[0].key ? "?" : "?tab=" + encodeURIComponent(tabKey);
806
+ return tabKey === DEFAULT_TAB ? "?" : "?tab=" + encodeURIComponent(tabKey);
697
807
  }
698
808
  function go(id) {
699
809
  var url = id ? "?id=" + encodeURIComponent(id) : tabUrl(listState.tab);
@@ -702,7 +812,7 @@ const CLIENT_JS = `
702
812
  window.scrollTo(0, 0);
703
813
  }
704
814
 
705
- var listState = { tab: TABS[0].key, q: "", category: "", month: "", page: 1 };
815
+ var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", page: 1 };
706
816
 
707
817
  // 一覧のページネーション(描画コスト対策。1ページあたりの件数)
708
818
  var PAGE_SIZE = 30;
@@ -792,6 +902,7 @@ const CLIENT_JS = `
792
902
  renderTabs();
793
903
  renderInfoBar();
794
904
 
905
+ if (listState.tab === "tutorial") { renderTutorial(); return; }
795
906
  if (listState.tab === "home") { renderHome(); return; }
796
907
  if (listState.tab === "decisions") { renderDecisionList(); return; }
797
908
  if (listState.tab === "graph") { renderKnowledge(); return; }
@@ -813,6 +924,55 @@ const CLIENT_JS = `
813
924
  return SITE.home && SITE.home.fm ? adoptionInfo(SITE.home.fm.adoption) : null;
814
925
  }
815
926
 
927
+ // ---------- はじめに(チュートリアル) ----------
928
+ // localStorageキーは案件別(repoSlug)。同一ブラウザで複数案件のビューアを見ても混ざらない
929
+ function tutStoreKey(n) { return "cortex-tutorial-done:" + (TUTORIAL.slug || "") + ":" + n; }
930
+ function tutIsDone(n) {
931
+ try { return localStorage.getItem(tutStoreKey(n)) === "1"; } catch (e) { return false; }
932
+ }
933
+ function tutSetDone(n, done) {
934
+ try { if (done) localStorage.setItem(tutStoreKey(n), "1"); else localStorage.removeItem(tutStoreKey(n)); } catch (e) {}
935
+ }
936
+ function renderTutorial() {
937
+ countEl.textContent = "";
938
+ var challenges = TUTORIAL.challenges || [];
939
+ app.appendChild(el("p", { class: "tut-intro", text: "AIS Viewer と Claude Code を使ってこのプロジェクトを掴むための5ステップです。各ボタンを押すと、質問が入力済みの状態で Claude Code Web が開きます(このリポジトリへのアクセス権が必要です)。" }));
940
+
941
+ // 進捗ヘッダ({完了数}/5・バー・全完了で祝福)
942
+ var doneCount = challenges.filter(function (c) { return tutIsDone(c.n); }).length;
943
+ var progText = el("span", { class: "tut-progress", text: doneCount + "/" + challenges.length + " 完了" });
944
+ var fill = el("div", { class: "tut-progress-fill" });
945
+ fill.style.width = (challenges.length ? Math.round((doneCount / challenges.length) * 100) : 0) + "%";
946
+ var bar = el("div", { class: "tut-progress-bar" }, [fill]);
947
+ var celebrate = el("span", { class: "tut-celebrate" });
948
+ if (doneCount === challenges.length && challenges.length) celebrate.textContent = "🎉 全ステップ達成!オンボーディング完了です。";
949
+ var head = el("div", { class: "tut-head" }, [progText, bar, celebrate]);
950
+ app.appendChild(head);
951
+
952
+ var list = el("div", { class: "tut-list" });
953
+ challenges.forEach(function (c) {
954
+ var done = tutIsDone(c.n);
955
+ var card = el("div", { class: "tut-card" + (done ? " tut-done" : "") });
956
+ var btn = el("a", { class: "tut-btn", href: c.url, target: "_blank", rel: "noopener", text: "🚀 挑戦する" });
957
+ var cb = el("input", { type: "checkbox" });
958
+ cb.checked = done;
959
+ var check = el("label", { class: "tut-check" }, [cb, el("span", { text: "完了" })]);
960
+ cb.addEventListener("change", function () {
961
+ tutSetDone(c.n, cb.checked);
962
+ // 進捗・祝福・カードの見た目を即時更新(再描画で状態を反映)
963
+ renderList();
964
+ });
965
+ card.appendChild(el("div", { class: "tut-num", text: String(c.n) }));
966
+ card.appendChild(el("div", { class: "tut-body" }, [
967
+ el("h3", { class: "tut-title", text: c.title }),
968
+ el("p", { class: "tut-desc", text: c.desc }),
969
+ el("div", { class: "tut-actions" }, [btn, check]),
970
+ ]));
971
+ list.appendChild(card);
972
+ });
973
+ app.appendChild(list);
974
+ }
975
+
816
976
  // ---------- ホーム(着地画面: Markdownをそのまま表示) ----------
817
977
  function renderHome() {
818
978
  var h = SITE.home;
@@ -917,7 +1077,6 @@ const CLIENT_JS = `
917
1077
  ilist.appendChild(card);
918
1078
  });
919
1079
  sec.appendChild(ilist);
920
- sec.appendChild(el("p", { class: "ds-note", text: "これらの更新は毎晩の Gold 昇格が読み取ります。" }));
921
1080
  }
922
1081
 
923
1082
  if (sources.length || chatOff || devOff) {
@@ -967,6 +1126,12 @@ const CLIENT_JS = `
967
1126
  sec.appendChild(wrap);
968
1127
  }
969
1128
 
1129
+ // 注記は内部・外部の両グループの後ろに1つ(内部の直下だけに置くと外部ソースが
1130
+ // Gold昇格の対象外に見えるため)。除外の方法もここで案内する。
1131
+ if (internals.length || sources.length || chatOff || devOff) {
1132
+ sec.appendChild(el("p", { class: "ds-note", text: "上記の情報源はすべて毎晩の Gold 昇格が読み取ります(「除外」バッジのものを除く。チャンネル・リポジトリ単位で除外できます)。" }));
1133
+ }
1134
+
970
1135
  if (pipes.length) {
971
1136
  sec.appendChild(el("div", { class: "ds-sub", text: "自動化パイプライン" }));
972
1137
  var plist = el("div", { class: "ds-pipes" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.25",
3
+ "version": "0.4.28",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,