front/IDE官网/assets/js/ai-advisor.js
zzh a24dcb41ff
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m28s
feat: COS 直传、仓储上传带客户、9610 表格与 Nana 体验修复
管理端包裹/SKU 上传携带 customerId;服务渠道分组列表解析;9610 操作列渲染;Nana FAB 去掉数字角标并恢复顶栏消息。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 12:09:36 +08:00

2693 lines
101 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 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:
'<ellipse cx="27" cy="44" rx="13" ry="3" fill="#C7923E" opacity="0.28"/>' +
'<path d="M17 23.5c0-1.5 1.2-2.7 2.7-2.7H29c2.6 0 4.7 2.1 4.7 4.7v1.3h3.4c1.7 0 3 1.5 2.8 3.2l-.9 7.5c-.2 1.6-1.5 2.7-3.1 2.7H19.7c-1.5 0-2.7-1.2-2.7-2.7V23.5z" fill="url(#ai-g-quote)"/>' +
'<path d="M20.2 20.8v-1.3c0-2.2 1.8-4 4-4h5.6c2.2 0 4 1.8 4 4v1.3" stroke="#B58232" stroke-width="2.2" stroke-linecap="round"/>' +
'<circle cx="28" cy="32.5" r="3.4" fill="#FFF8EC"/>' +
'<path d="M28 30.6v3.8M26.5 32h3" stroke="#C7923E" stroke-width="1.5" stroke-linecap="round"/>',
track:
'<ellipse cx="27" cy="44" rx="13" ry="3" fill="#2A9BB5" opacity="0.28"/>' +
'<path d="M11 28h17.8l4.1-7.1c.4-.7 1.1-1.1 1.9-1.1H40.2c1.1 0 2 .9 2 2v12.6c0 .8-.7 1.5-1.5 1.5H11V28z" fill="url(#ai-g-track)"/>' +
'<path d="M11 22h15.2v18H13c-1.1 0-2-.9-2-2V22z" fill="#054F74"/>' +
'<rect x="14.2" y="25.6" width="6.6" height="4" rx="1" fill="#E6F6FA"/>' +
'<circle cx="17.6" cy="39.6" r="3.4" fill="#1D2129"/>' +
'<circle cx="17.6" cy="39.6" r="1.5" fill="#C9CDD4"/>' +
'<circle cx="35.6" cy="39.6" r="3.4" fill="#1D2129"/>' +
'<circle cx="35.6" cy="39.6" r="1.5" fill="#C9CDD4"/>' +
'<path d="M6.2 24.6h4M5.2 28h3.6M6.6 31.6h3.2" stroke="#2A9BB5" stroke-width="1.8" stroke-linecap="round" opacity="0.9"/>',
hard:
'<ellipse cx="27" cy="44" rx="12" ry="3" fill="#E07A5F" opacity="0.28"/>' +
'<path d="M27 12.6c1.1 0 2.1.58 2.64 1.52l12.1 21.1c.54.95.53 2.12-.03 3.06A3.05 3.05 0 0 1 39.07 40H14.93c-1.1 0-2.12-.58-2.64-1.52a3.05 3.05 0 0 1-.03-3.06l12.1-21.1A3.05 3.05 0 0 1 27 12.6z" fill="url(#ai-g-hard)"/>' +
'<rect x="25.4" y="21.2" width="3.2" height="9.4" rx="1.6" fill="#fff"/>' +
'<circle cx="27" cy="34.3" r="2" fill="#fff"/>',
clearance:
'<ellipse cx="27" cy="44" rx="12" ry="3" fill="#054F74" opacity="0.22"/>' +
'<path d="M16.5 13.5c0-1.4 1.1-2.5 2.5-2.5h10.4c.7 0 1.4.3 1.9.8l6.2 6.2c.5.5.8 1.2.8 1.9V36c0 1.4-1.1 2.5-2.5 2.5H19c-1.4 0-2.5-1.1-2.5-2.5V13.5z" fill="url(#ai-g-clear)"/>' +
'<path d="M30.2 11.2V16c0 1.1.9 2 2 2h4.7L30.2 11.2z" fill="#9ED7E4"/>' +
'<rect x="21" y="23.5" width="13.5" height="2.2" rx="1.1" fill="#fff" opacity="0.92"/>' +
'<rect x="21" y="28.5" width="9.5" height="2.2" rx="1.1" fill="#fff" opacity="0.75"/>' +
'<circle cx="37.5" cy="34.2" r="7.6" fill="#2A9BB5"/>' +
'<path d="M34.6 34.4l1.8 1.8 4.2-4.4" stroke="#fff" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/>',
human:
'<ellipse cx="27" cy="44" rx="13" ry="3" fill="#2A9BB5" opacity="0.28"/>' +
'<circle cx="27" cy="18.2" r="7.2" fill="url(#ai-g-human)"/>' +
'<path d="M14.8 40.2c1.4-7.2 5.8-10.8 12.2-10.8s10.8 3.6 12.2 10.8" fill="#054F74"/>' +
'<path d="M18.5 20.2c.4-5.2 3.4-8.4 8.5-8.4s8.1 3.2 8.5 8.4" stroke="#9ED7E4" stroke-width="2" stroke-linecap="round" opacity="0.9"/>' +
'<rect x="15.6" y="19.4" width="3.4" height="5.4" rx="1.7" fill="#2A9BB5"/>' +
'<rect x="35" y="19.4" width="3.4" height="5.4" rx="1.7" fill="#2A9BB5"/>' +
'<path d="M22.4 28.6h9.2c1.4 0 2.5 1.1 2.5 2.5v2.2H19.9v-2.2c0-1.4 1.1-2.5 2.5-2.5z" fill="#E6F6FA"/>',
};
const defs = {
quote:
'<linearGradient id="ai-g-quote" x1="17" y1="16" x2="40" y2="40" gradientUnits="userSpaceOnUse">' +
'<stop stop-color="#E0B35A"/><stop offset="1" stop-color="#C7923E"/></linearGradient>',
track:
'<linearGradient id="ai-g-track" x1="11" y1="19" x2="42" y2="40" gradientUnits="userSpaceOnUse">' +
'<stop stop-color="#7DCFDD"/><stop offset="1" stop-color="#2A9BB5"/></linearGradient>',
hard:
'<linearGradient id="ai-g-hard" x1="12" y1="12" x2="42" y2="40" gradientUnits="userSpaceOnUse">' +
'<stop stop-color="#F0A090"/><stop offset="1" stop-color="#E07A5F"/></linearGradient>',
clearance:
'<linearGradient id="ai-g-clear" x1="16" y1="11" x2="40" y2="39" gradientUnits="userSpaceOnUse">' +
'<stop stop-color="#5EC4D6"/><stop offset="1" stop-color="#054F74"/></linearGradient>',
human:
'<linearGradient id="ai-g-human" x1="20" y1="11" x2="34" y2="26" gradientUnits="userSpaceOnUse">' +
'<stop stop-color="#7DCFDD"/><stop offset="1" stop-color="#2A9BB5"/></linearGradient>',
};
if (illus[name]) {
return (
'<svg class="ai-icon ai-icon--illus" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
'<defs>' +
defs[name] +
'</defs>' +
illus[name] +
'</svg>'
);
}
// Legacy stroke icons for guide / other surfaces
const art = {
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"/>',
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;
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 = `
<div class="ide-kf-modal__backdrop" data-kf-close></div>
<div class="ide-kf-modal__panel">
<button type="button" class="ide-kf-modal__close" data-kf-close aria-label="${zh ? '关闭' : 'Close'}">
<i class="bi bi-x-lg" aria-hidden="true"></i>
</button>
<p class="ide-kf-modal__kicker">${zh ? '企业微信客服' : 'WeCom support'}</p>
<h3 class="ide-kf-modal__title">${zh ? '微信扫码咨询' : 'Scan with WeChat'}</h3>
<p class="ide-kf-modal__hint">${zh ? '打开手机微信,扫一扫下方二维码即可对话顾问' : 'Open WeChat on your phone and scan the code to chat'}</p>
<div class="ide-kf-modal__qr-wrap">
<img class="ide-kf-modal__qr" src="${qrSrc}" width="220" height="220" alt="WeCom QR">
<p class="ide-kf-modal__qr-fallback" hidden>${zh ? '二维码加载失败,请点下方打开客服页后用微信扫码' : 'QR failed to load — open the support page and scan there'}</p>
</div>
<a class="ide-kf-modal__open" href="${url}" target="_blank" rel="noopener noreferrer">${zh ? '在新窗口打开客服页' : 'Open support page'}</a>
</div>
`;
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 = `
<div class="ai-shell-bg" aria-hidden="true">
<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>
<div class="ai-welcome__actions">
<a class="ai-welcome-cta" href="#"></a>
<a class="ai-welcome-human" href="#contact"></a>
</div>
</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-new-chat-tip" hidden role="status" aria-live="polite">
<span class="ai-new-chat-tip__text"></span>
</div>
<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">
<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>
<div class="ai-partner-marquee" aria-label="">
<span class="ai-partner-marquee__tag"></span>
<div class="ai-partner-marquee__viewport">
<div class="ai-partner-marquee__track"></div>
</div>
</div>
<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>
<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>
<aside class="ai-routes-panel" id="ai-routes-panel" aria-label="Routes">
<div class="ai-routes-panel__head">
<div class="ai-routes-panel__intro">
<h3 class="ai-routes-panel__title"></h3>
<p class="ai-routes-panel__sub"></p>
</div>
<button type="button" class="ai-routes-panel__close" aria-label="Close">
<i class="bi bi-x-lg" aria-hidden="true"></i>
</button>
</div>
<div class="ai-routes-panel__tools">
<label class="ai-routes-search">
<i class="bi bi-search" aria-hidden="true"></i>
<input type="search" class="ai-routes-search__input" autocomplete="off" />
</label>
<div class="ai-routes-groups" role="tablist"></div>
</div>
<div class="ai-routes-panel__body"></div>
<div class="ai-routes-panel__foot">
<button type="button" class="ai-routes-sticky"></button>
</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-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 (
'<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);
});
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 = `
<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;
}
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 (
'<button type="button" class="ai-routes-chip' +
active +
'" data-routes-group="' +
escapeHtml(g.code) +
'" role="tab" aria-selected="' +
(routesGroupFilter === g.code ? 'true' : 'false') +
'">' +
escapeHtml(g.name) +
'</button>'
);
})
.join('');
}
function renderRoutesList() {
const body = shell?.querySelector('.ai-routes-panel__body');
if (!body) return;
if (routesLoadState === 'loading') {
body.innerHTML =
'<div class="ai-routes-state">' +
'<div class="ai-routes-skeleton"></div><div class="ai-routes-skeleton"></div><div class="ai-routes-skeleton"></div>' +
'<p class="ai-routes-state__text">' +
escapeHtml(t('routesLoading')) +
'</p></div>';
return;
}
if (routesLoadState === 'error') {
body.innerHTML =
'<div class="ai-routes-state">' +
'<p class="ai-routes-state__text">' +
escapeHtml(t('routesEmpty')) +
'</p>' +
'<button type="button" class="ai-routes-retry" data-routes-retry="1">' +
escapeHtml(t('routesRetry')) +
'</button></div>';
return;
}
const items = filteredRouteItems();
if (!items.length) {
body.innerHTML =
'<div class="ai-routes-state"><p class="ai-routes-state__text">' +
escapeHtml(t('routesEmpty')) +
'</p></div>';
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
? '<p class="ai-route-card__desc' +
(expanded ? ' is-expanded' : '') +
'">' +
escapeHtml(desc) +
'</p>' +
(longDesc
? '<button type="button" class="ai-route-card__more" data-routes-expand="' +
escapeHtml(String(item.id)) +
'">' +
escapeHtml(expanded ? t('routesCollapse') : t('routesExpand')) +
'</button>'
: '')
: '';
const etaBlock = eta
? '<div class="ai-route-card__eta"><strong>' +
escapeHtml(eta) +
'</strong><span>' +
escapeHtml(t('routesDays')) +
'</span></div>'
: '<div class="ai-route-card__eta is-muted"><strong>—</strong><span>' +
escapeHtml(t('routesAskEta')) +
'</span></div>';
const termsHtml = terms
? '<span class="ai-route-card__terms"><b>' +
escapeHtml(terms.code) +
'</b> ' +
escapeHtml(terms.hint) +
'</span>'
: '';
return (
'<article class="ai-route-card">' +
etaBlock +
'<div class="ai-route-card__main">' +
'<span class="ai-route-card__group">' +
escapeHtml(item.productGroupName || item.productGroup) +
'</span>' +
'<h4 class="ai-route-card__name">' +
escapeHtml(item.productName) +
'</h4>' +
termsHtml +
descHtml +
'<div class="ai-route-card__actions">' +
'<button type="button" class="ai-route-card__ask" data-routes-ask="' +
escapeHtml(askText) +
'">' +
escapeHtml(t('routesAsk')) +
'</button>' +
'<a class="ai-route-card__reg" href="' +
escapeHtml(registerUrl()) +
'">' +
escapeHtml(t('routesRegister')) +
'</a>' +
'</div></div></article>'
);
})
.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 (
'<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');
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' ? '<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|退税|专票|免税|免征|合规|报关|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();
}
})();