convai / front /js-src /controller.ts
Julien Chaumond
Feedback from @thomwolf , @victorsanh et. al.
e246102
const App = {
persona: {
slug: "",
text: "",
},
messages: [] as Message[],
/// HTMLElements
messagesRoot: document.querySelector('div.messages') as HTMLDivElement,
divPersona: document.querySelector('div.persona') as HTMLDivElement,
linkSuggest: document.querySelector('.chat-suggestion .js-suggestion') as HTMLLinkElement,
sliders: Array.from(
document.querySelectorAll('.decoder-settings input.slider')
) as HTMLInputElement[],
};
document.addEventListener('DOMContentLoaded', () => {
/**
* Persona
* - in simple mode, we just re-format the text slightly to make it prettier.
* - in attention mode, we tokenize and add spans around every token.
*/
const simplePersona = () => {
const text = App.persona.text;
const html = text.split('.').map(x => Utils.capitalize(x)).join(`.<br>`);
App.divPersona.innerHTML = html;
};
const tokenizePersona = () => {
const text = App.persona.text;
const tokens = text.split(/\b/).filter(x => ! /\s/.test(x));
let html = ``;
for (let [i, tok] of tokens.entries()) {
if (i === 0) {
html += `<span data-idx="${i}">${ Utils.capitalize(tok) }</span>`;
} else if (i > 0 && tokens[i-1] === '.') {
html += `<br><span data-idx="${i}">${ Utils.capitalize(tok) }</span>`;
} else if (/[.,']/.test(tok)) {
html += `<span data-idx="${i}">${ tok }</span>`;
} else {
html += ` <span data-idx="${i}">${ tok }</span>`;
}
}
App.divPersona.innerHTML = html;
};
App.persona = (<any>window).PERSONA_ATLOAD;
// ^^ tokenizePersona();
simplePersona();
document.querySelector('.js-shuffle')!.addEventListener('click', async (evt) => {
evt.preventDefault();
App.persona = await Api.shared.getShuffle();
tokenizePersona();
history.replaceState(null, "", `/persona/${App.persona.slug}`);
/// Also reset messages and reload suggestion.
App.messages = [];
App.messagesRoot.innerHTML = "";
loadSuggestion();
});
document.querySelector('.js-share')!.addEventListener('click', async (evt) => {
evt.preventDefault();
history.replaceState(null, "", `/persona/${App.persona.slug}`);
const text = `Chat with me: ${ App.divPersona.innerText.replace(/\n/g, " ") }`;
window.open(`https://twitter.com/share?url=${ encodeURIComponent(window.location.href) }&text=${ encodeURIComponent(text) }`);
});
/**
* Settings
*/
const handleSliderChange = (slider: HTMLInputElement) => {
const div = slider.parentNode as HTMLDivElement;
const spanVal = div.querySelector('.js-val') as HTMLSpanElement;
const value = Number.isInteger(slider.valueAsNumber)
? slider.valueAsNumber
: Number(slider.valueAsNumber.toFixed(2))
;
spanVal.innerText = value.toString();
const min = Number(slider.getAttribute('min'));
const max = Number(slider.getAttribute('max'));
if (value < min + (max - min) / 3) {
spanVal.className = "js-val green";
} else if (value < min + 2 * (max - min) / 3) {
spanVal.className = "js-val orange";
} else {
spanVal.className = "js-val red";
}
const isInverted = slider.classList.contains('js-inverted');
if (isInverted) {
if (spanVal.classList.contains('green')) {
spanVal.classList.remove('green');
spanVal.classList.add('red');
} else if (spanVal.classList.contains('red')) {
spanVal.classList.remove('red');
spanVal.classList.add('green');
}
}
};
for (const slider of App.sliders) {
handleSliderChange(slider);
slider.addEventListener('input', () => {
handleSliderChange(slider);
});
}
const gauge = document.querySelector('div.gauge') as HTMLDivElement;
gauge.addEventListener('click', () => {
gauge.classList.add('active');
});
const gaugeEls = Array.from(
document.querySelectorAll('.gauge .gauge-el-wrapper')
);
for (const gaugeEl of gaugeEls) {
gaugeEl.addEventListener('click', () => {
const i = gaugeEls.indexOf(gaugeEl);
if (i === 0) {
App.sliders[0].value = `180`;
App.sliders[1].value = `0.1`;
App.sliders[2].value = `1.9`;
} else if (i === 1) {
App.sliders[0].value = `70`;
App.sliders[1].value = `0.5`;
App.sliders[2].value = `1.2`;
} else {
App.sliders[0].value = `0`;
App.sliders[1].value = `0.9`;
App.sliders[2].value = `0.6`;
}
for (const slider of App.sliders) {
handleSliderChange(slider);
}
});
}
/**
* Chat input
*/
const form = document.querySelector('form.js-form') as HTMLFormElement;
const input = document.querySelector('input.input-message') as HTMLInputElement;
form.addEventListener('submit', async (evt) => {
evt.preventDefault();
document.querySelector<HTMLDivElement>('.placeholder-start')!.style.display = 'none';
const content = input.value;
if (content.trim() === "") {
c.debug(`Empty input`);
return ;
}
input.value = "";
const um: Message = {
incoming: false,
content: content,
};
Markup.append(um);
const o = await Api.shared.postWithSettings({ text: content }, {
context: App.messages,
persona: App.persona.text,
});
c.log(o);
// c.log(o.attention[0]);
// c.log(o.attention[1]);
// c.log(o.attention[2]);
App.messages.push(um);
/// ^^ not before the API call because it shouldn't be included in context.
/**
* Visualize Attention
*/
// for (const [idx, [token, att]] of o.attention[0].entries()) {
// if (att === 0) {
// continue;
// }
// const span = document.querySelector(`.persona span[data-idx="${idx}"]`);
// if (!span) {
// c.error(`span not found`);
// return ;
// }
// span.className = `attention-level level${ Markup.attentionThreshold(att) }`;
// }
//////
const m: Message = {
incoming: true,
content: o.text,
};
App.messages.push(m);
Markup.append(m);
/// Finally, launch an auto-complete:
loadSuggestion();
});
/**
* Suggestion box
*/
const loadSuggestion = async () => {
const spanLoading = document.querySelector('.chat-suggestion .js-loading') as HTMLSpanElement;
spanLoading.classList.remove('hide');
App.linkSuggest.classList.add('hide');
const o = await Api.shared.postWithSettings({ completion: "" }, {
context: App.messages,
persona: App.persona.text,
});
c.log(o);
App.linkSuggest.innerText = o.text;
spanLoading.classList.add('hide');
App.linkSuggest.classList.remove('hide');
};
loadSuggestion();
App.linkSuggest.addEventListener('click', async (evt) => {
evt.preventDefault();
App.linkSuggest.classList.add('hide');
input.value = App.linkSuggest.innerText;
await Utils.delay(500);
const sendBtn = form.querySelector('button.input-button') as HTMLButtonElement;
sendBtn.click();
/// ^^ do not do `form.submit()` as we want to trigger our handler.
});
});