410 lines
14 KiB
JavaScript
410 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
import {
|
|
Fieldset, Section, Form, Div, Nav, UL, LI, Span, Pre, Article
|
|
} from "../Utils/HTMLDSL.ecma.js";
|
|
import {MarkDown} from "../Utils/MarkDown.ecma.js";
|
|
import {Common} from "../Utils/Common.ecma.js";
|
|
|
|
/**
|
|
* @typedef {import("../Application/AnP.ecma.js").AnP} AnP
|
|
*/
|
|
|
|
/**
|
|
* @class AIChatComponent
|
|
* @constructor
|
|
* @param {!AnP} anp
|
|
* @returns {void}
|
|
* @access private
|
|
* @static
|
|
*/
|
|
export const AIChatComponent = (function(){
|
|
|
|
/**
|
|
* @constructs AIChatComponent
|
|
* @param {!AnP} anp
|
|
* @returns {void}
|
|
* @access private
|
|
* @static
|
|
*/
|
|
const AIChatComponent = function(anp){
|
|
|
|
/** @type {AIChatComponent} */
|
|
const self = this,
|
|
/** @type {Object.<string, string|number|Object.<string, any|null>>} */
|
|
cache = {};
|
|
|
|
/**
|
|
* @returns {void}
|
|
* @access private
|
|
*/
|
|
const constructor = () => {
|
|
setTimeout(() => {
|
|
anp.components.aichat = self.build;
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* @param {!HTMLElement} item
|
|
* @returns {HTMLFielsetElement}
|
|
* @access private
|
|
*/
|
|
const get_chat_box = item => {
|
|
|
|
while((!item.classList || !item.classList.contains("aichat")) && (item = item.parentElement));
|
|
|
|
return item;
|
|
};
|
|
|
|
/**
|
|
* @param {!string} id
|
|
* @param {!string} type
|
|
* @param {?string} [content = ""]
|
|
* @param {?string} [mode = "waiting"]
|
|
* @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null]
|
|
* @returns {Array.<any|null>}
|
|
* @access private
|
|
*/
|
|
const build_message = (id, type, content = "", mode = "waiting", inputs = null) => {
|
|
|
|
/** @type {[string, string, string]} */
|
|
const [html, raw_html, md] = format(content),
|
|
/** @type {number} */
|
|
date_from = Common.get_value("date_from", inputs) || Date.now();
|
|
|
|
return Fieldset({
|
|
class : "message",
|
|
data_id : id,
|
|
data_type : type,
|
|
data_ok : true,
|
|
data_status : mode,
|
|
data_done : mode == "done",
|
|
data_mode : "html"
|
|
}, [
|
|
anp.components.i18n(type, "legend"),
|
|
Div({class : "message-box html-content"}, html),
|
|
Pre({class : "message-box raw-html-content"}, raw_html),
|
|
Pre({class : "message-box md-content"}, md),
|
|
Nav({class : "view buttons"}, [
|
|
anp.components.button("html", set_view),
|
|
anp.components.button("raw_html", set_view),
|
|
anp.components.button("md", set_view),
|
|
]),
|
|
UL({class : "data"}, [
|
|
build_data_icon("status", mode),
|
|
build_data_item("ok"),
|
|
build_data_item("done"),
|
|
build_data_item("date_from", date_from),
|
|
build_data_item("date_to", Common.get_value("date_to", inputs) || date_from),
|
|
build_data_item("time", 0),
|
|
build_data_item("length", content.length),
|
|
build_data_item("response_tokens", content.replace(/(?:[^a-z0-9]+|[\r\n]+)+/gi, " ").trim().split(" ").length)
|
|
])
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* @param {?string} session
|
|
* @param {!string} name
|
|
* @returns {Array.<any|null>}
|
|
* @access private
|
|
*/
|
|
const build_new_conversation = (session, name) => {
|
|
|
|
const session_key = session + "-" + name;
|
|
/** @type {string} */
|
|
conversation = anp.unique_keys.get();
|
|
|
|
cache[cache_key].conversations[conversation] = [];
|
|
|
|
document.querySelectorAll(".aichat[")
|
|
|
|
return Article({
|
|
class : "messages",
|
|
data_conversation : conversation,
|
|
data_selected : true
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null]
|
|
* @returns {Array.<any|null>}
|
|
* @access public
|
|
*/
|
|
this.build = (inputs = null) => {
|
|
|
|
/** @type {string} */
|
|
const name = Common.get_value("name", inputs, "aichat"),
|
|
/** @type {string|null} */
|
|
session = Common.get_value("session", inputs, null),
|
|
/** @type {string} */
|
|
cache_key = session + "-" + name;
|
|
|
|
if(session){
|
|
cache[cache_key] = {
|
|
selected : conversations[conversations.length - 1],
|
|
conversations : conversations.reduce((dictionary, conversation) => {
|
|
|
|
dictionary[conversation] = [];
|
|
|
|
return dictionary;
|
|
}, {})
|
|
};
|
|
Common.preload("[data-name=" + name + "]", (chats, asynchronous, ok) => {
|
|
if(ok){
|
|
|
|
const data = Common.data_decode(anp.sessions.get_value(session, name));
|
|
|
|
if(data){
|
|
cache[cache_key] = data;
|
|
return;
|
|
};
|
|
|
|
Common.HTML(chats.querySelector(".conversations"), build_new_conversation(session, name));
|
|
|
|
};
|
|
});
|
|
};
|
|
|
|
return Fieldset({
|
|
class : "aichat",
|
|
data_name : name,
|
|
data_session : session
|
|
}, [
|
|
anp.components.i18n(name, "legend"),
|
|
Section({class : "conversations"}, conversations.map(conversation => Article({
|
|
class : "messages",
|
|
data_conversation : conversation,
|
|
data_selected : true
|
|
}))),
|
|
Form({
|
|
method : "post",
|
|
action : "#",
|
|
on_submit : send,
|
|
on_key_down : check_keys,
|
|
data_connection : Common.data_encode([
|
|
Common.get_value("web_socket", inputs, "anp"),
|
|
Common.get_value("controller", inputs, "ai"),
|
|
Common.get_value(["action", "method"], inputs, "message")
|
|
])
|
|
}, [
|
|
anp.components.text("message", {
|
|
multiline : true
|
|
}),
|
|
anp.components.button("send", "submit")
|
|
])
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* @param {!string} name
|
|
* @param {!string} value
|
|
* @returns {Array.<any|null>}
|
|
* @access private
|
|
*/
|
|
const build_data_icon = (name, value) => LI({
|
|
data_name : name,
|
|
data_i18n : value,
|
|
data_i18n_without : true,
|
|
title : anp.i18n.get(value)
|
|
}, [
|
|
anp.components.icon(value),
|
|
anp.components.i18n(value)
|
|
]);
|
|
|
|
/**
|
|
* @param {!string} name
|
|
* @param {?string} [value = null]
|
|
* @returns {Array.<any|null>}
|
|
* @access private
|
|
*/
|
|
const build_data_item = (name, value = null) => LI({
|
|
data_name : name,
|
|
data_i18n : name,
|
|
data_i18n_without : true,
|
|
title : anp.i18n.get(name)
|
|
}, [
|
|
anp.components.icon(name),
|
|
anp.components.i18n(name),
|
|
value === null ? null : Span({class : "value"}, "" + value)
|
|
]);
|
|
|
|
/**
|
|
* @param {!HTMLElement} item
|
|
* @returns {HTMLTextAreaElement}
|
|
* @access private
|
|
*/
|
|
const get_message_box = item => {
|
|
|
|
while((!item.classList || !item.classList.contains("message")) && (item = item.parentElement));
|
|
|
|
return item;
|
|
};
|
|
|
|
/**
|
|
* @param {!HTMLElement} item
|
|
* @param {!Event} event
|
|
* @returns {void}
|
|
* @access private
|
|
*/
|
|
const set_view = (item, event) => {
|
|
|
|
/** @type {HTMLTextAreaElement} */
|
|
const box = get_message_box(item),
|
|
/** @type {string} */
|
|
mode = item.getAttribute("data-i18n");
|
|
|
|
box.getAttribute("data-mode") != mode && box.setAttribute("data-mode", mode);
|
|
|
|
};
|
|
|
|
/**
|
|
* @param {!string} content
|
|
* @returns {[string, string, string]}
|
|
* @access private
|
|
*/
|
|
const format = content => {
|
|
|
|
/** @type {string} */
|
|
const html = MarkDown.to_html(content);
|
|
|
|
return [html, html.replace(/</g, "<").replace(/>/g, ">"), content];
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {!HTMLElement} item
|
|
* @param {!Event} event
|
|
* @returns {boolean}
|
|
* @access private
|
|
*/
|
|
const send = (item, event) => {
|
|
|
|
/** @type {HTMLTextAreaElement} */
|
|
const text_box = item.querySelector("[name=message]"),
|
|
/** @type {string} */
|
|
text = text_box.value.trim(),
|
|
/** @type {HTMLArticleElement} */
|
|
conversation = anp.item_self.querySelector(".aichat [data-conversation][data-selected=true]").getAttribute("data-conversation");
|
|
|
|
event.preventDefault();
|
|
|
|
if(text){
|
|
|
|
/** @type {string} */
|
|
const data_id = anp.unique_keys.get(),
|
|
/** @type {[string, string, string]} */
|
|
[web_socket, controller, method] = Common.data_decode(item.querySelector("[name=message]").parentNode.parentNode.getAttribute("data-connection"));
|
|
|
|
Common.HTML(
|
|
".aichat [data-conversation=" + conversation + "]",
|
|
build_message(data_id, "user", text, "done"),
|
|
build_message(data_id, "bot")
|
|
);
|
|
text_box.value = "";
|
|
|
|
anp.web_sockets_clients.send(web_socket, controller, method, {
|
|
message_id : data_id,
|
|
message : text,
|
|
conversation : conversation
|
|
});
|
|
|
|
};
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @param {!HTMLElement} item
|
|
* @param {!Event} event
|
|
* @returns {void}
|
|
* @access private
|
|
*/
|
|
const check_keys = (item, event) => {
|
|
event.key == "Enter" && !event.shiftKey && send(item, event);
|
|
};
|
|
|
|
/**
|
|
* @param {!string} conversation
|
|
* @param {!string} message
|
|
* @param {!string} status
|
|
* @returns {void}
|
|
* @access private
|
|
*/
|
|
const set_status = (conversation, message, status) => {
|
|
|
|
/** @type {HTMLFieldSetElement} */
|
|
const box = anp.item_self.querySelector(".aichat [data-conversation='" + conversation + "']>[data-type=bot][data-id='" + message + "']"),
|
|
/** @type {HTMLSpanElement} */
|
|
status_box = box.querySelector("[data-name=status]"),
|
|
/** @type {HTMLSpanElement} */
|
|
status_i18n_box = status_box.querySelector("[data-i18n]"),
|
|
/** @type {string} */
|
|
status_text = anp.i18n.get(status);
|
|
|
|
|
|
box.setAttribute("data-status", status);
|
|
|
|
status_box.setAttribute("data-i18n", status);
|
|
status_box.setAttribute("title", status_text);
|
|
status_box.querySelector("[data-icon]").setAttribute("data-icon", status);
|
|
status_i18n_box.setAttribute("data-i18n", status_text);
|
|
status_i18n_box.innerHTML = status_text;
|
|
|
|
};
|
|
|
|
/**
|
|
* @param {!string} conversation
|
|
* @param {!string} message
|
|
* @param {!string} fragment
|
|
* @param {!boolean} ok
|
|
* @param {!boolean} done
|
|
* @returns {void}
|
|
* @access public
|
|
*/
|
|
this.write_response = (conversation, message, fragment, ok, done) => {
|
|
|
|
/** @type {HTMLFieldSetElement} */
|
|
const box = anp.item_self.querySelector(".aichat .messages>[data-type=bot][data-id='" + message + "']"),
|
|
/** @type {number} */
|
|
date = Date.now(),
|
|
/** @type {HTMLSpanElement} */
|
|
tokens_box = box.querySelector("[data-name=response_tokens] .value"),
|
|
/** @type {[string, string, string]} */
|
|
[html, raw_html, md] = format(box.querySelector(".md-content").textContent += fragment);
|
|
|
|
box.querySelector(".html-content").innerHTML = html;
|
|
box.querySelector(".raw-html-content").innerHTML = raw_html;
|
|
|
|
box.querySelector("[data-name=length] .value").innerText = md.length;
|
|
|
|
box.setAttribute("data-ok", ok);
|
|
box.setAttribute("data-done", done);
|
|
|
|
set_status(conversation, message, (
|
|
!ok ? "error" :
|
|
done ? "done" :
|
|
"loading"));
|
|
|
|
box.querySelector("[data-name=date_to]").querySelector(".value").innerHTML = date;
|
|
box.querySelector("[data-name=time]").querySelector(".value").innerHTML = date - Number(box.querySelector("[data-name=date_from] .value").innerText);
|
|
tokens_box.innerText = Number(tokens_box.innerText) + 1;
|
|
|
|
};
|
|
|
|
/**
|
|
* @param {!string} conversation
|
|
* @param {!string} message
|
|
* @param {!string} status
|
|
* @returns {void}
|
|
* @access public
|
|
*/
|
|
this.change_status = (conversation, message, status) => {
|
|
set_status(conversation, message, status);
|
|
};
|
|
|
|
constructor();
|
|
};
|
|
|
|
return AIChatComponent;
|
|
})(); |