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 = '') => `${esc(a.initials || a.name.slice(0, 1))}`;
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 `${button(label, 'navigate', page, '', 'back')}/详情
`;
}
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')}${searchField('chat', '搜索会话')}
${filterTabs([['all', '全部'], ['unread', '未读', agents.filter(a => a.unread).length]], state.chatFilter, 'chat-filter')}
${panelState('chats', list.map(a => chatRow(a)).join(''), !list.length)}
`;
}
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 `${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)}
`;
}
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')}
`;
}
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 `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 ``;
}
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', `
${icon('alert')}
栖云的部署任务已阻塞 4 小时环境凭证已失效,连续失败 3 次。需要负责人确认后继续。
${button('查看原因', 'task', 'T-106', '', 'arrow')}
${!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' ? `${icon('alert')}
已阻塞 ${t.blockedHours} 小时 · 连续失败 ${t.failures} 次原因:环境凭证失效
${t.blockKind}
` : ''}- 数字生命 / 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')} `).join('') : '
任务尚未运行,暂无记录。
'}
附件 ${t.attachments.length}
${t.attachments.length ? t.attachments.map(id => artifactRow(artifactById(id))).join('') : '
暂无附件。
'}
历史评论 只读
${t.comments.length ? t.comments.map(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} 份成果`)}
${panelState('results', grouped.filter(([, data]) => data.length).map(([title, data]) => `
成果名称来自交付时间
${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 ? '
尚未接入 FMODE,完成接入后即可开始会话。
' : ''}
CPU 使用率${a.cpu}%
内存使用率${a.memory}%
Token 用量${a.tokens}
账户余额${a.balance === '—' ? '—' : `¥${a.balance}`}
${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('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')}
账号与凭证
${button('修复 Token', 'repair-token', '', 'btn-outline', 'refresh')}
Fmode ${VERSION}${button('演示说明', 'about', '', 'text-link', 'info')}
`;
}
function loginPage() {
return `${brand()}YOUR DIGITAL WORKSPACE连接数字生命,
让协作持续发生。
从一次对话开始,了解工作进展,
收下每一份交付成果。
开始会话${icon('arrow')}跟进任务${icon('arrow')}查收成果
Fmode · 人与数字生命的协作空间 `;
}
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 = `${title}
${iconButton('close', '关闭弹窗', 'close')}${content}
`;
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 => `| ${esc(c)} | `).join('')}
${d.content.split('\n').slice(1).map(row => `${row.split(',').map(c => `| ${esc(c)} | `).join('')}
`).join('')}
` : `${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('通道详情', `连接状态${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);