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
.DS_Store vendored

Binary file not shown.

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;
@media (max-width: 720px) {
.panel-toolbar {
grid-template-columns: 1fr;
}
.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;
}
.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 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>
</div>
<footer>
<p>RAG Solution with LangChain | Chat Interface Demo</p>
</footer>
<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) {
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");
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",
);
}
_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();
}
});
}
focusInput() {
this.messageInput.focus();
}
_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;
}
// Add notification that endpoint was set
addMessage(`API endpoint set to: ${apiEndpoint}`, "bot-message");
this.apiEndpoint = candidate;
this._renderEndpointLabel();
this.addMessage(`API endpoint set to: ${candidate}`, "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;
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;
}
if (!apiEndpoint) {
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;
}
// Disable the send button and input during request
sendButton.disabled = true;
inputElement.disabled = true;
this.sendButton.disabled = true;
this.messageInput.disabled = true;
let typingIndicator = null;
try {
// Add user message to chat
addMessage(message, "user-message");
this.addMessage(message, "user-message");
this.messageInput.value = "";
// Clear input
inputElement.value = "";
typingIndicator = this.addMessage("Thinking...", "typing-indicator", "typing");
// Show typing indicator
const typingIndicator = addMessage(
"Thinking...",
"typing-indicator",
"typing",
);
// Send request to API
const response = await fetch(apiEndpoint, {
const response = await fetch(this.apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: message,
}),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: message }),
});
// Remove typing indicator
removeMessage(typingIndicator);
this.removeMessage(typingIndicator);
typingIndicator = null;
if (!response.ok) {
throw new Error(
`API request failed with status ${response.status}`,
);
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
// Add bot response to chat
if (data.success) {
addMessage(data.response, "bot-message");
this.addMessage(data.response, "bot-message");
} else {
addMessage(
this.addMessage(
`Error: ${data.error || "Unknown error occurred"}`,
"error-message",
);
}
} 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.`,
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 {
// Re-enable the send button and input
sendButton.disabled = false;
inputElement.disabled = false;
inputElement.focus();
this.sendButton.disabled = false;
this.messageInput.disabled = false;
this.messageInput.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;
chatMessages.appendChild(messageDiv);
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageDiv;
init() {
DEFAULT_TAB_ENDPOINTS.forEach((endpoint) => this.createTab(endpoint));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[0].id);
}
this.renderTabs();
}
// Remove a message from the chat
function removeMessage(element) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
// 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();
createTab(initialEndpoint = "") {
const tab = {
id: `tab-${crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`}`,
title: String(this.nextTabNumber++),
endpoint: initialEndpoint,
panel: null,
panelNode: null,
};
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;
}
selectTab(tabId) {
this.activeTabId = tabId;
const tab = this.tabs.find((item) => item.id === tabId);
if (!tab) return;
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);
}
}
window.addEventListener("DOMContentLoaded", () => {
const app = new MultiChatApp();
app.init();
});
</script>
</body>
</html>