// data.jsx — Popor 示例笔记数据（中英混排）
// 字段：id, title, en, date(制作), imported(导入), year, month, city, scene,
//        gps, notebook, cat(主分类·紫), notes(内容·绿), locs(地点·黄),
//        preview, body(markdown), hasPhoto

// ============================================================
// 数据来源：从后端 API 实时获取（http://localhost:8002）
// 如需改地址，在页面里设 window.POPOR_API_BASE 即可。
// 后端不可达时自动回落到下方的本地示例数据（FALLBACK_NOTES）。
// ============================================================
const POPOR_API_BASE = (typeof window !== 'undefined' && window.POPOR_API_BASE) || 'http://localhost:8002';

// —— 本子样式：按主分类(category)给深色书脊 + 图标 + 描述 ——
// 本子通过 category 关联笔记：note.notebook === note.cat（即 category）
const CATEGORY_STYLE = {
  '读书': { bg: '#2C1A0E', fg: '#F5E6CC', icon: 'catBook',  desc: '把读过的句子收进来' },
  '学习': { bg: '#0E2A1E', fg: '#C8F0D8', icon: 'book',     desc: '学到的、想通的' },
  '音乐': { bg: '#1E0A2E', fg: '#E8C8FF', icon: 'catMusic', desc: '一首歌停下的瞬间' },
  '旅行': { bg: '#15263F', fg: '#C8DDF0', icon: 'catTravel',desc: '走过的地方与遇见' },
  '美食': { bg: '#2E1A0A', fg: '#F0D8B0', icon: 'coffee',   desc: '吃过的好东西' },
  '创意': { bg: '#2A0E22', fg: '#F5C8E0', icon: 'spark',    desc: '一闪而过的念头' },
  '生活': { bg: '#2E1212', fg: '#F5D0C0', icon: 'catLife',  desc: '日子里的细节' },
  '工作': { bg: '#14162E', fg: '#C8C8F0', icon: 'layers',   desc: '手头在做的事' },
  // —— 示例数据可能用到的额外分类 ——
  '电影': { bg: '#0D1B2A', fg: '#C8DDF0', icon: 'catFilm',  desc: '黑暗里亮起的光' },
  '运动': { bg: '#0E2A1E', fg: '#C8F0D8', icon: 'catRun',   desc: '身体丈量过的路' },
  '健康': { bg: '#2E0A0A', fg: '#F5D0C0', icon: 'catHealth',desc: '身体写下的日记' },
  '账本': { bg: '#2a2a0a', fg: '#EEE8C0', icon: 'catMoney', desc: '钱去了哪里' },
};
// 始终展示的基础本子（后端主分类枚举）
const NOTEBOOK_BASE = ['读书', '学习', '音乐', '旅行', '美食', '创意', '生活', '工作'];

function notebookStyle(cat) {
  return CATEGORY_STYLE[cat] || { bg: '#222428', fg: '#E6E1D6', icon: 'book', desc: '' };
}
// 由笔记动态生成本子列表：基础本子 + 数据里出现的其它分类
function buildNotebooks(notes) {
  const cats = NOTEBOOK_BASE.slice();
  (notes || []).forEach(n => { if (n.cat && cats.indexOf(n.cat) < 0) cats.push(n.cat); });
  return cats.map(c => {
    const s = notebookStyle(c);
    return { id: c, name: c, desc: s.desc, bg: s.bg, fg: s.fg, lineIcon: s.icon };
  });
}

