demo ui now has 3 tabs for each rag OSS solution
This commit is contained in:
BIN
services/.DS_Store
vendored
BIN
services/.DS_Store
vendored
Binary file not shown.
BIN
services/rag/langchain/.DS_Store
vendored
BIN
services/rag/langchain/.DS_Store
vendored
Binary file not shown.
@@ -121,7 +121,7 @@ During this Phase we create asynchronous process of enrichment, utilizing async/
|
||||
|
||||
# Phase 16 (making demo ui scalable)
|
||||
|
||||
- [ ] Make demo-ui window containable and reusable part of html + js. This part will be used for creating multi-windowed demo ui.
|
||||
- [ ] Make tabbed UI with top level tabs. First tab exists and is selected. Each tab should have copy of demo ui, meaning the chat window with ability to specify the api url
|
||||
- [ ] At the end of the tabs there should be button with plus sign, which will add new tab. Tabs to be called by numbers.
|
||||
- [ ] There should predefined 3 tabs opened. First one should have predefined api url "https://rag.langchain.overwatch.su/api/test-query", second "https://rag.llamaindex.overwatch.su/api/test-query", third "https://rag.haystack.overwatch.su/api/test-query"
|
||||
- [x] Make demo-ui window containable and reusable part of html + js. This part will be used for creating multi-windowed demo ui.
|
||||
- [x] Make tabbed UI with top level tabs. First tab exists and is selected. Each tab should have copy of demo ui, meaning the chat window with ability to specify the api url
|
||||
- [x] At the end of the tabs there should be button with plus sign, which will add new tab. Tabs to be called by numbers.
|
||||
- [x] There should predefined 3 tabs opened. First one should have predefined api url "https://rag.langchain.overwatch.su/api/test-query", second "https://rag.llamaindex.overwatch.su/api/test-query", third "https://rag.haystack.overwatch.su/api/test-query"
|
||||
|
||||
@@ -3,117 +3,297 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>RAG Solution Chat Interface</title>
|
||||
<title>RAG Multi-Window Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f1efe8;
|
||||
--paper: #fffdf7;
|
||||
--ink: #1f2937;
|
||||
--muted: #6b7280;
|
||||
--line: #dfd8c9;
|
||||
--accent: #0f766e;
|
||||
--accent-2: #d97706;
|
||||
--bot: #ece8dc;
|
||||
--user: #115e59;
|
||||
--danger-bg: #fde8e8;
|
||||
--danger-ink: #9b1c1c;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
background:
|
||||
radial-gradient(circle at 15% 15%, #f9d9a8 0%, transparent 35%),
|
||||
radial-gradient(circle at 85% 20%, #b7e3d8 0%, transparent 40%),
|
||||
linear-gradient(180deg, #f5f0e4 0%, #ede7d8 100%);
|
||||
color: var(--ink);
|
||||
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
.app {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding: 20px 14px 24px;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
.shell {
|
||||
border: 1px solid rgba(70, 62, 43, 0.15);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 253, 247, 0.92);
|
||||
box-shadow: 0 18px 45px rgba(47, 41, 30, 0.12);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
.shell-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.9), rgba(244,239,228,0.85));
|
||||
}
|
||||
|
||||
.api-endpoint-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
flex-wrap: wrap;
|
||||
.shell-header h1 {
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.api-endpoint-container label {
|
||||
.shell-header p {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.tabs-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(247, 243, 233, 0.9);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.api-endpoint-container input {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
.tab-btn {
|
||||
border: 1px solid var(--line);
|
||||
background: #faf7ee;
|
||||
color: var(--ink);
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.api-endpoint-container button {
|
||||
background-color: #fff;
|
||||
color: #2575fc;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.api-endpoint-container button:hover {
|
||||
background-color: #e6f0ff;
|
||||
.tab-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #c9bea6;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
background-color: white;
|
||||
.tab-btn.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-btn.add-tab {
|
||||
min-width: 38px;
|
||||
padding-inline: 0;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
background: #fff;
|
||||
color: var(--accent-2);
|
||||
border-color: #e5c792;
|
||||
}
|
||||
|
||||
.panel-host {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.panel-toolbar label {
|
||||
font-weight: 700;
|
||||
color: #4b5563;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.endpoint-input {
|
||||
width: 100%;
|
||||
border: 1px solid #d8cfbd;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 10px 12px;
|
||||
font-size: 0.94rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.endpoint-input:focus,
|
||||
.message-input:focus {
|
||||
outline: 2px solid rgba(15, 118, 110, 0.16);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.panel-toolbar button,
|
||||
.send-btn {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.set-endpoint-btn {
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
color: var(--accent);
|
||||
border: 1px solid #b9d4cf;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
height: 60vh;
|
||||
background: #fff;
|
||||
min-height: 62vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #fbf8ef;
|
||||
}
|
||||
|
||||
.chat-header-title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.chat-header-endpoint {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
gap: 12px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.92), rgba(250,247,238,0.95)),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(218, 206, 181, 0.15),
|
||||
rgba(218, 206, 181, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 28px
|
||||
);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 80%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
position: relative;
|
||||
animation: fadeIn 0.3s ease;
|
||||
max-width: 82%;
|
||||
padding: 11px 13px;
|
||||
border-radius: 14px;
|
||||
line-height: 1.45;
|
||||
animation: slideIn 0.2s ease;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
.message.user-message {
|
||||
align-self: flex-end;
|
||||
background: var(--user);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.message.bot-message {
|
||||
align-self: flex-start;
|
||||
background: var(--bot);
|
||||
color: #3c3f44;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.message.error-message {
|
||||
align-self: flex-start;
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-ink);
|
||||
border: 1px solid #f3bcbc;
|
||||
}
|
||||
|
||||
.message.typing-indicator {
|
||||
align-self: flex-start;
|
||||
background: #eef2f7;
|
||||
color: #475569;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chat-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: #fbf8ef;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
border: 1px solid #d8cfbd;
|
||||
border-radius: 12px;
|
||||
padding: 11px 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
padding: 0 16px;
|
||||
min-width: 86px;
|
||||
}
|
||||
|
||||
.send-btn:disabled,
|
||||
.set-endpoint-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
padding: 8px 14px 14px;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@@ -121,281 +301,274 @@
|
||||
}
|
||||
}
|
||||
|
||||
.user-message {
|
||||
align-self: flex-end;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
align-self: flex-start;
|
||||
background-color: #e9ecef;
|
||||
color: #495057;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
align-self: flex-start;
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.input-area input {
|
||||
flex: 1;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 24px;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 24px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.input-area button:hover {
|
||||
background-color: #1a68e8;
|
||||
}
|
||||
|
||||
.input-area button:disabled {
|
||||
background-color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
align-self: flex-start;
|
||||
background-color: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
@media (max-width: 720px) {
|
||||
.panel-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.api-endpoint-container {
|
||||
.chat-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.api-endpoint-container input {
|
||||
min-width: auto;
|
||||
.chat-header-endpoint {
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 90%;
|
||||
max-width: 92%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>RAG Solution Chat Interface</h1>
|
||||
<div class="api-endpoint-container">
|
||||
<label for="apiEndpoint">API Endpoint:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiEndpoint"
|
||||
value="http://localhost:8000/api/test-query"
|
||||
placeholder="Enter API endpoint URL"
|
||||
/>
|
||||
<button onclick="setApiEndpoint()">Set Endpoint</button>
|
||||
<div class="app">
|
||||
<div class="shell">
|
||||
<div class="shell-header">
|
||||
<h1>RAG Demo Control Room</h1>
|
||||
<p>Multiple chat windows with independent API endpoints.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">Chat with RAG Agent</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="message bot-message">
|
||||
Hello! I'm your RAG agent. Please enter your API endpoint and start
|
||||
chatting.
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<input
|
||||
type="text"
|
||||
id="userInput"
|
||||
placeholder="Type your message here..."
|
||||
onkeypress="handleKeyPress(event)"
|
||||
/>
|
||||
<button onclick="sendMessage()" id="sendButton">Send</button>
|
||||
</div>
|
||||
<div class="tabs-bar" id="tabsBar"></div>
|
||||
<div class="panel-host" id="panelHost"></div>
|
||||
<div class="footer-note">Phase 16 scalable demo UI: reusable chat panel + tabbed windows.</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>RAG Solution with LangChain | Chat Interface Demo</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<template id="chatPanelTemplate">
|
||||
<div class="chat-panel">
|
||||
<div class="panel-toolbar">
|
||||
<label>API Endpoint</label>
|
||||
<input class="endpoint-input" type="text" placeholder="Enter API endpoint URL" />
|
||||
<button class="set-endpoint-btn" type="button">Set Endpoint</button>
|
||||
</div>
|
||||
|
||||
<div class="chat-card">
|
||||
<div class="chat-header">
|
||||
<div class="chat-header-title">Chat with RAG Agent</div>
|
||||
<div class="chat-header-endpoint">Endpoint: not set</div>
|
||||
</div>
|
||||
<div class="chat-messages"></div>
|
||||
<div class="chat-input-row">
|
||||
<input class="message-input" type="text" placeholder="Type your message here..." />
|
||||
<button class="send-btn" type="button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Store the API endpoint
|
||||
let apiEndpoint = document.getElementById("apiEndpoint").value;
|
||||
const DEFAULT_TAB_ENDPOINTS = [
|
||||
"https://rag.langchain.overwatch.su/api/test-query",
|
||||
"https://rag.llamaindex.overwatch.su/api/test-query",
|
||||
"https://rag.haystack.overwatch.su/api/test-query",
|
||||
];
|
||||
|
||||
// Set the API endpoint from the input field
|
||||
function setApiEndpoint() {
|
||||
const input = document.getElementById("apiEndpoint");
|
||||
apiEndpoint = input.value.trim();
|
||||
class ChatPanel {
|
||||
constructor(rootElement, initialEndpoint = "") {
|
||||
this.root = rootElement;
|
||||
this.apiEndpoint = initialEndpoint;
|
||||
|
||||
if (!apiEndpoint) {
|
||||
alert("Please enter a valid API endpoint URL");
|
||||
return;
|
||||
}
|
||||
this.endpointInput = this.root.querySelector(".endpoint-input");
|
||||
this.setEndpointButton = this.root.querySelector(".set-endpoint-btn");
|
||||
this.headerEndpoint = this.root.querySelector(".chat-header-endpoint");
|
||||
this.messagesEl = this.root.querySelector(".chat-messages");
|
||||
this.messageInput = this.root.querySelector(".message-input");
|
||||
this.sendButton = this.root.querySelector(".send-btn");
|
||||
|
||||
// Add notification that endpoint was set
|
||||
addMessage(`API endpoint set to: ${apiEndpoint}`, "bot-message");
|
||||
}
|
||||
|
||||
// Send a message to the API
|
||||
async function sendMessage() {
|
||||
const inputElement = document.getElementById("userInput");
|
||||
const message = inputElement.value.trim();
|
||||
const sendButton = document.getElementById("sendButton");
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiEndpoint) {
|
||||
alert("Please set the API endpoint first");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the send button and input during request
|
||||
sendButton.disabled = true;
|
||||
inputElement.disabled = true;
|
||||
|
||||
try {
|
||||
// Add user message to chat
|
||||
addMessage(message, "user-message");
|
||||
|
||||
// Clear input
|
||||
inputElement.value = "";
|
||||
|
||||
// Show typing indicator
|
||||
const typingIndicator = addMessage(
|
||||
"Thinking...",
|
||||
"typing-indicator",
|
||||
"typing",
|
||||
this.endpointInput.value = initialEndpoint;
|
||||
this._renderEndpointLabel();
|
||||
this._bindEvents();
|
||||
this.addMessage(
|
||||
initialEndpoint
|
||||
? `Ready. Endpoint preset to: ${initialEndpoint}`
|
||||
: "Hello. Set an API endpoint and start chatting.",
|
||||
"bot-message",
|
||||
);
|
||||
}
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(apiEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: message,
|
||||
}),
|
||||
_bindEvents() {
|
||||
this.setEndpointButton.addEventListener("click", () => this.setApiEndpoint());
|
||||
this.sendButton.addEventListener("click", () => this.sendMessage());
|
||||
this.messageInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove typing indicator
|
||||
removeMessage(typingIndicator);
|
||||
focusInput() {
|
||||
this.messageInput.focus();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`API request failed with status ${response.status}`,
|
||||
);
|
||||
_renderEndpointLabel() {
|
||||
this.headerEndpoint.textContent = this.apiEndpoint
|
||||
? `Endpoint: ${this.apiEndpoint}`
|
||||
: "Endpoint: not set";
|
||||
}
|
||||
|
||||
setApiEndpoint() {
|
||||
const candidate = this.endpointInput.value.trim();
|
||||
if (!candidate) {
|
||||
alert("Please enter a valid API endpoint URL");
|
||||
return;
|
||||
}
|
||||
this.apiEndpoint = candidate;
|
||||
this._renderEndpointLabel();
|
||||
this.addMessage(`API endpoint set to: ${candidate}`, "bot-message");
|
||||
}
|
||||
|
||||
addMessage(text, className, extraClass = "") {
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.className = `message ${className} ${extraClass}`.trim();
|
||||
messageDiv.innerHTML = String(text).replace(/\n/g, "<br>");
|
||||
this.messagesEl.appendChild(messageDiv);
|
||||
this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
|
||||
return messageDiv;
|
||||
}
|
||||
|
||||
removeMessage(node) {
|
||||
if (node && node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const message = this.messageInput.value.trim();
|
||||
if (!message) return;
|
||||
if (!this.apiEndpoint) {
|
||||
alert("Please set the API endpoint first");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.sendButton.disabled = true;
|
||||
this.messageInput.disabled = true;
|
||||
|
||||
// Add bot response to chat
|
||||
if (data.success) {
|
||||
addMessage(data.response, "bot-message");
|
||||
} else {
|
||||
addMessage(
|
||||
`Error: ${data.error || "Unknown error occurred"}`,
|
||||
let typingIndicator = null;
|
||||
try {
|
||||
this.addMessage(message, "user-message");
|
||||
this.messageInput.value = "";
|
||||
|
||||
typingIndicator = this.addMessage("Thinking...", "typing-indicator", "typing");
|
||||
|
||||
const response = await fetch(this.apiEndpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: message }),
|
||||
});
|
||||
|
||||
this.removeMessage(typingIndicator);
|
||||
typingIndicator = null;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.addMessage(data.response, "bot-message");
|
||||
} else {
|
||||
this.addMessage(
|
||||
`Error: ${data.error || "Unknown error occurred"}`,
|
||||
"error-message",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (typingIndicator) this.removeMessage(typingIndicator);
|
||||
this.addMessage(
|
||||
`Connection error: ${error.message}. Please check the API endpoint and try again. Reload page if needed to reinitialize endpoint state.`,
|
||||
"error-message",
|
||||
);
|
||||
} finally {
|
||||
this.sendButton.disabled = false;
|
||||
this.messageInput.disabled = false;
|
||||
this.messageInput.focus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
// Remove typing indicator if still present
|
||||
const typingElements = document.querySelectorAll(".typing");
|
||||
typingElements.forEach((el) => el.remove());
|
||||
|
||||
// Add error message to chat
|
||||
addMessage(
|
||||
`Connection error: ${error.message}. Please check the API endpoint and try again.`,
|
||||
"error-message",
|
||||
);
|
||||
} finally {
|
||||
// Re-enable the send button and input
|
||||
sendButton.disabled = false;
|
||||
inputElement.disabled = false;
|
||||
inputElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Add a message to the chat
|
||||
function addMessage(text, className, id = null) {
|
||||
const chatMessages = document.getElementById("chatMessages");
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.className = `message ${className}`;
|
||||
|
||||
if (id) {
|
||||
messageDiv.id = id;
|
||||
class MultiChatApp {
|
||||
constructor() {
|
||||
this.tabsBar = document.getElementById("tabsBar");
|
||||
this.panelHost = document.getElementById("panelHost");
|
||||
this.panelTemplate = document.getElementById("chatPanelTemplate");
|
||||
this.tabs = [];
|
||||
this.activeTabId = null;
|
||||
this.nextTabNumber = 1;
|
||||
}
|
||||
|
||||
// Format text with line breaks
|
||||
const formattedText = text.replace(/\n/g, "<br>");
|
||||
messageDiv.innerHTML = formattedText;
|
||||
init() {
|
||||
DEFAULT_TAB_ENDPOINTS.forEach((endpoint) => this.createTab(endpoint));
|
||||
if (this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0].id);
|
||||
}
|
||||
this.renderTabs();
|
||||
}
|
||||
|
||||
chatMessages.appendChild(messageDiv);
|
||||
createTab(initialEndpoint = "") {
|
||||
const tab = {
|
||||
id: `tab-${crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`}`,
|
||||
title: String(this.nextTabNumber++),
|
||||
endpoint: initialEndpoint,
|
||||
panel: null,
|
||||
panelNode: null,
|
||||
};
|
||||
|
||||
// Scroll to bottom
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
const fragment = this.panelTemplate.content.cloneNode(true);
|
||||
const panelNode = fragment.firstElementChild;
|
||||
const panel = new ChatPanel(panelNode, initialEndpoint);
|
||||
tab.panel = panel;
|
||||
tab.panelNode = panelNode;
|
||||
this.tabs.push(tab);
|
||||
this.renderTabs();
|
||||
return tab;
|
||||
}
|
||||
|
||||
return messageDiv;
|
||||
}
|
||||
selectTab(tabId) {
|
||||
this.activeTabId = tabId;
|
||||
const tab = this.tabs.find((item) => item.id === tabId);
|
||||
if (!tab) return;
|
||||
|
||||
// Remove a message from the chat
|
||||
function removeMessage(element) {
|
||||
if (element && element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
this.panelHost.innerHTML = "";
|
||||
this.panelHost.appendChild(tab.panelNode);
|
||||
tab.panel._renderEndpointLabel();
|
||||
tab.panel.focusInput();
|
||||
this.renderTabs();
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
this.tabsBar.innerHTML = "";
|
||||
|
||||
this.tabs.forEach((tab) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = `tab-btn ${tab.id === this.activeTabId ? "active" : ""}`.trim();
|
||||
btn.textContent = tab.title;
|
||||
btn.title = tab.endpoint || `Tab ${tab.title}`;
|
||||
btn.addEventListener("click", () => this.selectTab(tab.id));
|
||||
this.tabsBar.appendChild(btn);
|
||||
});
|
||||
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.type = "button";
|
||||
addBtn.className = "tab-btn add-tab";
|
||||
addBtn.textContent = "+";
|
||||
addBtn.title = "Add new tab";
|
||||
addBtn.addEventListener("click", () => {
|
||||
const tab = this.createTab("");
|
||||
this.selectTab(tab.id);
|
||||
});
|
||||
this.tabsBar.appendChild(addBtn);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Enter key press in the input field
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === "Enter") {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Focus on the input field when the page loads
|
||||
window.onload = function () {
|
||||
document.getElementById("userInput").focus();
|
||||
};
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const app = new MultiChatApp();
|
||||
app.init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user