1/* Generate UUID function */ 2function generateUUID() { 3 try { 4 return uuid.v4(); 5 } catch (e) { 6 // Génère une chaîne aléatoire pour chaque composant de l'UUID 7 function generateComponent() { 8 return Math.floor((1 + Math.random()) * 0x10000) 9 .toString(16) 10 .substring(1); 11 } 12 13 // Concatène les composants pour former l'UUID complet 14 return ( 15 generateComponent() + 16 generateComponent() + 17 "-" + 18 generateComponent() + 19 "-" + 20 generateComponent() + 21 "-" + 22 generateComponent() + 23 "-" + 24 generateComponent() + 25 generateComponent() + 26 generateComponent() 27 ); 28 } 29} 30 31async function sendReview(rating, content = "", threadID) { 32 let ajaxUrl = props.ajaxUrl; 33 34 jQuery.post(ajaxUrl, { 35 action: "faiq_plugin_send_review", 36 threadID: threadID, 37 rating: rating, 38 content: content, 39 page: window.location.pathname, 40 }); 41} 42 43/* Init function */ 44function init(container) { 45 // Generate unique threadID for the conversation 46 let threadID = generateUUID(); 47 48 const button = container.querySelector("#faiq-plugin-submit-button"); 49 const resetButton = container.querySelector("#faiq-plugin-reset"); 50 const input = container.querySelector("#faiq-plugin-input"); 51 const historyList = container.querySelector("#faiq-plugin-history-list"); 52 53 // Datas is datas from editor, passed by a trick with script 54 const datas = window[`block_data_${container.id}`]; 55 56 let outputFormat = datas.outputFormat; 57 58 let payload = { 59 outputFormat, 60 metadatas: {}, 61 }; 62 // Set listeners 63 let messageInColor = datas.messageInColor; 64 let messageOutColor = datas.messageOutColor; 65 let firstBotMessage = datas.firstBotMessage; 66 67 let colors = { 68 messageInColor, 69 messageOutColor, 70 }; 71 72 // Create default message for bot 73 createMessage( 74 historyList, 75 firstBotMessage, 76 [], 77 "in", 78 false, 79 colors, 80 null, 81 false, 82 ); 83 84 // rating 85 86 /* Delay function */ 87 let displayRatingModal = 88 (datas?.displayRatingModal === "yes" ? true : false) ?? true; 89 let started = false; 90 let hasSendRating = false; 91 let timer; 92 93 let delay = datas?.ratingDelay ?? 70; // Délai initial en secondes 94 let ratingTitle = 95 datas?.ratingTitle ?? "Comment avez-vous trouvé la conversation ?"; 96 97 let ratingContainer = document.createElement("div"); 98 ratingContainer.classList.add("faiq-rating-container-dialog"); 99 ratingContainer.innerHTML = ` 100 <div class="faiq-rating-container"> 101 102 <i class="fa-solid fa-xmark faiq-rating-container-close" id="faiq-rating-container-close"></i> 103 <div class="faiq-rating-container-text"> 104 ${ratingTitle} 105 </div> 106 107 <div class="faiq-rating-container-rating"></div> 108 109 <div class="faiq-rating-container-comment"> 110 <textarea id="faiq-rating-container-comment" placeholder="Ajouter un commentaire..."></textarea> 111 </div> 112 113 <div class="faiq-rating-container-button"> 114 <button id="faiq-rating-container-button" style="background-color:${datas.messageOutColor};">envoyer</button> 115 </div> 116 </div> 117 `; 118 119 const openRatingDialog = () => { 120 ratingContainer.classList.add("faiq-rating-open-dialog"); 121 }; 122 123 const closeRatingDialog = () => { 124 closed = true; 125 ratingContainer.classList.remove("faiq-rating-open-dialog"); 126 }; 127 128 jQuery(container).append(ratingContainer); 129 130 function executeAfterDelay(callback, delayInSeconds) { 131 if (!displayRatingModal) { 132 return; 133 } 134 135 clearTimeout(timer); // Réinitialise le compteur si appelé de nouveau 136 if (!hasSendRating) { 137 timer = setTimeout(callback, delayInSeconds * 1000); // Convertir secondes en millisecondes 138 } 139 } 140 141 function resetTimer() { 142 if (!displayRatingModal) { 143 return; 144 } 145 146 clearTimeout(timer); // Réinitialise le compteur 147 if (!hasSendRating) { 148 timer = setTimeout(callbackFunction, delay * 1000); // Redémarre le timer 149 } 150 } 151 152 function startTimer(force = false) { 153 if (!displayRatingModal) { 154 return; 155 } 156 // Start delay 157 if (!started || force) { 158 executeAfterDelay(callbackFunction, delay); 159 started = true; 160 } 161 } 162 163 // Callback function 164 function callbackFunction() { 165 if (!hasSendRating) { 166 openRatingDialog(); 167 } 168 } 169 170 /** 171 * Rating buttons 172 */ 173 /* Add star rating */ 174 let rating = 4; 175 jQuery(".faiq-rating-container-rating").starRating({ 176 totalStars: 5, 177 starShape: "rounded", 178 starSize: 30, 179 initialRating: rating, 180 strokeWidth: 0, 181 emptyColor: "lightgray", 182 hoverColor: "gold", 183 activeColor: "gold", 184 useGradient: false, 185 disableAfterRate: false, 186 callback: function (currentRating, $el) { 187 rating = currentRating; 188 }, 189 }); 190 191 /* Send button */
192 jQuery("#faiq-rating-container-button").click(() => { 193 let comment = jQuery("#faiq-rating-container-comment").val(); 194 195 sendReview(rating, comment, threadID); 196 hasSendRating = true; 197 198 closeRatingDialog(); 199 }); 200 201 /* Close button */ 202 jQuery("#faiq-rating-container-close").click(() => { 203 closeRatingDialog(); 204 startTimer(true); 205 }); 206 207 /** 208 * Chat button 209 */ 210 /* On click send button */ 211 jQuery(button).on("click", async function (e) { 212 let question = jQuery(input).val(); 213 await submitQuestion( 214 container, 215 input, 216 button, 217 historyList, 218 question, 219 threadID, 220 payload, 221 colors, 222 resetTimer, 223 startTimer, 224 ); 225 }); 226 227 /* On click reset button */ 228 jQuery(resetButton).on("click", async function (e) { 229 jQuery(historyList).empty(); 230 threadID = generateUUID(); 231 // Create default message for bot 232 createMessage( 233 historyList, 234 firstBotMessage, 235 [], 236 "in", 237 false, 238 colors, 239 null, 240 false, 241 ); 242 243 clearTimeout(timer); 244 hasSendRating = false; 245 started = false; 246 }); 247 248 /* On key down input */ 249 jQuery(input).keypress(async function (e) { 250 var keycode = e.keyCode ? e.keyCode : e.which; 251 if (keycode == "13") { 252 let question = jQuery(input).val(); 253 await submitQuestion( 254 container, 255 input, 256 button, 257 historyList, 258 question, 259 threadID, 260 payload, 261 colors, 262 resetTimer, 263 startTimer, 264 ); 265 } 266 }); 267} 268 269function createMessageDot() { 270 let dot = document.createElement("div"); 271 dot.classList.add("dot"); 272 return dot; 273} 274 275function createLoadingMessage(className, colors) { 276 let messageContainer = document.createElement("div"); 277 messageContainer.id = "faiq-plugin-loading-message"; 278 messageContainer.classList.add( 279 `faiq-plugin-message`, 280 `faiq-plugin-chat-bubble`, 281 `${className}`, 282 ); 283 messageContainer.style.backgroundColor = colors.messageInColor; 284 285 let typingSection = document.createElement("div"); 286 typingSection.classList.add("faiq-plugin-typing"); 287 288 typingSection.appendChild(createMessageDot()); 289 typingSection.appendChild(createMessageDot()); 290 typingSection.appendChild(createMessageDot()); 291 292 messageContainer.appendChild(typingSection); 293 return messageContainer; 294} 295 296function createSourceDisplay(source, index) { 297 let container = document.createElement("div"); 298 container.classList.add( 299 "faiq-plugin-message-source", 300 `faiq-plugin-message-source-${index}`, 301 ); 302 303 let titleText = source.title; 304 let descriptionText = source.description; 305 let urlText = source.url; 306 let imageUrl = source.image; 307 308 let linkContainer = null; 309 if (urlText !== null && urlText !== "") { 310 linkContainer = document.createElement("a"); 311 } else { 312 linkContainer = document.createElement("div"); 313 } 314 315 linkContainer.classList.add("faiq-plugin-message-source-link-container"); 316 linkContainer.href = urlText; 317 linkContainer.setAttribute("target", "_blank"); 318 319 let textContainer = document.createElement("div"); 320 textContainer.classList.add("faiq-plugin-message-source-text-container"); 321 322 let titleElement = document.createElement("span"); 323 titleElement.classList.add("faiq-plugin-message-source-title-element"); 324 titleElement.innerText = titleText; 325 let descriptionElement = document.createElement("span"); 326 descriptionElement.classList.add( 327 "faiq-plugin-message-source-description-element", 328 ); 329 descriptionElement.innerText = descriptionText; 330 331 let imageElement = document.createElement("img"); 332 imageElement.classList.add("faiq-plugin-message-source-image-element"); 333 imageElement.src = imageUrl; 334 335 textContainer.appendChild(titleElement); 336 textContainer.appendChild(descriptionElement); 337 338 linkContainer.appendChild(imageElement); 339 linkContainer.appendChild(textContainer); 340 341 container.appendChild(linkContainer); 342 343 return container; 344} 345 346function refreshSources(container, selectedSourceIndex) { 347 let allSources = container.querySelectorAll(".faiq-plugin-message-source"); 348 for (let aSource of allSources) { 349 aSource.classList.add("display-none"); 350 } 351 352 let selectedSource = container.querySelector( 353 ".faiq-plugin-message-source-" + selectedSourceIndex, 354 ); 355 selectedSource.classList.remove("display-none"); 356} 357 358function createSwitchSourcePannel( 359 sources,
360 sourcesCarousel, 361 selectedSourceIndex, 362) { 363 let pannelContainer = document.createElement("div"); 364 pannelContainer.classList.add("faiq-plugin-message-source-pannel-container"); 365 366 let pannelText = document.createElement("span"); 367 pannelText.innerText = `${selectedSourceIndex + 1}/${sources.length}`; 368 369 let previous = document.createElement("i"); 370 previous.classList.add("fa-solid", "fa-chevron-left"); 371 372 let next = document.createElement("i"); 373 next.classList.add("fa-solid", "fa-chevron-right"); 374 375 pannelContainer.appendChild(previous); 376 pannelContainer.appendChild(pannelText); 377 pannelContainer.appendChild(next); 378 379 previous.addEventListener("click", () => { 380 if (selectedSourceIndex - 1 < 0) { 381 selectedSourceIndex = sources.length - 1; 382 } else { 383 selectedSourceIndex = selectedSourceIndex - 1; 384 } 385 386 refreshSources(sourcesCarousel, selectedSourceIndex); 387 pannelText.innerText = `${selectedSourceIndex + 1}/${sources.length}`; 388 }); 389 390 next.addEventListener("click", () => { 391 if (selectedSourceIndex + 1 >= sources.length) { 392 selectedSourceIndex = 0; 393 } else { 394 selectedSourceIndex = selectedSourceIndex + 1; 395 } 396 397 refreshSources(sourcesCarousel, selectedSourceIndex); 398 pannelText.innerText = `${selectedSourceIndex + 1}/${sources.length}`; 399 }); 400 401 return pannelContainer; 402} 403 404async function sendFeedback(feedback, questionID) { 405 let ajaxUrl = props.ajaxUrl; 406 407 jQuery.post(ajaxUrl, { 408 action: "faiq_plugin_send_feedback", 409 questionID: questionID, 410 feedback: feedback, 411 page: window.location.pathname, 412 }); 413} 414 415function createDefaultMessage( 416 className, 417 direction, 418 message, 419 sources, 420 colors, 421 questionID, 422 feedbacks, 423) { 424 let messageID = generateUUID(); 425 426 // Message container 427 let messageContainer = document.createElement("div"); 428 messageContainer.id = messageID; 429 messageContainer.classList.add(`faiq-plugin-message`, `${className}`); 430 431 if (direction === "in") { 432 messageContainer.style.backgroundColor = colors.messageInColor; 433 } else if (direction === "out") { 434 messageContainer.style.backgroundColor = colors.messageOutColor; 435 } 436 437 // Message text 438 let messageText = document.createElement("span"); 439 messageText.style.fontSize = 12; 440 441 if (direction === "error") { 442 let errorMessageIcon = createMessageErrorIcon(); 443 messageText.appendChild(errorMessageIcon); 444 } 445 446 let spanText = document.createElement("span"); 447 spanText.innerHTML = message; 448 messageText.appendChild(spanText); 449 450 messageContainer.appendChild(messageText); 451 452 let moreActionsContainer = document.createElement("div"); 453 moreActionsContainer.classList.add("faiq-plugin-more-actions-container"); 454 455 let actionsPannel = document.createElement("div"); 456 actionsPannel.classList.add("faiq-plugin-more-actions-pannel"); 457 458 moreActionsContainer.appendChild(actionsPannel); 459 460 if (feedbacks && direction === "in") { 461 let downVote = document.createElement("i"); 462 downVote.classList.add("fa-regular", "fa-thumbs-down"); 463 464 let upVote = document.createElement("i"); 465 upVote.classList.add("fa-regular", "fa-thumbs-up"); 466 467 actionsPannel.appendChild(downVote); 468 actionsPannel.appendChild(upVote); 469 470 function addSolidClassDownVote() { 471 downVote.classList.remove("fa-regular"); 472 downVote.classList.add("fa-solid"); 473 upVote.removeEventListener("click", addSolidClassUpVote); 474 downVote.removeEventListener("click", addSolidClassDownVote); 475 sendFeedback("negative", questionID); 476 } 477 478 function addSolidClassUpVote() { 479 upVote.classList.remove("fa-regular"); 480 upVote.classList.add("fa-solid"); 481 downVote.removeEventListener("click", addSolidClassDownVote); 482 upVote.removeEventListener("click", addSolidClassUpVote); 483 sendFeedback("positive", questionID); 484 } 485 486 downVote.addEventListener("click", addSolidClassDownVote); 487 upVote.addEventListener("click", addSolidClassUpVote); 488 } 489 490 // Message sources 491 if (sources.length > 0) { 492 // Sources container 493 let sourcesContainer = document.createElement("div"); 494 sourcesContainer.classList.add("faiq-plugin-message-sources-container"); 495 // Source title clickable 496 let sourcesInfoTextContainer = document.createElement("div"); 497 sourcesInfoTextContainer.classList.add( 498 "faiq-plugin-message-sources-info-text-container", 499 ); 500 501 let icon = document.createElement("i"); 502 icon.classList.add("fa-solid", "fa-circle-info"); 503 504 let span = document.createElement("span"); 505 span.classList.add("faiq-plugin-message-sources-info-text"); 506 span.innerText = "Voir les sources"; 507 508 // Source content
509 let sourcesCarousel = document.createElement("div"); 510 sourcesCarousel.classList.add( 511 "faiq-plugin-message-sources-carousel", 512 "display-none", 513 ); 514 515 let selectedSourceIndex = 0; 516 517 // pannel controls 518 let pannelContainer = createSwitchSourcePannel( 519 sources, 520 sourcesCarousel, 521 selectedSourceIndex, 522 ); 523 sourcesCarousel.appendChild(pannelContainer); 524 525 // Create each source 526 for (let index in sources) { 527 let actualSource = sources[index]; 528 let sourceContainer = createSourceDisplay( 529 actualSource, 530 index, 531 selectedSourceIndex, 532 ); 533 sourcesCarousel.appendChild(sourceContainer); 534 } 535 536 refreshSources(sourcesCarousel, selectedSourceIndex); 537 538 sourcesInfoTextContainer.appendChild(icon); 539 sourcesInfoTextContainer.appendChild(span); 540 541 actionsPannel.appendChild(sourcesInfoTextContainer); 542 sourcesContainer.appendChild(sourcesCarousel); 543 moreActionsContainer.appendChild(sourcesContainer); 544 545 sourcesInfoTextContainer.addEventListener("click", () => { 546 sourcesCarousel.classList.toggle("display-none"); 547 }); 548 } 549 550 messageContainer.appendChild(moreActionsContainer); 551 552 return messageContainer; 553} 554 555function createMessageErrorIcon() { 556 let errorMessageIcon = document.createElement("span"); 557 errorMessageIcon.classList.add("faiq-plugin-before-element-message"); 558 559 let errorMessageFontawesome = document.createElement("i"); 560 errorMessageFontawesome.classList.add("fa-solid", "fa-circle-exclamation"); 561 562 errorMessageIcon.appendChild(errorMessageFontawesome); 563 564 return errorMessageIcon; 565} 566 567/* Add message to history list */ 568function createMessage( 569 historyList, 570 message, 571 sources, 572 direction, 573 loading = false, 574 colors, 575 questionID, 576 feedbacks = true, 577) { 578 let loadingMessage = historyList.querySelector( 579 "#faiq-plugin-loading-message", 580 ); 581 582 let className = `faiq-plugin-message-${direction}`; 583 584 let messageElement; 585 586 // Remove loading message, it will be replaced by the real one 587 jQuery(loadingMessage).remove(); 588 589 if (loading) { 590 messageElement = createLoadingMessage(className, colors); 591 } else { 592 messageElement = createDefaultMessage( 593 className, 594 direction, 595 message, 596 sources, 597 colors, 598 questionID, 599 feedbacks, 600 ); 601 } 602 603 // Add message to history list 604 jQuery(historyList).append(messageElement); 605 // Scroll to bottom 606 jQuery(historyList).scrollTop(99999); 607} 608 609async function requestToBackEnd({ id }, question, threadID, payload, cb) { 610 let ajaxUrl = props.ajaxUrl; 611 const cookieTokenName = `faiq-plugin-token-${id}`; 612 613 let renewToken = false; 614 let cookieToken = JSON.parse(Cookies.get(cookieTokenName) ?? "{}"); 615 let currentTime = new Date().getTime(); 616 617 if (!cookieToken.expirationTs || currentTime > cookieToken.expirationTs) { 618 renewToken = true; 619 } 620 621 let response = await jQuery.ajax({ 622 type: "post", 623 url: ajaxUrl, 624 data: { 625 action: "faiq_plugin_get_token", 626 page: window.location.pathname, 627 renewToken: renewToken || true, 628 token: cookieToken?.token || "none", 629 expirationTs: cookieToken?.expirationTs || "none", 630 }, 631 }); 632 633 if (!response.success) { 634 return; 635 } 636 637 let { 638 result: { token }, 639 url, 640 } = response.data; 641 642 if (!token?.token) { 643 return; 644 } 645 646 if (renewToken) { 647 Cookies.set(cookieTokenName, JSON.stringify(token), { 648 expires: 1, 649 }); 650 } 651 652 return await new Promise((resolve, reject) => { 653 try { 654 // Global vars for reply 655 let traceResponse = ""; 656 let generatedText = ""; 657 let sources = ""; 658 let error = ""; 659 /* Add new chunck piece to the reply */ 660 function completeResponsePiece(piece, replace = false) { 661 if (replace) { 662 generatedText = piece; 663 } else { 664 generatedText += piece; 665 } 666 cb(generatedText); 667 } 668 /* Add new chunk piece to the sources */ 669 function completeSourcesPiece(piece) { 670 sources += piece; 671 } 672 /* Handle chunk received */ 673 function handleChunckReceived(chunk) { 674 // Before, we force try to check if we have an error 675 try { 676 let parsedMessage = JSON.parse(chunk); 677 if (parsedMessage.error) { 678 completeResponsePiece("", true); 679 error = JSON.stringify({ 680 error: { 681 message: `Une erreur est survenue. ${ 682 parsedMessage.code ? `(Code ${parsedMessage.code})` : "" 683 }`, 684 code: parsedMessage.code, 685 }, 686 }); 687 mode = "error"; 688 return; 689 } 690 } catch (e) {} 691 692 traceResponse += chunk; 693 694 const regexError = /({"type":"error")/; 695 const regexEnd = /({"type":"end")/; 696 697 switch (true) { 698 // If we got error 699 case regexError.test(traceResponse):
700 completeResponsePiece("", true); 701 error = traceResponse; 702 mode = "error"; 703 break; 704 // If this is the end 705 case regexEnd.test(traceResponse) && mode !== "sources": 706 mode = "sources"; 707 let match = traceResponse.match(regexEnd); 708 let before = traceResponse.substring(0, match.index); 709 let after = traceResponse.substring(match.index); 710 completeResponsePiece(before, true); 711 completeSourcesPiece(after); 712 break; 713 // Else, complete reply / sources 714 default: 715 switch (mode) { 716 case "sources": 717 completeSourcesPiece(chunk); 718 break; 719 case "generation": 720 completeResponsePiece(chunk); 721 break; 722 default: 723 break; 724 } 725 break; 726 } 727 } 728 /* On stream end */ 729 function handleStreamEnd() { 730 // Parse sources to JSON 731 sources === "" ? (sources = {}) : (sources = JSON.parse(sources)); 732 sources = sources.payload ?? {}; 733 // End of the request 734 switch (mode) { 735 case "error": 736 let errorObject = JSON.parse(error); 737 resolve({ 738 error: errorObject?.error?.message ?? errorObject?.payload, 739 }); 740 break; 741 default: 742 resolve({ 743 reply: generatedText, 744 sources: sources.sources, 745 threadID: sources.threadID, 746 questionID: sources.questionID, 747 }); 748 break; 749 } 750 } 751 752 let mode = "generation"; // generation | sources | error 753 754 /* Request to the back */ 755 fetch(`${url}/apiExt/v1/getReply`, { 756 method: "POST", 757 headers: { 758 "Content-Type": "application/json", 759 Authorization: token?.token, 760 stream: true, 761 }, 762 body: JSON.stringify({ 763 question: question, 764 threadID: threadID, 765 metadatas: payload.metadatas, 766 outputFormat: payload.outputFormat, 767 browser: { 768 page: window.location.pathname, 769 }, 770 }), 771 }) 772 .then((response) => { 773 const reader = response.body.getReader(); 774 775 const processStream = async () => { 776 while (true) { 777 const { done, value } = await reader.read(); 778 if (done) { 779 handleStreamEnd(); 780 break; 781 } 782 const chunk = new TextDecoder().decode(value); 783 handleChunckReceived(chunk); 784 } 785 }; 786 787 processStream(); 788 }) 789 .catch((error) => { 790 resolve({ 791 error: "Une erreur est survenue", 792 }); 793 }); 794 } catch (e) { 795 resolve({ 796 error: "Une erreur est survenue", 797 }); 798 } 799 }); 800} 801 802function updateMessage(container, id, text) { 803 let message = container.querySelector(id); 804 message.innerHTML = text; 805} 806 807const scrollToBottom = (node) => { 808 node.scrollTop = node.scrollHeight; 809}; 810 811async function submitQuestion( 812 container, 813 input, 814 button, 815 historyList, 816 question, 817 threadID, 818 payload, 819 colors, 820 resetTimer, 821 cb, 822) { 823 if (!question.trim()) { 824 return; 825 } 826 827 resetTimer(); 828 cb(); 829 830 // Clear input 831 jQuery(input).val(""); 832 // Disable button & textarea 833 jQuery(button).attr("disabled", true); 834 jQuery(input).attr("disabled", true); 835 836 createMessage(historyList, question, [], "out", false, colors, null); 837 // Loading dots 838 createMessage(historyList, null, [], "in", true, colors, null); 839 840 // Request 841 let response = await requestToBackEnd( 842 container, 843 question, 844 threadID, 845 payload, 846 (text) => { 847 updateMessage(container, "#faiq-plugin-loading-message", text); 848 849 jQuery(historyList).scrollTop(jQuery(historyList).prop("scrollHeight")); 850 851 resetTimer(); 852 }, 853 ); 854 855 // Reable button & textarea 856 jQuery(button).attr("disabled", false); 857 jQuery(input).attr("disabled", false); 858 jQuery(input).focus(); 859 860 try { 861 if (response.reply) { 862 createMessage( 863 historyList, 864 response.reply, 865 response.sources || [], 866 "in", 867 false, 868 colors, 869 response.questionID, 870 ); 871 } 872 if (response.error) { 873 createMessage( 874 historyList, 875 response.error, 876 [], 877 "error", 878 false, 879 colors, 880 null, 881 ); 882 } 883 } catch (err) { 884 createMessage( 885 historyList, 886 DATAS_JSON.errors.default, 887 [], 888 "error", 889 false, 890 colors, 891 null, 892 ); 893 } 894} 895 896/* Init all containers */ 897jQuery(".faiq-plugin-container").ready(function () { 898 let containers = jQuery(".faiq-plugin-container"); 899
900 for (let container of containers) { 901 init(container); 902 } 903});
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.