1<!doctype html> 2<html lang="vi"> 3 4<head> 5 <meta charset="UTF-8" /> 6 <link rel="icon" type="image/png" href="/favicon.png" /> 7 <meta name="generator" content="Hostinger Horizons" /> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 <meta name="theme-color" content="#F6F0E4" /> 10 <meta name="description" content="The Office AI â Khóa há»c ứng dụng AI tạo sinh cho dân vÄn phòng. Thá»±c tế, dá» hiá»u và áp dụng ÄÆ°á»£c ngay và o công viá»c hà ng ngà y." /> 11 <meta property="og:title" content="The Office AI â Ứng dụng AI cho dân vÄn phòng" /> 12 <meta property="og:description" content="Khóa há»c AI thá»±c tế, dá» hiá»u, áp dụng ngay và o công viá»c vÄn phòng hà ng ngà y." /> 13 <meta property="og:image" content="/favicon.png" /> 14 <meta property="og:type" content="website" /> 15 <meta property="og:locale" content="vi_VN" /> 16 <meta name="twitter:card" content="summary_large_image" /> 17 <meta name="twitter:title" content="The Office AI â Ứng dụng AI cho dân vÄn phòng" /> 18 <meta name="twitter:description" content="Khóa há»c AI thá»±c tế, dá» hiá»u, áp dụng ngay và o công viá»c vÄn phòng hà ng ngà y." /> 19 <title>The Office AI â Ứng dụng AI cho dân vÄn phòng</title> 20
20<script type="module" crossorigin src="/assets/index-BIhNcnF8.js"></script>
20 21 <link rel="stylesheet" crossorigin href="/assets/index-CpdmIJsl.css"> 22
22<script type="module"> 23window.onerror = (message, source, lineno, colno, errorObj) => { 24 const errorDetails = errorObj ? JSON.stringify({ 25 name: errorObj.name, 26 message: errorObj.message, 27 stack: errorObj.stack, 28 source, 29 lineno, 30 colno, 31 }) : null; 32 33 window.parent.postMessage({ 34 type: 'horizons-runtime-error', 35 message, 36 error: errorDetails 37 }, '*'); 38}; 39</script>
39 40
40<script type="module"> 41const observer = new MutationObserver((mutations) => { 42 for (const mutation of mutations) { 43 for (const addedNode of mutation.addedNodes) { 44 if ( 45 addedNode.nodeType === Node.ELEMENT_NODE && 46 ( 47 addedNode.tagName?.toLowerCase() === 'vite-error-overlay' || 48 addedNode.classList?.contains('backdrop') 49 ) 50 ) { 51 handleViteOverlay(addedNode); 52 } 53 } 54 } 55}); 56 57observer.observe(document.documentElement, { 58 childList: true, 59 subtree: true 60}); 61 62function handleViteOverlay(node) { 63 if (!node.shadowRoot) { 64 return; 65 } 66 67 const backdrop = node.shadowRoot.querySelector('.backdrop'); 68 69 if (backdrop) { 70 const overlayHtml = backdrop.outerHTML; 71 const parser = new DOMParser(); 72 const doc = parser.parseFromString(overlayHtml, 'text/html'); 73 const messageBodyElement = doc.querySelector('.message-body'); 74 const fileElement = doc.querySelector('.file'); 75 const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : ''; 76 const fileText = fileElement ? fileElement.textContent.trim() : ''; 77 const error = messageText + (fileText ? ' File:' + fileText : ''); 78 79 window.parent.postMessage({ 80 type: 'horizons-vite-error', 81 error, 82 }, '*'); 83 } 84} 85</script>
85 86
86<script type="module"> 87const originalConsoleError = console.error; 88const MATCH_LINE_COL_REGEX = /:(\d+):(\d+)\)?\s*$/; // regex to match the :lineNum:colNum 89const MATCH_AT_REGEX = /^\s*at\s+(?:async\s+)?(?:.*?\s+)?\(?/; // regex to remove the 'at' keyword and any 'async' or function name 90const MATCH_PATH_REGEX = /^\//; // regex to remove the leading slash 91 92function parseStackFrameLine(line) { 93 const lineColMatch = line.match(MATCH_LINE_COL_REGEX); 94 if (!lineColMatch) return null; 95 const [, lineNum, colNum] = lineColMatch; 96 const suffix = `:${lineNum}:${colNum}`; 97 const idx = line.lastIndexOf(suffix); 98 if (idx === -1) return null; 99 const before = line.substring(0, idx); 100 const path = before.replace(MATCH_AT_REGEX, '').trim(); 101 102 if (!path) return null; 103 104 try { 105 const pathname = new URL(path).pathname; 106 const filePath = pathname.replace(MATCH_PATH_REGEX, '') || pathname; 107 return `${filePath}:${lineNum}:${colNum}`; 108 } catch (e) { 109 const filePath = path.replace(MATCH_PATH_REGEX, '') || path; 110 return `${filePath}:${lineNum}:${colNum}`; 111 } 112} 113 114function getFilePathFromStack(stack, skipFrames = 0) { 115 if (!stack || typeof stack !== 'string') return null; 116 const lines = stack.split('\n').slice(1); 117 118 const frames = lines.map(line => parseStackFrameLine(line.replace(/\r$/, ''))).filter(Boolean); 119 120 return frames[skipFrames] ?? null; 121} 122 123function formatConsoleMessage(args, skipStackFrames = 1) { 124 let messageString = ''; 125 let filePath = null; 126 127 for (let i = 0; i < args.length; i++) { 128 const arg = args[i]; 129 if (arg instanceof Error) { 130 filePath = getFilePathFromStack(arg.stack, 0); 131 messageString = `${arg.name}: ${arg.message}`; 132 if (filePath) { 133 messageString = `${messageString} at ${filePath}`; 134 } 135 break; 136 } 137 } 138 139 if (!messageString) { 140 messageString = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); 141 const stack = new Error().stack; 142 filePath = getFilePathFromStack(stack, skipStackFrames); 143 if (filePath) { 144 messageString = `${messageString} at ${filePath}`; 145 } 146 } 147 148 return messageString; 149} 150 151console.error = function(...args) { 152 originalConsoleError.apply(console, args); 153 154 window.parent.postMessage({ 155 type: 'horizons-console-error', 156 error: formatConsoleMessage(args, 1) 157 }, '*'); 158}; 159 160const originalConsoleWarn = console.warn; 161 162console.warn = function(...args) { 163 originalConsoleWarn.apply(console, args); 164 165 window.parent.postMessage({ 166 type: 'horizons-console-warn', 167 warning: formatConsoleMessage(args, 1) 168 }, '*'); 169}; 170</script>
170 171
171<script type="module"> 172// Prevents authentication failures (PB returns 400) from crashing the app â use console.info, not console.error/PM2. 173// [urlPattern, bodyPattern] tuples; align with sandboxErrorUtils ERROR_BLACKLIST. 174const BENIGN_FETCH_ERRORS = [ 175 [/hcgi\/platform\/api\/collections\/.*auth-with-password.*/i, /Failed to authenticate/i], 176 [/hcgi\/api\//i, /Insufficient credits/i], 177]; 178 179function isBenignFetchError(url, body) { 180 return BENIGN_FETCH_ERRORS.some(([urlPattern, bodyPattern]) => 181 urlPattern.test(url) && (!bodyPattern || bodyPattern.test(body))); 182} 183 184const PLATFORM_URL_PATTERN = /hcgi\/platform\//i; 185const VALIDATION_CODE_TEXT_PATTERN = /validation_/; 186 187function hasValidationCode(value) { 188 if (value == null) { 189 return false; 190 } 191 if (typeof value === 'string') { 192 return value.startsWith('validation_'); 193 } 194 if (Array.isArray(value)) { 195 return value.some(hasValidationCode); 196 } 197 if (typeof value === 'object') { 198 return Object.values(value).some(hasValidationCode); 199 } 200 return false; 201} 202 203function isValidationFetchWarning(url, body) { 204 if (!PLATFORM_URL_PATTERN.test(url)) { 205 return false; 206 } 207 try { 208 return hasValidationCode(JSON.parse(body)); 209 } catch { 210 return VALIDATION_CODE_TEXT_PATTERN.test(body); 211 } 212} 213 214const originalFetch = window.fetch; 215 216window.fetch = function(...args) { 217 const url = args[0] instanceof Request ? args[0].url : args[0]; 218 219 // Skip WebSocket URLs 220 if (url.startsWith('ws:') || url.startsWith('wss:')) { 221 return originalFetch.apply(this, args); 222 } 223 224 return originalFetch.apply(this, args) 225 .then(async response => { 226 const contentType = response.headers.get('Content-Type') || ''; 227 228 // Exclude HTML document responses 229 const isDocumentResponse = 230 contentType.includes('text/html') || 231 contentType.includes('application/xhtml+xml'); 232 233 if (!response.ok && !isDocumentResponse) { 234 const responseClone = response.clone(); 235 const errorFromRes = await responseClone.text(); 236 const requestUrl = response.url; 237 const errorMessage = `Fetch error from ${requestUrl}: ${errorFromRes}`; 238 239 if (isBenignFetchError(requestUrl, errorFromRes)) { 240 console.info(errorMessage); 241 } else if (isValidationFetchWarning(requestUrl, errorFromRes)) { 242 console.warn(errorMessage); 243 } else { 244 console.error(errorMessage); 245 } 246 } 247 248 return response; 249 }) 250 .catch(error => { 251 if (!url.match(/.html?$/i)) { 252 // Cancelled in-flight fetch (e.g. preview reload) â not a server failure. 253 if (error?.name === 'AbortError') { 254 console.info(error); 255 } else { 256 console.error(error); 257 } 258 } 259 260 throw error; 261 }); 262}; 263</script>
263 264
264<script type="module"> 265if (window.navigation && window.self !== window.top) { 266 window.navigation.addEventListener('navigate', (event) => { 267 const url = event.destination.url; 268 269 try { 270 const destinationUrl = new URL(url); 271 const destinationOrigin = destinationUrl.origin; 272 const currentOrigin = window.location.origin; 273 274 if (destinationOrigin === currentOrigin) { 275 return; 276 } 277 } catch (error) { 278 return; 279 } 280 281 window.parent.postMessage({ 282 type: 'horizons-navigation-error', 283 url, 284 }, '*'); 285 }); 286} 287</script>
287 288</head> 289 290<body> 291 <div id="root"></div> 292</body> 293 294</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.