import { VERSION, agents, projects, deliverables, tasks, statuses, events, initialMessages } from './data.js'; const $ = (s, root = document) => root.querySelector(s); const esc = (value = '') => String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const icons = { chat: '', contacts: '', folder: '', events: '', grid: '', box: '', user: '', search: '', chevron: '', down: '', back: '', arrow: '', external: '', file: '', download: '', clip: '', send: '', plus: '', check: '', circleCheck: '', alert: '', clock: '', close: '', refresh: '', tasks: '', home: '', info: '', bolt: '', more: '', lock: '', monitor: '', phone: '', moon: '', sun: '', logout: '', layers: '', wifi: '', inbox: '', }; const icon = name => ``; const mark = () => ''; const brand = () => `
${mark()}fmode.
`; const button = (label, action, value = '', kind = '', i = '') => ``; const iconButton = (i, label, action, value = '', extra = '') => ``; const avatar = (a, size = '') => ``; const agentById = id => agents.find(a => a.id === id); const projectById = id => projects.find(p => p.id === id); const artifactById = id => deliverables.find(a => a.id === id); const taskById = id => tasks.find(t => t.id === id); const statusBadge = (text, style = '') => `${esc(text)}`; const agentStatus = a => statusBadge(a.statusText, a.status === 'blocked' ? 'danger' : a.status === 'working' ? 'accent' : ''); const taskBadge = status => statusBadge(statuses[status], status === 'blocked' ? 'danger' : status === 'done' ? 'success' : status === 'running' ? 'accent' : ''); const storage = { get(key, fallback) { try { return localStorage.getItem(`fmode-design-demo:${key}`) ?? fallback; } catch { return fallback; } }, set(key, value) { try { localStorage.setItem(`fmode-design-demo:${key}`, value); } catch { toast('浏览器存储不可用,本次设置仅在当前页面生效。'); } }, }; const state = { mode: 'desktop', page: 'chat', id: '', agentId: 'zhixia', projectId: 'autumn', taskId: 'T-108', contactsTab: 'all', colleague: '', chatFilter: 'all', taskFilter: 'all', eventFilter: 'all', resultView: 'project', queries: {}, scenario: 'normal', homeStates: {}, connection: 'connected', failNext: false, messages: structuredClone(initialMessages), drafts: {}, theme: storage.get('theme', 'light'), palette: storage.get('palette-v2', 'platinum'), loginMethod: 'phone', loginError: '', loginBusy: false, codeSent: false, scrolls: new Map(), overlay: null, focusReturn: null, uploading: false, }; const pageLabels = { chat: '会话', messages: '会话', contacts: '通讯录', projects: '项目', events: '事件', workbench: '工作台', tasks: '任务看板', deliverables: '交付成果', mine: '我的', agent: '数字生命详情', home: '首页' }; const navItems = [['chat', 'chat', '会话'], ['contacts', 'contacts', '通讯录'], ['projects', 'folder', '项目'], ['events', 'events', '事件'], ['workbench', 'grid', '工作台'], ['deliverables', 'box', '成果'], ['mine', 'user', '我的']]; const mobileItems = [['home', 'home', '首页'], ['messages', 'chat', '会话'], ['contacts', 'contacts', '通讯录'], ['workbench', 'grid', '工作台'], ['mine', 'user', '我的']]; const palettes = [ { id: 'platinum', name: '铂金曜黑', description: '炭黑 × 铂金灰' }, { id: 'ice', name: '冰白靛蓝', description: '冰白 × 靛青' }, { id: 'clay', name: '暖瓷陶橘', description: '暖瓷 × 陶橘' }, { id: 'slate', name: '岩灰鸢紫', description: '岩灰 × 鸢紫' }, ]; if (!palettes.some(palette => palette.id === state.palette)) state.palette = 'graphite'; if (!['light', 'dark'].includes(state.theme)) state.theme = 'light'; const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); let renderedRoute = null; let routeAnimations = []; let actionTrigger = null; function motionOptions(token) { const style = getComputedStyle(document.documentElement); return { duration: reducedMotion.matches ? 0 : parseFloat(style.getPropertyValue(token)), easing: style.getPropertyValue('--ease-standard').trim(), fill: 'both' }; } function animateRoute(previous, back = false) { routeAnimations.forEach(animation => animation.cancel()); routeAnimations = []; if (!previous || reducedMotion.matches || previous.path === location.pathname) return; const samePage = previous.page === state.page && previous.mode === state.mode; let target = $('#main')?.firstElementChild; if (state.mode === 'desktop' && samePage) { target = $(state.page === 'chat' ? '.messages' : state.page === 'tasks' ? '.task-detail' : '.project-detail'); } if (!target) return; const intoDetail = state.mode === 'mobile' && state.id && !previous.id; const fromDetail = state.mode === 'mobile' && previous.id && !state.id; const directional = state.mode === 'mobile' && (intoDetail || fromDetail); const offset = back || fromDetail ? -16 : 16; const frames = directional ? [{ opacity: .7, transform: `translateX(${offset}px)` }, { opacity: 1, transform: 'translateX(0)' }] : [{ opacity: .65 }, { opacity: 1 }]; const animation = target.animate(frames, motionOptions(directional ? '--motion-detail' : '--motion-page')); routeAnimations.push(animation); animation.finished.then(() => animation.cancel(), () => {}); } function initialMode() { const saved = storage.get('mode', ''); if (['desktop', 'mobile'].includes(saved)) return saved; return /Android|iPhone|iPad|Mobile/i.test(navigator.userAgent) || innerWidth < 768 ? 'mobile' : 'desktop'; } function href(page, id = '') { return `/${state.mode}/${page}${id ? `/${encodeURIComponent(id)}` : ''}`; } function navigate(page, id = '', replace = false) { const previous = renderedRoute; const main = $('#main'); state.scrolls.set(location.pathname, main?.scrollTop || 0); closeOverlay(false); const target = page.startsWith('/') ? page : href(page, id); if (target === location.pathname) { render(); return; } history[replace ? 'replaceState' : 'pushState']({}, '', target); readRoute(); render(false); requestAnimationFrame(() => { const el = $('#main'); if (el) el.scrollTop = state.scrolls.get(location.pathname) || 0; if (state.page === 'chat') scrollMessages(); animateRoute(previous); }); } function readRoute() { const parts = location.pathname.split('/').filter(Boolean).map(x => { try { return decodeURIComponent(x); } catch { return x; } }); if (!parts.length || parts[0] === 'index.html') { const mode = initialMode(); history.replaceState({}, '', `/${mode}/${mode === 'mobile' ? 'home' : 'chat'}`); return readRoute(); } if (parts[0] === 'login') { state.mode = initialMode(); state.page = 'login'; state.id = ''; return; } if (!['desktop', 'mobile'].includes(parts[0])) { state.mode = initialMode(); const legacy = { messages: state.mode === 'mobile' ? 'messages' : 'chat', workbench: 'workbench', deliverables: 'deliverables', mine: 'mine', chat: 'chat', agent: 'agent', call: 'agent' }; const mapped = legacy[parts[0]]; if (mapped) { if (parts[0] === 'call') setTimeout(() => toast('语音通话功能暂未开放,已转至数字生命详情。'), 100); history.replaceState({}, '', href(mapped, parts[1] || '')); return readRoute(); } state.page = 'not-found'; return; } state.mode = parts[0]; state.page = parts[1] || (state.mode === 'mobile' ? 'home' : 'chat'); state.id = parts[2] || ''; if (!parts[1]) history.replaceState({}, '', href(state.page)); if (!pageLabels[state.page]) state.page = 'not-found'; if (state.page === 'chat' && state.id && agentById(state.id)) state.agentId = state.id; if (state.page === 'tasks' && state.id && taskById(state.id)) { state.taskId = state.id; state.agentId = taskById(state.id).agentId; } if (state.page === 'projects' && projectById(state.id)) state.projectId = state.id; if (storage.get('signedOut', 'false') === 'true') { history.replaceState({}, '', `/login?redirect=${encodeURIComponent(location.pathname)}`); state.page = 'login'; } } function applyTheme() { document.documentElement.dataset.theme = state.theme; document.documentElement.dataset.palette = state.palette; const colors = getComputedStyle(document.documentElement); $('meta[name="theme-color"]').content = colors.getPropertyValue('--bg').trim(); const favicon = ``; $('link[rel="icon"]').href = `data:image/svg+xml,${encodeURIComponent(favicon)}`; } function toast(message) { const el = $('#toast'); el.textContent = message; el.classList.add('visible'); clearTimeout(toast.timer); toast.timer = setTimeout(() => el.classList.remove('visible'), 3600); } function markdown(text) { let inList = false; return text.split('\n').map(line => { const safe = esc(line); let prefix = ''; if (!line.startsWith('- ') && inList) { prefix = ''; inList = false; } if (line.startsWith('# ')) return `${prefix}

${safe.slice(2)}

`; if (line.startsWith('## ')) return `${prefix}

${safe.slice(3)}

`; if (line.startsWith('- ')) { const open = inList ? '' : '
    '; inList = true; return `${open}
  • ${safe.slice(2)}
  • `; } if (line === '---') return `${prefix}
    `; return `${prefix}${line ? `

    ${safe}

    ` : ''}`; }).join('') + (inList ? '
' : ''); } function fileIcon(a) { return `${icon('file')}${esc(a.type)}`; } function searchField(key, placeholder) { return ``; } function filterTabs(items, active, action, role = 'tablist') { return `
${items.map(([id, title, count]) => ``).join('')}
`; } function panelState(scope, content, empty = false) { const scenario = state.homeStates[scope] || state.scenario; if (scenario === 'normal' && !empty) return content; if (scenario === 'loading') return `

正在加载,请稍候…

`; const kind = scenario === 'normal' ? 'empty' : scenario; const texts = { empty: ['inbox', '这里暂时没有内容', empty ? '试试其他筛选条件,或等待新的内容。' : '有新的内容后,会显示在这里。'], error: ['alert', '加载失败', '连接暂时不可用,已有内容不会因此被删除。'], forbidden: ['lock', '无权访问此内容', '当前演示账号没有此资源的访问权限。请联系管理员。'], }; const [i, title, description] = texts[kind] || texts.empty; return `
${icon(i)}

${title}

${description}

${kind === 'error' ? button('重新加载', 'retry-state', scope, 'btn-outline', 'refresh') : ''}${kind === 'forbidden' ? '403 · 演示权限状态' : ''}
`; } function pageHeader(title, description, actions = '') { return `

${title}

${description ? `

${description}

` : ''}
${actions ? `
${actions}
` : ''}
`; } function backHeader(page, label) { return ``; } function sidebar() { const current = state.page === 'tasks' ? 'workbench' : state.page; return ``; } function mobileNav() { const current = state.page === 'chat' ? 'messages' : state.page === 'tasks' ? 'workbench' : state.page; return ``; } function demoStrip() { return `
${icon('layers')}交互原型 · 演示数据/ 不连接真实服务
`; } function chatList() { const query = (state.queries.chat || '').toLowerCase(); const list = agents.filter(a => a.connected && (state.chatFilter !== 'unread' || a.unread) && a.name.toLowerCase().includes(query)); return `

会话

${iconButton('plus', '从通讯录发起会话', 'navigate', 'contacts')}
${filterTabs([['all', '全部'], ['unread', '未读', agents.filter(a => a.unread).length]], state.chatFilter, 'chat-filter')}
${panelState('chats', list.map(a => chatRow(a)).join(''), !list.length)}
${icon('lock')}仅你与数字生命的一对一会话
`; } function chatRow(a, home = false) { return ``; } function messageFile(id) { const a = artifactById(id); return ``; } function messageView(m, a) { return `
${avatar(m.sender === 'me' ? { name: '林悦' } : a)}
${m.sender === 'me' ? '你' : a.name}
${m.text ? `
${esc(m.text)}
` : ''}${m.artifactId ? messageFile(m.artifactId) : ''}${m.file ? `
${icon('file')}${esc(m.file.name)}${esc(m.file.size)} · 本地文件,不上传
` : ''}${m.taskId ? `` : ''}${m.failed ? `
${icon('alert')}未发送${button('重试', 'retry-message', m.id)}
` : ''}${m.pending ? '
正在发送(模拟)…
' : ''}
`; } function chatMain() { const a = agentById(state.agentId); if (!a || (state.id && !agentById(state.id))) return notFound('这段会话不存在'); if (!a.connected) return agentPage(a.id); const p = projects.find(p => p.agentIds.includes(a.id)); const list = state.messages[a.id] || []; return `
${iconButton('back', '返回会话列表', 'navigate', 'messages', 'mobile-back')}${avatar(a)}

${a.name}

${a.specialty}

${iconButton('tasks', '查看此数字生命的任务', 'agent-tasks', a.id)}${iconButton('box', '打开交付成果', 'results-drawer', a.id, state.mode === 'mobile' ? '' : 'rail-toggle')}${iconButton('info', '查看数字生命详情', 'agent-detail', a.id, 'desktop-only')}
${state.connection !== 'connected' ? `
${icon('wifi')}${state.connection === 'connecting' ? '正在重新连接…' : '连接已断开,消息将保留'}${state.connection === 'disconnected' ? button('重新连接', 'reconnect') : ''}
` : ''}
${icon('folder')}${p.name}
${panelState('messages', `
今天 · 10 月 14 日
${list.map(m => messageView(m, a)).join('')}`, !list.length)}
${iconButton('clip', '添加本地文件(仅模拟上传)', 'attach')}${state.mode === 'desktop' ? 'Enter 发送 · Shift + Enter 换行' : '文件仅在本地模拟'}

内容由数字生命生成,请核实重要信息。

`; } function railContent(agentId) { const a = agentById(agentId); const p = projects.find(p => p.agentIds.includes(agentId)); const results = deliverables.filter(d => d.agentId === agentId); return `
当前项目
${p.name}

${p.description}

${button('打开项目', 'project', p.id, 'text-link', 'arrow')}
来自${a.name}${results.length} 份成果
${panelState('rail-results', results.map(d => ``).join(''), !results.length)}${button('查看全部成果', 'navigate', 'deliverables', 'text-link full-width', 'box')}
${icon('info')} 成果与当前数字生命关联。
打开文件即可预览或下载演示内容。
`; } function chatPage() { if (state.mode === 'mobile') return state.page === 'messages' || !state.id ? chatList() : chatMain(); return `
${chatList()}${chatMain()}
`; } function artifactRow(d, extended = false) { const a = agentById(d.agentId); return ``; } function homePage() { const recent = agents.filter(a => a.connected).slice(0, 6); return `
${brand()}${iconButton('user', '打开我的账号', 'navigate', 'mine')}
10 月 14 日,星期三

上午好,林悦。

看看新的进展,继续手头的工作。

${panelState('home-events', ``)}

最新交付3

${button('全部', 'navigate', 'deliverables', 'text-link', 'chevron')}
${panelState('home-results', deliverables.slice(0, 3).map(d => artifactRow(d)).join(''))}

最近会话${recent.length}

${button('全部', 'navigate', 'messages', 'text-link', 'chevron')}
${panelState('home-chats', recent.map(a => chatRow(a, true)).join(''))}
`; } function contactsPage() { const q = (state.queries.contacts || '').toLowerCase(); const list = agents.filter(a => a.name.toLowerCase().includes(q)); const colleagues = ['林悦', '陈序', '沈言'].filter(name => name.toLowerCase().includes(q)); const rows = list.map(a => ``).join(''); const people = colleagues.map(name => `${state.colleague === name ? agents.filter(a => a.owner === name && (!q || a.name.toLowerCase().includes(q))).map(a => ``).join('') : ''}`).join(''); const content = `${state.contactsTab !== 'people' ? `
AI 数字生命 · ${list.length}
${rows}` : ''}${state.contactsTab !== 'ai' ? `
同事 · ${colleagues.length}
${people}` : ''}`; const empty = state.contactsTab === 'ai' ? !list.length : state.contactsTab === 'people' ? !colleagues.length : !list.length && !colleagues.length; return `
${pageHeader('通讯录', '找到你的数字生命,开始一段协作。')}
${filterTabs([['all', '全部'], ['ai', 'AI 数字生命', agents.length], ['people', '同事', 3]], state.contactsTab, 'contacts-tab')}${searchField('contacts', '搜索名称')}
${panelState('contacts', content, empty)}
`; } function projectDetail(p) { return `
${p.code}${statusBadge(p.status, p.status === '有阻塞' ? 'warning' : 'accent')}

${p.name}

${p.brief}

项目负责人
${p.owner}
目标交付
${p.due}
最近更新
${p.updated}

关联会话

${p.agentIds.length} 位数字生命
${p.agentIds.map(id => { const a = agentById(id); return ``; }).join('')}

交付成果

${deliverables.filter(d => d.projectId === p.id).length}
${panelState('project-results', deliverables.filter(d => d.projectId === p.id).map(d => artifactRow(d)).join(''))}

每份成果均可展开预览,也可下载演示文件。

`; } function projectsPage() { const mobileDetail = state.mode === 'mobile' && state.id; const p = projectById(state.id || state.projectId); if (!p) return notFound('项目不存在'); const list = projects.filter(p => p.name.includes(state.queries.projects || '')); return `
${mobileDetail ? backHeader('projects', '全部项目') : pageHeader('项目', '把会话、进展与交付,放回同一个上下文。', button('交付成果', 'navigate', 'deliverables', 'btn-outline', 'box'))}${!mobileDetail ? `
全部项目 ${list.length}${searchField('projects', '搜索项目')}
` : ''}${panelState('projects', `
${!mobileDetail ? `
${list.map(p => ``).join('')}
` : ''}${state.mode === 'desktop' || mobileDetail ? projectDetail(p) : ''}
`, !list.length && !mobileDetail)}
`; } function metric(label, value) { return `
${label}${value}%
`; } function agentCard(a) { return `
${avatar(a)}
当前任务${agentStatus(a)}

${a.task}

${metric('CPU', a.cpu)}${metric('内存', a.memory)}
`; } function workbenchPage() { const q = state.queries.workbench || ''; const list = agents.filter(a => a.name.includes(q)); return `
${pageHeader('工作台', '数字生命的运行实况,一眼了解。', button('刷新实况', 'refresh', '', 'btn-outline', 'refresh'))}
${button('数字生命', 'navigate', 'workbench', 'tab active', 'grid')}${button('任务看板', 'agent-tasks', state.agentId, 'tab', 'tasks')}
${panelState('workbench', `
数字生命5位
正在执行2位
需要关注1位
今日交付3份
全部数字生命 ${list.length}
${searchField('workbench', '搜索数字生命')}
${!list.length ? panelState('workbench-search', '', true) : `
${list.map(a => agentCard(a)).join('')}
`}

最近更新 10:42:08 · 双端使用同一组演示实况 · 余额仅演示,不发生扣费

`)}
`; } function taskDetail(t) { return `
${t.id}${taskBadge(t.status)}

${t.title}

${t.status === 'blocked' ? `` : ''}
数字生命 / assignee
${agentById(t.agentId).name}
负责人
${t.owner}
最近更新
${t.time}
分支
${t.branch}
${markdown(t.body)}

运行流水 ${t.runs.length}

${t.runs.length ? t.runs.map(r => `
${icon(r.status === 'done' ? 'circleCheck' : r.status === 'blocked' ? 'alert' : 'clock')}
${r.title}

${r.detail}

`).join('') : '

任务尚未运行,暂无记录。

'}

附件 ${t.attachments.length}

${t.attachments.length ? t.attachments.map(id => artifactRow(artifactById(id))).join('') : '

暂无附件。

'}

历史评论 只读

${t.comments.length ? t.comments.map(c => `

${esc(c)}

`).join('') : '

暂无历史评论。

'}
${button('进入会话', 'agent-open', t.agentId, 'btn-outline', 'chat')}

任务看板仅供查看,指令通过一对一会话沟通。

`; } function tasksPage() { const t = taskById(state.id || state.taskId); if (state.id && !t) return notFound('任务不存在'); const mobileDetail = state.mode === 'mobile' && state.id; const list = tasks.filter(t => t.agentId === state.agentId && (state.taskFilter === 'all' || t.status === state.taskFilter)).sort((a, b) => b.time.localeCompare(a.time)); const selected = list.find(x => x.id === state.taskId) || list[0]; const detail = mobileDetail ? t : selected; return `
${mobileDetail ? backHeader('tasks', '任务看板') : pageHeader('任务看板', '关注真实任务的状态、运行过程与阻塞原因。', '只读视图')}${!mobileDetail ? `
${button('数字生命', 'navigate', 'workbench', 'tab', 'grid')}${button('任务看板', 'navigate', 'tasks', 'tab active', 'tasks')}
仅显示当前数字生命的任务
${filterTabs([['all', '全部', tasks.filter(t => t.agentId === state.agentId).length], ...Object.entries(statuses).map(([s, label]) => [s, label, tasks.filter(t => t.agentId === state.agentId && t.status === s).length])], state.taskFilter, 'task-filter', 'group')}
` : ''}${panelState('tasks', `
${!mobileDetail ? `
${list.length} 项任务按更新时间倒序
${list.map(t => ``).join('')}
` : ''}${detail && (state.mode === 'desktop' || mobileDetail) ? taskDetail(detail) : ''}
`, !list.length && !mobileDetail)}

演示数据 · 真实任务依赖云函数接入,本原型不读取容器或任务数据库。

`; } function eventsPage() { const list = events.filter(e => state.eventFilter === 'all' || e.category === state.eventFilter); return `
${pageHeader('事件', '交付、执行与异常,按发生时间汇集。', button('刷新', 'refresh', '', 'btn-outline', 'refresh'))}
${filterTabs([['all', '全部', events.length], ['delivery', '交付', 4], ['execution', '执行', 1], ['alert', '告警', 1]], state.eventFilter, 'event-filter', 'group')}
${panelState('events', ['今天', '昨天'].map(day => { const dayEvents = list.filter(e => e.day === day); if (!dayEvents.length) return ''; return `
${day}${dayEvents[0].date}
${dayEvents.map(e => ``).join('')}
`; }).join(''), !list.length)}

事件由交付与运行状态生成,不作为独立审计记录。

`; } function resultsPage() { const q = state.queries.results || ''; const list = deliverables.filter(d => d.title.includes(q)); const grouped = state.resultView === 'project' ? projects.map(p => [p.name, list.filter(d => d.projectId === p.id)]) : ['今天', '昨天'].map(day => [day === '今天' ? '今天 · 10 月 14 日' : '昨天 · 10 月 13 日', list.filter(d => d.day === day)]); return `
${pageHeader('交付成果', '每一次协作,都有可查阅的交付。', `${deliverables.length} 份成果`)}
${filterTabs([['project', '按项目'], ['timeline', '按时间线']], state.resultView, 'result-view')}
${searchField('results', '搜索交付成果')}
${panelState('results', grouped.filter(([, data]) => data.length).map(([title, data]) => `
${icon(state.resultView === 'project' ? 'folder' : 'clock')}

${title}

${data.length}
成果名称来自交付时间
${data.map(d => artifactRow(d, true)).join('')}
`).join(''), !list.length)}
`; } function agentPage(id = state.id) { const a = agentById(id); if (!a) return notFound('数字生命不存在'); return `
${backHeader('contacts', '通讯录')}${panelState('agent', `
${avatar(a, 'large')}

${a.name}

${agentStatus(a)}

${a.specialty}

归属 ${a.owner} · 数字生命

${a.connected ? button('进入会话', 'agent-open', a.id, 'btn-primary', 'chat') : ''}
${!a.connected ? '' : ''}
CPU 使用率${a.cpu}%
内存使用率${a.memory}%
Token 用量${a.tokens}
账户余额${a.balance === '—' ? '—' : `¥${a.balance}`}

当前任务

${a.task}

${button('查看任务', 'agent-tasks', a.id, 'btn-outline', 'tasks')}

连接通道

${a.channel} · 最近更新 今天 10:42:08

${button('通道详情', 'channel', a.id, '', 'wifi')}

交付记录

${deliverables.filter(d => d.agentId === a.id).length ? deliverables.filter(d => d.agentId === a.id).map(d => artifactRow(d)).join('') : '

暂无交付记录。

'}
`)}
`; } function minePage() { const themeChoices = [['light', '浅色', 'sun'], ['dark', '深色', 'moon']].map(([id, name, i]) => ` `).join(''); const paletteChoices = palettes.map(palette => ` `).join(''); return `
${pageHeader('我的', '管理账号、界面外观与使用偏好。')}
${icon('sun')}

界面外观

显示模式

选择适合当前环境的界面。

${themeChoices}

整体配色

导航、内容与控件统一切换。

${paletteChoices}
${icon('monitor')}

使用端

当前为${state.mode === 'mobile' ? '手机' : '桌面'}端

切换后保留偏好,不随窗口变化跳转。

${button(state.mode === 'mobile' ? '切换桌面端' : '切换手机端', 'switch-mode', state.mode === 'mobile' ? 'desktop' : 'mobile', 'btn-outline', state.mode === 'mobile' ? 'monitor' : 'phone')}

自动识别设备

恢复默认的设备访问方式。

${button('恢复自动识别', 'auto-mode', '', '', 'refresh')}
${icon('lock')}

账号与凭证

登录凭证

原型不保存真实 Token。

${button('修复 Token', 'repair-token', '', 'btn-outline', 'refresh')}
Fmode ${VERSION}${button('演示说明', 'about', '', 'text-link', 'info')}
`; } function loginPage() { return ``; } function notFound(title = '这个页面不存在') { return `
${icon('search')}

${title}

检查地址,或回到协作空间继续浏览。

${button('返回首页', 'navigate', state.mode === 'mobile' ? 'home' : 'chat', 'btn-primary')}
`; } // Reconcile stable nodes so typing, selection, scroll, and focus survive updates. function nodeKey(node) { if (node.nodeType !== Node.ELEMENT_NODE) return node.nodeName; const id = node.id || node.getAttribute('data-message-id') || node.getAttribute('data-testid'); const action = node.getAttribute('data-action'); return `${node.nodeName}:${id || (action ? `${action}:${node.getAttribute('data-value') || ''}` : node.getAttribute('href') || node.getAttribute('class')?.split(' ')[0] || '')}`; } function reconcile(current, next) { if (current.nodeType !== Node.ELEMENT_NODE && current.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue; return; } if (current.nodeType === Node.ELEMENT_NODE) { for (const attribute of [...current.attributes]) { if (!next.hasAttribute(attribute.name)) current.removeAttribute(attribute.name); } for (const attribute of next.attributes) { if (current.getAttribute(attribute.name) !== attribute.value) current.setAttribute(attribute.name, attribute.value); } if (current instanceof HTMLTextAreaElement) { if (current.value !== next.value) current.value = next.value; return; } if (current instanceof HTMLInputElement) { if (next.hasAttribute('value') && current.type !== 'file' && current.value !== next.value) current.value = next.value; return; } } const unused = new Set(current.childNodes); let cursor = current.firstChild; for (const child of [...next.childNodes]) { const match = [...unused].find(node => nodeKey(node) === nodeKey(child)); if (match) { unused.delete(match); if (match !== cursor) current.insertBefore(match, cursor); reconcile(match, child); cursor = match.nextSibling; } else { const added = child.cloneNode(true); current.insertBefore(added, cursor); cursor = added.nextSibling; } } for (const node of unused) node.remove(); if (current instanceof HTMLSelectElement) current.value = next.value; } function render(preserve = true) { routeAnimations.forEach(animation => animation.cancel()); routeAnimations = []; const active = document.activeElement; const focus = active?.id; const start = active?.selectionStart; const end = active?.selectionEnd; const scroll = preserve ? $('#main')?.scrollTop || 0 : 0; const msgScroll = preserve ? $('#messages')?.scrollTop : undefined; applyTheme(); document.body.dataset.mode = state.mode; const pages = { chat: chatPage, messages: chatPage, home: homePage, contacts: contactsPage, projects: projectsPage, workbench: workbenchPage, tasks: tasksPage, events: eventsPage, deliverables: resultsPage, agent: agentPage, mine: minePage, 'not-found': notFound }; const isChat = state.mode === 'mobile' && state.page === 'chat' && state.id; document.title = `${pageLabels[state.page] || '登录'} · Fmode 演示`; const template = document.createElement('template'); template.innerHTML = `
${demoStrip() + (state.page === 'login' ? loginPage() : `
${state.mode === 'desktop' ? sidebar() : ''}
${(pages[state.page] || notFound)()}
${state.mode === 'mobile' ? mobileNav() : `
${brand()}

当前窗口适合手机端

桌面端需要至少 768px 的宽度。你的端偏好不会被自动改变。

${button('切换到手机端', 'switch-mode', 'mobile', 'btn-primary', 'phone')}
`}
`)}
`; reconcile($('#app'), template.content.firstElementChild); $('#app').inert = !!state.overlay; if ($('#main')) $('#main').scrollTop = scroll; if (msgScroll !== undefined && $('#messages')) $('#messages').scrollTop = msgScroll; if (!active?.isConnected && focus && document.getElementById(focus)) { const el = document.getElementById(focus); el.focus({ preventScroll: true }); if (typeof el.setSelectionRange === 'function' && start !== null && start !== undefined) { try { el.setSelectionRange(start, end); } catch { /* Number-like inputs do not support text selection. */ } } } autoHeight(); renderedRoute = { mode: state.mode, page: state.page, id: state.id, path: location.pathname }; } function scrollMessages() { const el = $('#messages'); if (el) el.scrollTop = el.scrollHeight; } function autoHeight() { const el = $('#message-input'); if (!el) return; el.style.height = 'auto'; el.style.height = `${Math.min(el.scrollHeight, state.mode === 'mobile' ? 100 : 160)}px`; } function openAgent(id) { const a = agentById(id); if (!a) return; state.agentId = id; navigate(a.connected ? 'chat' : 'agent', id); } function showOverlay(title, content, kind = '') { if (!state.overlay) state.focusReturn = actionTrigger || document.activeElement; state.overlay = { title, content, kind }; $('#app').inert = true; $('#overlay-root').innerHTML = `
`; const overlay = state.overlay; const dialog = $('.dialog'); const startTransform = kind === 'drawer' ? (state.mode === 'mobile' ? 'translateY(100%)' : 'translateX(100%)') : 'none'; overlay.transform = startTransform; if (!reducedMotion.matches) { const backdropAnimation = $('.overlay-backdrop').animate([{ opacity: 0 }, { opacity: 1 }], motionOptions('--motion-page')); const dialogAnimation = dialog.animate([{ transform: startTransform }, { transform: 'none' }], motionOptions(kind === 'drawer' ? '--motion-panel' : '--motion-page')); overlay.animations = [backdropAnimation, dialogAnimation]; overlay.animations.forEach(animation => animation.finished.then(() => animation.cancel(), () => {})); } requestAnimationFrame(() => { if (state.overlay === overlay) dialog.focus({ preventScroll: true }); }); } function closeOverlay(restore = true) { if (!state.overlay) return; const closing = state.overlay; const finish = () => { if (state.overlay !== closing) return; $('#overlay-root').innerHTML = ''; state.overlay = null; $('#app').inert = false; if (restore && state.focusReturn?.isConnected) state.focusReturn.focus({ preventScroll: true }); }; if (!restore || reducedMotion.matches) { finish(); return; } if (closing.closing) return; closing.closing = true; const backdrop = $('.overlay-backdrop'); const dialog = $('.dialog'); const startOpacity = getComputedStyle(backdrop).opacity; const startTransform = getComputedStyle(dialog).transform; closing.animations?.forEach(animation => animation.cancel()); const animations = [ backdrop.animate([{ opacity: startOpacity }, { opacity: 0 }], motionOptions('--motion-exit')), dialog.animate([{ transform: startTransform }, { transform: closing.transform }], motionOptions('--motion-exit')), ]; Promise.allSettled(animations.map(animation => animation.finished)).then(finish); } function preview(id) { const d = artifactById(id); if (!d) return; showOverlay(d.title, '', 'preview'); const content = d.type === 'CSV' ? `
${d.content.split('\n')[0].split(',').map(c => ``).join('')}${d.content.split('\n').slice(1).map(row => `${row.split(',').map(c => ``).join('')}`).join('')}
${esc(c)}
${esc(c)}
` : `
${markdown(d.content)}
`; $('.preview-dialog').innerHTML = `

${d.title}

${d.version} · 演示文件
${button('下载', 'download', id, 'btn-outline', 'download')}${iconButton('external', '在新标签页打开演示文件', 'open-file', id)}${iconButton('close', '关闭预览', 'close')}
FMODE / DELIVERABLE
${content}
`; } function download(id, open = false) { const d = artifactById(id); if (!d) return; const html = `${esc(d.title)}

FMODE · 演示文件,不代表真实交付

${esc(d.title)}

${esc(d.content)}
`; const blob = new Blob([open ? html : '\uFEFF' + d.content], { type: open ? 'text/html;charset=utf-8' : d.type === 'CSV' ? 'text/csv;charset=utf-8' : 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; if (open) { a.target = '_blank'; a.rel = 'noopener'; } else a.download = d.filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 60000); if (!open) toast('演示文件已开始下载。'); } function lab() { const choices = [['normal', '正常内容'], ['loading', '加载中'], ['empty', '空状态'], ['error', '加载失败'], ['forbidden', '权限不足']]; showOverlay('演示控制台', `

所有场景均为本地模拟,不影响真实账号。可以切换页面状态、连接状态和使用端。

页面数据状态

${choices.map(([id, label]) => button(label, 'scenario', id, state.scenario === id ? 'btn-soft' : 'btn-outline', state.scenario === id ? 'check' : '')).join('')}

会话状态

${button('模拟断线', 'disconnect', '', 'btn-outline', 'wifi')}${button(state.failNext ? '下次发送将失败' : '下次发送失败', 'fail-next', '', state.failNext ? 'btn-soft' : 'btn-outline', 'alert')}

手机首页 · 分区独立状态

${[['home-events', '事件提醒'], ['home-results', '最新交付'], ['home-chats', '最近会话']].map(([key, label]) => `
`).join('')}

预览与登录

${button('桌面端', 'switch-mode', 'desktop', 'btn-outline', 'monitor')}${button('手机端', 'switch-mode', 'mobile', 'btn-outline', 'phone')}${button('登录页', 'show-login', '', 'btn-outline', 'lock')}${button('演示说明', 'about', '', 'btn-outline', 'info')}
${button('恢复正常演示', 'reset-demo', '', 'btn-primary', 'refresh')}
`, 'drawer'); } function reconnect() { state.connection = 'connecting'; render(); setTimeout(() => { state.connection = 'connected'; render(); toast('模拟连接已恢复,未发送的消息可以重试。'); }, 1200); } function sendMessage() { const input = $('#message-input'); const text = input?.value.trim(); if (!text) { input?.focus(); return; } const agentId = state.agentId; const failed = state.failNext || state.connection !== 'connected'; const m = { id: `local-${Date.now()}`, sender: 'me', time: '刚刚', text, failed, pending: !failed }; (state.messages[agentId] ||= []).push(m); state.drafts[agentId] = ''; state.failNext = false; render(); scrollMessages(); if (failed) { toast('消息未发送,内容已保留。可点击消息下方重试。'); return; } simulatedReply(agentId, m); } function simulatedReply(agentId, message) { setTimeout(() => { message.pending = false; state.messages[agentId].push({ id: `reply-${Date.now()}`, sender: 'agent', time: '刚刚', text: '收到你的补充。我会结合当前项目继续整理,已有内容可以在交付成果里查看。\n\n这是演示回复,未调用真实数字生命。' }); if (state.page === 'chat' && state.agentId === agentId) { render(); scrollMessages(); } }, 900); } function simulateUpload(file) { if (state.uploading) { toast('请等待当前文件处理完成。'); return; } if (file.size > 25 * 1024 * 1024) { toast('演示文件最大支持 25 MB,请选择较小的文件。'); return; } const agentId = state.agentId; state.uploading = true; let value = 0; const timer = setInterval(() => { value += 20; const status = $('#upload-status'); if (status) status.innerHTML = ``; if (value >= 100) { clearInterval(timer); state.uploading = false; state.messages[agentId].push({ id: `file-${Date.now()}`, sender: 'me', time: '刚刚', file: { name: file.name, size: `${Math.max(.1, file.size / 1024).toFixed(1)} KB` }, failed: state.connection !== 'connected' }); render(); scrollMessages(); toast('文件卡片已添加,仅模拟上传,文件未离开浏览器。'); } }, 240); } function loginSubmit() { if (state.loginBusy) return; let error = ''; if (state.loginMethod === 'phone') { const phone = $('#phone')?.value || ''; const code = $('#code')?.value || ''; if (!/^1[3-9]\d{9}$/.test(phone)) error = '请输入有效的 11 位手机号。'; else if (code === '000000') error = '验证码已过期,请重新获取。'; else if (code !== '123456') error = '验证码错误。演示验证码为 123456。'; } else if ($('#token')?.value !== 'demo-fmode') error = 'Token 无效或已失效,请检查后重试。'; if (error) { $('#login-error').textContent = error; return; } state.loginBusy = true; const submit = $('#login-form [type="submit"]'); submit.disabled = true; submit.textContent = '正在登录…'; setTimeout(() => { state.loginBusy = false; storage.set('signedOut', 'false'); const redirect = new URLSearchParams(location.search).get('redirect'); navigate(redirect && /^\/(desktop|mobile)\//.test(redirect) ? redirect : `/${state.mode}/${state.mode === 'mobile' ? 'home' : 'chat'}`, '', true); toast('已进入演示空间,未建立真实登录会话。'); }, 700); } document.addEventListener('click', event => { const route = event.target.closest('[data-route]'); if (route && !event.ctrlKey && !event.metaKey && !event.shiftKey) { event.preventDefault(); navigate(route.getAttribute('href')); return; } if (event.target.matches('[data-backdrop]')) { closeOverlay(); return; } const target = event.target.closest('[data-action]'); if (!target || target.disabled) return; const { action, value } = target.dataset; const actions = { navigate: () => navigate(value), 'agent-open': () => openAgent(value), 'agent-detail': () => navigate('agent', value), 'agent-tasks': () => { state.agentId = value || state.agentId; state.taskFilter = 'all'; state.taskId = tasks.find(t => t.agentId === state.agentId)?.id || ''; navigate('tasks'); }, project: () => { state.projectId = value; navigate('projects', value); }, task: () => { const t = taskById(value); if (t) { state.taskId = t.id; state.agentId = t.agentId; if (state.taskFilter !== 'all' && state.taskFilter !== t.status) state.taskFilter = 'all'; navigate('tasks', value); } }, 'contacts-tab': () => { state.contactsTab = value; render(); }, colleague: () => { state.colleague = state.colleague === value ? '' : value; render(); }, 'chat-filter': () => { state.chatFilter = value; render(); }, 'task-filter': () => { state.taskFilter = value; if (state.id) { history.replaceState({}, '', href('tasks')); state.id = ''; } render(); }, 'event-filter': () => { state.eventFilter = value; render(); }, 'result-view': () => { state.resultView = value; render(); }, preview: () => preview(value), download: () => download(value), 'open-file': () => download(value, true), 'results-drawer': () => showOverlay('交付成果', railContent(value || state.agentId), 'drawer'), 'retry-state': () => { if (state.homeStates[value]) state.homeStates[value] = 'normal'; else state.scenario = 'normal'; render(); toast('演示内容已重新加载。'); }, refresh: () => { state.scenario = 'loading'; render(); setTimeout(() => { state.scenario = 'normal'; render(); toast('演示数据已刷新。'); }, 650); }, channel: () => { const a = agentById(value); showOverlay('通道详情', `
${avatar(a)}

${a.name}

${a.channel}

连接状态${statusBadge(a.connected ? '已连接(模拟)' : '未接入', a.connected ? 'accent' : '')}
通道类型${a.connected ? 'Matrix' : '—'}
最近检查今天 10:42:08

此处仅演示连接信息,不建立 Matrix 连接,也不展示凭证。

`, 'drawer'); }, balance: () => showOverlay('余额与账单', `

此处将打开平台的余额管理页面。需求未提供真实账单地址,本原型不跳转到猜测的外部站点。

演示余额¥ ${agentById(value).balance}
${button('知道了', 'close', '', 'btn-primary')}
`), close: () => closeOverlay(), lab: () => lab(), scenario: () => { state.scenario = value; state.homeStates = {}; closeOverlay(); render(); toast(`已切换到${{ normal: '正常内容', loading: '加载中', empty: '空状态', error: '加载失败', forbidden: '权限不足' }[value]}场景。`); }, disconnect: () => { state.connection = 'disconnected'; closeOverlay(); if (state.page !== 'chat') navigate('chat', state.agentId); else render(); setTimeout(() => { if (state.connection === 'disconnected') reconnect(); }, 4500); }, reconnect, 'fail-next': () => { state.failNext = !state.failNext; closeOverlay(); if (state.page !== 'chat') navigate('chat', state.agentId); toast('下一条消息将模拟发送失败,之后可重试。'); }, 'retry-message': () => { const m = state.messages[state.agentId]?.find(m => m.id === value); if (!m) return; if (state.connection !== 'connected') { toast('请等待连接恢复后重试。'); return; } m.failed = false; m.pending = true; render(); simulatedReply(state.agentId, m); }, attach: () => { if (!state.uploading) $('#file-input')?.click(); else toast('文件正在处理中,请稍候。'); }, theme: () => { state.theme = value; storage.set('theme', value); render(); }, palette: () => { if (palettes.some(palette => palette.id === value)) { state.palette = value; storage.set('palette-v2', value); render(); } }, 'switch-mode': () => { state.mode = value; storage.set('mode', value); navigate(`/${value}/${value === 'mobile' ? 'home' : 'chat'}`); }, 'auto-mode': () => { storage.set('mode', ''); const mode = initialMode(); state.mode = mode; navigate(`/${mode}/${mode === 'mobile' ? 'home' : 'chat'}`); toast('已恢复按设备自动识别。'); }, 'repair-token': () => showOverlay('修复登录凭证', '

当前为演示会话,没有真实 Token 可修复。正式版本会重新校验登录凭证,租户归属以服务端返回为准。

' + `
${button('取消', 'close', '', 'btn-outline')}${button('模拟修复', 'repair-done', '', 'btn-primary', 'refresh')}
`), 'repair-done': () => { closeOverlay(); toast('模拟凭证校验完成,未请求真实服务。'); }, logout: () => showOverlay('退出当前账号?', `

退出后将回到登录页。此操作仅结束演示会话,不影响任何真实账号。

${button('取消', 'close', '', 'btn-outline')}${button('确认退出', 'confirm-logout', '', 'btn-danger', 'logout')}
`), 'confirm-logout': () => { storage.set('signedOut', 'true'); navigate('/login'); }, 'show-login': () => navigate('/login'), 'login-method': () => { state.loginMethod = value; state.loginError = ''; render(); }, 'send-code': () => { if (!/^1[3-9]\d{9}$/.test($('#phone')?.value || '')) { $('#login-error').textContent = '请先输入有效的 11 位手机号。'; return; } state.codeSent = true; target.disabled = true; target.textContent = '已发送(模拟)'; $('#login-error').textContent = ''; toast('未发送真实短信。演示验证码:123456。'); setTimeout(() => { state.codeSent = false; if (target.isConnected) { target.disabled = false; target.textContent = '重新获取'; } }, 30000); }, 'reset-demo': () => { state.scenario = 'normal'; state.homeStates = {}; state.connection = 'connected'; state.failNext = false; closeOverlay(); render(); toast('页面与会话状态已恢复正常。'); }, about: () => showOverlay('关于这个 Demo', `

依据指定的 PRD、SPEC 与用户画像独立设计,覆盖 7 个业务模块与 5 项通用能力的可见界面。

人物、任务、消息、用量和成果均为演示数据。任务看板只读;验证码、Token、上传与回复均为模拟。

真实 Matrix、预签名上传、云函数、租户权限、历史虚拟滚动及生产 ui-kit 验收不包含在本原型中。

${VERSION} · 本地设计评审版

${button('继续体验', 'close', '', 'btn-primary')}
`), }; actionTrigger = target; try { actions[action]?.(); } finally { actionTrigger = null; } }); document.addEventListener('input', event => { if (event.target.dataset.search) { state.queries[event.target.dataset.search] = event.target.value; render(); } if (event.target.id === 'message-input') { state.drafts[state.agentId] = event.target.value; autoHeight(); } }); document.addEventListener('change', event => { if (event.target.id === 'task-agent') { state.agentId = event.target.value; state.taskId = tasks.find(t => t.agentId === state.agentId)?.id || ''; state.taskFilter = 'all'; navigate('tasks'); } if (event.target.id === 'file-input' && event.target.files[0]) simulateUpload(event.target.files[0]); if (event.target.dataset.homeState) { state.homeStates[event.target.dataset.homeState] = event.target.value; render(); toast('首页分区状态已更新。'); } }); document.addEventListener('submit', event => { if (event.target.id === 'composer-form') { event.preventDefault(); sendMessage(); } if (event.target.id === 'login-form') { event.preventDefault(); loginSubmit(); } }); document.addEventListener('keydown', event => { if (event.target.id === 'message-input' && event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); sendMessage(); } if (state.overlay && event.key === 'Escape') { event.preventDefault(); closeOverlay(); } if (state.overlay && event.key === 'Tab') { const focusable = [...document.querySelectorAll('.dialog button:not(:disabled), .dialog a, .dialog input, .dialog select, .dialog [tabindex="0"]')]; const first = focusable[0], last = focusable.at(-1); if (!first) { event.preventDefault(); return; } if (event.shiftKey && (document.activeElement === first || document.activeElement === $('.dialog'))) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && (document.activeElement === last || document.activeElement === $('.dialog'))) { event.preventDefault(); first.focus(); } } if (event.target.matches('[role="tab"]') && ['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) { const tabs = [...event.target.closest('[role="tablist"]').querySelectorAll('[role="tab"]')]; const index = tabs.indexOf(event.target); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length; const { action, value } = tabs[next].dataset; event.preventDefault(); tabs[next].click(); document.querySelector(`[role="tab"][data-action="${action}"][data-value="${value}"]`)?.focus(); } }); window.addEventListener('popstate', () => { const previous = renderedRoute; if (previous) state.scrolls.set(previous.path, $('#main')?.scrollTop || 0); closeOverlay(false); readRoute(); render(false); requestAnimationFrame(() => { if ($('#main')) $('#main').scrollTop = state.scrolls.get(location.pathname) || 0; if (state.page === 'chat') scrollMessages(); animateRoute(previous, true); }); }); // Tokens are consumed before first render and are never persisted in the demo. const tokenParams = new URLSearchParams(location.hash.replace(/^#/, '')); const queryParams = new URLSearchParams(location.search); const token = tokenParams.get('token') ?? queryParams.get('token'); if (token !== null) { const params = new URLSearchParams(location.search); params.delete('token'); history.replaceState({}, '', location.pathname + (params.size ? `?${params}` : '')); if (token === 'demo-fmode') { storage.set('signedOut', 'false'); history.replaceState({}, '', '/'); } else { state.loginError = 'Token 无效或已失效,请重新登录。'; history.replaceState({}, '', '/login'); } } readRoute(); render(false); requestAnimationFrame(scrollMessages);