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

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>