// —— 字段格式化（API → 原型字段）——
function dotDate(s) { return s ? String(s).slice(0, 10).replace(/-/g, '.') : ''; }          // 'YYYY-MM-DD' → 'YYYY.MM.DD'
function yearOf(s) { const d = String(s || '').slice(0, 4); return /^\d{4}$/.test(d) ? d : ''; }
function monthOf(s) { const n = parseInt(String(s || '').slice(5, 7), 10); return n ? n + '月' : ''; }
function timeOf(ts) { const m = String(ts || '').match(/T(\d{2}:\d{2})/); return m ? m[1] : ''; } // ISO → 'HH:MM'
function stripMd(t) {
  return String(t || '')
    .replace(/```[\s\S]*?```/g, ' ')
    .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
    .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
    .replace(/^[ \t]*\|?[\s:|-]*-{2,}[\s:|-]*\|?[ \t]*$/gm, ' ') // 表格分隔行 / 水平线
    .replace(/^[ \t]*[-*+]\s+/gm, '')                            // 列表符号
    .replace(/[#>*`|_~]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}
function makePreview(t) { const s = stripMd(t); return s.length > 72 ? s.slice(0, 72) + '…' : s; }

// 把后端笔记对象转成原型组件需要的形状（字段映射见对接文档）
function transformNote(n) {
  const time = n.time || {};
  const media = n.media || {};
  const cdate = time.content_date || n.date || time.import_date || '';
  const cat = n.category || '未分类';
  const tags = Array.isArray(n.tags) ? n.tags : [];
  const loc = (time.content_location || '').trim();
  return {
    id: n.id,
    title: n.title || '未命名',
    en: '',                                              // 真实数据无英文副名
    date: dotDate(cdate),
    time: timeOf(time.import_ts || n.ts) || '12:00',
    imported: dotDate(time.import_date || n.date),
    year: yearOf(cdate),
    month: monthOf(cdate),
    city: loc || '未标注',
    scene: (time.content_scene || '').trim() || '未记录',
    gps: '',                                             // 后续从 time.geo 取
    notebook: cat,                                       // 本子 = 主分类
    cat: cat,
    notes: tags,                                         // 内容标签（绿）
    locs: loc ? [loc] : [],                              // 地点标签（黄）
    preview: makePreview(n.text),
    body: n.text || '',                                  // 完整 Markdown
    hasPhoto: !!media.original_image,
    img: media.original_image ? (POPOR_API_BASE + media.original_image) : '',
    // 原始字段，留给墨吞后续接入
    mood: n.mood || '',
    mokeyQuestion: n.mokey_question || '',
    mokeyDialogue: Array.isArray(n.mokey_dialogue) ? n.mokey_dialogue : [],
  };
}

// —— 本地示例数据：后端不可达时的回落（保证离线/预览仍可渲染）——
const FALLBACK_NOTES = [
  {
    id: 'n1',
    title: '京都的雨', en: 'Rain over Kyoto',
    date: '2021.10.14', time: '16:00', imported: '2021.10.16', year: '2021', month: '10月',
    city: '京都', scene: '旅途中', gps: '35.00°N 135.77°E',
    notebook: 'read', cat: '旅行', notes: ['寺院', '侘寂'], locs: ['京都'],
    hasPhoto: true,
    preview: '在龙安寺的廊下坐了一个下午。雨落在石庭上，十五块石头，从任何角度都只能看见十四块。',
    body: `# 龙安寺 · 石庭

在廊下坐了一个下午。雨一直落，落在白砂耙出的水波纹上。

> 十五块石头，从任何一个角度，都只能看见十四块。
> 缺的那一块，留给观者自己补。

这就是 *wabi-sabi* 吧——不是完整，而是**承认不完整**。

旅途读到 Leonard Koren 的话：

\`\`\`
Wabi-sabi is the beauty of things
imperfect, impermanent, and incomplete.
\`\`\`

雨停的时候，砂上的纹路已经被打乱了。没有人去重新耙。`,
  },
  {
    id: 'n2',
    title: '深夜的爵士', en: 'Midnight Jazz',
    date: '2026.05.22', time: '01:10', imported: '2026.05.22', year: '2026', month: '5月',
    city: '上海', scene: '家', gps: '31.23°N 121.47°E',
    notebook: 'music', cat: '音乐', notes: ['爵士', '深夜'], locs: ['上海'],
    hasPhoto: false,
    preview: '反复听 Bill Evans 的 Peace Piece。一个和弦按住不放，右手在上面慢慢游走，像在试探黑暗的形状。',
    body: `# Peace Piece — Bill Evans

凌晨一点，反复听这一首。

左手一个和弦按住不放，像一片不动的水面。右手在上面慢慢游走，*试探黑暗的形状*。

| 时间 | 在做的事 |
|---|---|
| 00:00 | 主题，几乎不动 |
| 02:30 | 开始有不安 |
| 06:00 | 又回到最初 |

> 最好的音乐，是让你愿意一直待在不解决的和弦里。`,
  },
  {
    id: 'n3',
    title: '三月账单', en: 'March Ledger',
    date: '2026.03.31', time: '22:00', imported: '2026.03.31', year: '2026', month: '3月',
    city: '上海', scene: '家', gps: '31.23°N 121.47°E',
    notebook: 'ledger', cat: '生活', notes: ['复盘', '克制'], locs: ['上海'],
    hasPhoto: false,
    preview: '这个月花在书上的钱，第一次超过了外卖。记下来不是为了省，是想看见自己把时间和钱给了什么。',
    body: `# 三月 · 复盘

记下来不是为了省，是想**看见自己把钱给了什么**。

- 书 ¥486
- 外卖 ¥412
- 唱片 ¥238
- 一束花 ¥68

> 这个月花在书上的钱，第一次超过了外卖。
> 一个微小的、自己跟自己的胜利。

剩下的不写了。账本最怕的是变成审判。`,
  },
  {
    id: 'n4',
    title: '晨跑七公里', en: 'Seven Kilometres at Dawn',
    date: '2026.05.30', time: '05:30', imported: '2026.05.30', year: '2026', month: '5月',
    city: '上海', scene: '户外', gps: '31.24°N 121.50°E',
    notebook: 'sport', cat: '运动', notes: ['晨跑', '身体'], locs: ['上海', '滨江'],
    hasPhoto: true,
    preview: '五点半出门，江面还没醒。跑到第五公里的时候，那个一直在脑子里吵的念头，忽然就安静了。',
    body: `# 滨江 · 晨跑

五点半出门，江面还没醒，雾贴着水。

跑到第五公里，那个一直在脑子里吵的念头，*忽然就安静了*。

\`\`\`
距离  7.2 km
配速  5'48"
心率  avg 152
\`\`\`

> 身体先于头脑想明白了一件事：
> 有些问题不是用想的，是用跑的。`,
  },
  {
    id: 'n5',
    title: '小津的留白', en: 'Ozu’s Empty Frames',
    date: '2024.11.08', time: '15:20', imported: '2024.11.09', year: '2024', month: '11月',
    city: '杭州', scene: '咖啡馆', gps: '30.27°N 120.16°E',
    notebook: 'film', cat: '电影', notes: ['小津', '留白'], locs: ['杭州'],
    hasPhoto: false,
    preview: '重看《东京物语》。最打动我的不是对话，是那些没有人的空镜——走廊、晾着的衣服、一列开过的火车。',
    body: `# 东京物语 · 空镜

最打动我的不是对话，是那些**没有人的空镜**。

走廊、晾着的衣服、一列开过的火车。

> 小津把摄影机放得很低，像一个跪坐在榻榻米上的人。
> 镜头不追人，人走出画框，画框还在那里多停三秒。

那三秒，是留给观众呼吸的。也是留给逝去的时间的。`,
  },
  {
    id: 'n6',
    title: '一杯手冲', en: 'A Cup, Pour-over',
    date: '2024.07.19', time: '10:00', imported: '2024.07.19', year: '2024', month: '7月',
    city: '杭州', scene: '图书馆', gps: '30.26°N 120.13°E',
    notebook: 'health', cat: '生活', notes: ['咖啡', '专注'], locs: ['杭州'],
    hasPhoto: false,
    preview: '学着自己冲。水温、粉量、时间，三个变量反复调。失败很多次后明白，所谓手艺就是和不确定性长期相处。',
    body: `# 手冲 · 笔记

水温、粉量、时间，三个变量反复调。

| 参数 | 这次 |
|---|---|
| 水温 | 92℃ |
| 粉水比 | 1:15 |
| 总时长 | 2'10" |

> 失败很多次后明白，所谓手艺，
> 就是和不确定性长期相处。`,
  },
  {
    id: 'n7',
    title: '奈良的鹿', en: 'The Deer of Nara',
    date: '2021.04.03', time: '11:30', imported: '2021.04.05', year: '2021', month: '4月',
    city: '奈良', scene: '旅途中', gps: '34.68°N 135.84°E',
    notebook: 'read', cat: '旅行', notes: ['奈良', '相遇'], locs: ['奈良'],
    hasPhoto: true,
    preview: '鹿不怕人，也不亲人。它们只是恰好和你在同一片草地上，各自低头吃草。这种距离，刚刚好。',
    body: `# 奈良 · 鹿

鹿不怕人，也不亲人。

它们只是恰好和你在同一片草地上，各自低头吃草。

> 这种距离，刚刚好。
> 不索取，也不躲闪。

回来后一直在想，人和人之间，能不能也这样。`,
  },
  {
    id: 'n8',
    title: '坂本龙一的最后', en: 'Sakamoto, the Last',
    date: '2026.05.11', time: '23:40', imported: '2026.05.11', year: '2026', month: '5月',
    city: '上海', scene: '家', gps: '31.23°N 121.47°E',
    notebook: 'music', cat: '音乐', notes: ['坂本龙一', '告别'], locs: ['上海'],
    hasPhoto: false,
    preview: '听 12。每一个音都像是用尽了力气才按下去的。呼吸声留在录音里，没有剪掉。那也是音乐的一部分。',
    body: `# 12 — 坂本龙一

每一个音，都像用尽了力气才按下去。

呼吸声留在录音里，没有剪掉。

> 那也是音乐的一部分。
> 一个人快走到尽头时，连喘息都成了旋律。

*Ars longa, vita brevis.*`,
  },
  {
    id: 'n9',
    title: '出差的清晨', en: 'A Working Dawn',
    date: '2026.05.26', time: '17:00', imported: '2026.05.26', year: '2026', month: '5月',
    city: '深圳', scene: '旅途中', gps: '22.54°N 114.06°E',
    notebook: 'sport', cat: '生活', notes: ['出差', '城市'], locs: ['深圳', '福田'],
    hasPhoto: false,
    preview: '福田的傍晚，写字楼一格一格亮起来。出差的人没有黄昏，只有从一个会议室到下一个会议室的间隙。',
    body: `# 深圳 · 福田

傍晚五点，写字楼一格一格亮起来。

出差的人没有黄昏，只有从一个会议室，到下一个会议室的*间隙*。

> 在别人的城市里，时间是借来的。
> 借来的时间，要还得格外用力。`,
  },
  {
    id: 'n10',
    title: '静安寺的银杏', en: 'Ginkgo at Jing’an',
    date: '2026.05.27', time: '11:30', imported: '2026.05.27', year: '2026', month: '5月',
    city: '上海', scene: '旅途中', gps: '31.22°N 121.45°E',
    notebook: 'read', cat: '旅行', notes: ['银杏', '城市'], locs: ['上海', '静安'],
    hasPhoto: true,
    preview: '从深圳飞回，落地就去了静安寺。五月的银杏还是绿的，但风里已经有了要黄的预感。',
    body: `# 上海 · 静安

从深圳飞回，落地就去了静安寺。

五月的银杏还是绿的，但风里已经有了*要黄的预感*。

> 金顶在阴天里也亮着。
> 城市再快，总要有一处地方，是慢的。`,
  },
];

// —— 城市 = 地铁线路色 —— //
const CITY_LINE = {
  '奈良': '#7A8C4A',   // 橄榄 · 奈良线
  '京都': '#B8503A',   // 朱砂 · 京都线
  '杭州': '#4A6B8A',   // 青灰 · 杭州线
  '上海': '#2C6E63',   // 深青 · 上海线（海水）
  '深圳': '#9A5B9C',   // 紫 · 深圳线
};

// 城市经纬度（数字地图用）
const CITY_GEO = {
  '奈良': { lat: 34.68, lon: 135.83 },
  '京都': { lat: 35.01, lon: 135.77 },
  '上海': { lat: 31.23, lon: 121.47 },
  '杭州': { lat: 30.27, lon: 120.16 },
  '深圳': { lat: 22.54, lon: 114.06 },
};

// —— 把笔记按时间排成时空站点（地铁式行程）—— //
function buildStations() {
  const list = (typeof window !== 'undefined' && window.NOTES) ? window.NOTES : [];
  const toNum = n => parseInt(String(n.date || '').replace(/\./g, '') + String(n.time || '').replace(':', ''), 10) || 0;
  return [...list].sort((a, b) => toNum(a) - toNum(b)).map((n, i, arr) => ({
    note: n,
    line: CITY_LINE[n.city] || '#666',
    transfer: i > 0 && arr[i - 1].city !== n.city,   // 换乘（跨城）
  }));
}

// —— 确定性哈希（演示用·非密码学）—— //
function poporHash(seed) {
  let h1 = 0x811c9dc5, h2 = 0x1000193;
  const s = String(seed);
  for (let i = 0; i < s.length; i++) {
    const c = s.charCodeAt(i);
    h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
    h2 = Math.imul(h2 + c + i, 0x85ebca6b) >>> 0;
  }
  const hex = (h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0'));
  return (hex + hex.split('').reverse().join('')).slice(0, 40);
}

// —— 现在：时空轴的主轴锚点 —— //
const NOW = { date: '2026.06.01', time: '11:15', city: '上海', scene: '家', label: '现在' };

// ============================================================
// 墨吞 · 书写时的对话式助手（可提问、可找资料，资料带来源 index）
// 演示用：确定性脚本，不接真实 API
// ============================================================

// 每则笔记的可被引用资料库（论文式来源）
const MOTUN_SOURCES = {
  n1: [
    { title: 'Wabi-Sabi: for Artists, Designers, Poets & Philosophers', author: 'Leonard Koren', year: 1994, kind: '专著', snippet: 'Wabi-sabi is the beauty of things imperfect, impermanent, and incomplete.' },
    { title: '龙安寺石庭の十五石配置に関する考察', author: 'van Tonder, G. J. 等', year: 2002, kind: '论文 · Nature', snippet: '十五块石的布局，使任一视点恰好被遮去一块——缺,被设计成可见的。' },
    { title: '阴翳礼赞', author: '谷崎润一郎', year: 1933, kind: '随笔', snippet: '美,不在物之本身,而在物与物投下的阴翳的浓淡之间。' },
  ],
  n5: [
    { title: '小津安二郎の映画美学：ローポジションと空ショット', author: 'David Bordwell', year: 1988, kind: '论文', snippet: '空镜并非过场,而是让叙事呼吸的"枕镜"(pillow shot)。' },
    { title: '东京物语 · 分镜笔记', author: '松竹株式会社', year: 1953, kind: '资料', snippet: '人物走出画框后,镜头停留约三秒,容纳逝去的时间。' },
  ],
  n2: [
    { title: 'Peace Piece 和声分析', author: 'Jazz Theory Archive', year: 2011, kind: '乐谱注', snippet: '左手 C–G 持续音上,右手在不解决的属功能里游移。' },
    { title: 'Bill Evans: How My Heart Sings', author: 'Peter Pettinger', year: 1998, kind: '传记', snippet: '埃文斯把钢琴当作"会呼吸的乐器",让和弦悬而不落。' },
  ],
  n8: [
    { title: '坂本龙一《12》创作访谈', author: 'NHK', year: 2023, kind: '访谈', snippet: '把每天的身体状态录成声音日记,呼吸也是乐句的一部分。' },
    { title: 'Ars longa, vita brevis 词源考', author: 'Hippocrates / Seneca', year: '—', kind: '辞条', snippet: '艺术长存,人生短暂——原指医术须穷尽一生方得。' },
  ],
};
const MOTUN_SOURCES_FALLBACK = [
  { title: '与本页主题相关的公开资料', author: '网络检索', year: 2025, kind: '资料', snippet: '墨吞为你检索到的一段可引用内容,点开可查看完整出处。' },
  { title: '同主题的延伸阅读', author: '编辑整理', year: 2024, kind: '资料', snippet: '若需要,我可以继续按时间或作者帮你筛。' },
];

function motunSources(note) {
  return (note && MOTUN_SOURCES[note.id]) || MOTUN_SOURCES_FALLBACK;
}

// ============================================================
// 墨吞 · 牵线（共创）：在他人的笔记里，用向量检索找能补这一页的人
// 多人 + 多 AI —— 墨吞这里只做"建立联系"，引用时带对方 ID 与来源
// 演示用：确定性匹配，不接真实 faiss
// ============================================================

// 他人笔记的语境库（每个人 = 一种职业 / 地点 / 经历 / 爱好的语境）
const PEOPLE = [
  { id: '@lakeside_zhou', name: '周屿', prof: '风光摄影师', av: '#2C6E63',
    now: '杭州', been: ['西湖', '京都', '奈良'], hobby: ['慢门', '黑白胶片'],
    note: '雾里的西湖', offer: '可现拍 · 有旧照',
    snippet: '等了四个清晨，才等到苏堤完全没有人的那一分钟。雾把对岸的雷峰塔擦掉了一半。',
    kw: ['西湖', '杭州', '京都', '奈良', '摄影', '照片', '旅行', '雾', '留白', '侘寂', '相遇'] },
  { id: '@wabi_keng', name: '耿默', prof: '古建与庭园研究者', av: '#7A5A2A',
    now: '奈良', been: ['京都', '西湖', '苏州'], hobby: ['枯山水', '拓印'],
    note: '十四块石头', offer: '语境补全',
    snippet: '龙安寺缺的那一块，不是失误，是工匠请你走进来，用自己补满它。',
    kw: ['侘寺', '侘寂', '寺院', '京都', '奈良', '西湖', '留白', '庭园', '旅行', '不完整'] },
  { id: '@evans_fan', name: '苏黎', prof: '爵士钢琴手', av: '#5B3A6E',
    now: '上海', been: ['纽约', '东京'], hobby: ['即兴', '黑胶'],
    note: '不解决的和弦', offer: '语境补全',
    snippet: 'Peace Piece 的左手，是一个人决定不往前走——把不安按住，听它自己慢慢化开。',
    kw: ['爵士', '音乐', '深夜', '钢琴', 'Bill Evans', '坂本龙一', '和弦', '告别', '即兴'] },
  { id: '@ozu_low', name: '川岛', prof: '纪录片导演', av: '#3A4A6B',
    now: '京都', been: ['东京', '杭州'], hobby: ['空镜', '低机位'],
    note: '低机位的耐心', offer: '可现拍 · 语境',
    snippet: '把摄影机放到一个跪坐者的高度，人走出画框之后，那三秒就有了重量。',
    kw: ['小津', '电影', '留白', '空镜', '东京', '杭州', '镜头', '耐心', '城市'] },
  { id: '@dr_lin', name: '林述', prof: '内科医生', av: '#8B2020',
    now: '上海', been: ['滨江'], hobby: ['晨跑', '古典乐'],
    note: '值夜班的呼吸', offer: '专业视角',
    snippet: '心率掉到 152 以下那一刻，身体其实比仪器先一步知道——它在替你做决定。',
    kw: ['身体', '晨跑', '健康', '心率', '呼吸', '滨江', '上海', '运动', '坂本龙一', '克制'] },
  { id: '@pourover_an', name: '安和', prof: '咖啡师', av: '#6B4A2A',
    now: '杭州', been: ['图书馆'], hobby: ['手冲', '烘焙'],
    note: '三个变量',  offer: '语境补全',
    snippet: '水温、粉量、时间——失败到第七次才学会：手艺就是和不确定性长期相处。',
    kw: ['咖啡', '手冲', '专注', '杭州', '图书馆', '手艺', '不确定', '生活', '变量'] },
  { id: '@ledger_he', name: '何拾', prof: '独立财务顾问', av: '#5A5A2A',
    now: '上海', been: [], hobby: ['复盘', '极简'],
    note: '月底的诚实', offer: '专业视角',
    snippet: '账本最怕变成审判。我把它写成一封给自己的信——只记，不判。',
    kw: ['账本', '复盘', '克制', '钱', '生活', '上海', '极简', '诚实'] },
  { id: '@ginkgo_x', name: '夏行', prof: '城市行走作家', av: '#7A8C4A',
    now: '上海', been: ['静安', '福田', '深圳', '杭州'], hobby: ['city walk', '银杏'],
    note: '借来的黄昏', offer: '可现拍 · 旧照',
    snippet: '出差的人没有黄昏，只有从一个会议室到下一个会议室的缝隙。城市是借来的。',
    kw: ['城市', '出差', '银杏', '静安', '深圳', '福田', '上海', '黄昏', '旅行', '相遇'] },
];

// 一个人能提供哪种帮助：现成的（写过 · 过去）/ 实时的（能现在去做）/ 专业
function coIsLive(p) { return /拍|现拍|到场/.test(p.offer); }
function coIsPro(p)  { return /专业/.test(p.offer) || ['内科医生','独立财务顾问','古建与庭园研究者','纪录片导演','风光摄影师'].includes(p.prof); }

// 墨吞 · 判断这一页缺什么，并把"找人帮助"结构成两条路
// 这是墨吞常用的问题结构：① 现成的（有人写过 · 过去）② 实时的（需要现在有人去做）
function motunCoJudge(note) {
  const place = (note && note.city) || '某地';
  const topic = (note && note.notes && note.notes[0]) || '这一页';
  const hay = note ? [note.title, note.cat, ...(note.notes || []), ...(note.locs || []), note.preview || '', note.body || ''].join(' ') : '';
  let missing, proRole = null, liveLine;
  if (/照片|拍|影像|实景|风光|风景|画面|看见|一幕/.test(hay) || ['旅行', '电影'].includes(note && note.cat)) {
    missing = `一张「${place}」的实景画面`;
    proRole = '摄影师';
    liveLine = `这一页缺的是现场——也许该请一位此刻在「${place}」、能去拍的人。`;
  } else if (/身体|健康|心率|呼吸|晨跑|运动|值夜/.test(hay)) {
    missing = '一个专业的身体判断';
    proRole = '医生';
    liveLine = '这里牵涉身体与健康——建议让一位医生来把关，而不是只凭感觉。';
  } else if (/账|钱|财务|复盘|预算|开销/.test(hay)) {
    missing = '一份更冷静的财务视角';
    proRole = '财务顾问';
    liveLine = '钱的事容易当局者迷——可以请一位财务顾问替你看一眼。';
  } else if (/爵士|音乐|和弦|谱|乐/.test(hay)) {
    missing = `「${topic}」更深的那层语境`;
    proRole = null;
    liveLine = `这层意思也许有人正经历着——可以请此刻懂「${topic}」的人来补。`;
  } else {
    missing = `「${topic}」缺的那一块`;
    proRole = null;
    liveLine = `如果需要现在有人去做点什么，也可以从这里约。`;
  }
  return {
    missing,
    proRole,
    pastLine: `这类内容，很可能已经有人写过了——我帮你找几则带出处的，引用时会自动署上来源。`,
    liveLine,
    place, topic,
  };
}

// 检索维度：按模式（past 现成 / live 实时）给"选择要找什么"
function coDimensions(note, mode) {
  const place = (note && note.city) || '某地';
  const topic = (note && note.notes && note.notes[0]) || '这一页';
  if (mode === 'live') {
    return [
      { id: 'prof', icon: 'users',  label: '按职业找',          hint: '摄影师 / 医生 / 乐手…', q: 'pro:' },
      { id: 'hobby', icon: 'heart', label: '按爱好找',          hint: '同好 · 懂这件事的人',   q: topic },
      { id: 'exp',  icon: 'quote',  label: '相似经历的人',       hint: '走过同一段路的人',     q: note ? note.cat : '' },
      { id: 'here', icon: 'camera', label: `在「${place}」拍一张`, hint: '请现场的人替你拍照',   q: 'now:' + place },
    ];
  }
  return [
    { id: 'prof',  icon: 'users',  label: '按职业找人',   hint: '医生 / 摄影师 / 乐手…', q: '' },
    { id: 'topic', icon: 'spark',  label: `懂「${topic}」`, hint: '写过这层语境的人',     q: topic },
    { id: 'exp',   icon: 'quote',  label: '相似经历的人',   hint: '走过同一段路的人',     q: note ? note.cat : '' },
  ];
}

// faiss 风格匹配：把笔记 + 查询拼成向量，按 kw 命中算相关度
// rawQuery 前缀：now:城市（此刻在）/ been:城市（去过）/ pro:（找专业人士）/ live（仅能现场的人）
function faissMatch(note, rawQuery, onlyLive) {
  const q = String(rawQuery || '');
  let placeFilter = null, mode = null;
  let qm = q.match(/^(now|been):(.+)$/);
  if (qm) { mode = qm[1]; placeFilter = qm[2]; }
  const proMode = /^pro:/.test(q);
  // 话题/职业检索：只看主题 + 内容标签（不掺城市/地点，免得同城的人被无脑抬高）
  // 地点检索：由 placeFilter 单独加权
  const free = (mode || proMode) ? [] : q.split(/[\s,，、]+/).filter(Boolean);
  const ctx = (note && !mode && !proMode) ? [note.cat, ...(note.notes || [])] : [];
  const tokens = free.concat(ctx);

  const scored = PEOPLE.map(p => {
    if (note && p.note === note.title) return null;               // 不匹配自己这页
    if (mode === 'now'  && p.now !== placeFilter) return null;     // 此刻在某地
    if (mode === 'been' && p.now !== placeFilter && !p.been.includes(placeFilter)) return null;
    if (proMode && !coIsPro(p)) return null;                       // 仅专业人士
    if (onlyLive && !(coIsLive(p) || coIsPro(p))) return null;     // 实时模式：能现场 / 专业
    const hay = [p.prof, p.now, ...p.been, ...p.hobby, ...p.kw];
    let hits = 0;
    tokens.forEach(t => { if (hay.some(h => h.includes(t) || t.includes(h))) hits++; });
    if (placeFilter && (p.now === placeFilter || p.been.includes(placeFilter))) hits += 3;
    if (proMode && coIsPro(p)) hits += 3;
    const jit = (parseInt(poporHash(p.id + (note ? note.id : '') + q).slice(0, 3), 16) % 60) / 10;
    const score = Math.min(97, 70 + hits * 6 + jit);
    const seenNow = mode === 'now' || p.now === placeFilter;
    return { p, score: Math.round(score * 10) / 10, hits, pro: coIsPro(p), live: coIsLive(p),
      whenPlace: placeFilter ? (seenNow ? '此刻在' + placeFilter : '曾去过' + placeFilter) : null };
  }).filter(Boolean);

  scored.sort((a, b) => b.score - a.score);
  const keep = s => s.hits > 0 || placeFilter || proMode;
  return scored.filter(keep).slice(0, 3).length ? scored.filter(keep).slice(0, 3) : scored.slice(0, 2);
}

// 墨吞开场：先抛一个与本页相关的问题，并给出可点的快捷追问
function motunOpening(note) {
  const tag = note && note.notes && note.notes[0] ? note.notes[0] : '这一页';
  return {
    text: `在写《${note ? note.title : '新的一页'}》。我读到你提到「${tag}」——要不要我帮你把它说清楚一点？我也可以替你找些有出处的资料。`,
    suggestions: [
      `「${tag}」到底指什么？`,
      '帮我找几则可引用的资料',
      '这一段还能怎么展开？',
    ],
  };
}

// 墨吞回应：返回一段话 + 引用到的资料（带 index）
function motunReply(note, prompt, usedCount) {
  const pool = motunSources(note);
  const wantsSource = /资料|出处|引用|来源|找/.test(prompt);
  const cite = wantsSource ? pool.slice(0, 2) : [pool[usedCount % pool.length]];
  let text;
  if (/指什么|是什么|意思|清楚/.test(prompt)) {
    text = `我的理解是这样——${pool[0].snippet} 你这一页其实已经写到了这层意思,要不要我把它接到你"${note ? note.city : ''}"那一段后面?`;
  } else if (wantsSource) {
    text = `找到几则有出处的资料,按相关度排了序。下面标了序号,正文里需要的话可以直接当脚注引。`;
  } else {
    text = `可以顺着这个方向写:先写你看见的具体一幕,再让它自己说出道理,别替它总结。我把一段相关的话放在下面,供你参考。`;
  }
  return { text, cite };
}

// ============================================================
// 墨吞 · 找灵感（第四面）—— 帮你写下一句
// 把"下一句"做成检索 + 提问：跨域 / 跨时 / 跨地 / 接续 / 借声 五个方向。
// 每条灵感都带来源（你的标签 / 你的旧页 / 你的心情 / 通讯录的人），不是黑箱补全。
// 演示用：确定性地在 window.NOTES 上做真实关联；接入引擎后把"生成"那步换成 LLM。
// ============================================================
const INSPIRE_MODES = [
  { id: 'cross',  icon: 'spark',  label: '跨域', hint: '换一个面，看同一个词' },
  { id: 'time',   icon: 'clock',  label: '跨时', hint: '另一段时间里的同一件事' },
  { id: 'place',  icon: 'mapPin', label: '跨地', hint: '另一座城里的同一个主题' },
  { id: 'thread', icon: 'route',  label: '接续', hint: '接一条还没收口的旧线' },
  { id: 'voice',  icon: 'users',  label: '借声', hint: '换一个人的笔法来写' },
];

// 已知标签的"切面"——跨域提问的种子（演示策展；未知标签回落到通用切面）
const TAG_FACETS = {
  '寺院': ['空间是怎么替人安静下来的', '为什么"慢"的地方让人想停留'],
  '侘寂': ['缺的那一块，是留给谁补的', '不完整怎么成了一种邀请'],
  '深夜': ['夜里听见的，白天为什么听不见', '一个人的深夜，和很多人的深夜'],
  '爵士': ['不解决的和弦，像生活里哪件事', '即兴是自由，还是更深的纪律'],
  '复盘': ['记录是为了看见，还是为了审判', '数字背后藏着的那个决定'],
  '克制': ['没买的那束花，和买了的那本书', '克制是匮乏，还是另一种丰盛'],
  '晨跑': ['身体先于头脑想明白的那件事', '第五公里之后安静下来的念头'],
  '身体': ['仪器之前，身体先知道了什么', '疼痛在替你写的那种日记'],
  '小津': ['镜头不追人，人走出去画框还在', '低机位看到的是谁的世界'],
  '留白': ['没有人的画面，到底在说什么', '多停那三秒的重量'],
  '咖啡': ['三个变量，和不确定性长期相处', '一杯手冲里的手艺与失败'],
  '专注': ['专注是排除，还是沉入', '把一件小事做到底，尽头是什么'],
  '奈良': ['不索取也不躲闪的那种距离', '人和人，能不能像人和鹿'],
  '相遇': ['恰好在同一片草地上', '擦肩而过里没说出口的那句'],
  '坂本龙一': ['连喘息都成了旋律', '走到尽头的人，留下的声音'],
  '告别': ['没剪掉的那段呼吸声', '告别是结束，还是另一种保存'],
  '出差': ['借来的时间，要还得格外用力', '别人的城市里，没有黄昏'],
  '城市': ['再快的城市，也要留一处慢的', '写字楼一格一格亮起来的傍晚'],
  '银杏': ['绿里已经有了要黄的预感', '一棵树，是怎么记住季节的'],
  '银杏树': ['绿里已经有了要黄的预感', '一棵树，是怎么记住季节的'],
};
function tagFacets(tag) {
  return TAG_FACETS[tag] || ['它在另一个领域里是什么——物性、时令，还是一段记忆', '把「' + tag + '」放到一个意想不到的场景里'];
}

// 借声 · 名家笔法（让某位作家重写这一段）
const FAMOUS_VOICES = [
  { name: '海明威',   style: '短句、克制，只写动作与名词，情绪压在水面下' },
  { name: '汪曾祺',   style: '平淡里见滋味，写吃食与日常，闲笔不闲' },
  { name: '张爱玲',   style: '苍凉的比喻，华丽又冷，世故的眼' },
  { name: '鲁迅',     style: '冷峻反讽，一刀见血，不留情面' },
  { name: '村上春树', style: '疏离的第一人称，奇特比喻，孤独与爵士' },
  { name: '沈从文',   style: '抒情的乡土，句子像水一样流' },
  { name: '卡尔维诺', style: '轻盈奇想，把抽象写得可触可感' },
  { name: '周作人',   style: '冲淡平和，知堂小品，淡而有味' },
];
// 借声 · 职业之声（把这页写成两位某职业的对话）
const PROFESSION_VOICES = [
  { prof: '摄影师',     lens: '用光、构图与按下快门的那一瞬去看' },
  { prof: '医生',       lens: '冷静临床，把感受翻译成身体的信号' },
  { prof: '美食家',     lens: '用味觉、火候与时令去写' },
  { prof: '建筑师',     lens: '从空间、结构与尺度去看' },
  { prof: '心理咨询师', lens: '替情绪命名，问一个温柔的问题' },
  { prof: '侦探',       lens: '盯住细节，从蛛丝马迹往回推' },
];
const VOICE_SUBS = [
  { id: 'author', label: '名家笔法' },
  { id: 'prof',   label: '职业之声' },
  { id: 'people', label: '找人' },
];

// —— 语料关系（真实计算，演示无需 faiss）—— //
function inspireCorpus(note) {
  const list = (typeof window !== 'undefined' && window.NOTES) ? window.NOTES : [];
  return list.filter(n => n && (!note || n.id !== note.id));
}
function noteThemes(n) { return [n.cat].concat(n.notes || []).filter(Boolean); }
function sharedThemes(a, b) {
  const set = new Set(noteThemes(b));
  return noteThemes(a).filter(t => set.has(t));
}
function dateNum(d) { return parseInt(String(d || '').replace(/\D/g, '').slice(0, 8) || '0', 10); }

// 找灵感主入口：给一页 + 一个方向，返回带来源的灵感卡
function inspireCards(note, mode, focusTag) {
  if (!note) return [];
  const corpus = inspireCorpus(note);
  const out = [];

  if (mode === 'cross') {
    const tag = focusTag || (note.notes && note.notes[0]) || note.cat;
    if (!tag) return [];
    tagFacets(tag).forEach((f, i) => out.push({
      id: 'cross-' + tag + '-' + i, mode, q: f,
      whyKind: 'tag', why: '标签「' + tag + '」· 换一个面', tag,
      draftSeed: '顺着「' + tag + '」的另一个面——' + f + '——替我接着写一两句，写具体的画面，别替我总结。',
    }));
    return out;
  }

  if (mode === 'time') {
    const hits = corpus.filter(n => sharedThemes(n, note).length)
      .sort((a, b) => dateNum(b.date) - dateNum(a.date)).slice(0, 3);
    hits.forEach((n, i) => out.push({
      id: 'time-' + i, mode,
      q: '你 ' + n.date + ' 在《' + n.title + '》里也碰过「' + sharedThemes(n, note)[0] + '」——那时和现在，变了什么？',
      whyKind: 'note', why: '你的旧页 ·《' + n.title + '》' + (n.mood ? ' · 心情 ' + n.mood : ''), refId: n.id,
      draftSeed: '把这一页，和我 ' + n.date + ' 写的《' + n.title + '》接起来，找出之间变了什么，替我写一句过渡。',
    }));
    if (!hits.length) out.push(genericCard(note, mode));
    return out;
  }

  if (mode === 'place') {
    const hits = corpus.filter(n => sharedThemes(n, note).length && n.city && n.city !== note.city)
      .sort((a, b) => dateNum(b.date) - dateNum(a.date)).slice(0, 3);
    hits.forEach((n, i) => out.push({
      id: 'place-' + i, mode,
      q: '同样的「' + sharedThemes(n, note)[0] + '」，你在' + n.city + '也写过——换一座城，它会不一样吗？',
      whyKind: 'note', why: '你在 ' + n.city + ' 的《' + n.title + '》', refId: n.id,
      draftSeed: '比一比' + note.city + '和' + n.city + '里同样的「' + sharedThemes(n, note)[0] + '」，替我写一两句。',
    }));
    if (!hits.length) out.push(genericCard(note, mode));
    return out;
  }

  if (mode === 'thread') {
    // 你给这页留过、却没答的问题 —— 答它就是最省力的下一句
    if (note.mokeyQuestion) out.push({
      id: 'thread-q', mode,
      q: '你给这页留过一个没答的问题：「' + note.mokeyQuestion + '」——现在答它，就是下一句。',
      whyKind: 'mood', why: '墨吞留下的问题 · 待答',
      draftSeed: '顺着这个问题往下答，替我写一两句：' + note.mokeyQuestion,
    });
    // 反复出现的标签 = 一直没收口的线
    const freq = {};
    inspireCorpus(null).forEach(n => noteThemes(n).forEach(t => { freq[t] = (freq[t] || 0) + 1; }));
    const recurring = noteThemes(note).filter(t => (freq[t] || 0) >= 2)
      .sort((a, b) => freq[b] - freq[a]).slice(0, 3);
    recurring.forEach((t, i) => {
      const earliest = corpus.filter(n => noteThemes(n).includes(t)).sort((a, b) => dateNum(a.date) - dateNum(b.date))[0];
      out.push({
        id: 'thread-' + i, mode,
        q: '「' + t + '」从 ' + (earliest ? earliest.date : '很早') + ' 起就在你本子里反复出现，这条线一直没收口——这一页要不要替它收个尾？',
        whyKind: 'mood', why: '反复出现 · ' + freq[t] + ' 处', refId: earliest && earliest.id,
        draftSeed: '「' + t + '」在我本子里出现了好几次，替我写一句，把这些零散的页收成一条线。',
      });
    });
    if (!out.length) out.push(genericCard(note, mode));
    return out;
  }

  if (mode === 'voice') {
    const topic = (note.notes && note.notes[0]) || note.cat || '这一页';
    const sub = focusTag || 'author';
    if (sub === 'people') {
      const people = (typeof faissMatch === 'function') ? faissMatch(note, '') : [];
      people.slice(0, 3).forEach((m, i) => {
        const p = m.p;
        out.push({
          id: 'voice-p-' + i, mode,
          q: '借 ' + p.name + '（' + p.prof + '）的眼睛，写「' + topic + '」缺的那一句。',
          whyKind: 'person', why: '通讯录 · ' + p.prof + ' · ' + p.id, person: p,
          draftSeed: '以一位' + p.prof + '的笔法和视角，替我写一两句关于「' + topic + '」的话。',
        });
      });
      if (!people.length) out.push(genericCard(note, mode));
    } else if (sub === 'prof') {
      PROFESSION_VOICES.slice(0, 5).forEach((v, i) => out.push({
        id: 'voice-pr-' + i, mode,
        q: '写成两位' + v.prof + '的对话 · ' + v.lens,
        whyKind: 'voice', why: '职业之声 · ' + v.prof,
        draftSeed: '把下面这一页改写成两位' + v.prof + '之间的一段简短对话，' + v.lens + '，借对话把意思讲出来，别替我总结。',
      }));
    } else {
      FAMOUS_VOICES.slice(0, 6).forEach((v, i) => out.push({
        id: 'voice-a-' + i, mode,
        q: '让' + v.name + '重写这一段 · ' + v.style,
        whyKind: 'voice', why: '名家笔法 · ' + v.name,
        restyle: v.name,   // 借声 · 接 /api/notes/{id}/restyle 的笔法名
        draftSeed: '用' + v.name + '的笔法重写下面这段，抓住其特征：' + v.style + '。保留我的原意与主题，不要改写事实。',
      }));
    }
    return out;
  }

  return out;
}

function genericCard(note, mode) {
  const topic = (note.notes && note.notes[0]) || note.cat || '这一页';
  return {
    id: 'generic-' + mode, mode,
    q: '关于「' + topic + '」，还有一个你没写到、但答得上来的角度——会是什么？',
    whyKind: 'tag', why: '本页主题 · ' + topic,
    draftSeed: '就「' + topic + '」给我一个反常识的角度，并替我写一两句开头。',
  };
}

// —— 为数据中出现的新城市补全线路色 / 经纬度（避免地铁图缺值）——
function ensureCityMaps(notes) {
  (notes || []).forEach(n => {
    const c = n.city; if (!c) return;
    if (!CITY_LINE[c] || !CITY_GEO[c]) {
      const known = Object.keys(CITY_GEO).find(k => c.indexOf(k) >= 0);
      if (!CITY_LINE[c]) {
        const hue = parseInt(poporHash(c).slice(0, 2), 16) * 360 / 255 | 0;
        CITY_LINE[c] = known ? CITY_LINE[known] : ('hsl(' + hue + ' 36% 42%)');
      }
      if (!CITY_GEO[c]) {
        CITY_GEO[c] = known ? CITY_GEO[known] : {
          lat: 22 + (parseInt(poporHash(c).slice(0, 2), 16) % 28),
          lon: 100 + (parseInt(poporHash(c).slice(2, 4), 16) % 35),
        };
      }
    }
  });
}

// —— 主入口：从后端拉取并转换；失败回落到示例数据 ——
async function loadNotes() {
  const url = POPOR_API_BASE + '/api/notes?limit=200';
  try {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), 2500);
    const res = await fetch(url, { signal: ctrl.signal });
    clearTimeout(timer);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const data = await res.json();
    const raw = Array.isArray(data.notes) ? data.notes : [];
    const notes = raw.map(transformNote);
    ensureCityMaps(notes);
    if (document.body) document.body.setAttribute('data-source', 'api');
    console.info('[Popor] 已从 ' + url + ' 载入 ' + notes.length + ' 则真实笔记');
    return { notes, notebooks: buildNotebooks(notes) };
  } catch (err) {
    console.warn('[Popor] 无法连接后端 ' + POPOR_API_BASE + '，已回落到本地示例数据。原因：' + (err && err.message));
    const notes = FALLBACK_NOTES.map(n => Object.assign({}, n, { notebook: n.cat }));
    ensureCityMaps(notes);
    if (document.body) document.body.setAttribute('data-source', 'demo');
    return { notes, notebooks: buildNotebooks(notes) };
  }
}

// ============================================================
// 浮现引擎 · 真实语义接口（见 popor_emerge_api_spec.md）
// 后端可达时优先用语义结果；不可达 / 出错由调用方回落到 inspireCards()。
// 接口一：POST /api/emerge（书写台停顿浮现 / 找灵感 time·place·cross）
// 接口三：POST /api/notes/{id}/restyle（借声 · 名家笔法）
// ============================================================

// 浮现：传入当前正文，拿回语义相关的旧笔记 hits（失败返回 null，调用方回落）
async function emergeFetch(text, excludeId, topK) {
  if (!text || !text.trim()) return null;
  try {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), 3000);
    const res = await fetch(POPOR_API_BASE + '/api/emerge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      signal: ctrl.signal,
      body: JSON.stringify({ text: text.slice(-600), exclude_id: excludeId || '', top_k: topK || 3 }),
    });
    clearTimeout(timer);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const data = await res.json();
    const hits = (data && Array.isArray(data.hits)) ? data.hits : [];
    return hits.length ? hits : null;
  } catch (err) {
    console.warn('[Popor] /api/emerge 不可达，回落本地 inspireCards()。原因：' + (err && err.message));
    return null;
  }
}

