Skip to content

Commit 68db1b5

Browse files
author
beaglebyte
committed
Frontend
1 parent b65e744 commit 68db1b5

File tree

4 files changed

+677
-2
lines changed

4 files changed

+677
-2
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,29 @@
1-
# Spring-AI-Topic-RAG
2-
Spring AI RAG with different topics for usage
1+
# Spring AI RAG Multi-Topic System
2+
3+
A production-ready multi-topic Retrieval-Augmented Generation (RAG) system built with Spring AI, Ollama, and Qdrant.
4+
5+
## Features
6+
7+
**Multi-Topic RAGs**: Separate isolated RAGs for different domains (Pentesting, IoT, Blockchain, Cloud, etc.)
8+
🔍 **Intelligent Retrieval**: Semantic search using vector embeddings
9+
📄 **Document Support**: PDF and Markdown files with automatic metadata extraction
10+
🤖 **Local LLM**: Runs entirely locally using Ollama
11+
🔒 **No External APIs**: All processing happens on your machine
12+
**Fast Indexing**: Efficient vector storage with Qdrant
13+
🚀 **Easy to Extend**: Add new topics with simple configuration
14+
15+
## Quick Start
16+
17+
### Prerequisites
18+
19+
- Java 17+
20+
- Docker & Docker Compose
21+
- Ollama installed locally
22+
- Maven
23+
24+
### Installation
25+
26+
1. **Clone the repository**
27+
```bash
28+
git clone https://github.com/beaglebyte/spring-ai-rag-multi-topic.git
29+
cd spring-ai-rag-multi-topic

frontend/index.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Spring AI RAG - Multi-Topic</title>
7+
<link rel="stylesheet" href="style.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<!-- Header -->
12+
<div class="header">
13+
<h1>🤖 Spring AI RAG Multi-Topic</h1>
14+
<p>Separate RAGs for different knowledge domains</p>
15+
</div>
16+
17+
<!-- Topic Selector -->
18+
<div class="section">
19+
<h2>📚 Select Topic</h2>
20+
<div class="topic-selector">
21+
<select id="topicSelect">
22+
<option value="">-- Choose a Topic --</option>
23+
</select>
24+
<div id="topicInfo" class="topic-info"></div>
25+
</div>
26+
</div>
27+
28+
<!-- Upload Section -->
29+
<div class="section">
30+
<h2>📤 Upload Documents</h2>
31+
32+
<!-- PDF Upload -->
33+
<div style="margin-bottom: 20px;">
34+
<h3>PDF Files</h3>
35+
<div class="upload-area" id="pdfUploadArea">
36+
<div>📄 Drag & drop PDF files or click to select</div>
37+
<input type="file" id="pdfInput" accept=".pdf" multiple>
38+
</div>
39+
<div id="pdfStatus"></div>
40+
</div>
41+
42+
<!-- Markdown Upload -->
43+
<div>
44+
<h3>Markdown Files</h3>
45+
<div class="upload-area" id="mdUploadArea">
46+
<div>📝 Drag & drop Markdown files or click to select</div>
47+
<input type="file" id="mdInput" accept=". md" multiple>
48+
</div>
49+
<div id="mdStatus"></div>
50+
</div>
51+
52+
<!-- File List -->
53+
<div class="file-list" id="fileList" style="display: none;">
54+
<h3 style="margin-bottom: 10px;">📚 Uploaded Files</h3>
55+
<div id="fileItems"></div>
56+
</div>
57+
</div>
58+
59+
<!-- Query Section -->
60+
<div class="section">
61+
<h2>❓ Ask a Question</h2>
62+
<div class="query-box">
63+
<input type="text" id="queryInput" placeholder="Ask anything about your documents...">
64+
<button class="btn" onclick="submitQuery()">Search & Answer</button>
65+
</div>
66+
<div class="loader" id="loader">
67+
<div class="spinner"></div>
68+
<p>Processing your query...</p>
69+
</div>
70+
<div id="result"></div>
71+
</div>
72+
73+
<!-- Stats Section -->
74+
<div class="section">
75+
<h2>📊 System Status</h2>
76+
<div id="stats"></div>
77+
</div>
78+
</div>
79+
80+
<script src="script.js"></script>
81+
</body>
82+
</html>

frontend/script.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
const API_BASE = 'http://localhost:8080/api/v1';
2+
let uploadedFiles = [];
3+
let selectedTopic = null;
4+
5+
// Initialize
6+
document.addEventListener('DOMContentLoaded', () => {
7+
loadTopics();
8+
loadStats();
9+
setupUploadListeners();
10+
});
11+
12+
// Load topics
13+
async function loadTopics() {
14+
try {
15+
const response = await fetch(`${API_BASE}/topics`);
16+
const topics = await response.json();
17+
18+
const select = document.getElementById('topicSelect');
19+
Object.keys(topics).forEach(topic => {
20+
const option = document.createElement('option');
21+
option.value = topic;
22+
option.textContent = topic. charAt(0).toUpperCase() + topic.slice(1);
23+
select.appendChild(option);
24+
});
25+
26+
select.addEventListener('change', (e) => {
27+
selectedTopic = e.target.value;
28+
if (selectedTopic) {
29+
const info = topics[selectedTopic];
30+
const infoDiv = document.getElementById('topicInfo');
31+
infoDiv.innerHTML = `<strong>${info.description}</strong>`;
32+
infoDiv.classList.add('show');
33+
}
34+
});
35+
} catch (error) {
36+
console.error('Error loading topics:', error);
37+
}
38+
}
39+
40+
// Setup upload listeners
41+
function setupUploadListeners() {
42+
setupUploadArea('pdf', '#pdfUploadArea', '#pdfInput', '#pdfStatus', uploadPdf);
43+
setupUploadArea('md', '#mdUploadArea', '#mdInput', '#mdStatus', uploadMarkdown);
44+
}
45+
46+
function setupUploadArea(type, areaSelector, inputSelector, statusSelector, uploadHandler) {
47+
const area = document.querySelector(areaSelector);
48+
const input = document. querySelector(inputSelector);
49+
50+
area.addEventListener('click', () => input.click());
51+
area.addEventListener('dragover', (e) => e.preventDefault());
52+
area.addEventListener('drop', (e) => {
53+
e.preventDefault();
54+
uploadHandler(e.dataTransfer.files);
55+
});
56+
input.addEventListener('change', (e) => uploadHandler(e.target.files));
57+
}
58+
59+
async function uploadPdf(files) {
60+
if (! selectedTopic) {
61+
alert('Please select a topic first');
62+
return;
63+
}
64+
65+
for (let file of files) {
66+
const formData = new FormData();
67+
formData.append('file', file);
68+
69+
const statusDiv = document.getElementById('pdfStatus');
70+
statusDiv.innerHTML = `<div class="status info">⏳ Uploading ${file.name}...</div>`;
71+
72+
try {
73+
const response = await fetch(
74+
`${API_BASE}/topics/${selectedTopic}/documents/upload/pdf`,
75+
{ method: 'POST', body: formData }
76+
);
77+
78+
if (response.ok) {
79+
const data = await response.json();
80+
uploadedFiles.push(data);
81+
statusDiv.innerHTML = `<div class="status success">✅ ${file.name} - ${data.chunksCount} chunks indexed</div>`;
82+
updateFileList();
83+
} else {
84+
statusDiv. innerHTML = `<div class="status error">❌ Failed to upload ${file.name}</div>`;
85+
}
86+
} catch (error) {
87+
statusDiv.innerHTML = `<div class="status error">❌ Error: ${error.message}</div>`;
88+
}
89+
}
90+
}
91+
92+
async function uploadMarkdown(files) {
93+
if (!selectedTopic) {
94+
alert('Please select a topic first');
95+
return;
96+
}
97+
98+
for (let file of files) {
99+
const formData = new FormData();
100+
formData.append('file', file);
101+
102+
const statusDiv = document.getElementById('mdStatus');
103+
statusDiv.innerHTML = `<div class="status info">⏳ Uploading ${file.name}... </div>`;
104+
105+
try {
106+
const response = await fetch(
107+
`${API_BASE}/topics/${selectedTopic}/documents/upload/markdown`,
108+
{ method: 'POST', body: formData }
109+
);
110+
111+
if (response.ok) {
112+
const data = await response.json();
113+
uploadedFiles.push(data);
114+
statusDiv.innerHTML = `<div class="status success">✅ ${file.name} - ${data. chunksCount} chunks indexed</div>`;
115+
updateFileList();
116+
} else {
117+
statusDiv.innerHTML = `<div class="status error">❌ Failed to upload ${file.name}</div>`;
118+
}
119+
} catch (error) {
120+
statusDiv.innerHTML = `<div class="status error">❌ Error: ${error.message}</div>`;
121+
}
122+
}
123+
}
124+
125+
function updateFileList() {
126+
if (uploadedFiles.length > 0) {
127+
document.getElementById('fileList').style.display = 'block';
128+
document.getElementById('fileItems').innerHTML = uploadedFiles
129+
.map(f => `
130+
<div class="file-item">
131+
<div class="info">
132+
<strong>${f.filename}</strong>
133+
<p>${f.chunksCount} chunks • ${f.type. toUpperCase()}</p>
134+
</div>
135+
<span class="badge">${f.status}</span>
136+
</div>
137+
`)
138+
.join('');
139+
}
140+
}
141+
142+
async function submitQuery() {
143+
if (!selectedTopic) {
144+
alert('Please select a topic first');
145+
return;
146+
}
147+
148+
const query = document.getElementById('queryInput').value.trim();
149+
if (!query) {
150+
alert('Please enter a question');
151+
return;
152+
}
153+
154+
const loader = document.getElementById('loader');
155+
const resultDiv = document.getElementById('result');
156+
157+
loader.classList.add('active');
158+
resultDiv.innerHTML = '';
159+
160+
try {
161+
const response = await fetch(
162+
`${API_BASE}/topics/${selectedTopic}/query`,
163+
{
164+
method: 'POST',
165+
headers: { 'Content-Type': 'application/json' },
166+
body: JSON.stringify({ query, topK: 5 })
167+
}
168+
);
169+
170+
const data = await response.json();
171+
172+
let sourcesHtml = `<strong>Sources (${data.sourceCount}):</strong>`;
173+
if (data.sources && data.sources.length > 0) {
174+
sourcesHtml += data.sources.map(s => `
175+
<div class="source-item">
176+
<div class="title">${s.filename}</div>
177+
<div class="meta">${s.title}${s.author ? ' • ' + s.author : ''}${s.publishingYear ? ' (' + s.publishingYear + ')' : ''}</div>
178+
</div>
179+
`).join('');
180+
}
181+
182+
resultDiv.innerHTML = `
183+
<div class="result">
184+
<div class="answer">
185+
<strong>Answer:</strong><br>
186+
${data. answer}
187+
</div>
188+
<div class="sources">
189+
${sourcesHtml}
190+
</div>
191+
</div>
192+
`;
193+
} catch (error) {
194+
resultDiv.innerHTML = `<div class="status error">Error: ${error.message}</div>`;
195+
} finally {
196+
loader.classList.remove('active');
197+
}
198+
}
199+
200+
async function loadStats() {
201+
try {
202+
const response = await fetch(`${API_BASE}/topics/stats`);
203+
const stats = await response.json();
204+
205+
const statsDiv = document. getElementById('stats');
206+
statsDiv.innerHTML = Object.entries(stats)
207+
.map(([topic, info]) => `
208+
<div class="stat-card">
209+
<div class="stat-label">${topic.toUpperCase()}</div>
210+
<div class="stat-value">${info.vectors_count || 0}</div>
211+
<div class="stat-label">Vectors</div>
212+
</div>
213+
`)
214+
.join('');
215+
} catch (error) {
216+
console.error('Error loading stats:', error);
217+
}
218+
}
219+
220+
document.getElementById('queryInput')?. addEventListener('keypress', (e) => {
221+
if (e. key === 'Enter') submitQuery();
222+
});

0 commit comments

Comments
 (0)