PageSourceSearch

https://faibrik.com/wp-content/plugins/faiq-plugin-1.3.1/view.js?ver=1.0.0

js faibrik.com collected 2026-10-01 16:24:06 UTC 21,752 bytes, 903 lines download raw bytes

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.