// 把 emerge hits 映射成「找灵感」卡片结构（与 inspireCards 输出同形）
function emergeToCards(hits, mode) {
  return (hits || []).map((h, i) => ({
    id: 'emerge-' + mode + '-' + (h.id || i),
    mode,
    q: (h.reason || '你早先也碰过这个') + ' —— 要不要接上那一页？',
    whyKind: 'note',
    why: '你的旧页 ·《' + (h.title || '未命名') + '》',
    refId: h.id,
    // 后端 hit 自带预览，详情面板不依赖 window.NOTES 也能展开
    emergeRef: { preview: h.preview || '', title: h.title || '', date: h.date || '', city: h.location || '' },
    draftSeed: '把这一页，和我' + (h.date ? h.date + ' ' : '')
      + '写的《' + (h.title || '那一页') + '》接起来——' + (h.reason || '') + '。替我写一句过渡，写具体画面，别替我总结。',
  }));
}

// 借声 · 名家笔法：让某位作家改写整段(rewrite)或接着写(continue)
// 成功返回 { styled_text, style, styles_available }；失败抛错（调用方提示并保留原卡）
async function restyleNote(noteId, style, mode, text) {
  const res = await fetch(POPOR_API_BASE + '/api/notes/' + encodeURIComponent(noteId) + '/restyle', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(Object.assign({ style: style, mode: mode || 'rewrite' }, text ? { text } : {})),
  });
  if (!res.ok) {
    let detail = '借声失败（' + res.status + '）';
    try { const e = await res.json(); if (e && e.detail) detail = e.detail; } catch (_) {}
    throw new Error(detail);
  }
  return res.json();
}

