217 lines
7.0 KiB
JavaScript
217 lines
7.0 KiB
JavaScript
|
|
// retoor <retoor@molodetz.nl>
|
||
|
|
|
||
|
|
import { Component } from "./Component.js";
|
||
|
|
import { assetUrl } from "../assetVersion.js";
|
||
|
|
|
||
|
|
const CHAT_CSS_ID = "deepsearch-chat-css";
|
||
|
|
const RETRY_CODE = 4013;
|
||
|
|
const RETRY_MS = 200;
|
||
|
|
const RECONNECT_MS = 1500;
|
||
|
|
|
||
|
|
export class AppDeepsearchChat extends Component {
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
this.ws = null;
|
||
|
|
this._shouldRun = true;
|
||
|
|
this._busy = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
_ensureCss() {
|
||
|
|
if (document.getElementById(CHAT_CSS_ID)) return;
|
||
|
|
const link = document.createElement("link");
|
||
|
|
link.id = CHAT_CSS_ID;
|
||
|
|
link.rel = "stylesheet";
|
||
|
|
link.href = assetUrl("/static/css/deepsearch.css");
|
||
|
|
document.head.appendChild(link);
|
||
|
|
}
|
||
|
|
|
||
|
|
connectedCallback() {
|
||
|
|
if (this._built) return;
|
||
|
|
this._built = true;
|
||
|
|
this._ensureCss();
|
||
|
|
this.uid = this.attr("uid");
|
||
|
|
this.chatPath = this.attr("chat-ws") || `/tools/deepsearch/${this.uid}/chat`;
|
||
|
|
this._build();
|
||
|
|
this._open();
|
||
|
|
}
|
||
|
|
|
||
|
|
disconnectedCallback() {
|
||
|
|
this._shouldRun = false;
|
||
|
|
if (this.ws) this.ws.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
_build() {
|
||
|
|
this.innerHTML = "";
|
||
|
|
this.log = document.createElement("div");
|
||
|
|
this.log.className = "ds-chat-log";
|
||
|
|
this.log.setAttribute("role", "log");
|
||
|
|
this.log.setAttribute("aria-live", "polite");
|
||
|
|
|
||
|
|
this.statusEl = document.createElement("div");
|
||
|
|
this.statusEl.className = "ds-chat-status";
|
||
|
|
this.statusEl.hidden = true;
|
||
|
|
|
||
|
|
this.form = document.createElement("form");
|
||
|
|
this.form.className = "ds-chat-input";
|
||
|
|
this.field = document.createElement("textarea");
|
||
|
|
this.field.className = "ds-chat-field";
|
||
|
|
this.field.rows = 1;
|
||
|
|
this.field.placeholder = "Ask about the gathered evidence...";
|
||
|
|
this.send = document.createElement("button");
|
||
|
|
this.send.type = "submit";
|
||
|
|
this.send.className = "ds-chat-send";
|
||
|
|
this.send.textContent = "Ask";
|
||
|
|
this.form.append(this.field, this.send);
|
||
|
|
|
||
|
|
this.append(this.log, this.statusEl, this.form);
|
||
|
|
|
||
|
|
this.form.addEventListener("submit", (event) => {
|
||
|
|
event.preventDefault();
|
||
|
|
this._submit(this.field.value);
|
||
|
|
});
|
||
|
|
this.field.addEventListener("keydown", (event) => {
|
||
|
|
if (event.key === "Enter" && !event.shiftKey) {
|
||
|
|
event.preventDefault();
|
||
|
|
this._submit(this.field.value);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.field.addEventListener("input", () => this._autosize());
|
||
|
|
}
|
||
|
|
|
||
|
|
_wsUrl() {
|
||
|
|
const scheme = location.protocol === "https:" ? "wss://" : "ws://";
|
||
|
|
return `${scheme}${location.host}${this.chatPath}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
_open() {
|
||
|
|
this.ws = new WebSocket(this._wsUrl());
|
||
|
|
this.ws.addEventListener("message", (event) => {
|
||
|
|
let frame;
|
||
|
|
try {
|
||
|
|
frame = JSON.parse(event.data);
|
||
|
|
} catch {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._onMessage(frame);
|
||
|
|
});
|
||
|
|
this.ws.addEventListener("close", (event) => {
|
||
|
|
if (event.code === RETRY_CODE && this._shouldRun) {
|
||
|
|
window.setTimeout(() => this._open(), RETRY_MS);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (this._shouldRun) {
|
||
|
|
this._setStatus("Reconnecting...");
|
||
|
|
window.setTimeout(() => this._open(), RECONNECT_MS);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.ws.addEventListener("error", () => this._setStatus("Connection error."));
|
||
|
|
}
|
||
|
|
|
||
|
|
_onMessage(frame) {
|
||
|
|
if (frame.type === "ready") {
|
||
|
|
this._setStatus("");
|
||
|
|
this._renderHistory(frame.history || []);
|
||
|
|
} else if (frame.type === "status") {
|
||
|
|
this._setStatus(frame.text);
|
||
|
|
} else if (frame.type === "reply") {
|
||
|
|
this._busy = false;
|
||
|
|
this._setStatus("");
|
||
|
|
this._agent(frame.text, frame.citations);
|
||
|
|
} else if (frame.type === "error") {
|
||
|
|
this._busy = false;
|
||
|
|
this._setStatus("");
|
||
|
|
this._errorLine(frame.text);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_submit(raw) {
|
||
|
|
const text = (raw || "").trim();
|
||
|
|
if (!text || this._busy) return;
|
||
|
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||
|
|
this._errorLine("Not connected.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._busy = true;
|
||
|
|
this._userLine(text);
|
||
|
|
this.field.value = "";
|
||
|
|
this._autosize();
|
||
|
|
this.ws.send(JSON.stringify({ type: "input", text }));
|
||
|
|
this._setStatus("Searching the research collection");
|
||
|
|
}
|
||
|
|
|
||
|
|
_renderHistory(messages) {
|
||
|
|
this.log.innerHTML = "";
|
||
|
|
messages.forEach((message) => {
|
||
|
|
if (message.role === "user") {
|
||
|
|
this._userLine(message.content);
|
||
|
|
} else if (message.role === "assistant" && message.content) {
|
||
|
|
this._agent(message.content, []);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
_userLine(text) {
|
||
|
|
const row = document.createElement("div");
|
||
|
|
row.className = "ds-chat-msg user";
|
||
|
|
const bubble = document.createElement("div");
|
||
|
|
bubble.className = "ds-chat-bubble";
|
||
|
|
bubble.textContent = text;
|
||
|
|
row.appendChild(bubble);
|
||
|
|
this._append(row);
|
||
|
|
}
|
||
|
|
|
||
|
|
_agent(text, citations) {
|
||
|
|
const row = document.createElement("div");
|
||
|
|
row.className = "ds-chat-msg agent";
|
||
|
|
const bubble = document.createElement("div");
|
||
|
|
bubble.className = "ds-chat-bubble";
|
||
|
|
const content = document.createElement("dp-content");
|
||
|
|
content.setAttribute("data-render", "");
|
||
|
|
content.textContent = text;
|
||
|
|
bubble.appendChild(content);
|
||
|
|
if (citations && citations.length) {
|
||
|
|
const list = document.createElement("div");
|
||
|
|
list.className = "ds-chat-citations";
|
||
|
|
citations.forEach((cite) => {
|
||
|
|
const link = document.createElement("a");
|
||
|
|
link.href = cite.url;
|
||
|
|
link.target = "_blank";
|
||
|
|
link.rel = "noopener nofollow";
|
||
|
|
link.textContent = `[${cite.index}] ${cite.title}`;
|
||
|
|
list.appendChild(link);
|
||
|
|
});
|
||
|
|
bubble.appendChild(list);
|
||
|
|
}
|
||
|
|
row.appendChild(bubble);
|
||
|
|
this._append(row);
|
||
|
|
}
|
||
|
|
|
||
|
|
_errorLine(text) {
|
||
|
|
const row = document.createElement("div");
|
||
|
|
row.className = "ds-chat-msg error";
|
||
|
|
const bubble = document.createElement("div");
|
||
|
|
bubble.className = "ds-chat-bubble";
|
||
|
|
bubble.textContent = text;
|
||
|
|
row.appendChild(bubble);
|
||
|
|
this._append(row);
|
||
|
|
}
|
||
|
|
|
||
|
|
_append(node) {
|
||
|
|
const pinned = this.log.scrollHeight - this.log.scrollTop - this.log.clientHeight < 80;
|
||
|
|
this.log.appendChild(node);
|
||
|
|
if (pinned) this.log.scrollTop = this.log.scrollHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
_autosize() {
|
||
|
|
this.field.style.height = "auto";
|
||
|
|
this.field.style.height = `${Math.min(this.field.scrollHeight, 160)}px`;
|
||
|
|
}
|
||
|
|
|
||
|
|
_setStatus(text) {
|
||
|
|
this.statusEl.textContent = text || "";
|
||
|
|
this.statusEl.hidden = !text;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
customElements.define("dp-deepsearch-chat", AppDeepsearchChat);
|