demo ui now has 3 tabs for each rag OSS solution

This commit is contained in:
2026-02-26 15:05:18 +03:00
parent 3e29ea70ed
commit ba1b7abf0a
5 changed files with 469 additions and 296 deletions

BIN
services/.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@@ -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"

View File

@@ -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>