// ============================================================
// 通讯录 · 人际关系（在 PEOPLE 语境库之上补社交字段）
// 一个联系人可有多重关系；并按本子归类；可标注导入来源
// ============================================================
const RELATION_TYPES = [
  { id: 'collab', label: '合作者', tone: 'pp' },
  { id: 'work',   label: '工作',   tone: 'sea' },
  { id: 'friend', label: '朋友',   tone: 'gr' },
  { id: 'fan',    label: '粉丝',   tone: 'fl' },
  { id: 'family', label: '家人',   tone: 'yl' },
];
const SOURCE_LABEL = { popor: 'Popor', wechat: '微信', instagram: 'Instagram', phone: '手机' };

// 联系人 = PEOPLE 语境 + 社交字段；另补两位（家人/粉丝）做关系多样性
const CONTACT_EXTRA = {
  '@lakeside_zhou': { phone: '138 0021 7745', rel: ['collab', 'friend'], books: ['旅行', '读书'], src: 'wechat', skills: ['风光摄影', '慢门长曝', '黑白胶片'] },
  '@wabi_keng':     { phone: '139 5567 2210', rel: ['collab'],            books: ['旅行', '读书'], src: 'wechat', skills: ['庭园考据', '拓印', '语境补全'] },
  '@evans_fan':     { phone: '137 8890 3321', rel: ['friend'],            books: ['音乐'],         src: 'popor',  skills: ['即兴编曲', '谱曲', '黑胶选曲'] },
  '@ozu_low':       { phone: '136 4412 8890', rel: ['collab', 'work'],    books: ['电影', '旅行'], src: 'wechat', skills: ['剪辑视频', '空镜拍摄', '分镜设计'] },
  '@dr_lin':        { phone: '135 0098 4567', rel: ['work', 'friend'],    books: ['健康'],         src: 'phone',  skills: ['健康解读', '数据判读'] },
  '@pourover_an':   { phone: '188 7741 0092', rel: ['friend'],            books: ['生活'],         src: 'instagram', skills: ['手冲教学', '风味笔记'] },
  '@ledger_he':     { phone: '186 3320 5567', rel: ['work'],              books: ['账本'],         src: 'popor',  skills: ['财务复盘', '极简记账'] },
  '@ginkgo_x':      { phone: '159 6678 1123', rel: ['collab', 'fan'],     books: ['旅行', '生活'], src: 'instagram', skills: ['城市漫步', '随笔', '智能排版'] },
};
const CONTACT_GUESTS = [
  { id: '@mama_lin', name: '林知秋', prof: '母亲 · 退休教师', av: '#A8643C',
    now: '苏州', phone: '139 1100 2233', rel: ['family'], books: ['生活', '读书'], src: 'phone',
    skills: ['家常菜谱', '旧信整理'], note: '外婆的腌梅子', offer: '语境补全' },
  { id: '@reader_ye', name: '阿野', prof: '读者 · 独立书店店员', av: '#4A6B5A',
    now: '成都', phone: '', rel: ['fan'], books: ['读书'], src: 'popor',
    skills: ['读书摘录', '手写卡片'], note: 'replied 你的《雾里的西湖》', offer: '语境补全' },
];
function buildContacts() {
  const base = PEOPLE.map(p => Object.assign({}, p, CONTACT_EXTRA[p.id] || { phone: '', rel: ['friend'], books: [], src: 'popor', skills: p.hobby || [] }));
  return base.concat(CONTACT_GUESTS);
}
const CONTACTS = buildContacts();
function relMeta(id) { return RELATION_TYPES.find(r => r.id === id) || { label: id, tone: 'gr' }; }

