No notes defined.
<div class="sds-chatbot__container sds-stackXl">
<!-- Chatbot -->
<div class="sds-chatbot -hasValue" data-js-chatbot>
<label class="sr-only" for="chatbot-question">Posez votre question</label>
<div class="sds-chatbot__bar">
<input id="chatbot-question" class="sds-chatbot__input form-control" data-chatbot-input type="text" name="question" value="Est-ce que ma fille de 12 ans peut ouvrir un compte à la Spuerkeess ?" placeholder="Posez-moi votre question" autocomplete="off" />
<button type="button" class="sds-btn -iconBtn -btnSecondary -ghost sds-chatbot__clear" data-chatbot-clear="true">
<span aria-hidden="true" class="sds-icon sds-icon-cross"></span>
<span class="sr-only">Effacer la question</span>
</button>
<button type="button" class="sds-btn -btnPrimary sds-chatbot__submit" data-chatbot-submit="true" disabled>
<span class="sds-btn__text">
Envoyer
</span>
</button>
<div class="sds-circularProgress -circularProgressTokenUsage sds-chatbot__tokenUsage" role="meter" aria-label="Utilisation des crédits IA" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100" aria-valuetext="80 % des crédits IA utilisés">
<svg class="sds-circularProgress__svg" viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<circle class="sds-circularProgress__bgShape" cx="10" cy="10" r="9" fill="none" />
<circle class="sds-circularProgress__shape" cx="10" cy="10" r="9" fill="none" stroke-dasharray="45.2389336 56.548667" transform="rotate(-90 10 10)" />
</svg>
</div>
</div>
</div>
<!-- Réponse -->
<div class="sds-chatbot__response sds-stackXl">
<h3 class="h3">Réponse</h3>
<section aria-labelledby="chatbot-response-heading" aria-live="polite">
<h4 class="sr-only" id="chatbot-response-heading">Réponse de l’assistant</h4>
<div class="sds-box -boxInsetLg sds-chatbot__responseBox">
<div class="sds-wysiwyg">
<p>Désolé, je ne trouve pas d’information pertinente pour le moment. Essayer avec une autre question.</p>
</div>
</div>
</section>
</div>
</div>
<div class="{{ namespace }}chatbot__container {{ namespace }}stackXl">
<!-- Chatbot -->
<div class="{{ namespace }}chatbot{% for mod in modifiers %} {{ mod }}{% endfor %}{% for componentClass in classes %} {{ componentClass }}{% endfor %}"
data-js-chatbot
{% for attrKey, attr in attrs %} {{ attrKey }}="{{ attr }}"{% endfor %}
{% if isLoading %} aria-busy="true"{% endif %}>
<label class="sr-only" for="{{ id }}">{{ label }}</label>
<div class="{{ namespace }}chatbot__bar">
<input
id="{{ id }}"
class="{{ namespace }}chatbot__input form-control"
data-chatbot-input
type="text"
name="{{ inputName }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
autocomplete="off"
{% if isLoading %} readonly{% endif %}
{% if suggestions.length and not value and not isLoading %} aria-describedby="{{ suggestionsHintId }}"{% endif %}
/>
{% if showClear %}
{% render "@icon-btn-secondary--ghost", {
classes: [namespace + "chatbot__clear"],
icon: "icon-cross",
action: clearActionLabel,
attrs: {
"data-chatbot-clear": "true"
}
}, true %}
{% endif %}
{% render "@btn-primary", {
classes: [namespace + "chatbot__submit"],
text: submitLabel,
disabled: submitDisabled,
attrs: {
"data-chatbot-submit": "true"
}
}, true %}
{% if tokenUsage %}
{% render "@circular-progress--token-usage", {
classes: [namespace + "chatbot__tokenUsage"],
attrs: tokenUsage.attrs,
progress: tokenUsage.progress,
tokenUsageDanger: tokenUsage.danger,
a11yLabel: tokenUsage.a11yLabel,
a11yValueText: tokenUsage.a11yValueText
}, true %}
{% endif %}
</div>
{% if suggestions.length and not value and not isLoading %}
<p class="sr-only" id="{{ suggestionsHintId }}">{{ suggestionsHint }}</p>
<ul class="{{ namespace }}chatbot__suggestions list-unstyled" data-chatbot-suggestions>
{% for suggestion in suggestions %}
<li class="{{ namespace }}chatbot__suggestionItem">
<button class="{{ namespace }}chatbot__suggestion" type="button" data-chatbot-suggestion value="{{ suggestion }}">
<span class="{{ namespace }}icon {{ namespace }}icon-search" aria-hidden="true"></span>
<span>{{ suggestion }}</span>
</button>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if isLoading or answerText or noResultText %}
<!-- Réponse -->
<div class="{{ namespace }}chatbot__response {{ namespace }}stackXl">
{% if showResponseHeading %}
<h3 class="h3">Réponse</h3>
{% endif %}
{% if isLoading %}
<p class="sr-only" role="status">{{ loadingLabel }}</p>
<section aria-labelledby="{{ responseHeadingId }}">
<h4 class="sr-only" id="{{ responseHeadingId }}">{{ responseHeading }}</h4>
<div class="{{ namespace }}box -boxInsetLg {{ namespace }}chatbot__responseBox">
<div class="{{ namespace }}stackSm" aria-hidden="true">
{% for skeletonLine in ["100%", "100%", "100%", "100%", "40%"] %}
{% render "@skeleton-unit--small", { inlineCSS: { "width": skeletonLine } }, true %}
{% endfor %}
</div>
</div>
</section>
{% elseif answerText %}
<section aria-labelledby="{{ responseHeadingId }}" aria-live="polite">
<h4 class="sr-only" id="{{ responseHeadingId }}">{{ responseHeading }}</h4>
{% render "@chat-message--chatbot-without-indicator", {
classes: [namespace + "chatbot__responseBox"],
text: answerText,
rate: true,
ratingActive: false
}, true %}
</section>
{% elseif noResultText %}
<section aria-labelledby="{{ responseHeadingId }}" aria-live="polite">
<h4 class="sr-only" id="{{ responseHeadingId }}">{{ responseHeading }}</h4>
<div class="{{ namespace }}box -boxInsetLg {{ namespace }}chatbot__responseBox">
<div class="{{ namespace }}wysiwyg">
<p>{{ noResultText }}</p>
</div>
</div>
</section>
{% endif %}
</div>
{% endif %}
</div>
export default class Chatbot {
// Enhances the server-rendered component with input state and suggestion navigation.
constructor() {
this.chatbots = document.querySelectorAll("[data-js-chatbot]");
this.chatbots.forEach((chatbot) => this.init(chatbot));
}
init(chatbot) {
const input = chatbot.querySelector("[data-chatbot-input]");
if (!input) return;
const submitButton = chatbot.querySelector("[data-chatbot-submit]");
const clearButton = chatbot.querySelector("[data-chatbot-clear]");
const suggestions = Array.from(chatbot.querySelectorAll("[data-chatbot-suggestion]"));
const suggestionsHintId = input.getAttribute("aria-describedby");
// Keep the Figma variant's initial button state, then derive it from user input.
const updateValueState = ({ preserveSubmitState = false } = {}) => {
const hasValue = input.value.trim() !== "";
chatbot.classList.toggle("-hasValue", hasValue);
if (submitButton && !preserveSubmitState && !chatbot.hasAttribute("aria-busy")) {
submitButton.disabled = !hasValue;
}
if (suggestionsHintId) {
input.toggleAttribute("aria-describedby", !hasValue);
if (!hasValue) input.setAttribute("aria-describedby", suggestionsHintId);
}
};
input.addEventListener("input", updateValueState);
input.addEventListener("keydown", (event) => {
// Suggestions are regular buttons, so move real focus into the list.
if (event.key === "ArrowDown" && suggestions.length && input.value.trim() === "") {
event.preventDefault();
suggestions[0].focus();
}
});
clearButton?.addEventListener("click", () => {
input.value = "";
updateValueState();
input.focus();
});
suggestions.forEach((suggestion, index) => {
suggestion.addEventListener("keydown", (event) => {
// Keep keyboard focus inside the suggestion list, with input as its first item.
if (event.key === "ArrowDown" && index < suggestions.length - 1) {
event.preventDefault();
suggestions[index + 1].focus();
}
if (event.key === "ArrowUp") {
event.preventDefault();
(index === 0 ? input : suggestions[index - 1]).focus();
}
if (event.key === "Escape") {
event.preventDefault();
input.focus();
}
});
suggestion.addEventListener("click", () => {
input.value = suggestion.value;
updateValueState();
input.focus();
});
});
// Do not overwrite the disabled state supplied by the current Fractal/backend variant.
updateValueState({ preserveSubmitState: true });
}
}
.#{$namespace}chatbot {
$self: &;
display: flex;
flex-direction: column;
gap: var(--ui-space-stack-static-500);
width: 100%;
padding: var(--ui-space-inset-vertical-static-500) var(--ui-space-inset-horizontal-static-1000);
border-radius: var(--sys-border-radius-1000);
@include custom-prop-fallback("background-color", "comp-box-background-color");
box-shadow: map-deep-get($token-shadow-map, "active");
&__bar {
display: flex;
align-items: center;
gap: var(--ui-space-inline-static-1000);
}
&__input {
flex: 1 1 auto;
padding-inline: var(--comp-input-inset-h);
border: 0;
background-color: transparent !important;
@include custom-prop-fallback("color", "comp-search-filled-text-color");
&::placeholder {
@include custom-prop-fallback("color", "comp-search-enabled-text-color");
}
}
&__clear,
&__submit {
flex: 0 0 auto;
}
&__suggestions {
padding-block-start: var(--ui-space-inset-vertical-static-500);
}
&__suggestion {
display: flex;
align-items: flex-start;
gap: var(--ui-space-inline-static-500);
width: 100%;
padding: var(--ui-space-inset-vertical-static-375) var(--ui-space-inset-horizontal-static-250);
@include custom-prop-fallback("color", "comp-input-text-color");
text-align: left;
&:hover {
@media (hover: hover) {
@include custom-prop-fallback("background-color", "comp-box-sunken-background-color");
}
}
}
&__input,
&__suggestion {
&:focus-visible {
outline-width: map-deep-get($design-tokens, "comp-search-focused-border-width");
outline-color: map-deep-get($design-tokens, "comp-search-focused-border-color");
outline-style: solid;
outline-offset: 0;
}
}
&__responseBox {
&#{$self}__responseBox#{$self}__responseBox {
width: 100%;
padding-bottom: var(--comp-box-large-inset-v) !important;
box-shadow: map-deep-get($token-shadow-map, "active");
}
}
&__container {
display: flex;
flex-direction: column;
width: 100%;
}
&.-hasValue {
#{$self}__suggestions {
display: none;
}
}
}