<html>
-
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
align-items: stretch;
}
- .right {
+ .message-controls {
display: flex;
- flex-direction: row;
- gap: 0.5em;
justify-content: flex-end;
}
+ .message-controls > div:nth-child(2) {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
+ }
+ .message-controls > div:nth-child(2) > div {
+ display: flex;
+ margin-left: auto;
+ gap: 0.5em;
+ }
fieldset {
border: none;
import { llama } from './completion.js';
import { SchemaConverter } from './json-schema-to-grammar.mjs';
+
let selected_image = false;
var slot_id = -1;
/* END: Support for storing prompt templates and parameters in browsers LocalStorage */
+ const tts = window.speechSynthesis;
+ const ttsVoice = signal(null)
+
const llamaStats = signal(null)
const controller = signal(null)
});
}
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ const talkRecognition = SpeechRecognition ? new SpeechRecognition() : null;
function MessageInput() {
- const message = useSignal("")
+ const message = useSignal("");
+
+ const talkActive = useSignal(false);
+ const sendOnTalk = useSignal(false);
+ const talkStop = (e) => {
+ if (e) e.preventDefault();
+
+ talkActive.value = false;
+ talkRecognition?.stop();
+ }
+ const talk = (e) => {
+ e.preventDefault();
+
+ if (talkRecognition)
+ talkRecognition.start();
+ else
+ alert("Speech recognition is not supported by this browser.");
+ }
+ if(talkRecognition) {
+ talkRecognition.onstart = () => {
+ talkActive.value = true;
+ }
+ talkRecognition.onresult = (e) => {
+ if (event.results.length > 0) {
+ message.value = event.results[0][0].transcript;
+ if (sendOnTalk.value) {
+ submit(e);
+ }
+ }
+ }
+ talkRecognition.onspeechend = () => {
+ talkStop();
+ }
+ }
+
+ const ttsVoices = useSignal(tts?.getVoices() || []);
+ const ttsVoiceDefault = computed(() => ttsVoices.value.find(v => v.default));
+ if (tts) {
+ tts.onvoiceschanged = () => {
+ ttsVoices.value = tts.getVoices();
+ }
+ }
const submit = (e) => {
stop(e);
value="${message}"
/>
</div>
- <div class="right">
- <button type="submit" disabled=${generating.value}>Send</button>
- <button onclick=${uploadImage}>Upload Image</button>
- <button onclick=${stop} disabled=${!generating.value}>Stop</button>
- <button onclick=${reset}>Reset</button>
+ <div class="message-controls">
+ <div> </div>
+ <div>
+ <div>
+ <button type="submit" disabled=${generating.value || talkActive.value}>Send</button>
+ <button disabled=${generating.value || talkActive.value} onclick=${uploadImage}>Upload Image</button>
+ <button onclick=${stop} disabled=${!generating.value}>Stop</button>
+ <button onclick=${reset}>Reset</button>
+ </div>
+ <div>
+ <a href="#" style="cursor: help;" title="Help" onclick=${e => {
+ e.preventDefault();
+ alert(`STT supported by your browser: ${SpeechRecognition ? 'Yes' : 'No'}\n` +
+ `(TTS and speech recognition are not provided by llama.cpp)\n` +
+ `Note: STT requires HTTPS to work.`);
+ }}>[?]</a>
+ <button disabled=${generating.value} onclick=${talkActive.value ? talkStop : talk}>${talkActive.value ? "Stop Talking" : "Talk"}</button>
+ <div>
+ <input type="checkbox" id="send-on-talk" name="send-on-talk" checked="${sendOnTalk}" onchange=${(e) => sendOnTalk.value = e.target.checked} />
+ <label for="send-on-talk" style="line-height: initial;">Send after talking</label>
+ </div>
+ </div>
+ <div>
+ <a href="#" style="cursor: help;" title="Help" onclick=${e => {
+ e.preventDefault();
+ alert(`TTS supported by your browser: ${tts ? 'Yes' : 'No'}\n(TTS and speech recognition are not provided by llama.cpp)`);
+ }}>[?]</a>
+ <label for="tts-voices" style="line-height: initial;">Bot Voice:</label>
+ <select id="tts-voices" name="tts-voices" onchange=${(e) => ttsVoice.value = e.target.value} style="max-width: 100px;">
+ <option value="" selected="${!ttsVoice.value}">None</option>
+ ${[
+ ...(ttsVoiceDefault.value ? [ttsVoiceDefault.value] : []),
+ ...ttsVoices.value.filter(v => !v.default),
+ ].map(
+ v => html`<option value="${v.name}" selected="${ttsVoice.value === v.name}">${v.name} (${v.lang}) ${v.default ? '(default)' : ''}</option>`
+ )}
+ </select>
+ </div>
+ </div>
</div>
</form>
`
}
}, [messages])
+ const ttsChatLineActiveIx = useSignal(undefined);
+ const ttsChatLine = (e, ix, msg) => {
+ if (e) e.preventDefault();
+
+ if (!tts || !ttsVoice.value || !('SpeechSynthesisUtterance' in window)) return;
+
+ const ttsVoices = tts.getVoices();
+ const voice = ttsVoices.find(v => v.name === ttsVoice.value);
+ if (!voice) return;
+
+ if (ttsChatLineActiveIx.value !== undefined) {
+ tts.cancel();
+ if (ttsChatLineActiveIx.value === ix) {
+ ttsChatLineActiveIx.value = undefined;
+ return;
+ }
+ }
+
+ ttsChatLineActiveIx.value = ix;
+ let ttsUtter = new SpeechSynthesisUtterance(msg);
+ ttsUtter.voice = voice;
+ ttsUtter.onend = e => {
+ ttsChatLineActiveIx.value = undefined;
+ };
+ tts.speak(ttsUtter);
+ }
+
const isCompletionMode = session.value.type === 'completion'
+
+ // Try play the last bot message
+ const lastCharChatLinesIxs = useSignal([]);
+ const lastCharChatLinesIxsOld = useSignal([]);
+ useEffect(() => {
+ if (
+ !isCompletionMode
+ && lastCharChatLinesIxs.value.length !== lastCharChatLinesIxsOld.value.length
+ && !generating.value
+ ) {
+ const ix = lastCharChatLinesIxs.value[lastCharChatLinesIxs.value.length - 1];
+ if (ix !== undefined) {
+ const msg = messages[ix];
+ ttsChatLine(null, ix, Array.isArray(msg) ? msg[1].map(m => m.content).join('') : msg);
+ }
+
+ lastCharChatLinesIxsOld.value = structuredClone(lastCharChatLinesIxs.value);
+ }
+ }, [generating.value]);
+
const chatLine = ([user, data], index) => {
let message
- const isArrayMessage = Array.isArray(data)
+ const isArrayMessage = Array.isArray(data);
+ const text = isArrayMessage ?
+ data.map(msg => msg.content).join('') :
+ data;
if (params.value.n_probs > 0 && isArrayMessage) {
message = html`<${Probabilities} data=${data} />`
} else {
- const text = isArrayMessage ?
- data.map(msg => msg.content).join('') :
- data;
message = isCompletionMode ?
text :
html`<${Markdownish} text=${template(text)} />`
}
+
+ const fromBot = user && user === '{{char}}';
+ if (fromBot && !lastCharChatLinesIxs.value.includes(index))
+ lastCharChatLinesIxs.value.push(index);
+
if (user) {
- return html`<p key=${index}><strong>${template(user)}:</strong> ${message}</p>`
+ return html`
+ <div>
+ <p key=${index}><strong>${template(user)}:</strong> ${message}</p>
+ ${
+ fromBot && ttsVoice.value
+ && html`<button disabled=${generating.value} onclick=${e => ttsChatLine(e, index, text)} aria-label=${ttsChatLineActiveIx.value === index ? 'Pause' : 'Play'}>${ ttsChatLineActiveIx.value === index ? '⏸️' : '▶️' }</div>`
+ }
+ </div>
+ `;
} else {
return isCompletionMode ?
html`<span key=${index}>${message}</span>` :
- html`<p key=${index}>${message}</p>`
+ html`<div><p key=${index}>${message}</p></div>`
}
};