// ============================================================
// 技能 Skills · 工具箱（和文生图 / 谱曲同级的能力）
// 官方技能 + 用户自制；通过人脉网络传播；每次调用消耗积分
// ============================================================
const SKILL_CATS = [
  { id: 'create', label: '创作' },
  { id: 'visual', label: '影像' },
  { id: 'read',   label: '识别' },
  { id: 'tidy',   label: '整理' },
];
const SKILLS = [
  { id: 'text-gen',  name: '文生文 · 续写',   icon: 'pencil', cat: 'create', credits: 2,  by: '官方',  uses: 24800, installed: true,  desc: '顺着你的语气把没写完的那句续下去，只补语境不抢话。' },
  { id: 'img-gen',   name: '文生图 · 插画',   icon: 'image',  cat: 'visual', credits: 8,  by: '官方',  uses: 18600, installed: true,  desc: '把一段文字生成一张克制的插画，风格随本子走。' },
  { id: 'compose',   name: '谱曲 · 配乐',     icon: 'music',  cat: 'create', credits: 10, by: '官方',  uses: 9200,  installed: false, desc: '给这一页配一小段旋律，像给文字找一个呼吸。' },
  { id: 'video-edit',name: '剪辑视频 · 短片', icon: 'eye',    cat: 'visual', credits: 12, by: '@ozu_low', byName: '川岛 · 纪录片导演', uses: 3400, installed: false, shared: true, desc: '把零散素材剪成一支有节奏的短片。川岛的低机位手法。' },
  { id: 'ocr',       name: 'OCR · 手写识别',  icon: 'camera', cat: 'read',   credits: 1,  by: '官方',  uses: 41200, installed: true,  desc: '把拍下来的手写稿转成可编辑文字，保留分段。' },
  { id: 'layout',    name: '智能排版 · 成册', icon: 'type',   cat: 'tidy',   credits: 3,  by: '@ginkgo_x', byName: '夏行 · 城市行走作家', uses: 5600, installed: false, shared: true, desc: '把零散笔记自动排成一册，留白与节奏都替你想好。' },
  { id: 'translate', name: '双语对照 · 中英', icon: 'globe',  cat: 'read',   credits: 2,  by: '官方',  uses: 7700,  installed: false, desc: '为这一页生成中英对照，保留原文的克制。' },
  { id: 'summarize', name: '读书摘录 · 卡片', icon: 'quote',  cat: 'tidy',   credits: 2,  by: '@reader_ye', byName: '阿野 · 书店店员', uses: 6100, installed: true, shared: true, desc: '把长文提炼成几张可引用的摘录卡片。' },
  { id: 'voice',     name: '朗读配音 · TTS',  icon: 'bell',   cat: 'visual', credits: 4,  by: '官方',  uses: 4300,  installed: false, desc: '用温和的声线把这一页读出来。' },
];
function skillById(id) { return SKILLS.find(s => s.id === id); }

