/** * 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'}

WeCom QR
${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 + '
    ' + '' + '' + escapeHtml(t('routesRegister')) + '' + '
    ' ); }) .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(); } })();