/**
* 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 WECOM_KF_URL = 'https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a';
const ICP = '粤ICP备2025463970号-2';
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, your cross-border logistics advisor',
welcomeTitleShort: 'IDE · Your logistics advisor',
welcomeSubLead: 'Preferential rates · proactive service · faster transit · ',
welcomeSubAccent: 'AI logistics',
welcomeSubTail: ' · dedicated lines, packets, air & DHL/UPS/FedEX/3PE express covered.',
placeholder: 'Ask about lanes, tracking, or 9610…',
sendLabel: 'Ask',
human: 'Talk to a human advisor',
you: 'You',
historyTitle: 'Chat history',
newChat: 'New chat',
newChatHint: 'New chat started — ask here',
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 support',
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',
partnerTag: 'Partners',
partnerAria: 'Connected platforms and carriers',
routesTitle: 'Shipping lanes',
routesKicker: 'Routes',
routesSub: 'Compare transit time and trade terms, then ask for a quote.',
routesSearchPh: 'Search by product name…',
routesAll: 'All',
routesAsk: 'Ask advisor',
routesRegister: 'Register to quote',
routesSticky: 'Not sure which lane? Ask an advisor by destination',
routesEmpty: 'No lanes match right now',
routesRetry: 'Retry',
routesLoading: 'Loading lanes…',
routesDays: 'days',
routesAskEta: 'Ask advisor',
routesDdpHint: 'Duties prepaid',
routesDduHint: 'Duties unpaid',
routesExpand: 'More',
routesCollapse: 'Less',
},
zh: {
welcomeTitle: 'IDE,您身边的跨境物流顾问',
welcomeTitleShort: 'IDE · 跨境物流顾问',
welcomeSubLead: '价格更优惠 · 服务更主动 · 时效更快 · ',
welcomeSubAccent: 'AI 物流',
welcomeSubTail: ' · 专线/小包/空派/DHL/UPS/FedEX/3PE快递全覆盖。',
placeholder: '问线路、查轨迹、9610 政策…',
sendLabel: '提问',
human: '联系在线顾问',
you: '你',
historyTitle: '历史记录',
newChat: '新会话',
newChatHint: '已开启新会话,在这里提问',
emptyHistory: '暂无会话',
fallback: '官网不提供询价。完成注册登记后,即可在客户系统里看明码价。也可以先联系顾问。',
trustPhone: PHONE,
trustAddress: ADDRESS_ZH,
trustWechat: '微信客服',
trustEmail: EMAIL,
trustIcp: ICP,
guide9610Title: '先发货,再办税',
guide9610Kicker: '服务',
guide9610Sub: '先选怎么走货。税务和备案跟在后面,点一张卡就能问。',
loginTrack: '登录查真实轨迹',
registerNow: '去注册登记',
loginExisting: '已有账号,去登录',
heroCta: '立即发货',
statsAria: '服务数据',
statsCoverageHint: '点我查看',
mapZoomHint: '点击国家,地图上会用定位图标标出',
composeLead: '先问线路,或选快捷入口',
partnerTag: '合作伙伴',
partnerAria: '对接平台与服务商',
routesTitle: '线路',
routesKicker: 'Routes',
routesSub: '对比时效与贸易条款,选好运再询价',
routesSearchPh: '搜索产品名称…',
routesAll: '全部',
routesAsk: '问顾问报价',
routesRegister: '注册询价',
routesSticky: '不确定选哪条?让顾问按目的国推荐',
routesEmpty: '暂无匹配线路',
routesRetry: '重试',
routesLoading: '线路加载中…',
routesDays: '天',
routesAskEta: '询顾问',
routesDdpHint: '包税到门',
routesDduHint: '关税自理',
routesExpand: '展开',
routesCollapse: '收起',
},
};
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: 'Tired of list prices?',
hint: 'Low volume means low commission for sales — our member tiers get better as you ship more, or become an agent and earn',
label: 'I want member or agent rates',
},
zh: {
title: '拿不到优惠价?',
hint: '单少提成低,销售爱搭不理 → 会员成长体系发越多越优惠,还能做代理拿分成',
label: '我想看会员价/代理价',
},
},
{
id: 'track',
icon: 'track',
en: {
title: 'Lost mid-route?',
hint: 'Order → pickup → warehouse → clearance → delivery, with exception alerts',
label: 'Where is my shipment from pickup to delivery?',
},
zh: {
title: '货丢在半路?',
hint: '下单→揽收→出入库→清关→派送,一站可视,异常有人盯',
label: '这票货从揽收到派送卡在哪',
},
},
{
id: 'hard',
icon: 'hard',
en: {
title: 'Others declined it?',
hint: 'Battery, special cargo, cosmetics — if others refuse, ask us first',
label: 'Others declined this cargo. Can you take it?',
},
zh: {
title: '别人不接的单?',
hint: '3C带电、特殊品、化妆品,别处拒收,先问我们',
label: '别人不接的货,你们能发吗?',
},
},
{
id: 'compliance',
icon: 'clearance',
en: {
title: 'Need export compliance?',
hint: 'Formal declaration, VAT refund, and invoice-free exemption',
label: 'I need export compliance — declaration, refund, or exemption',
},
zh: {
title: '想出口合规?',
hint: '正式报关、出口退税、无票免征,我们都能办',
label: '想出口合规:正式报关 / 退税 / 无票免征',
},
},
{
id: 'human',
icon: 'human',
en: {
title: 'Stuck in a group chat?',
hint: 'No mega group chats — one owner from quote to POD, even for small volume',
label: 'Give me a dedicated advisor, no group chat',
},
zh: {
title: '拉群找不到人?',
hint: '不拉大群、不踢皮球;单量少也有人认,报价跟到签收',
label: '给我一个专属顾问,别拉群',
},
},
];
const PARTNERS = [
{
id: 'lazada',
group: 'platform',
icon: 'assets/img/partners/lazada.svg',
label: { zh: 'Lazada', en: 'Lazada' },
ask: {
zh: 'Lazada 店铺怎么授权、同步订单做一件代发?',
en: 'How do I authorize a Lazada shop and sync orders for dropship?',
},
},
{
id: 'shopee',
group: 'platform',
icon: 'assets/img/partners/shopee.svg',
label: { zh: 'Shopee', en: 'Shopee' },
ask: {
zh: 'Shopee 店铺怎么授权同步订单?',
en: 'How do I authorize a Shopee shop and sync orders?',
},
},
{
id: 'tiktok',
group: 'platform',
icon: 'assets/img/partners/tiktok.svg',
label: { zh: 'TikTok', en: 'TikTok' },
ask: {
zh: 'TikTok 店铺订单怎么同步到你们系统?',
en: 'How do TikTok shop orders sync into your system?',
},
},
{
id: 'temu',
group: 'platform',
icon: 'assets/img/partners/temu.svg',
label: { zh: 'Temu', en: 'Temu' },
ask: {
zh: 'Temu 店铺怎么授权同步、一件代发?',
en: 'How do I authorize Temu for order sync and dropship?',
},
},
{
id: 'dhl',
group: 'carrier',
icon: 'assets/img/partners/dhl.svg',
label: { zh: 'DHL', en: 'DHL' },
ask: {
zh: '有没有 DHL 线路可以询价?',
en: 'Do you have DHL lanes I can quote?',
},
},
{
id: 'fedex',
group: 'carrier',
icon: 'assets/img/partners/fedex.svg',
label: { zh: 'FedEx', en: 'FedEx' },
ask: {
zh: '有没有 FedEx 线路可以询价?',
en: 'Do you have FedEx lanes I can quote?',
},
},
{
id: 'ups',
group: 'carrier',
icon: 'assets/img/partners/ups.svg',
label: { zh: 'UPS', en: 'UPS' },
ask: {
zh: '有没有 UPS 线路报价?',
en: 'Do you have UPS lanes I can quote?',
},
},
{
id: '3pe',
group: 'carrier',
label: { zh: '3PE', en: '3PE' },
ask: {
zh: '3PE 国际快递怎么走、怎么报价?',
en: 'How does 3PE express work, and how do I get a quote?',
},
},
];
function iconMarkup(name) {
const illus = {
quote:
'' +
'' +
'' +
'' +
'',
track:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'',
hard:
'' +
'' +
'' +
'',
clearance:
'' +
'' +
'' +
'' +
'' +
'' +
'',
human:
'' +
'' +
'' +
'' +
'' +
'' +
'',
};
const defs = {
quote:
'' +
'',
track:
'' +
'',
hard:
'' +
'',
clearance:
'' +
'',
human:
'' +
'',
};
if (illus[name]) {
return (
''
);
}
// Legacy stroke icons for guide / other surfaces
const art = {
lane:
'' +
'' +
'',
advise:
'' +
'' +
'',
refund:
'' +
'' +
'' +
'',
exempt:
'' +
'',
filing:
'' +
'' +
'' +
'',
warehouse:
'' +
'' +
'' +
'',
};
const body = art[name] || art.lane;
return (
''
);
}
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;
let newChatTipTimer = null;
let routesCatalog = null;
let routesLoadState = 'idle';
let routesGroupFilter = 'all';
let routesSearch = '';
let routesExpanded = {};
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(loginPortalUrl());
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 ? '微信客服' : 'WeCom', zh ? '扫码 / 手机直达' : 'Scan or tap to chat'],
[zh ? '微信号' : 'WeChat ID', 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
? '点「在线咨询」用微信对接顾问。询价请先注册登记。'
: 'Tap Live chat to reach an advisor on WeChat. Register first if you need a quote.'
)
);
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 isWeChatEnv() {
return /MicroMessenger/i.test(navigator.userAgent);
}
function isMobileEnv() {
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
}
function closeKfQrModal() {
document.getElementById('ide-kf-modal')?.remove();
}
function showKfQrModal(url) {
closeKfQrModal();
const zh = lang === 'zh';
const modal = document.createElement('div');
modal.id = 'ide-kf-modal';
modal.className = 'ide-kf-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-label', zh ? '微信扫码咨询' : 'Scan to chat on WeChat');
const qrSrc =
'https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=8&data=' +
encodeURIComponent(url);
modal.innerHTML = `
${zh ? '企业微信客服' : 'WeCom support'}
${zh ? '微信扫码咨询' : 'Scan with WeChat'}
${zh ? '打开手机微信,扫一扫下方二维码即可对话顾问' : 'Open WeChat on your phone and scan the code to chat'}
${zh ? '二维码加载失败,请点下方打开客服页后用微信扫码' : 'QR failed to load — open the support page and scan there'}
${zh ? '在新窗口打开客服页' : 'Open support page'}
`;
const qrImg = modal.querySelector('.ide-kf-modal__qr');
const qrFallback = modal.querySelector('.ide-kf-modal__qr-fallback');
qrImg?.addEventListener('error', () => {
qrImg.hidden = true;
if (qrFallback) qrFallback.hidden = false;
});
modal.addEventListener('click', (e) => {
if (e.target.closest('[data-kf-close]')) closeKfQrModal();
});
document.addEventListener(
'keydown',
function onEsc(e) {
if (e.key === 'Escape') {
closeKfQrModal();
document.removeEventListener('keydown', onEsc);
}
},
{ once: true }
);
document.body.appendChild(modal);
}
/** 手机 / 微信内:直跳唤起会话;PC:弹二维码浮层 */
function openWecomKf(e) {
if (e) e.preventDefault();
const url = WECOM_KF_URL;
if (isWeChatEnv() || isMobileEnv()) {
window.location.href = url;
return;
}
showKfQrModal(url);
}
function contactActions() {
const actions = el('div', 'ai-card__actions');
const kf = el('button', 'ai-card-link is-primary', lang === 'zh' ? '在线咨询' : 'Live chat');
kf.type = 'button';
kf.addEventListener('click', openWecomKf);
const tel = el('a', 'ai-card-link', PHONE);
tel.href = `tel:${PHONE}`;
const mail = el('a', 'ai-card-link', EMAIL);
mail.href = `mailto:${EMAIL}`;
actions.appendChild(kf);
actions.appendChild(tel);
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 = `
`;
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-routes-panel__close').addEventListener('click', () => closeRoutesPanel());
shell.querySelector('.ai-routes-search__input').addEventListener('input', (e) => {
routesSearch = String(e.target.value || '').trim();
renderRoutesList();
});
shell.querySelector('.ai-routes-groups').addEventListener('click', (e) => {
const btn = e.target.closest('[data-routes-group]');
if (!btn) return;
routesGroupFilter = btn.dataset.routesGroup || 'all';
renderRoutesGroups();
renderRoutesList();
});
shell.querySelector('.ai-routes-panel__body').addEventListener('click', (e) => {
const expand = e.target.closest('[data-routes-expand]');
if (expand) {
const id = expand.dataset.routesExpand;
routesExpanded[id] = !routesExpanded[id];
renderRoutesList();
return;
}
const ask = e.target.closest('[data-routes-ask]');
if (ask) {
const text = ask.dataset.routesAsk;
closeRoutesPanel();
prefillComposer(text);
return;
}
const retry = e.target.closest('[data-routes-retry]');
if (retry) {
routesCatalog = null;
routesLoadState = 'idle';
void loadRoutesCatalog(true);
}
});
shell.querySelector('.ai-routes-sticky').addEventListener('click', () => {
const text =
lang === 'zh'
? '按目的国帮我推荐合适线路,并说明时效和贸易条款'
: 'Recommend a shipping lane by destination, with transit time and trade terms';
closeRoutesPanel();
prefillComposer(text);
});
shell.querySelector('.ai-trust-wechat').addEventListener('click', openWecomKf);
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', () => {
hideNewChatTip();
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 (
'' +
''
);
}
return (
'' +
'' +
value +
'' +
'' +
label +
''
);
}).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 = `
${iconMarkup(intent.icon)}
`;
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);
});
renderPartnerMarquee();
renderCoverage();
}
function partnerAsk(partner) {
return lang === 'zh' ? partner.ask.zh : partner.ask.en;
}
function partnerLabel(partner) {
return lang === 'zh' ? partner.label.zh : partner.label.en;
}
function prefillComposer(text) {
if (!inputEl || isBusy || !text) return;
hideNewChatTip();
inputEl.value = text;
inputEl.style.height = 'auto';
inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px';
syncComposerSendState();
inputEl.focus();
try {
const len = text.length;
inputEl.setSelectionRange(len, len);
} catch (_) {
/* ignore */
}
inputEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
function renderPartnerMarquee() {
if (!shell) return;
const root = shell.querySelector('.ai-partner-marquee');
const track = shell.querySelector('.ai-partner-marquee__track');
const tag = shell.querySelector('.ai-partner-marquee__tag');
if (!root || !track || !tag) return;
root.setAttribute('aria-label', t('partnerAria'));
tag.textContent = t('partnerTag');
track.innerHTML = '';
const makeChip = (partner) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ai-partner-chip ai-partner-chip--' + partner.group;
btn.dataset.partner = partner.id;
btn.title = partnerAsk(partner);
if (partner.icon) {
const img = document.createElement('img');
img.className = 'ai-partner-chip__logo';
img.src = partner.icon;
img.alt = '';
img.decoding = 'async';
img.setAttribute('aria-hidden', 'true');
btn.appendChild(img);
}
const label = document.createElement('span');
label.className = 'ai-partner-chip__label';
label.textContent = partnerLabel(partner);
btn.appendChild(label);
btn.addEventListener('click', () => prefillComposer(partnerAsk(partner)));
return btn;
};
// Duplicate once for seamless loop (skip when user prefers reduced motion)
const loopTwice = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
(loopTwice ? [0, 1] : [0]).forEach(() => {
PARTNERS.forEach((partner) => track.appendChild(makeChip(partner)));
});
}
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 = `
`;
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;
}
hideNewChatTip();
syncActiveThreadMessages();
activeThreadId = threadId;
applyThreadMessages(getActiveThread());
saveThreads();
renderHistorySidebar();
renderMessages();
updateViewMode();
closeHistorySidebar();
inputEl.focus();
}
function hideNewChatTip() {
const tip = shell?.querySelector('.ai-new-chat-tip');
const wrap = shell?.querySelector('.ai-composer-wrap');
if (newChatTipTimer) {
clearTimeout(newChatTipTimer);
newChatTipTimer = null;
}
if (tip) {
tip.classList.remove('is-visible');
tip.hidden = true;
}
wrap?.classList.remove('has-new-chat-tip');
}
function showNewChatTip() {
const tip = shell?.querySelector('.ai-new-chat-tip');
const wrap = shell?.querySelector('.ai-composer-wrap');
const textEl = tip?.querySelector('.ai-new-chat-tip__text');
if (!tip || !wrap || !textEl) return;
if (newChatTipTimer) clearTimeout(newChatTipTimer);
textEl.textContent = t('newChatHint');
tip.hidden = false;
wrap.classList.add('has-new-chat-tip');
// Restart enter animation if already visible
tip.classList.remove('is-visible');
void tip.offsetWidth;
tip.classList.add('is-visible');
newChatTipTimer = setTimeout(() => hideNewChatTip(), 4200);
}
function startNewChat() {
if (isBusy) return;
syncActiveThreadMessages();
const current = getActiveThread();
if (current && current.messages.length === 0) {
closeHistorySidebar();
updateViewMode();
inputEl.focus();
showNewChatTip();
return;
}
createThread(true);
renderHistorySidebar();
renderMessages();
updateViewMode();
closeHistorySidebar();
setNavActive('home');
inputEl.focus();
showNewChatTip();
}
function toggleHistorySidebar() {
close9610Panel();
closeRoutesPanel();
shell?.classList.toggle('ai-history-open');
}
function closeHistorySidebar() {
shell?.classList.remove('ai-history-open');
}
function open9610Panel() {
closeHistorySidebar();
closeRoutesPanel();
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 openRoutesPanel() {
closeHistorySidebar();
close9610Panel();
closeMobileNav();
shell?.classList.add('ai-routes-open');
setNavActive('routes');
if (location.hash !== '#routes') {
history.replaceState(null, '', '#routes');
}
renderRoutesChrome();
void loadRoutesCatalog(false);
}
function closeRoutesPanel() {
shell?.classList.remove('ai-routes-open');
if (location.hash === '#routes') {
history.replaceState(null, '', location.pathname + location.search);
setNavActive('home');
}
}
function closeDrawers() {
closeHistorySidebar();
close9610Panel();
closeRoutesPanel();
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function unwrapApiData(payload) {
if (!payload || typeof payload !== 'object') return null;
let cur = payload;
for (let i = 0; i < 4; i++) {
if (Array.isArray(cur.groups) || Array.isArray(cur.Groups) || Array.isArray(cur.items) || Array.isArray(cur.Items)) {
return cur;
}
const next = cur.data ?? cur.Data ?? cur.result ?? cur.Result;
if (!next || typeof next !== 'object') break;
cur = next;
}
return cur;
}
function normalizeRoutesCatalog(raw) {
const data = unwrapApiData(raw) || {};
const groups = (data.groups || data.Groups || []).map((g) => ({
code: String(g.code ?? g.Code ?? '').trim(),
name: String(g.name ?? g.Name ?? g.code ?? g.Code ?? '').trim(),
})).filter((g) => g.code);
const items = (data.items || data.Items || []).map((item) => ({
id: item.id ?? item.Id,
productName: String(item.productName ?? item.ProductName ?? '').trim(),
productGroup: String(item.productGroup ?? item.ProductGroup ?? '').trim() || '_ungrouped',
productGroupName: String(item.productGroupName ?? item.ProductGroupName ?? '').trim(),
minTimeliness: item.minTimeliness ?? item.MinTimeliness ?? null,
maxTimeliness: item.maxTimeliness ?? item.MaxTimeliness ?? null,
tradeTerms: String(item.tradeTerms ?? item.TradeTerms ?? '').trim().toUpperCase(),
productDescription: String(item.productDescription ?? item.ProductDescription ?? '').trim(),
})).filter((item) => item.productName);
return { groups, items };
}
async function loadRoutesCatalog(force) {
if (!force && (routesLoadState === 'loading' || (routesLoadState === 'ready' && routesCatalog))) {
renderRoutesList();
return;
}
routesLoadState = 'loading';
renderRoutesList();
try {
const res = await fetch(omsUrl('/channel/sales-product/portal/catalog'), {
method: 'GET',
headers: { Accept: 'application/json' },
credentials: 'omit',
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const json = await res.json();
routesCatalog = normalizeRoutesCatalog(json);
routesLoadState = 'ready';
if (routesGroupFilter !== 'all') {
const exists = routesCatalog.groups.some((g) => g.code === routesGroupFilter);
if (!exists) routesGroupFilter = 'all';
}
renderRoutesGroups();
renderRoutesList();
} catch (_) {
routesLoadState = 'error';
routesCatalog = null;
renderRoutesList();
}
}
function formatRouteTimeliness(item) {
const min = item.minTimeliness;
const max = item.maxTimeliness;
if (min != null && max != null && min !== max) return min + '–' + max;
if (min != null && max != null) return String(min);
if (min != null) return String(min) + '+';
if (max != null) return '≤' + max;
return '';
}
function tradeTermsMeta(code) {
if (code === 'DDP') return { code: 'DDP', hint: t('routesDdpHint') };
if (code === 'DDU') return { code: 'DDU', hint: t('routesDduHint') };
return null;
}
function filteredRouteItems() {
const items = routesCatalog?.items || [];
const q = routesSearch.toLowerCase();
return items.filter((item) => {
if (routesGroupFilter !== 'all' && item.productGroup !== routesGroupFilter) return false;
if (!q) return true;
return (
item.productName.toLowerCase().includes(q) ||
item.productDescription.toLowerCase().includes(q) ||
item.productGroupName.toLowerCase().includes(q)
);
});
}
function renderRoutesChrome() {
if (!shell) return;
const title = shell.querySelector('.ai-routes-panel__title');
const sub = shell.querySelector('.ai-routes-panel__sub');
const search = shell.querySelector('.ai-routes-search__input');
const sticky = shell.querySelector('.ai-routes-sticky');
if (title) title.textContent = t('routesTitle');
if (sub) sub.textContent = t('routesSub');
if (search) {
search.placeholder = t('routesSearchPh');
search.value = routesSearch;
}
if (sticky) sticky.textContent = t('routesSticky');
renderRoutesGroups();
renderRoutesList();
}
function renderRoutesGroups() {
const elGroups = shell?.querySelector('.ai-routes-groups');
if (!elGroups) return;
const groups = routesCatalog?.groups || [];
const chips = [{ code: 'all', name: t('routesAll') }].concat(groups);
elGroups.innerHTML = chips
.map((g) => {
const active = routesGroupFilter === g.code ? ' is-active' : '';
return (
''
);
})
.join('');
}
function renderRoutesList() {
const body = shell?.querySelector('.ai-routes-panel__body');
if (!body) return;
if (routesLoadState === 'loading') {
body.innerHTML =
'' +
'
' +
'
' +
escapeHtml(t('routesLoading')) +
'
';
return;
}
if (routesLoadState === 'error') {
body.innerHTML =
'' +
'
' +
escapeHtml(t('routesEmpty')) +
'
' +
'
';
return;
}
const items = filteredRouteItems();
if (!items.length) {
body.innerHTML =
'' +
escapeHtml(t('routesEmpty')) +
'
';
return;
}
body.innerHTML = items
.map((item) => {
const eta = formatRouteTimeliness(item);
const terms = tradeTermsMeta(item.tradeTerms);
const askText =
lang === 'zh'
? '想了解【' + item.productName + '】的报价、时效和走货要求'
: 'I want a quote for [' + item.productName + '], including transit time and shipping requirements';
const expanded = !!routesExpanded[String(item.id)];
const desc = item.productDescription;
const longDesc = desc.length > 72;
const descHtml = desc
? '' +
escapeHtml(desc) +
'
' +
(longDesc
? ''
: '')
: '';
const etaBlock = eta
? '' +
escapeHtml(eta) +
'' +
escapeHtml(t('routesDays')) +
'
'
: '—' +
escapeHtml(t('routesAskEta')) +
'
';
const termsHtml = terms
? '' +
escapeHtml(terms.code) +
' ' +
escapeHtml(terms.hint) +
''
: '';
return (
'' +
etaBlock +
'' +
'
' +
escapeHtml(item.productGroupName || item.productGroup) +
'' +
'
' +
escapeHtml(item.productName) +
'
' +
termsHtml +
descHtml +
'
'
);
})
.join('');
}
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 (
''
);
}).join('');
}
function renderWelcomeSub() {
const subEl = shell?.querySelector('.ai-welcome-sub');
if (!subEl) return;
subEl.innerHTML =
t('welcomeSubLead') +
'' +
t('welcomeSubAccent') +
'' +
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');
const tipText = shell.querySelector('.ai-new-chat-tip__text');
if (tipText) tipText.textContent = t('newChatHint');
applyWelcomeTitle();
renderWelcomeSub();
const heroCta = shell.querySelector('.ai-welcome-cta');
if (heroCta) {
heroCta.textContent = t('heroCta');
heroCta.href = loginPortalUrl();
}
const welcomeHuman = shell.querySelector('.ai-welcome-human');
if (welcomeHuman) welcomeHuman.textContent = t('human') + ' →';
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-wechat', t('trustWechat'));
setTrustLabel('.ai-trust-icp', t('trustIcp'));
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();
renderRoutesChrome();
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' ? '' : '';
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 = `
`;
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|退税|专票|免税|免征|合规|报关|declaration|refund|exempt/.test(lower)) return 'compliance';
if (/疑难|小批量|不接|带电|3c|特殊品|化妆品/.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
? '点「在线咨询」用微信对接顾问,也可打电话。询价请先注册登记。'
: 'Tap Live chat for WeChat support, or call 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 = null;
return {
text: zh
? '带电、特殊品、化妆品这类别人不接的单,我们按品类选线路。先说下货是什么,或点在线顾问细聊。'
: 'Battery, special cargo, cosmetics — we pick a lane by commodity. Tell us what it is, or talk to an advisor.',
card: { type: 'contact' },
};
}
if (intentId === 'compliance') {
flow = { id: 'hard', step: 0, data: {} };
return {
text: zh
? '出口合规我们能办:正式报关、退税、无票免征。先确认一张票,再给你对应方案。'
: 'We handle formal declaration, VAT refund, and invoice-free exemption. 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, .ai-partner-chip'
).forEach((node) => {
node.disabled = busy;
});
syncComposerSendState();
}
function onIntentClick(intent) {
if (isBusy) return;
sendUserMessage(intentCopy(intent).label, { intentId: intent.id });
}
function sendUserMessage(text, extra) {
hideNewChatTip();
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 an online 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();
});
});
document.querySelectorAll('#navbar a[href="#routes"]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
openRoutesPanel();
});
});
shell?.querySelector('.ai-welcome-human')?.addEventListener('click', (e) => {
e.preventDefault();
openNavTopic('contact');
});
window.IDEOpenWecomKf = openWecomKf;
}
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') {
closeKfQrModal();
closeDrawers();
}
});
const bootTopic = (location.hash || '').replace('#', '');
if (bootTopic === 'services' || bootTopic === '9610') {
open9610Panel();
} else if (bootTopic === 'routes') {
openRoutesPanel();
} else if (NAV_TOPICS[bootTopic]) {
openNavTopic(bootTopic);
}
}
window.IDEAdvisor = {
setLanguage(nextLang) {
lang = nextLang === 'zh' ? 'zh' : 'en';
renderStaticCopy();
},
open: () => {
if (!isHomePage()) window.location.href = '/';
},
close: () => closeDrawers(),
openWecomKf,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();