// 这一页是「怎么写成的」——根据内容推断用过的技能足迹（确权用）
function noteSkillStack(note) {
  if (!note) return [];
  const hay = [note.title, note.cat, ...(note.notes || []), note.preview || '', note.body || ''].join(' ');
  const stack = [];
  if (note.hasPhoto || note.img) stack.push('ocr');
  if (/旅行|风景|画面|插画|城市|电影|光/.test(hay) || ['旅行', '电影', '创意'].includes(note.cat)) stack.push('img-gen');
  if (/音乐|爵士|和弦|谱|乐|旋律/.test(hay) || note.cat === '音乐') stack.push('compose');
  if (/视频|短片|剪|影像|纪录/.test(hay)) stack.push('video-edit');
  if (/书|读|摘|引用|句子/.test(hay) || ['读书', '学习'].includes(note.cat)) stack.push('summarize');
  stack.push('text-gen');                                  // 墨吞润色几乎人人都用
  if (note.body && note.body.length > 240) stack.push('layout');
  // 去重 + 保序，最多 4 个
  return stack.filter((s, i) => stack.indexOf(s) === i).slice(0, 4);
}

// ============================================================
// 墨吞计费 · 积分 + 订阅档位 + 模型分档
// ============================================================
const MODEL_TIERS = [
  { id: 'std',  name: '标准', sub: '墨吞 · 日常',  perMsg: 1, needsPlan: false, desc: '回应、找资料、润色，日常够用' },
  { id: 'deep', name: '深度', sub: '墨吞 · 深读',  perMsg: 4, needsPlan: true,  desc: '更长的上下文与更细的推敲，适合长文' },
];
const PLANS = [
  { id: 'free',    name: '清茶',   price: 0,  unit: '',     monthly: 200,  tiers: ['std'],         blurb: '随手记，够用就好' },
  { id: 'resident',name: '常驻',   price: 19, unit: '/月',  monthly: 2000, tiers: ['std', 'deep'], blurb: '常写的人，深度模型管够' },
  { id: 'deep',    name: '深耕',   price: 39, unit: '/月',  monthly: 6000, tiers: ['std', 'deep'], blurb: '创作者，技能与深读不设限' },
];
const USER_WALLET = { credits: 1280, plan: 'free', name: '林知夏', handle: '@zhixia' };

Object.assign(window, {
  RELATION_TYPES, SOURCE_LABEL, CONTACTS, relMeta, buildContacts,
  SKILL_CATS, SKILLS, skillById, noteSkillStack,
  MODEL_TIERS, PLANS, USER_WALLET,
});

// 初始占位（真正数据由 app 启动时经 loadNotes 注入）
window.NOTES = window.NOTES || [];
window.NOTEBOOKS = window.NOTEBOOKS || buildNotebooks([]);

Object.assign(window, {
  buildNotebooks, transformNote, loadNotes, ensureCityMaps, FALLBACK_NOTES, CATEGORY_STYLE,
  CITY_LINE, CITY_GEO, buildStations, poporHash, NOW,
  motunSources, motunOpening, motunReply, PEOPLE, coDimensions, faissMatch, motunCoJudge, coIsLive, coIsPro,
  INSPIRE_MODES, TAG_FACETS, tagFacets, inspireCards, FAMOUS_VOICES, PROFESSION_VOICES, VOICE_SUBS,
  emergeFetch, emergeToCards, restyleNote,
});
