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) # 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. - [x] 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 - [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
- [ ] 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] 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] 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> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RAG Solution Chat Interface</title> <title>RAG Multi-Window Demo</title>
<style> <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; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
} }
body { body {
background-color: #f5f7fa; background:
color: #333; radial-gradient(circle at 15% 15%, #f9d9a8 0%, transparent 35%),
line-height: 1.6; 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 { .app {
max-width: 900px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px 14px 24px;
} }
header { .shell {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); border: 1px solid rgba(70, 62, 43, 0.15);
color: white; border-radius: 16px;
padding: 20px; background: rgba(255, 253, 247, 0.92);
border-radius: 10px; box-shadow: 0 18px 45px rgba(47, 41, 30, 0.12);
margin-bottom: 20px; overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); backdrop-filter: blur(6px);
} }
h1 { .shell-header {
font-size: 1.8rem; padding: 14px 18px;
margin-bottom: 10px; 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 { .shell-header h1 {
display: flex; font-size: 1.25rem;
gap: 10px; letter-spacing: 0.02em;
margin-top: 15px;
flex-wrap: wrap;
} }
.api-endpoint-container label { .shell-header p {
margin-top: 4px;
color: var(--muted);
font-size: 0.92rem;
}
.tabs-bar {
display: flex; display: flex;
align-items: center; 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 { .tab-btn {
flex: 1; border: 1px solid var(--line);
min-width: 300px; background: #faf7ee;
color: var(--ink);
padding: 8px 12px; padding: 8px 12px;
border: none; border-radius: 999px;
border-radius: 4px;
margin-left: 5px;
}
.api-endpoint-container button {
background-color: #fff;
color: #2575fc;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: 700;
transition: background-color 0.3s; white-space: nowrap;
transition: transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
} }
.api-endpoint-container button:hover { .tab-btn:hover {
background-color: #e6f0ff; transform: translateY(-1px);
border-color: #c9bea6;
} }
.chat-container { .tab-btn.active {
background-color: white; 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; 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; overflow: hidden;
height: 60vh; background: #fff;
min-height: 62vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.chat-header { .chat-header {
background-color: #f8f9fa; display: flex;
padding: 15px; justify-content: space-between;
border-bottom: 1px solid #eaeaea; align-items: center;
font-weight: bold; gap: 12px;
color: #495057; 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 { .chat-messages {
flex: 1; flex: 1;
padding: 20px;
overflow-y: auto; overflow-y: auto;
padding: 14px;
display: flex; display: flex;
flex-direction: column; 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 { .message {
max-width: 80%; max-width: 82%;
padding: 12px 16px; padding: 11px 13px;
border-radius: 18px; border-radius: 14px;
position: relative; line-height: 1.45;
animation: fadeIn 0.3s ease; 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 { from {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(6px);
} }
to { to {
opacity: 1; opacity: 1;
@@ -121,281 +301,274 @@
} }
} }
.user-message { @media (max-width: 720px) {
align-self: flex-end; .panel-toolbar {
background-color: #2575fc; grid-template-columns: 1fr;
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;
} }
.api-endpoint-container { .chat-header {
flex-direction: column; flex-direction: column;
align-items: flex-start;
} }
.api-endpoint-container input { .chat-header-endpoint {
min-width: auto; text-align: left;
white-space: normal;
word-break: break-all;
} }
.message { .message {
max-width: 90%; max-width: 92%;
} }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="app">
<header> <div class="shell">
<h1>RAG Solution Chat Interface</h1> <div class="shell-header">
<div class="api-endpoint-container"> <h1>RAG Demo Control Room</h1>
<label for="apiEndpoint">API Endpoint:</label> <p>Multiple chat windows with independent API endpoints.</p>
<input
type="text"
id="apiEndpoint"
value="http://localhost:8000/api/test-query"
placeholder="Enter API endpoint URL"
/>
<button onclick="setApiEndpoint()">Set Endpoint</button>
</div> </div>
</header>
<div class="chat-container"> <div class="tabs-bar" id="tabsBar"></div>
<div class="chat-header">Chat with RAG Agent</div> <div class="panel-host" id="panelHost"></div>
<div class="chat-messages" id="chatMessages"> <div class="footer-note">Phase 16 scalable demo UI: reusable chat panel + tabbed windows.</div>
<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> </div>
<footer>
<p>RAG Solution with LangChain | Chat Interface Demo</p>
</footer>
</div> </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> <script>
// Store the API endpoint const DEFAULT_TAB_ENDPOINTS = [
let apiEndpoint = document.getElementById("apiEndpoint").value; "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 class ChatPanel {
function setApiEndpoint() { constructor(rootElement, initialEndpoint = "") {
const input = document.getElementById("apiEndpoint"); this.root = rootElement;
apiEndpoint = input.value.trim(); this.apiEndpoint = initialEndpoint;
if (!apiEndpoint) { this.endpointInput = this.root.querySelector(".endpoint-input");
alert("Please enter a valid API endpoint URL"); this.setEndpointButton = this.root.querySelector(".set-endpoint-btn");
return; 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 this.endpointInput.value = initialEndpoint;
addMessage(`API endpoint set to: ${apiEndpoint}`, "bot-message"); this._renderEndpointLabel();
} this._bindEvents();
this.addMessage(
// Send a message to the API initialEndpoint
async function sendMessage() { ? `Ready. Endpoint preset to: ${initialEndpoint}`
const inputElement = document.getElementById("userInput"); : "Hello. Set an API endpoint and start chatting.",
const message = inputElement.value.trim(); "bot-message",
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",
); );
}
// Send request to API _bindEvents() {
const response = await fetch(apiEndpoint, { this.setEndpointButton.addEventListener("click", () => this.setApiEndpoint());
method: "POST", this.sendButton.addEventListener("click", () => this.sendMessage());
headers: { this.messageInput.addEventListener("keydown", (event) => {
"Content-Type": "application/json", if (event.key === "Enter") {
}, event.preventDefault();
body: JSON.stringify({ this.sendMessage();
query: message, }
}),
}); });
}
// Remove typing indicator focusInput() {
removeMessage(typingIndicator); this.messageInput.focus();
}
if (!response.ok) { _renderEndpointLabel() {
throw new Error( this.headerEndpoint.textContent = this.apiEndpoint
`API request failed with status ${response.status}`, ? `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 let typingIndicator = null;
if (data.success) { try {
addMessage(data.response, "bot-message"); this.addMessage(message, "user-message");
} else { this.messageInput.value = "";
addMessage(
`Error: ${data.error || "Unknown error occurred"}`, 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", "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 class MultiChatApp {
function addMessage(text, className, id = null) { constructor() {
const chatMessages = document.getElementById("chatMessages"); this.tabsBar = document.getElementById("tabsBar");
const messageDiv = document.createElement("div"); this.panelHost = document.getElementById("panelHost");
messageDiv.className = `message ${className}`; this.panelTemplate = document.getElementById("chatPanelTemplate");
this.tabs = [];
if (id) { this.activeTabId = null;
messageDiv.id = id; this.nextTabNumber = 1;
} }
// Format text with line breaks init() {
const formattedText = text.replace(/\n/g, "<br>"); DEFAULT_TAB_ENDPOINTS.forEach((endpoint) => this.createTab(endpoint));
messageDiv.innerHTML = formattedText; 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 const fragment = this.panelTemplate.content.cloneNode(true);
chatMessages.scrollTop = chatMessages.scrollHeight; 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 this.panelHost.innerHTML = "";
function removeMessage(element) { this.panelHost.appendChild(tab.panelNode);
if (element && element.parentNode) { tab.panel._renderEndpointLabel();
element.parentNode.removeChild(element); 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 window.addEventListener("DOMContentLoaded", () => {
function handleKeyPress(event) { const app = new MultiChatApp();
if (event.key === "Enter") { app.init();
sendMessage(); });
}
}
// Focus on the input field when the page loads
window.onload = function () {
document.getElementById("userInput").focus();
};
</script> </script>
</body> </body>
</html> </html>