front/IDE官网/assets/js/ai-advisor.js
zzh 70d59a0ea9 feat: 接入 IDE 官网与地区地图,并同步运营/财务/订单前端改动
新增 IDE 官网静态资源与 D3 分区域地图布局,构建时复制至 dist/site;同步应收应付、代发打包、订单详情等页面优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 19:24:05 +08:00

1945 lines
74 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* IDE Command Center — logistics advisor (mock scripts + cards)
*/
(function () {
'use strict';
const STORAGE_THREADS = 'ide-ai-threads';
const STORAGE_ACTIVE_THREAD = 'ide-ai-active-thread';
const STORAGE_SESSION = 'ide-ai-session';
const PHONE = '18123959245';
const EMAIL = 'bd01@1st56.com';
const WECHAT = 'ydgjzzh';
const ICP = '粤ICP备19018720号';
const ADDRESS_ZH = '深圳宝安区福永怀德翠岗5区36栋';
const ADDRESS_EN = 'Bldg 36, Cuigang 5, Fuyong, Bao’an, Shenzhen';
const HERO_STATS = [
{
id: 'coverage',
interactive: true,
value: { zh: '30+', en: '30+' },
label: { zh: '覆盖国家/地区', en: 'Countries & regions' },
},
{
value: { zh: '8,000+', en: '8,000+' },
label: { zh: '服务跨境卖家', en: 'SMB sellers served' },
},
{
value: { zh: '1,200万+', en: '12M+' },
label: { zh: '累计运送包裹', en: 'Parcels shipped' },
},
];
const COPY = {
en: {
welcomeTitle: 'IDE, a digital one-stop cross-border logistics provider',
welcomeTitleShort: 'IDE · Cross-border logistics',
welcomeSubLead: 'Parcels, express, dropship, and 9610 clearance — ',
welcomeSubAccent: 'built for SMB cross-border sellers',
welcomeSubTail: '.',
placeholder: 'Ask about lanes, tracking, or 9610…',
sendLabel: 'Ask',
human: 'Talk to a human advisor',
you: 'You',
historyTitle: 'Chat history',
newChat: 'New chat',
emptyHistory: 'No conversations yet',
fallback: 'Quotes are in the customer system after you register. You can also talk to an advisor.',
trustPhone: PHONE,
trustAddress: ADDRESS_EN,
trustWechat: 'WeChat ' + WECHAT,
trustEmail: EMAIL,
trustIcp: ICP,
guide9610Title: 'Ship first, tax second',
guide9610Kicker: 'Services',
guide9610Sub: 'Pick a lane. Tax and filings come after the parcel can leave.',
loginTrack: 'Sign in to track',
registerNow: 'Register to quote',
loginExisting: 'Already registered? Sign in',
heroCta: 'Ship now',
statsAria: 'Service highlights',
statsCoverageHint: 'Tap to view',
mapZoomHint: 'Tap a country to pin it on the map',
composeLead: 'Ask about a lane, or pick a shortcut below',
},
zh: {
welcomeTitle: 'IDE,全球数字化一站式跨境物流服务商',
welcomeTitleShort: 'IDE · 一站式跨境物流',
welcomeSubLead: '专线小包、国际快递、一件代发与 9610 报关,',
welcomeSubAccent: '专为中小跨境卖家而生',
welcomeSubTail: '。',
placeholder: '问线路、查轨迹、9610 政策…',
sendLabel: '提问',
human: '联系人工顾问',
you: '你',
historyTitle: '历史记录',
newChat: '新会话',
emptyHistory: '暂无会话',
fallback: '官网不提供询价。完成注册登记后,即可在客户系统里看明码价。也可以先联系顾问。',
trustPhone: PHONE,
trustAddress: ADDRESS_ZH,
trustWechat: '微信 ' + WECHAT,
trustEmail: EMAIL,
trustIcp: ICP,
guide9610Title: '先发货,再办税',
guide9610Kicker: '服务',
guide9610Sub: '先选怎么走货。税务和备案跟在后面,点一张卡就能问。',
loginTrack: '登录查真实轨迹',
registerNow: '去注册登记',
loginExisting: '已有账号,去登录',
heroCta: '立即发货',
statsAria: '服务数据',
statsCoverageHint: '点我查看',
mapZoomHint: '点击国家,地图上会用定位图标标出',
composeLead: '先问线路,或选快捷入口',
},
};
const GUIDE_SERVICES = [
{
icon: 'lane',
tag: { en: 'Ship', zh: '发货' },
en: { title: 'US / EU / SEA parcels', body: 'US, Mexico, Canada, EU 27, the UK, Australia, New Zealand, and Southeast Asia parcels. Tracked end to end.' },
zh: { title: '欧美 / 东南亚专线', body: '美国、墨西哥、加拿大、欧盟 27 国及英国、澳大利亚、新西兰,以及东南亚小包。全程可追踪。' },
ask: { en: 'I need a line to the US or Southeast Asia', zh: '我要走欧美或东南亚专线' },
intent: 'quote',
},
{
icon: 'warehouse',
tag: { en: 'Warehouse', zh: '仓储' },
en: { title: 'Warehouse dropship', body: 'Inbound, pick and pack, ship when the order lands. SMB sellers do not need their own warehouse.' },
zh: { title: '仓储一件代发', body: '入仓、拣货打包,订单来了就发出去。中小卖家不用自己养仓。' },
ask: { en: 'I need warehouse dropshipping', zh: '我要仓储一件代发' },
intent: 'warehouse',
},
{
icon: 'clearance',
tag: { en: 'Ship', zh: '发货' },
en: { title: '9610 clearance', body: 'List release, consolidated declaration, and three-document match — so this parcel can actually leave.' },
zh: { title: '9610 报关出货', body: '清单核放、汇总申报、三单对碰。先保证这票货出得去。' },
ask: { en: 'I need to ship a 9610 parcel', zh: '我有一票 9610 要出货' },
intent: 'hard',
},
{
icon: 'advise',
tag: { en: 'Ship', zh: '发货' },
en: { title: 'Tough cargo', body: 'Battery, oversize, no invoice, or declined elsewhere. We pick the lane and the tax path first.' },
zh: { title: '疑难货怎么发', body: '带电、超长、无票、别人不接的单。先帮你选线路和税务路径。' },
ask: { en: 'Others declined this cargo. Can you take it?', zh: '别人不接的货,你们能发吗?' },
intent: 'hard',
},
{
icon: 'refund',
tag: { en: 'Tax', zh: '税务' },
en: { title: 'Export VAT refund', body: 'If you have special VAT invoices: we assemble the refund pack and file it on the e-tax bureau.' },
zh: { title: '出口退税', body: '有增值税专票:整理退税资料,并在电子税务局申报。进项税按退税率退还。' },
ask: { en: 'Can you handle export tax refund?', zh: '出口退税可以代办吗?' },
intent: 'hard',
},
{
icon: 'exempt',
tag: { en: 'Tax', zh: '税务' },
en: { title: 'Exemption without invoice', body: 'No special invoice: pilot-zone filing and exemption registration, so export VAT is waived.' },
zh: { title: '无票免征', body: '没有专票:综试区备案和免税登记。出口环节增值税免掉。' },
ask: { en: 'Help me register for VAT-free export', zh: '帮我办无票免征登记' },
intent: 'hard',
},
{
icon: 'filing',
tag: { en: 'Filing', zh: '备案' },
en: { title: 'Customs credentials', body: 'Importer/exporter filing, cross-border seller status, and e-port card — when volume is ready to scale.' },
zh: { title: '海关资质', body: '进出口收发货人、跨境电商资质、电子口岸卡。量起来再办也不迟。' },
ask: { en: 'I need customs credentials filed', zh: '我要办海关资质备案' },
intent: 'hard',
},
];
const MAP_COORDS = {
eu: [10.5, 50.5],
us: [-98.5, 39.8],
mx: [-102.5, 23.6],
ca: [-106.3, 56.1],
au: [134.5, -25.7],
nz: [174.8, -41.3],
de: [10.45, 51.16],
fr: [2.35, 46.85],
it: [12.5, 42.85],
es: [-3.7, 40.4],
nl: [5.3, 52.15],
be: [4.35, 50.85],
at: [14.55, 47.52],
ie: [-7.7, 53.35],
gb: [-2.5, 54.0],
pt: [-8.6, 39.4],
gr: [23.73, 37.98],
se: [18.07, 59.33],
dk: [12.57, 55.68],
fi: [24.94, 60.17],
pl: [21.01, 52.23],
cz: [14.44, 50.08],
hu: [19.05, 47.5],
ro: [24.97, 45.94],
bg: [23.32, 42.7],
sk: [17.11, 48.15],
si: [14.51, 46.05],
hr: [15.98, 45.81],
lt: [25.28, 54.69],
lv: [24.11, 56.95],
ee: [24.75, 59.44],
lu: [6.13, 49.61],
mt: [14.51, 35.9],
cy: [33.38, 35.18],
my: [101.98, 4.21],
sg: [103.82, 1.35],
ph: [121.77, 12.88],
th: [100.5, 13.75],
vn: [105.85, 21.03],
id: [113.92, -0.79],
};
const COVER_REGIONS = [
{ id: 'eu', dest: { zh: '欧盟', en: 'European Union' }, label: { zh: '欧盟 27 国 + 英国', en: 'EU 27 + UK' }, expand: true },
{ id: 'us', dest: { zh: '美国', en: 'United States' }, label: { zh: '美国', en: 'United States' } },
{ id: 'mx', dest: { zh: '墨西哥', en: 'Mexico' }, label: { zh: '墨西哥', en: 'Mexico' } },
{ id: 'ca', dest: { zh: '加拿大', en: 'Canada' }, label: { zh: '加拿大', en: 'Canada' } },
{ id: 'au', dest: { zh: '澳大利亚', en: 'Australia' }, label: { zh: '澳大利亚', en: 'Australia' } },
{ id: 'nz', dest: { zh: '新西兰', en: 'New Zealand' }, label: { zh: '新西兰', en: 'New Zealand' } },
];
const COVER_EU = [
{ mapId: 'de', zh: '德国', en: 'Germany' },
{ mapId: 'fr', zh: '法国', en: 'France' },
{ mapId: 'it', zh: '意大利', en: 'Italy' },
{ mapId: 'es', zh: '西班牙', en: 'Spain' },
{ mapId: 'nl', zh: '荷兰', en: 'Netherlands' },
{ mapId: 'be', zh: '比利时', en: 'Belgium' },
{ mapId: 'at', zh: '奥地利', en: 'Austria' },
{ mapId: 'ie', zh: '爱尔兰', en: 'Ireland' },
{ mapId: 'gb', zh: '英国', en: 'United Kingdom' },
{ mapId: 'pt', zh: '葡萄牙', en: 'Portugal' },
{ mapId: 'gr', zh: '希腊', en: 'Greece' },
{ mapId: 'se', zh: '瑞典', en: 'Sweden' },
{ mapId: 'dk', zh: '丹麦', en: 'Denmark' },
{ mapId: 'fi', zh: '芬兰', en: 'Finland' },
{ mapId: 'pl', zh: '波兰', en: 'Poland' },
{ mapId: 'cz', zh: '捷克', en: 'Czechia' },
{ mapId: 'hu', zh: '匈牙利', en: 'Hungary' },
{ mapId: 'ro', zh: '罗马尼亚', en: 'Romania' },
{ mapId: 'bg', zh: '保加利亚', en: 'Bulgaria' },
{ mapId: 'sk', zh: '斯洛伐克', en: 'Slovakia' },
{ mapId: 'si', zh: '斯洛文尼亚', en: 'Slovenia' },
{ mapId: 'hr', zh: '克罗地亚', en: 'Croatia' },
{ mapId: 'lt', zh: '立陶宛', en: 'Lithuania' },
{ mapId: 'lv', zh: '拉脱维亚', en: 'Latvia' },
{ mapId: 'ee', zh: '爱沙尼亚', en: 'Estonia' },
{ mapId: 'lu', zh: '卢森堡', en: 'Luxembourg' },
{ mapId: 'mt', zh: '马耳他', en: 'Malta' },
{ mapId: 'cy', zh: '塞浦路斯', en: 'Cyprus' },
];
const COVER_SEA = [
{ mapId: 'my', zh: '马来西亚', en: 'Malaysia' },
{ mapId: 'sg', zh: '新加坡', en: 'Singapore' },
{ mapId: 'ph', zh: '菲律宾', en: 'Philippines' },
{ mapId: 'th', zh: '泰国', en: 'Thailand' },
{ mapId: 'vn', zh: '越南', en: 'Vietnam' },
{ mapId: 'id', zh: '印度尼西亚', en: 'Indonesia' },
];
const INTENTS = [
{
id: 'quote',
icon: 'quote',
en: { title: 'Clear rates', hint: 'Tailored solutions — better, faster. Register to inquire.', label: 'I need a shipping quote' },
zh: { title: '明码报价', hint: '量身定制解决方案,更优更快等你来询', label: '我要报价' },
},
{
id: 'track',
icon: 'track',
en: { title: 'Track a shipment', hint: 'End-to-end tracking, timely exception alerts', label: 'Track my shipment' },
zh: { title: '轨迹追踪', hint: '轨迹全程可追踪,异常及时提醒', label: '我想查这票货到哪了' },
},
{
id: 'hard',
icon: 'hard',
en: { title: 'Tough orders', hint: 'Refund, exemption, 3C battery, special & cosmetic goods', label: 'Can you take a difficult or 9610 order?' },
zh: { title: '疑难杂单', hint: '退税、免征、3C带电、特殊品、化妆品', label: '疑难杂单或 9610 怎么发?' },
},
{
id: 'human',
icon: 'human',
en: { title: 'Your advisor', hint: 'One person from quote to delivery', label: 'Talk to a human advisor' },
zh: { title: '专属顾问', hint: '报价跟到签收,售后有人认', label: '联系专属顾问' },
},
];
function iconMarkup(name) {
const art = {
quote:
'<rect x="6.5" y="4.5" width="14" height="19" rx="3"/>' +
'<path d="M10 10h7M10 14h7M10 18h4"/>' +
'<path class="ai-icon__accent" d="M18.2 20.8l5.4-5.4a2 2 0 012.8 0l.6.6a2 2 0 010 2.8l-5.4 5.4-3.4-3.4z"/>' +
'<circle class="ai-icon__accent-fill" cx="24.2" cy="16.8" r="1.05"/>',
track:
'<circle cx="13.5" cy="17" r="8.2"/>' +
'<ellipse cx="13.5" cy="17" rx="3.2" ry="8.2"/>' +
'<path d="M5.3 17h16.4"/>' +
'<path class="ai-icon__accent" d="M23 8.2c0 3.4-4.2 7.3-4.2 7.3S14.6 11.6 14.6 8.2a4.2 4.2 0 118.4 0z"/>' +
'<circle class="ai-icon__accent-fill" cx="23" cy="8.2" r="1.45"/>',
hard:
'<path d="M16 6.2l10 4.8v10.2L16 26 6 21.2V11l10-4.8z"/>' +
'<path d="M16 6.2V26M6 11l10 4.8L26 11"/>' +
'<path class="ai-icon__accent" d="M11.5 14.6l4.5 2.2 4.5-2.2" stroke-width="2.2"/>' +
'<path class="ai-icon__accent-fill" d="M21.2 8.1l1.1 2.4 2.6.2-2 1.6.6 2.5-2.3-1.3-2.3 1.3.6-2.5-2-1.6 2.6-.2z"/>',
human:
'<circle cx="16" cy="11.2" r="4.3"/>' +
'<path d="M8.2 26.2c1.1-5.2 4.2-7.8 7.8-7.8s6.7 2.6 7.8 7.8"/>' +
'<path class="ai-icon__accent" d="M10 12.4c.2-4.6 2.8-7.4 6-7.4s5.8 2.8 6 7.4"/>' +
'<rect class="ai-icon__accent-fill" x="8" y="11.6" width="3.1" height="5" rx="1.5"/>' +
'<rect class="ai-icon__accent" x="20.9" y="11.6" width="3.1" height="5" rx="1.5"/>' +
'<path class="ai-icon__accent" d="M9.6 16.6v3.2h4.8"/>',
lane:
'<path d="M4.5 21.5c4.2-8.4 10.4-12.6 15-12.6 5.8 0 8.8 4.6 9.6 8.8"/>' +
'<path d="M4.5 21.5h23"/>' +
'<path class="ai-icon__accent-fill" d="M9.8 16.4l13.8-4.2-2.4 6.2-4.6-1.2-2.8 3.6-2.1-1.1 1.9-3.6-3.8.3z"/>',
clearance:
'<rect x="6.5" y="4.5" width="14.5" height="18.5" rx="2.5"/>' +
'<path d="M10 10h7.5M10 14h7.5M10 18h4.5"/>' +
'<circle class="ai-icon__accent" cx="22.2" cy="22.2" r="5.6"/>' +
'<path class="ai-icon__accent" d="M19.6 22.4l1.7 1.7 3.6-3.8"/>',
advise:
'<circle cx="16" cy="16" r="10"/>' +
'<path d="M16 6.2v2.2M16 23.6v2.2M6.2 16h2.2M23.6 16h2.2"/>' +
'<path class="ai-icon__accent-fill" d="M16 9.4l3.6 11.2L16 18.2 12.4 20.6 16 9.4z"/>',
refund:
'<ellipse cx="13.2" cy="11.4" rx="7.4" ry="2.8"/>' +
'<path d="M5.8 11.4v8.2c0 1.6 3.3 2.8 7.4 2.8s7.4-1.2 7.4-2.8v-8.2"/>' +
'<ellipse cx="13.2" cy="19.6" rx="7.4" ry="2.8"/>' +
'<path class="ai-icon__accent" d="M24.2 19.5v-9.2M21 13.4l3.2-3.2 3.2 3.2"/>',
exempt:
'<path d="M16 4.8l10 3.8v8.4c0 6.2-4.4 9.8-10 11.2-5.6-1.4-10-5-10-11.2V8.6L16 4.8z"/>' +
'<path class="ai-icon__accent" d="M11.4 16.4l3.2 3.2 6.4-7"/>',
filing:
'<circle cx="16" cy="16" r="10"/>' +
'<circle cx="16" cy="16" r="6.1"/>' +
'<circle class="ai-icon__accent-fill" cx="16" cy="16" r="2.1"/>' +
'<path class="ai-icon__accent" d="M16 6.2v2.6M16 23.2v2.6M6.2 16h2.6M23.2 16h2.6"/>',
warehouse:
'<path d="M4.2 14.4L16 5.6l11.8 8.8v12.1H4.2V14.4z"/>' +
'<path d="M16 5.6v21.3"/>' +
'<rect class="ai-icon__accent" x="12" y="18" width="8" height="8.9" rx="1.2"/>' +
'<path class="ai-icon__accent-fill" d="M8.2 12.6h3.2v3.4H8.2zM20.6 12.6h3.2v3.4h-3.2z"/>',
};
const body = art[name] || art.lane;
return (
'<svg class="ai-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
body +
'</svg>'
);
}
function resolveInitialLang() {
const saved = localStorage.getItem('ide-language');
if (saved === 'zh' || saved === 'en') return saved;
return 'zh';
}
let lang = resolveInitialLang();
let isBusy = false;
let sessionMessages = [];
let hasUserMessage = false;
let threads = [];
let activeThreadId = null;
let flow = null;
let shell;
let welcomeEl;
let messagesEl;
let followupEl;
let inputEl;
let sendBtn;
let intentGridEl;
let mainScrollEl;
let historyListEl;
let globeController = null;
let coverageOpen = false;
const MAP_REGION_EU = new Set(['eu'].concat(COVER_EU.map((item) => item.mapId)));
const MAP_REGION_AM = new Set(['us', 'mx', 'ca']);
const MAP_REGION_AU = new Set(['au', 'nz']);
const MAP_REGION_SEA = new Set(COVER_SEA.map((item) => item.mapId));
function mapRegionForId(mapId) {
if (MAP_REGION_EU.has(mapId)) return 'eu';
if (MAP_REGION_AM.has(mapId)) return 'am';
if (MAP_REGION_AU.has(mapId)) return 'au';
if (MAP_REGION_SEA.has(mapId)) return 'sea';
return 'cn';
}
function preferOmsH5() {
const ua = navigator.userAgent || '';
return /MicroMessenger/i.test(ua) || /Android|iPhone|iPod|iPad|Mobile/i.test(ua);
}
function resolveOmsOrigin() {
const configured = window.IDE_OMS_ORIGIN || localStorage.getItem('ide-oms-origin');
if (configured) return String(configured).replace(/\/$/, '');
const path = (window.location.pathname || '').replace(/\\/g, '/');
const port = window.location.port;
const isFile = window.location.protocol === 'file:';
const standalone =
isFile ||
/\/IDE官网(\/|$)/.test(path) ||
(port && !['80', '443', '5173', '4173'].includes(port) && !document.getElementById('app'));
if (standalone) {
const host = window.location.hostname || '127.0.0.1';
return `${isFile ? 'http:' : window.location.protocol}//${host}:5173`;
}
return window.location.origin;
}
function omsUrl(pathname) {
return `${resolveOmsOrigin()}${pathname}`;
}
function loginTrackUrl(number) {
const target = preferOmsH5() ? '/h5/login' : '/login';
const redirect = preferOmsH5() ? '/h5/orders' : '/home/workplace';
const url = new URL(omsUrl(target));
url.searchParams.set('redirect', redirect);
if (number) url.searchParams.set('track', number);
return url.toString();
}
function loginPortalUrl() {
return omsUrl(preferOmsH5() ? '/h5/login' : '/login');
}
function registerUrl(dest) {
const url = new URL(omsUrl(preferOmsH5() ? '/wechat/register' : '/register'));
url.searchParams.set('from', 'site');
if (dest) url.searchParams.set('dest', String(dest));
return url.toString();
}
function registerActions(dest) {
const actions = el('div', 'ai-card__actions');
const reg = el('a', 'ai-card-link is-primary', t('registerNow'));
reg.href = registerUrl(dest);
const login = el('a', 'ai-card-link', t('loginExisting'));
login.href = loginPortalUrl();
actions.appendChild(reg);
actions.appendChild(login);
return actions;
}
function isHomePage() {
const path = (window.location.pathname || '').replace(/\\/g, '/');
return (
/(?:^|\/)index\.html?$/.test(path) ||
path.endsWith('/') ||
path === '' ||
/\/IDE官网\/?$/.test(path)
);
}
function t(key) {
return COPY[lang][key] || COPY.en[key] || key;
}
function setTrustLabel(selector, text) {
const label = shell.querySelector(selector + ' .ai-trust-label');
if (label) label.textContent = text;
}
function intentCopy(intent) {
return lang === 'zh' ? intent.zh : intent.en;
}
function newThreadId() {
return 't_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
}
function deriveThreadTitle(messages) {
const firstUser = messages.find((m) => m.role === 'user');
if (!firstUser) return t('newChat');
const text = String(firstUser.text || '').trim();
return text.length > 28 ? `${text.slice(0, 28)}…` : text;
}
function formatThreadTime(ts) {
const date = new Date(ts);
const now = new Date();
const sameDay =
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (sameDay) {
return date.toLocaleTimeString(lang === 'zh' ? 'zh-CN' : 'en-US', {
hour: '2-digit',
minute: '2-digit',
});
}
return date.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
month: 'short',
day: 'numeric',
});
}
function getActiveThread() {
return threads.find((thread) => thread.id === activeThreadId) || null;
}
function syncActiveThreadMessages() {
const thread = getActiveThread();
if (!thread) return;
thread.messages = sessionMessages.map((msg) => ({
role: msg.role,
text: msg.text,
card: msg.card || null,
}));
thread.flow = flow ? { ...flow, data: { ...(flow.data || {}) } } : null;
thread.updatedAt = Date.now();
if (thread.messages.some((m) => m.role === 'user')) {
thread.title = deriveThreadTitle(thread.messages);
}
}
function saveThreads() {
syncActiveThreadMessages();
localStorage.setItem(STORAGE_THREADS, JSON.stringify(threads));
if (activeThreadId) {
localStorage.setItem(STORAGE_ACTIVE_THREAD, activeThreadId);
}
}
function applyThreadMessages(thread) {
sessionMessages = thread?.messages
? thread.messages.map((msg) => ({ role: msg.role, text: msg.text, card: msg.card || null }))
: [];
hasUserMessage = sessionMessages.some((m) => m.role === 'user');
flow = thread?.flow ? { ...thread.flow, data: { ...(thread.flow.data || {}) } } : null;
}
function createThread(setActive) {
const thread = {
id: newThreadId(),
title: t('newChat'),
updatedAt: Date.now(),
messages: [],
flow: null,
};
threads.unshift(thread);
if (setActive !== false) {
activeThreadId = thread.id;
applyThreadMessages(thread);
}
saveThreads();
return thread;
}
function loadThreads() {
try {
const raw = localStorage.getItem(STORAGE_THREADS);
threads = raw ? JSON.parse(raw) : [];
if (!Array.isArray(threads)) threads = [];
} catch {
threads = [];
}
activeThreadId = localStorage.getItem(STORAGE_ACTIVE_THREAD);
if (threads.length === 0) {
try {
const legacy = sessionStorage.getItem(STORAGE_SESSION);
const legacyMessages = legacy ? JSON.parse(legacy) : [];
if (Array.isArray(legacyMessages) && legacyMessages.length > 0) {
const thread = createThread(true);
thread.messages = legacyMessages;
thread.title = deriveThreadTitle(legacyMessages);
applyThreadMessages(thread);
saveThreads();
sessionStorage.removeItem(STORAGE_SESSION);
return;
}
} catch {
/* ignore */
}
createThread(true);
return;
}
if (!activeThreadId || !threads.some((thread) => thread.id === activeThreadId)) {
activeThreadId = threads[0].id;
}
applyThreadMessages(getActiveThread());
}
function syncHeaderOffset() {
const header = document.getElementById('header');
const height = header ? Math.round(header.getBoundingClientRect().height) : 72;
document.documentElement.style.setProperty('--ai-header-h', `${height}px`);
}
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text != null) node.textContent = text;
return node;
}
function renderCard(card) {
if (!card || !card.type) return null;
const wrap = el('div', `ai-card ai-card--${card.type}`);
const zh = lang === 'zh';
if (card.type === 'choice') {
if (card.kicker) wrap.appendChild(el('p', 'ai-card__kicker', card.kicker));
if (card.title) wrap.appendChild(el('p', 'ai-card__title', card.title));
const actions = el('div', 'ai-card__actions');
(card.options || []).forEach((opt) => {
const btn = el('button', opt.primary ? 'ai-card-opt is-primary' : 'ai-card-opt', opt.label);
btn.type = 'button';
btn.dataset.choice = opt.id;
btn.dataset.flow = card.flowId || '';
btn.dataset.label = opt.label;
actions.appendChild(btn);
});
wrap.appendChild(actions);
return wrap;
}
if (card.type === 'quote') {
wrap.appendChild(el('p', 'ai-card__kicker', zh ? '询价' : 'Quote'));
wrap.appendChild(el('p', 'ai-card__title', zh ? '注册登记后即可询价' : 'Register to see rates'));
wrap.appendChild(
el(
'p',
'ai-card__body',
card.dest
? zh
? '发到' + card.dest + '可以走。官网不提供询价,完成注册后在客户系统里按线路看明码价。'
: 'We ship to ' + card.dest + '. Quotes are not on this site — register, then see list rates in the customer system.'
: zh
? '官网不提供询价。完成注册登记后,即可在客户系统里按线路看明码价,一口价不砍。'
: 'This site does not quote. Register first, then see list rates by lane in the customer system — no haggling.'
)
);
wrap.appendChild(registerActions(card.dest));
return wrap;
}
if (card.type === 'track') {
wrap.appendChild(el('p', 'ai-card__kicker', zh ? '真实轨迹' : 'Live tracking'));
wrap.appendChild(el('p', 'ai-card__title', t('loginTrack')));
wrap.appendChild(
el(
'p',
'ai-card__body',
card.number
? zh
? `单号 ${card.number} 已记下。登录后看本票真实节点。`
: `Got ${card.number}. Sign in to see live scans for this shipment.`
: zh
? '有单号请登录查看。询价需先注册登记。'
: 'Sign in if you have a number. Quotes require registration.'
)
);
const actions = el('div', 'ai-card__actions');
const login = el('a', 'ai-card-link is-primary', t('loginTrack'));
login.href = loginTrackUrl(card.number || '');
actions.appendChild(login);
wrap.appendChild(actions);
wrap.appendChild(intentActions(['quote', 'human']));
return wrap;
}
if (card.type === 'hard-refund' || card.type === 'hard-exempt' || card.type === 'hard-basic') {
const meta = {
'hard-refund': {
kicker: zh ? '9610 · 利润最优' : '9610 · best margin',
title: zh ? '出口退税(免抵退)' : 'Export VAT refund',
body: zh
? '有进项专票:汇总正式报关、完整 10 位 HS、报关单退税联 + 专票。进项税额按退税率退还。'
: 'You have special VAT invoices: formal consolidated declaration, full 10-digit HS, declaration refund copy + invoice. Input VAT is refunded at the rebate rate.',
},
'hard-exempt': {
kicker: zh ? '9610 · 合规推荐' : '9610 · compliant',
title: zh ? '无票免征' : 'Exemption without invoice',
body: zh
? '无专票但要免税:简易/汇总均可,HS 前 4 位,综服平台登记。出口环节增值税/消费税免征。'
: 'No special invoice, but you want exemption: simplified or consolidated filing, first 4 HS digits, comprehensive-service platform. Export VAT/excise is exempted.',
},
'hard-basic': {
kicker: zh ? '9610 · 快捷出货' : '9610 · fastest exit',
title: zh ? '基础通关服务' : 'Basic clearance',
body: zh
? '仅需出货:物流商统一简易申报。出得快,但税务未规范可能被视同内销(增值税 + 所得税 + 滞纳金)。量起来后建议升到免征或退税,IDE 可评估路径。'
: 'Ship-only: carrier files a simplified declaration. Fast, but unstructured tax handling can be treated as domestic sales. We can review an upgrade path when volume grows.',
},
}[card.type];
if (card.type === 'hard-basic') wrap.classList.add('ai-card--warn');
wrap.appendChild(el('p', 'ai-card__kicker', meta.kicker));
wrap.appendChild(el('p', 'ai-card__title', meta.title));
wrap.appendChild(el('p', 'ai-card__body', meta.body));
wrap.appendChild(intentActions(['human']));
return wrap;
}
if (card.type === 'contact') {
wrap.appendChild(el('p', 'ai-card__kicker', zh ? '人工顾问' : 'Human advisor'));
wrap.appendChild(el('p', 'ai-card__title', zh ? '专人跟,不排队工单' : 'One advisor — not a ticket queue'));
const rows = el('div', 'ai-card__rows');
[
[zh ? '电话' : 'Phone', PHONE],
[zh ? '微信' : 'WeChat', WECHAT],
[zh ? '邮箱' : 'Email', EMAIL],
[zh ? '地址' : 'Address', zh ? ADDRESS_ZH : ADDRESS_EN],
].forEach(([label, value]) => {
const row = el('div', 'ai-card__row');
row.appendChild(el('span', '', label));
row.appendChild(el('strong', '', value));
rows.appendChild(row);
});
wrap.appendChild(rows);
wrap.appendChild(
el(
'p',
'ai-card__body',
zh ? '要询价请先注册登记,或直接打电话、加微信。' : 'Register first to quote, or call / WeChat us directly.'
)
);
wrap.appendChild(contactActions());
return wrap;
}
if (card.type === 'company') {
wrap.appendChild(el('p', 'ai-card__kicker', zh ? '关于我们' : 'About IDE'));
wrap.appendChild(el('p', 'ai-card__title', zh ? '中小卖家的跨境物流顾问' : 'Cross-border logistics for SMB sellers'));
wrap.appendChild(
el(
'p',
'ai-card__body',
zh
? '深圳宝安运营,备案主体可核(' + ICP + ')。主做美欧与东南亚小包。接小批量、带电、别人不接的 9610。一人从报价跟到售后。'
: 'Operated from Bao’an, Shenzhen (ICP ' + ICP + '). US, EU and Southeast Asia parcels. We take low volume, battery, and 9610 others decline. One advisor from quote to after-sales.'
)
);
appendCardItems(wrap, [
{ title: zh ? '主线:美加墨 / 欧盟 / 澳新 / 东南亚' : 'Lanes: US-CA-MX / EU / AU-NZ / SEA', hint: zh ? '欧盟 27 国及英国,东南亚列马来、新、菲、泰、越、印尼。' : 'EU 27 and the UK, plus Malaysia, Singapore, Philippines, Thailand, Vietnam, Indonesia.' },
{ title: zh ? '难单先给出货路径' : 'Tough orders get a ship path first', hint: zh ? '别人不接的带电、无票、超长,先保证出得去,再谈退税或免征。' : 'Battery, no invoice, oversize — we get it out, then pick refund or exemption.' },
{ title: zh ? '微信 ' + WECHAT : 'WeChat ' + WECHAT, hint: zh ? '卖家常用这一个号找顾问,底栏也可复制。' : 'The number sellers use. Copy it from the bar below.' },
]);
wrap.appendChild(intentActions(['quote', 'human']));
return wrap;
}
if (card.type === 'pricing') {
wrap.appendChild(el('p', 'ai-card__kicker', zh ? '价格' : 'Pricing'));
wrap.appendChild(el('p', 'ai-card__metric', zh ? '按线路明码报价,一口价不砍' : 'Lane list rates — no haggling'));
wrap.appendChild(
el(
'p',
'ai-card__body',
zh
? '中小卖家按计费重看价即可,不磨、不砍。税怎么走取决于有没有专票。询价请先注册登记。'
: 'SMB sellers see the list rate by chargeable weight — no haggling. Tax savings still depend on invoices. Register first to quote.'
)
);
appendCardItems(wrap, [
{ title: zh ? '计费重 = 实重和体积重取大' : 'Chargeable weight = max(actual, volumetric)', hint: zh ? '体积重按渠道系数折算,超长另计。' : 'Volumetric uses the lane factor; oversize is extra.' },
{ title: zh ? '明码一口价' : 'Published list rate', hint: zh ? '注册登记后,在客户系统里按线路看价,不用讨价还价。' : 'After you register, see list rates by lane in the customer system. No bargaining.' },
{ title: zh ? '有票退税,无票免征或基础通关' : 'Refund / exemption / basic clearance', hint: zh ? '差在税,不在口头运费。带电、敏感货加收。' : 'The gap is tax, not a spoken freight. Battery and sensitive cargo surcharge.' },
]);
wrap.appendChild(registerActions());
return wrap;
}
if (card.type === 'warehouse') {
wrap.appendChild(el('p', 'ai-card__kicker', zh ? '仓储' : 'Warehouse'));
wrap.appendChild(el('p', 'ai-card__title', zh ? '仓储一件代发' : 'Warehouse dropship'));
wrap.appendChild(
el(
'p',
'ai-card__body',
zh
? '入仓、拣货打包,订单来了就发出去。中小卖家不用自己养仓。开通请先注册登记,在客户系统里操作。'
: 'Inbound, pick and pack, ship when the order lands. Register first — warehouse dropship runs in the customer system.'
)
);
appendCardItems(wrap, [
{ title: zh ? '入仓管理' : 'Inbound', hint: zh ? '收货上架,库存可查。' : 'Receive, put away, inventory you can see.' },
{ title: zh ? '一件代发' : 'Dropship', hint: zh ? '订单过来就拣货打包发出。' : 'Pick, pack, and ship as orders come in.' },
{ title: zh ? '对接出库' : 'Outbound', hint: zh ? '和专线、9610 出货打通。' : 'Tied to parcel lanes and 9610 exit.' },
]);
wrap.appendChild(registerActions());
return wrap;
}
return wrap;
}
function appendCardItems(wrap, items) {
const list = el('div', 'ai-card__list');
items.forEach((item) => {
const row = el('div', 'ai-card__item');
row.appendChild(el('strong', '', item.title));
if (item.hint) row.appendChild(el('span', '', item.hint));
list.appendChild(row);
});
wrap.appendChild(list);
}
function intentActions(ids) {
const zh = lang === 'zh';
const labels = {
quote: zh ? '去注册询价 →' : 'Register to quote →',
hard: zh ? '疑难单 / 9610 →' : 'Tough / 9610 →',
human: zh ? '联系顾问 →' : 'Talk to advisor →',
};
const actions = el('div', 'ai-card__actions');
ids.forEach((id, index) => {
if (id === 'quote') {
const link = el('a', index === 0 ? 'ai-card-opt is-primary' : 'ai-card-opt', labels.quote);
link.href = registerUrl();
actions.appendChild(link);
return;
}
const btn = el('button', index === 0 ? 'ai-card-opt is-primary' : 'ai-card-opt', labels[id] || id);
btn.type = 'button';
btn.dataset.intent = id;
actions.appendChild(btn);
});
return actions;
}
function contactActions() {
const actions = el('div', 'ai-card__actions');
const tel = el('a', 'ai-card-link is-primary', PHONE);
tel.href = `tel:${PHONE}`;
const mail = el('a', 'ai-card-link', EMAIL);
mail.href = `mailto:${EMAIL}`;
const wx = el('span', 'ai-card-link', (lang === 'zh' ? '微信 ' : 'WeChat ') + WECHAT);
actions.appendChild(tel);
actions.appendChild(wx);
actions.appendChild(mail);
return actions;
}
function renderShell() {
const existing = document.getElementById('ai-advisor-shell');
if (existing) {
shell = existing;
return;
}
if (!isHomePage()) return;
shell = document.createElement('div');
shell.id = 'ai-advisor-shell';
shell.innerHTML = `
<div class="ai-shell-bg" aria-hidden="true">
<div class="ai-shell-bg__blobs" aria-hidden="true"></div>
<div class="ai-shell-bg__veil" aria-hidden="true"></div>
</div>
<div class="ai-stage-maps" aria-hidden="true"></div>
<div class="ai-advisor-layout">
<div class="ai-advisor-app">
<div class="ai-stage-tools">
<button type="button" class="ai-sidebar-toggle" aria-label="Chat history">
<i class="bi bi-clock-history" aria-hidden="true"></i>
</button>
<button type="button" class="ai-new-chat-ghost">
<i class="bi bi-plus-lg" aria-hidden="true"></i>
<span class="ai-new-chat-ghost__label"></span>
</button>
</div>
<div class="ai-app-main">
<section class="ai-welcome">
<div class="ai-welcome__hero">
<div class="ai-welcome-plane" aria-hidden="true">
<img src="assets/img/hero-plane-3d.png" alt="">
</div>
<div class="ai-welcome__stack">
<div class="ai-welcome__deck">
<div class="ai-welcome__intro">
<h2 class="ai-welcome-title"></h2>
<p class="ai-welcome-sub"></p>
</div>
<div class="ai-welcome__foot">
<div class="ai-welcome-stats-block">
<ul class="ai-welcome-stats" aria-label="Service highlights"></ul>
<div class="ai-cover-panel" hidden>
<p class="ai-cover-panel__hint"></p>
<div class="ai-cover__chips"></div>
<div class="ai-cover__eu" hidden></div>
<div class="ai-cover__sea">
<div class="ai-cover__sea-chips"></div>
</div>
</div>
</div>
<a class="ai-welcome-cta" href="#"></a>
</div>
</div>
<div class="ai-welcome__command">
<div class="ai-welcome-compose">
<p class="ai-welcome-compose__lead"></p>
<div class="ai-composer-slot--welcome"></div>
</div>
<div class="ai-intent-grid"></div>
</div>
</div>
</div>
</section>
<div class="ai-messages" role="log" aria-live="polite"></div>
</div>
<div class="ai-composer-dock">
<div class="ai-followup" role="toolbar"></div>
<div class="ai-composer-slot--chat"></div>
<div class="ai-composer-wrap">
<div class="ai-composer">
<span class="ai-composer__caustic" aria-hidden="true"></span>
<span class="ai-composer__sheen" aria-hidden="true"></span>
<form class="ai-composer-form" autocomplete="off">
<textarea class="ai-composer-input" rows="1" maxlength="2000"></textarea>
<button type="submit" class="ai-composer-send" aria-label="联系客服"><i class="bi bi-send-fill"></i></button>
</form>
</div>
</div>
<div class="ai-trust-bar">
<div class="ai-trust-bar__contacts">
<a class="ai-trust-phone" href="tel:${PHONE}">
<i class="bi bi-telephone" aria-hidden="true"></i>
<span class="ai-trust-label"></span>
</a>
<button type="button" class="ai-trust-wechat" title="${WECHAT}">
<i class="bi bi-wechat" aria-hidden="true"></i>
<span class="ai-trust-label"></span>
</button>
<a class="ai-trust-email" href="mailto:${EMAIL}">
<i class="bi bi-envelope" aria-hidden="true"></i>
<span class="ai-trust-label"></span>
</a>
<a class="ai-trust-bar__human" href="#contact"></a>
</div>
<div class="ai-trust-bar__meta">
<p class="ai-trust-address">
<i class="bi bi-geo-alt" aria-hidden="true"></i>
<span class="ai-trust-label"></span>
</p>
<a class="ai-trust-icp" href="https://beian.miit.gov.cn" target="_blank" rel="noopener">
<i class="bi bi-shield-check" aria-hidden="true"></i>
<span class="ai-trust-label"></span>
</a>
</div>
</div>
</div>
</div>
</div>
<aside class="ai-history-sidebar" aria-label="Chat history">
<p class="ai-history-sidebar__title"></p>
<button type="button" class="ai-new-chat-btn">
<i class="bi bi-plus-lg" aria-hidden="true"></i>
<span class="ai-new-chat-btn__label"></span>
</button>
<div class="ai-history-list" role="list"></div>
</aside>
<aside class="ai-guide-panel" id="ai-9610-panel" aria-label="9610">
<div class="ai-guide-panel__head">
<div>
<p class="ai-guide-panel__kicker"></p>
<h3 class="ai-guide-panel__title"></h3>
</div>
<button type="button" class="ai-guide-panel__close" aria-label="Close">
<i class="bi bi-x-lg" aria-hidden="true"></i>
</button>
</div>
<p class="ai-guide-panel__sub"></p>
<div class="ai-guide-panel__cards"></div>
</aside>
<button type="button" class="ai-history-backdrop" aria-hidden="true" tabindex="-1"></button>
`;
document.body.appendChild(shell);
welcomeEl = shell.querySelector('.ai-welcome');
messagesEl = shell.querySelector('.ai-messages');
followupEl = shell.querySelector('.ai-followup');
inputEl = shell.querySelector('.ai-composer-input');
sendBtn = shell.querySelector('.ai-composer-send');
intentGridEl = shell.querySelector('.ai-intent-grid');
mainScrollEl = shell.querySelector('.ai-app-main');
historyListEl = shell.querySelector('.ai-history-list');
bindMainScroll();
bindStageChrome();
shell.querySelector('.ai-new-chat-btn').addEventListener('click', () => startNewChat());
shell.querySelector('.ai-new-chat-ghost').addEventListener('click', () => startNewChat());
shell.querySelector('.ai-sidebar-toggle').addEventListener('click', () => toggleHistorySidebar());
shell.querySelector('.ai-history-backdrop').addEventListener('click', () => closeDrawers());
shell.querySelector('.ai-guide-panel__close').addEventListener('click', () => close9610Panel());
shell.querySelector('.ai-guide-panel__cards').addEventListener('click', (e) => {
const card = e.target.closest('[data-guide-ask]');
if (!card || isBusy) return;
close9610Panel();
sendUserMessage(card.dataset.guideAsk, { intentId: card.dataset.guideIntent || 'hard' });
});
shell.querySelector('.ai-trust-wechat').addEventListener('click', async () => {
const btn = shell.querySelector('.ai-trust-wechat');
const label = btn.querySelector('.ai-trust-label');
try {
await navigator.clipboard.writeText(WECHAT);
if (label) label.textContent = lang === 'zh' ? '微信已复制' : 'WeChat copied';
} catch {
if (label) label.textContent = WECHAT;
}
window.setTimeout(() => {
if (label) label.textContent = t('trustWechat');
}, 1600);
});
shell.querySelector('.ai-composer-form').addEventListener('submit', (e) => {
e.preventDefault();
sendUserMessage(inputEl.value.trim());
});
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendUserMessage(inputEl.value.trim());
}
});
inputEl.addEventListener('input', () => {
inputEl.style.height = 'auto';
inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px';
syncComposerSendState();
});
syncComposerSendState();
messagesEl.addEventListener('click', (e) => {
const intentBtn = e.target.closest('[data-intent]');
if (intentBtn && !isBusy) {
const intent = INTENTS.find((item) => item.id === intentBtn.dataset.intent);
const label = intent ? intentCopy(intent).label : intentBtn.textContent;
sendUserMessage(label, { intentId: intentBtn.dataset.intent });
return;
}
const btn = e.target.closest('[data-choice]');
if (!btn || isBusy) return;
sendUserMessage(btn.dataset.label || btn.textContent, {
kind: 'choice',
flowId: btn.dataset.flow,
choice: btn.dataset.choice,
});
});
}
function buildCoverageMarkers() {
const markers = [];
const pushMarker = (id, label) => {
const coords = MAP_COORDS[id];
if (!coords) return;
markers.push({ id, coords, label, regionId: mapRegionForId(id) });
};
COVER_REGIONS.forEach((region) => {
const label = lang === 'zh' ? region.label.zh : region.label.en;
pushMarker(region.id, label);
});
COVER_EU.forEach((item) => {
pushMarker(item.mapId, coverName(item));
});
COVER_SEA.forEach((item) => {
pushMarker(item.mapId, coverName(item));
});
return markers;
}
function setCoverageOpen(open) {
coverageOpen = !!open;
const panel = shell?.querySelector('.ai-cover-panel');
const trigger = shell?.querySelector('.ai-welcome-stats__trigger');
const block = shell?.querySelector('.ai-welcome-stats-block');
if (panel) {
if (coverageOpen) panel.removeAttribute('hidden');
else panel.setAttribute('hidden', '');
}
if (trigger) trigger.setAttribute('aria-expanded', coverageOpen ? 'true' : 'false');
if (block) block.classList.toggle('is-open', coverageOpen);
if (!globeController) return;
globeController.setActive(coverageOpen);
if (coverageOpen) {
globeController.setMarkers(buildCoverageMarkers());
globeController.resetView();
} else {
globeController.setMarkers([]);
globeController.resetView();
}
window.requestAnimationFrame(syncMainOverflow);
}
function focusCoverageOnMap(mapId) {
if (!mapId) return;
if (!coverageOpen) setCoverageOpen(true);
if (!globeController) return;
globeController.setMarkers(buildCoverageMarkers());
globeController.focusMarker(mapId);
shell?.querySelectorAll('.ai-cover-chip.is-map-active').forEach((node) => {
node.classList.remove('is-map-active');
});
const chip = shell?.querySelector('.ai-cover-chip[data-map-id="' + mapId + '"]');
if (chip) chip.classList.add('is-map-active');
}
function renderWelcomeStats() {
if (!shell) return;
const listEl = shell.querySelector('.ai-welcome-stats');
if (!listEl) return;
listEl.setAttribute('aria-label', t('statsAria'));
listEl.innerHTML = HERO_STATS.map((item) => {
const value = lang === 'zh' ? item.value.zh : item.value.en;
const label = lang === 'zh' ? item.label.zh : item.label.en;
if (item.interactive) {
return (
'<li class="ai-welcome-stats__item ai-welcome-stats__item--coverage">' +
'<button type="button" class="ai-welcome-stats__trigger" aria-expanded="' +
(coverageOpen ? 'true' : 'false') +
'">' +
'<strong class="ai-welcome-stats__value">' +
value +
'</strong>' +
'<span class="ai-welcome-stats__meta">' +
'<span class="ai-welcome-stats__label">' +
label +
'</span>' +
'<span class="ai-welcome-stats__hint">' +
t('statsCoverageHint') +
'</span></span></button></li>'
);
}
return (
'<li class="ai-welcome-stats__item">' +
'<strong class="ai-welcome-stats__value">' +
value +
'</strong>' +
'<span class="ai-welcome-stats__label">' +
label +
'</span></li>'
);
}).join('');
const trigger = listEl.querySelector('.ai-welcome-stats__trigger');
if (trigger && !trigger.dataset.bound) {
trigger.dataset.bound = '1';
trigger.addEventListener('click', () => {
setCoverageOpen(!coverageOpen);
});
}
}
function applyWelcomeTitle() {
if (!shell) return;
const titleEl = shell.querySelector('.ai-welcome-title');
if (!titleEl) return;
const useShort = window.matchMedia('(max-width: 640px)').matches;
titleEl.textContent = useShort ? t('welcomeTitleShort') : t('welcomeTitle');
}
function renderStageGlobe() {
if (!shell || !window.IDEStageGlobe) return;
const el = shell.querySelector('.ai-stage-maps');
const cnHost = shell.querySelector('.ai-welcome-plane');
globeController =
window.IDEStageGlobe.render(el, { lang, cnHost }) || el?.__globeApi || null;
if (coverageOpen && globeController) {
globeController.setActive(true);
globeController.setMarkers(buildCoverageMarkers());
}
}
function bindStageChrome() {
if (!shell || shell.dataset.chromeBound === '1') return;
shell.dataset.chromeBound = '1';
}
function renderIntentGrid() {
if (!intentGridEl) return;
intentGridEl.innerHTML = '';
INTENTS.forEach((intent) => {
const copy = intentCopy(intent);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ai-intent-door' + (intent.id === 'quote' ? ' is-primary' : '');
btn.dataset.intent = intent.id;
btn.innerHTML = `
<span class="ai-intent-door__icon">${iconMarkup(intent.icon)}</span>
<span class="ai-intent-door__copy">
<span class="ai-intent-door__title"></span>
<span class="ai-intent-door__hint"></span>
</span>
`;
btn.querySelector('.ai-intent-door__title').textContent = copy.title;
btn.querySelector('.ai-intent-door__hint').textContent = copy.hint;
btn.addEventListener('click', () => onIntentClick(intent));
intentGridEl.appendChild(btn);
});
renderCoverage();
}
function coverName(item) {
return lang === 'zh' ? item.zh : item.en;
}
function coverChip(label, dest, extraClass, mapId) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = extraClass ? 'ai-cover-chip ' + extraClass : 'ai-cover-chip';
btn.textContent = label;
if (dest) btn.dataset.dest = dest;
if (mapId) btn.dataset.mapId = mapId;
return btn;
}
function renderCoverage() {
if (!welcomeEl) return;
const chipsEl = welcomeEl.querySelector('.ai-cover__chips');
const euEl = welcomeEl.querySelector('.ai-cover__eu');
const seaChips = welcomeEl.querySelector('.ai-cover__sea-chips');
if (!chipsEl || !euEl || !seaChips) return;
chipsEl.innerHTML = '';
euEl.innerHTML = '';
seaChips.innerHTML = '';
COVER_REGIONS.forEach((region) => {
const label = lang === 'zh' ? region.label.zh : region.label.en;
const dest = lang === 'zh' ? region.dest.zh : region.dest.en;
if (region.expand) {
const btn = coverChip(label, '', 'is-eu', region.id);
btn.setAttribute('aria-expanded', euEl.hidden ? 'false' : 'true');
btn.addEventListener('click', () => {
const open = euEl.hasAttribute('hidden');
if (open) euEl.removeAttribute('hidden');
else euEl.setAttribute('hidden', '');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
focusCoverageOnMap(region.id);
window.requestAnimationFrame(syncMainOverflow);
});
chipsEl.appendChild(btn);
return;
}
const btn = coverChip(label, dest, '', region.id);
btn.addEventListener('click', () => {
focusCoverageOnMap(region.id);
startQuoteTo(dest);
});
chipsEl.appendChild(btn);
});
COVER_EU.forEach((item) => {
const name = coverName(item);
const btn = coverChip(name, name, '', item.mapId);
btn.addEventListener('click', () => {
focusCoverageOnMap(item.mapId);
startQuoteTo(name);
});
euEl.appendChild(btn);
});
COVER_SEA.forEach((item) => {
const name = coverName(item);
const btn = coverChip(name, name, '', item.mapId);
btn.addEventListener('click', () => {
focusCoverageOnMap(item.mapId);
startQuoteTo(name);
});
seaChips.appendChild(btn);
});
}
function startQuoteTo(dest) {
if (isBusy || !dest) return;
const label = lang === 'zh' ? '发到' + dest : 'Ship to ' + dest;
sendUserMessage(label, { intentId: 'quote', dest });
}
function renderFollowupChips() {
if (!followupEl) return;
followupEl.innerHTML = '';
INTENTS.forEach((intent) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ai-followup-chip';
btn.textContent = intentCopy(intent).title;
btn.addEventListener('click', () => onIntentClick(intent));
followupEl.appendChild(btn);
});
}
function placeComposer(chatting) {
if (!shell) return;
const wrap = shell.querySelector('.ai-composer-wrap');
const welcomeSlot = shell.querySelector('.ai-composer-slot--welcome');
const chatSlot = shell.querySelector('.ai-composer-slot--chat');
if (!wrap || !welcomeSlot || !chatSlot) return;
const target = chatting ? chatSlot : welcomeSlot;
if (wrap.parentElement !== target) {
target.appendChild(wrap);
}
wrap.classList.toggle('is-welcome', !chatting);
wrap.classList.toggle('is-docked', chatting);
const leadEl = shell.querySelector('.ai-welcome-compose__lead');
if (leadEl) leadEl.hidden = !!chatting;
}
function updateViewMode() {
const chatting = hasUserMessage || sessionMessages.some((m) => m.role === 'user');
if (welcomeEl) welcomeEl.classList.toggle('is-hidden', chatting);
if (messagesEl) messagesEl.classList.toggle('is-active', chatting);
if (followupEl) followupEl.classList.toggle('is-visible', chatting);
placeComposer(chatting);
window.requestAnimationFrame(syncMainOverflow);
}
function renderHistorySidebar() {
if (!historyListEl) return;
const sorted = [...threads].sort((a, b) => b.updatedAt - a.updatedAt);
historyListEl.innerHTML = '';
if (sorted.length === 0) {
historyListEl.appendChild(el('p', 'ai-history-empty', t('emptyHistory')));
return;
}
sorted.forEach((thread) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ai-history-item';
btn.setAttribute('role', 'listitem');
if (thread.id === activeThreadId) btn.classList.add('is-active');
btn.innerHTML = `
<span class="ai-history-item__icon"><i class="bi bi-chat-left-text" aria-hidden="true"></i></span>
<span class="ai-history-item__body">
<span class="ai-history-item__title"></span>
<span class="ai-history-item__time"></span>
</span>
`;
btn.querySelector('.ai-history-item__title').textContent = thread.title || t('newChat');
btn.querySelector('.ai-history-item__time').textContent = formatThreadTime(thread.updatedAt);
btn.addEventListener('click', () => switchThread(thread.id));
historyListEl.appendChild(btn);
});
}
function switchThread(threadId) {
if (isBusy || threadId === activeThreadId) {
closeHistorySidebar();
return;
}
syncActiveThreadMessages();
activeThreadId = threadId;
applyThreadMessages(getActiveThread());
saveThreads();
renderHistorySidebar();
renderMessages();
updateViewMode();
closeHistorySidebar();
inputEl.focus();
}
function startNewChat() {
if (isBusy) return;
syncActiveThreadMessages();
const current = getActiveThread();
if (current && current.messages.length === 0) {
closeHistorySidebar();
inputEl.focus();
return;
}
createThread(true);
renderHistorySidebar();
renderMessages();
updateViewMode();
closeHistorySidebar();
setNavActive('home');
inputEl.focus();
}
function toggleHistorySidebar() {
close9610Panel();
shell?.classList.toggle('ai-history-open');
}
function closeHistorySidebar() {
shell?.classList.remove('ai-history-open');
}
function open9610Panel() {
closeHistorySidebar();
closeMobileNav();
shell?.classList.add('ai-guide-open');
setNavActive('services');
if (location.hash !== '#services') {
history.replaceState(null, '', '#services');
}
}
function close9610Panel() {
shell?.classList.remove('ai-guide-open');
if (location.hash === '#services' || location.hash === '#9610') {
history.replaceState(null, '', location.pathname + location.search);
setNavActive('home');
}
}
function closeDrawers() {
closeHistorySidebar();
close9610Panel();
}
function render9610Panel() {
if (!shell) return;
const titleEl = shell.querySelector('.ai-guide-panel__title');
const subEl = shell.querySelector('.ai-guide-panel__sub');
const cardsEl = shell.querySelector('.ai-guide-panel__cards');
if (!titleEl || !subEl || !cardsEl) return;
const kickerEl = shell.querySelector('.ai-guide-panel__kicker');
titleEl.textContent = t('guide9610Title');
if (kickerEl) kickerEl.textContent = t('guide9610Kicker');
subEl.textContent = t('guide9610Sub');
cardsEl.innerHTML = GUIDE_SERVICES.map((item) => {
const copy = lang === 'zh' ? item.zh : item.en;
const tag = lang === 'zh' ? item.tag.zh : item.tag.en;
const ask = lang === 'zh' ? item.ask.zh : item.ask.en;
return (
'<button type="button" class="ai-guide-card" data-guide-ask="' +
ask.replace(/"/g, '&quot;') +
'" data-guide-intent="' +
(item.intent || 'hard') +
'">' +
'<span class="ai-guide-card__icon">' +
iconMarkup(item.icon) +
'</span>' +
'<span class="ai-guide-card__copy">' +
'<span class="ai-guide-card__head">' +
'<span class="ai-guide-card__tag">' +
tag +
'</span>' +
'<strong class="ai-guide-card__title">' +
copy.title +
'</strong>' +
'</span>' +
'<span class="ai-guide-card__body">' +
copy.body +
'</span>' +
'</span>' +
'</button>'
);
}).join('');
}
function renderWelcomeSub() {
const subEl = shell?.querySelector('.ai-welcome-sub');
if (!subEl) return;
subEl.innerHTML =
t('welcomeSubLead') +
'<span class="ai-welcome-sub__accent">' +
t('welcomeSubAccent') +
'</span>' +
t('welcomeSubTail');
}
function renderStaticCopy() {
if (!shell) return;
shell.querySelector('.ai-history-sidebar__title').textContent = t('historyTitle');
shell.querySelector('.ai-new-chat-btn__label').textContent = t('newChat');
shell.querySelector('.ai-new-chat-ghost__label').textContent = t('newChat');
applyWelcomeTitle();
renderWelcomeSub();
const heroCta = shell.querySelector('.ai-welcome-cta');
if (heroCta) {
heroCta.textContent = t('heroCta');
heroCta.href = registerUrl();
}
renderWelcomeStats();
const mapHint = shell.querySelector('.ai-cover-panel__hint');
if (mapHint) mapHint.textContent = t('mapZoomHint');
renderCoverage();
if (coverageOpen) {
const panel = shell.querySelector('.ai-cover-panel');
const block = shell.querySelector('.ai-welcome-stats-block');
if (panel) panel.removeAttribute('hidden');
if (block) block.classList.add('is-open');
if (globeController) {
globeController.setActive(true);
globeController.setMarkers(buildCoverageMarkers());
}
}
setTrustLabel('.ai-trust-phone', t('trustPhone'));
setTrustLabel('.ai-trust-address', t('trustAddress'));
setTrustLabel('.ai-trust-wechat', t('trustWechat'));
setTrustLabel('.ai-trust-email', t('trustEmail'));
setTrustLabel('.ai-trust-icp', t('trustIcp'));
shell.querySelector('.ai-trust-bar__human').textContent = t('human') + ' →';
const composeLead = shell.querySelector('.ai-welcome-compose__lead');
if (composeLead) composeLead.textContent = t('composeLead');
inputEl.placeholder = t('placeholder');
if (sendBtn) sendBtn.setAttribute('aria-label', t('sendLabel'));
render9610Panel();
renderIntentGrid();
renderFollowupChips();
renderHistorySidebar();
renderMessages();
updateViewMode();
renderStageGlobe();
}
function renderMessages() {
if (!messagesEl) return;
messagesEl.innerHTML = '';
sessionMessages.forEach((msg) => appendMessageRow(msg.role, msg.text, msg.card, false));
scrollToEnd();
}
function appendMessageRow(role, text, card, save) {
const row = el('div', `ai-msg-row ai-msg-row--${role}`);
const avatar = el('div', 'ai-msg-avatar');
avatar.innerHTML = role === 'assistant' ? '<i class="bi bi-truck"></i>' : '';
if (role === 'user') avatar.textContent = t('you').charAt(0).toUpperCase();
const bubble = el('div', 'ai-msg-bubble');
if (text) bubble.appendChild(el('p', 'ai-msg-text', text));
if (card) {
const cardNode = renderCard(card);
if (cardNode) bubble.appendChild(cardNode);
}
row.appendChild(avatar);
row.appendChild(bubble);
messagesEl.appendChild(row);
if (save) {
sessionMessages.push({ role, text: text || '', card: card || null });
if (role === 'user') hasUserMessage = true;
saveThreads();
updateViewMode();
renderHistorySidebar();
}
scrollToEnd();
return row;
}
function showTyping() {
const row = el('div', 'ai-msg-row ai-msg-row--assistant ai-msg-row--typing');
row.innerHTML = `
<div class="ai-msg-avatar"><i class="bi bi-truck"></i></div>
<div class="ai-msg-bubble"><span></span><span></span><span></span></div>
`;
messagesEl.appendChild(row);
scrollToEnd();
return row;
}
function scrollToEnd() {
if (!mainScrollEl) return;
mainScrollEl.scrollTop = mainScrollEl.scrollHeight;
syncMainOverflow();
}
function syncMainOverflow() {
const el = mainScrollEl;
if (!el) return;
const max = el.scrollHeight - el.clientHeight;
const canScroll = max > 12;
el.classList.toggle('is-scrolled', canScroll && el.scrollTop > 12);
el.classList.toggle('is-overflowing', canScroll && el.scrollTop < max - 12);
}
function bindMainScroll() {
if (!mainScrollEl || mainScrollEl.dataset.scrollBound === '1') return;
mainScrollEl.dataset.scrollBound = '1';
mainScrollEl.addEventListener('scroll', syncMainOverflow, { passive: true });
window.addEventListener('resize', syncMainOverflow);
window.requestAnimationFrame(syncMainOverflow);
}
function looksLikeTracking(text) {
if (typeof text !== 'string') return false;
return /^[A-Za-z0-9][A-Za-z0-9\-_]{7,}$/.test(text.replace(/\s+/g, ''));
}
function detectIntent(text) {
const lower = text.toLowerCase();
if (/track|tracking|物流|单号|追踪/.test(lower) || looksLikeTracking(text)) return 'track';
if (/9610|退税|专票|免税|疑难|小批量|不接/.test(lower)) return 'hard';
if (/一件代发|代发|dropship|fulfillment/.test(lower)) return 'warehouse';
if (/仓储|清关|空运|海运|服务|warehous|customs|services/.test(lower)) return 'guide';
if (/定价|收费|价目|pricing/.test(lower)) return 'pricing';
if (/price|cost|quote|报价|价格|多少钱/.test(lower)) return 'quote';
if (/human|人工|顾问|电话|联系我们|联系/.test(lower)) return 'human';
if (/who are you|about ide|你们是谁|关于我们|关于|公司|靠谱/.test(lower)) return 'company';
return null;
}
function replyForIntent(intentId, payload) {
const zh = lang === 'zh';
if (intentId === 'human') {
flow = null;
return {
text: zh ? '直接打给我们或加微信。询价请先注册登记。' : 'Call or WeChat us. Register first if you need a quote.',
card: { type: 'contact' },
};
}
if (intentId === 'company') {
flow = null;
return {
text: zh ? '先看这张公司卡。要询价请先注册登记。' : 'Here is who we are. Register first when you are ready to quote.',
card: { type: 'company' },
};
}
if (intentId === 'guide') {
flow = null;
open9610Panel();
return {
text: zh ? '服务在右侧,点一张卡继续。' : 'Services are on the right — pick a card to continue.',
card: null,
};
}
if (intentId === 'pricing') {
flow = null;
return {
text: zh
? '计费重取实重和体积重较大值;有票走退税,无票走免征。询价请先注册登记。'
: 'Chargeable weight is the greater of actual and volumetric. Refund if you have invoices, exemption if not. Register first to quote.',
card: { type: 'pricing' },
};
}
if (intentId === 'quote') {
flow = null;
const dest = payload && payload.dest ? String(payload.dest) : '';
return {
text: dest
? zh
? '发到' + dest + '可以走。官网不提供询价,请先注册登记。'
: 'We ship to ' + dest + '. Quotes are not on this site — please register first.'
: zh
? '官网不提供询价。完成注册登记后,即可在客户系统里看明码价。'
: 'This site does not quote. Register first, then see list rates in the customer system.',
card: { type: 'quote', dest },
};
}
if (intentId === 'warehouse') {
flow = null;
return {
text: zh
? '仓储一件代发要先注册登记,入仓和代发在客户系统里操作。'
: 'Warehouse dropship needs an account. Inbound and outbound run in the customer system.',
card: { type: 'warehouse' },
};
}
if (intentId === 'track') {
flow = null;
const number = payload && looksLikeTracking(payload) ? String(payload).replace(/\s+/g, '') : '';
if (number) {
try {
sessionStorage.setItem('ide-pending-track', number);
} catch {
/* ignore */
}
}
return {
text: number
? zh
? '单号已记下。请登录客户系统看本票真实节点。'
: 'Number saved. Sign in to see live scans for this shipment.'
: zh
? '有单号请登录看真实轨迹。询价需先注册登记。'
: 'Sign in if you have a tracking number. Quotes require registration.',
card: { type: 'track', number },
};
}
if (intentId === 'hard') {
flow = { id: 'hard', step: 0, data: {} };
return {
text: zh
? '别人不接的单我们接。如果走 9610,先确认一张票,再给你对应方案。'
: 'We take orders others decline. For 9610, one tax question first — then the matching scheme.',
card: {
type: 'choice',
flowId: 'hard',
kicker: '9610',
title: zh ? '货物能取得增值税专用发票(进项票)吗?' : 'Can you obtain special VAT invoices (input invoices)?',
options: [
{ id: 'vat-yes', label: zh ? '能取得专票' : 'Yes, we have invoices', primary: true },
{ id: 'vat-no', label: zh ? '无法取得专票' : 'No special invoices' },
],
},
};
}
return { text: t('fallback'), card: { type: 'contact' } };
}
function continueFlow(userText, extra) {
const zh = lang === 'zh';
if (flow.id === 'quote') {
flow = null;
return replyForIntent('quote');
}
if (flow.id === 'track') {
return replyForIntent('track', userText);
}
if (flow.id === 'hard') {
const choice = extra && extra.kind === 'choice' ? extra.choice : null;
const normalized = (choice || userText || '').toLowerCase();
if (flow.step === 0) {
const yes = choice === 'vat-yes' || /能|有票|yes|invoice/.test(normalized);
if (yes) {
flow = null;
return {
text: zh ? '有专票,走退税最划算。' : 'With special invoices, refund is the strongest path.',
card: { type: 'hard-refund' },
};
}
flow.step = 1;
return {
text: zh ? '没有专票的话,还想不想用出口免税?' : 'Without special invoices — do you still want export tax exemption?',
card: {
type: 'choice',
flowId: 'hard',
title: zh ? '是否希望享受出口免税政策?' : 'Do you want the export tax exemption?',
options: [
{ id: 'exempt-yes', label: zh ? '希望免税' : 'Yes, exemption', primary: true },
{ id: 'exempt-no', label: zh ? '仅需出货' : 'Just need to ship' },
],
},
};
}
const wantExempt = choice === 'exempt-yes' || /免税|希望|yes|exempt/.test(normalized);
flow = null;
if (wantExempt) {
return {
text: zh ? '无票免征,合规且能免出口环节税。' : 'Exemption without invoice — compliant, and export tax is waived.',
card: { type: 'hard-exempt' },
};
}
return {
text: zh ? '可以先出货,但税务风险要心里有数。' : 'We can ship first — keep the tax risk in view.',
card: { type: 'hard-basic' },
};
}
flow = null;
return { text: t('fallback'), card: { type: 'contact' } };
}
function buildReply(userText, extra) {
if (extra && extra.intentId) {
return replyForIntent(extra.intentId, extra);
}
if (flow) {
return continueFlow(userText, extra);
}
const guessed = detectIntent(userText);
if (guessed === 'track' && looksLikeTracking(userText)) {
return replyForIntent('track', userText);
}
if (guessed) return replyForIntent(guessed, userText);
return { text: t('fallback'), card: { type: 'contact' } };
}
function syncComposerSendState() {
if (!sendBtn || !inputEl) return;
const hasText = Boolean(inputEl.value.trim());
sendBtn.classList.toggle('is-empty', !hasText && !isBusy);
sendBtn.disabled = isBusy;
}
function setBusy(busy) {
isBusy = busy;
if (inputEl) inputEl.disabled = busy;
shell?.querySelectorAll('.ai-intent-door, .ai-followup-chip, .ai-card-opt, .ai-cover-chip').forEach((node) => {
node.disabled = busy;
});
syncComposerSendState();
}
function onIntentClick(intent) {
if (isBusy) return;
sendUserMessage(intentCopy(intent).label, { intentId: intent.id });
}
function sendUserMessage(text, extra) {
if (!text || isBusy || !shell) return;
appendMessageRow('user', text, null, true);
if (inputEl) {
inputEl.value = '';
inputEl.style.height = 'auto';
syncComposerSendState();
}
setBusy(true);
const typing = showTyping();
const reply = buildReply(text, extra || null);
window.setTimeout(() => {
typing.remove();
appendMessageRow('assistant', reply.text, reply.card, true);
setBusy(false);
inputEl.focus();
}, 520 + Math.random() * 280);
}
function markLangNavItem() {
document.getElementById('language-toggle-btn')?.closest('li')?.classList.add('site-nav-tools');
}
const NAV_TOPICS = {
about: { intentId: 'company', hash: 'about' },
pricing: { intentId: 'pricing', hash: 'pricing' },
contact: { intentId: 'human', hash: 'contact' },
};
function closeMobileNav() {
document.body.classList.remove('mobile-nav-active');
document.querySelectorAll('.mobile-nav-show').forEach((node) => node.classList.remove('d-none'));
document.querySelectorAll('.mobile-nav-hide').forEach((node) => node.classList.add('d-none'));
}
function setNavActive(topic) {
document.querySelectorAll('#navbar ul li > a').forEach((link) => {
if (link.id === 'language-toggle-btn') return;
const href = link.getAttribute('href') || '';
if (!topic || topic === 'home') {
link.classList.toggle('active', href === '/' || href === 'index.html');
return;
}
link.classList.toggle('active', href === `#${topic}` || href.endsWith(`#${topic}`));
});
}
function openNavTopic(topic) {
const spec = NAV_TOPICS[topic];
if (!spec || isBusy || !shell) return;
const intent = INTENTS.find((item) => item.id === spec.intentId);
const labels = {
company: lang === 'zh' ? '关于我们' : 'About IDE',
pricing: lang === 'zh' ? '价格怎么算?' : 'How does pricing work?',
human: lang === 'zh' ? '联系人工顾问' : 'Talk to a human advisor',
};
setNavActive(topic);
if (location.hash !== `#${spec.hash}`) {
history.replaceState(null, '', `#${spec.hash}`);
}
sendUserMessage(labels[spec.intentId], { intentId: spec.intentId });
void intent;
}
function bindHomeReset() {
const reset = (e) => {
if (!isHomePage()) return;
e.preventDefault();
startNewChat();
closeDrawers();
setNavActive('home');
if (location.hash) history.replaceState(null, '', location.pathname + location.search);
};
document.querySelector('#header .logo')?.addEventListener('click', reset);
document.querySelector('#navbar a[href="/"], #navbar a[href="index.html"]')?.addEventListener('click', reset);
document.querySelectorAll('#navbar a[href^="#"], #navbar a[href*="#"]').forEach((link) => {
const href = link.getAttribute('href') || '';
const topic = href.split('#')[1];
if (!NAV_TOPICS[topic]) return;
link.addEventListener('click', (e) => {
if (!isHomePage()) return;
e.preventDefault();
closeMobileNav();
closeDrawers();
openNavTopic(topic);
});
});
document.querySelectorAll('#navbar a[href="#services"], #navbar a[href="#9610"], #navbar a[href="/9610.html"]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
open9610Panel();
});
});
shell?.querySelector('.ai-trust-bar__human')?.addEventListener('click', (e) => {
e.preventDefault();
openNavTopic('contact');
});
}
function init() {
if (!isHomePage()) return;
document.body.classList.add('command-home');
loadThreads();
renderShell();
markLangNavItem();
bindHomeReset();
renderStaticCopy();
syncHeaderOffset();
window.addEventListener('resize', () => {
syncHeaderOffset();
applyWelcomeTitle();
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDrawers();
});
const bootTopic = (location.hash || '').replace('#', '');
if (bootTopic === 'services' || bootTopic === '9610') {
open9610Panel();
} else if (NAV_TOPICS[bootTopic]) {
openNavTopic(bootTopic);
}
}
window.IDEAdvisor = {
setLanguage(nextLang) {
lang = nextLang === 'zh' ? 'zh' : 'en';
renderStaticCopy();
},
open: () => {
if (!isHomePage()) window.location.href = 'index.html';
},
close: () => closeDrawers(),
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();