rails_chatbot 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +20 -3
- data/app/assets/config/rails_chatbot_manifest.js +4 -0
- data/app/controllers/rails_chatbot/chat_controller.rb +5 -0
- data/app/helpers/rails_chatbot/application_helper.rb +8 -0
- data/app/javascript/rails_chatbot/application.js +8 -8
- data/app/javascript/rails_chatbot/widget.js +201 -0
- data/app/services/rails_chatbot/application_content_service.rb +233 -0
- data/app/services/rails_chatbot/chat_service.rb +30 -10
- data/app/views/layouts/rails_chatbot/application.html.erb +4 -0
- data/app/views/rails_chatbot/chat/index.html.erb +5 -7
- data/app/views/rails_chatbot/shared/_chat_widget.html.erb +56 -0
- data/lib/rails_chatbot/version.rb +1 -1
- data/lib/rails_chatbot.rb +8 -1
- data/lib/tasks/rails_chatbot_tasks.rake +29 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c20ad83f73c15bb9cbed232cb5a2425aeef5ab8f430892cac4cf342b57fe3135
|
|
4
|
+
data.tar.gz: ea50ffd6cd98868149cbd3519637bd0502af3a244313f654d8f0f636f9710cfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44d85729a3c0f7c20a593f06828dbae018b6efaa4e1fa604d1c631143458e9f46598a82b22d35a6a49eecef58caa8cb6a9f578b9cf1e9849ef351a3632fa7977
|
|
7
|
+
data.tar.gz: cac2f5c9ba771535c109707a3fa5289a4467bb89994de80772d15009984e8abb8b0483ac321a1e7a0dabe9e9efee0b203539602df6e0a3cc7e97335a3f5dafbe
|
data/README.md
CHANGED
|
@@ -55,13 +55,30 @@ Rails.application.routes.draw do
|
|
|
55
55
|
end
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
### Step 5:
|
|
58
|
+
### Step 5: Enable the Widget (Optional)
|
|
59
|
+
|
|
60
|
+
To enable the floating chatbot widget, add this to your application layout:
|
|
61
|
+
|
|
62
|
+
```erb
|
|
63
|
+
<%# In app/views/layouts/application.html.erb %>
|
|
64
|
+
<%= RailsChatbot::ApplicationHelper.new.include_chatbot_widget if RailsChatbot::ApplicationHelper.new.chatbot_widget_enabled? %>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or simply add to any view:
|
|
68
|
+
|
|
69
|
+
```erb
|
|
70
|
+
<%= include_chatbot_widget %>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The widget will automatically appear in the bottom-right corner of your screen.
|
|
74
|
+
|
|
75
|
+
### Step 6: Start Your Server
|
|
59
76
|
|
|
60
77
|
```bash
|
|
61
78
|
rails server
|
|
62
79
|
```
|
|
63
80
|
|
|
64
|
-
Visit `http://localhost:3000/chatbot` to see your chatbot!
|
|
81
|
+
Visit `http://localhost:3000/chatbot` to see your chatbot, or look for the floating chat icon in the bottom-right corner of any page!
|
|
65
82
|
|
|
66
83
|
## 🧪 Testing Your Chatbot
|
|
67
84
|
|
|
@@ -280,7 +297,7 @@ For comprehensive documentation, visit our [Documentation Site](./docs/README.md
|
|
|
280
297
|
|
|
281
298
|
---
|
|
282
299
|
|
|
283
|
-
##
|
|
300
|
+
## �� Requirements
|
|
284
301
|
|
|
285
302
|
- Ruby on Rails 8.0.4 or higher
|
|
286
303
|
- PostgreSQL (for full-text search)
|
|
@@ -2,6 +2,11 @@ module RailsChatbot
|
|
|
2
2
|
class ChatController < ApplicationController
|
|
3
3
|
def index
|
|
4
4
|
# Main chat interface
|
|
5
|
+
@rails_chatbot_routes = {
|
|
6
|
+
conversations_path: rails_chatbot.conversations_path,
|
|
7
|
+
conversation_messages_path_template: rails_chatbot.conversation_messages_path(':id'),
|
|
8
|
+
messages_path: rails_chatbot.messages_path
|
|
9
|
+
}.to_json
|
|
5
10
|
end
|
|
6
11
|
|
|
7
12
|
def search
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Chatbot controller using
|
|
1
|
+
// Chatbot controller using vanilla JavaScript
|
|
2
2
|
document.addEventListener('DOMContentLoaded', function() {
|
|
3
|
-
const chatbotContainer = document.querySelector('
|
|
3
|
+
const chatbotContainer = document.querySelector('.chatbot-container');
|
|
4
4
|
if (!chatbotContainer) return;
|
|
5
5
|
|
|
6
|
-
const messagesContainer =
|
|
7
|
-
const input =
|
|
8
|
-
const sendButton =
|
|
6
|
+
const messagesContainer = document.getElementById('chatbot-messages');
|
|
7
|
+
const input = document.getElementById('chatbot-input');
|
|
8
|
+
const sendButton = document.getElementById('chatbot-send');
|
|
9
9
|
let conversationId = null;
|
|
10
10
|
let isLoading = false;
|
|
11
11
|
|
|
@@ -156,8 +156,8 @@ if (window.RailsChatbot && window.RailsChatbot.routes) {
|
|
|
156
156
|
// Fallback routes
|
|
157
157
|
window.RailsChatbot = window.RailsChatbot || {};
|
|
158
158
|
window.RailsChatbot.routes = {
|
|
159
|
-
conversationsPath: () => '/
|
|
160
|
-
conversationMessagesPath: (id) => `/
|
|
161
|
-
messagesPath: () => '/
|
|
159
|
+
conversationsPath: () => '/chatbot/conversations',
|
|
160
|
+
conversationMessagesPath: (id) => `/chatbot/conversations/${id}/messages`,
|
|
161
|
+
messagesPath: () => '/chatbot/messages'
|
|
162
162
|
};
|
|
163
163
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Chatbot Widget JavaScript
|
|
2
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3
|
+
const chatbotIcon = document.getElementById('chatbot-toggle');
|
|
4
|
+
const chatbotWindow = document.getElementById('chatbot-window');
|
|
5
|
+
const chatbotClose = document.getElementById('chatbot-close');
|
|
6
|
+
const messagesContainer = document.getElementById('chatbot-messages');
|
|
7
|
+
const input = document.getElementById('chatbot-input');
|
|
8
|
+
const sendButton = document.getElementById('chatbot-send');
|
|
9
|
+
const unreadBadge = document.getElementById('unread-badge');
|
|
10
|
+
|
|
11
|
+
let conversationId = null;
|
|
12
|
+
let isLoading = false;
|
|
13
|
+
let isWindowOpen = false;
|
|
14
|
+
|
|
15
|
+
// Toggle chat window
|
|
16
|
+
function toggleChatWindow() {
|
|
17
|
+
isWindowOpen = !isWindowOpen;
|
|
18
|
+
|
|
19
|
+
if (isWindowOpen) {
|
|
20
|
+
chatbotWindow.classList.add('open');
|
|
21
|
+
unreadBadge.style.display = 'none';
|
|
22
|
+
input.focus();
|
|
23
|
+
} else {
|
|
24
|
+
chatbotWindow.classList.remove('open');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Close chat window
|
|
29
|
+
function closeChatWindow() {
|
|
30
|
+
isWindowOpen = false;
|
|
31
|
+
chatbotWindow.classList.remove('open');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Initialize conversation
|
|
35
|
+
function initializeConversation() {
|
|
36
|
+
fetch(window.RailsChatbot.routes.conversationsPath(), {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({})
|
|
43
|
+
})
|
|
44
|
+
.then(response => response.json())
|
|
45
|
+
.then(data => {
|
|
46
|
+
conversationId = data.conversation_id;
|
|
47
|
+
})
|
|
48
|
+
.catch(error => console.error('Error initializing conversation:', error));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Add message to UI
|
|
52
|
+
function addMessage(role, content, knowledgeSources = []) {
|
|
53
|
+
const messageDiv = document.createElement('div');
|
|
54
|
+
messageDiv.className = `message ${role}`;
|
|
55
|
+
|
|
56
|
+
const bubble = document.createElement('div');
|
|
57
|
+
bubble.className = 'message-bubble';
|
|
58
|
+
bubble.textContent = content;
|
|
59
|
+
|
|
60
|
+
messageDiv.appendChild(bubble);
|
|
61
|
+
|
|
62
|
+
if (knowledgeSources && knowledgeSources.length > 0) {
|
|
63
|
+
const sourcesDiv = document.createElement('div');
|
|
64
|
+
sourcesDiv.className = 'knowledge-sources';
|
|
65
|
+
sourcesDiv.innerHTML = 'Sources: ' + knowledgeSources.map(s => {
|
|
66
|
+
if (s.source_url) {
|
|
67
|
+
return `<a href="${s.source_url}" target="_blank">${s.title}</a>`;
|
|
68
|
+
}
|
|
69
|
+
return s.title;
|
|
70
|
+
}).join(', ');
|
|
71
|
+
messageDiv.appendChild(sourcesDiv);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const timeDiv = document.createElement('div');
|
|
75
|
+
timeDiv.className = 'message-time';
|
|
76
|
+
timeDiv.textContent = new Date().toLocaleTimeString();
|
|
77
|
+
messageDiv.appendChild(timeDiv);
|
|
78
|
+
|
|
79
|
+
messagesContainer.appendChild(messageDiv);
|
|
80
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Show loading indicator
|
|
84
|
+
function showLoading() {
|
|
85
|
+
const loadingDiv = document.createElement('div');
|
|
86
|
+
loadingDiv.className = 'message assistant';
|
|
87
|
+
loadingDiv.id = 'loading-message';
|
|
88
|
+
loadingDiv.innerHTML = '<div class="message-bubble"><div class="loading"></div></div>';
|
|
89
|
+
messagesContainer.appendChild(loadingDiv);
|
|
90
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remove loading indicator
|
|
94
|
+
function removeLoading() {
|
|
95
|
+
const loading = document.getElementById('loading-message');
|
|
96
|
+
if (loading) loading.remove();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Show unread indicator
|
|
100
|
+
function showUnreadIndicator() {
|
|
101
|
+
if (!isWindowOpen) {
|
|
102
|
+
unreadBadge.style.display = 'flex';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Send message
|
|
107
|
+
function sendMessage() {
|
|
108
|
+
const message = input.value.trim();
|
|
109
|
+
if (!message || isLoading) return;
|
|
110
|
+
|
|
111
|
+
// Add user message to UI
|
|
112
|
+
addMessage('user', message);
|
|
113
|
+
input.value = '';
|
|
114
|
+
isLoading = true;
|
|
115
|
+
sendButton.disabled = true;
|
|
116
|
+
showLoading();
|
|
117
|
+
|
|
118
|
+
// Determine endpoint
|
|
119
|
+
const url = conversationId
|
|
120
|
+
? window.RailsChatbot.routes.conversationMessagesPath(conversationId)
|
|
121
|
+
: window.RailsChatbot.routes.messagesPath();
|
|
122
|
+
|
|
123
|
+
// Send to server
|
|
124
|
+
fetch(url, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({ message: message })
|
|
131
|
+
})
|
|
132
|
+
.then(response => response.json())
|
|
133
|
+
.then(data => {
|
|
134
|
+
removeLoading();
|
|
135
|
+
|
|
136
|
+
if (data.error) {
|
|
137
|
+
addMessage('assistant', `Error: ${data.error}`);
|
|
138
|
+
} else {
|
|
139
|
+
if (!conversationId && data.conversation_id) {
|
|
140
|
+
conversationId = data.conversation_id;
|
|
141
|
+
}
|
|
142
|
+
addMessage('assistant', data.message.content, data.knowledge_sources || []);
|
|
143
|
+
showUnreadIndicator();
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.catch(error => {
|
|
147
|
+
removeLoading();
|
|
148
|
+
addMessage('assistant', 'Sorry, there was an error processing your message. Please try again.');
|
|
149
|
+
console.error('Error:', error);
|
|
150
|
+
})
|
|
151
|
+
.finally(() => {
|
|
152
|
+
isLoading = false;
|
|
153
|
+
sendButton.disabled = false;
|
|
154
|
+
input.focus();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle key press
|
|
159
|
+
function handleKeyDown(event) {
|
|
160
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
sendMessage();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Attach event listeners
|
|
167
|
+
if (chatbotIcon) chatbotIcon.addEventListener('click', toggleChatWindow);
|
|
168
|
+
if (chatbotClose) chatbotClose.addEventListener('click', closeChatWindow);
|
|
169
|
+
if (sendButton) sendButton.addEventListener('click', sendMessage);
|
|
170
|
+
if (input) input.addEventListener('keydown', handleKeyDown);
|
|
171
|
+
|
|
172
|
+
// Initialize conversation on load
|
|
173
|
+
initializeConversation();
|
|
174
|
+
|
|
175
|
+
// Show welcome message if no messages
|
|
176
|
+
const existingMessages = messagesContainer.querySelectorAll('.message');
|
|
177
|
+
if (existingMessages.length === 0) {
|
|
178
|
+
addMessage('assistant', 'Hello! I\'m your application assistant. How can I help you today?');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Routes helper - populated from server
|
|
183
|
+
if (window.RailsChatbot && window.RailsChatbot.routes) {
|
|
184
|
+
const routes = typeof window.RailsChatbot.routes === 'string'
|
|
185
|
+
? JSON.parse(window.RailsChatbot.routes)
|
|
186
|
+
: window.RailsChatbot.routes;
|
|
187
|
+
|
|
188
|
+
window.RailsChatbot.routes = {
|
|
189
|
+
conversationsPath: () => routes.conversations_path,
|
|
190
|
+
conversationMessagesPath: (id) => routes.conversation_messages_path_template.replace(':id', id),
|
|
191
|
+
messagesPath: () => routes.messages_path
|
|
192
|
+
};
|
|
193
|
+
} else {
|
|
194
|
+
// Fallback routes
|
|
195
|
+
window.RailsChatbot = window.RailsChatbot || {};
|
|
196
|
+
window.RailsChatbot.routes = {
|
|
197
|
+
conversationsPath: () => '/chatbot/conversations',
|
|
198
|
+
conversationMessagesPath: (id) => `/chatbot/conversations/${id}/messages`,
|
|
199
|
+
messagesPath: () => '/chatbot/messages'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
module RailsChatbot
|
|
2
|
+
class ApplicationContentService
|
|
3
|
+
class << self
|
|
4
|
+
def index_application_content
|
|
5
|
+
return unless RailsChatbot.configuration.enable_knowledge_base_indexing
|
|
6
|
+
|
|
7
|
+
# Index common application files
|
|
8
|
+
index_readme_files
|
|
9
|
+
index_documentation_files
|
|
10
|
+
index_route_files
|
|
11
|
+
index_model_files
|
|
12
|
+
index_custom_content
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def search_application_content(query)
|
|
16
|
+
# First search in knowledge base
|
|
17
|
+
knowledge_results = KnowledgeRetrievalService.new(query: query).retrieve
|
|
18
|
+
|
|
19
|
+
# If no results, try to match with predefined responses
|
|
20
|
+
if knowledge_results.empty?
|
|
21
|
+
return [find_default_response(query)]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
knowledge_results
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def index_readme_files
|
|
30
|
+
readme_paths = [
|
|
31
|
+
Rails.root.join('README.md'),
|
|
32
|
+
Rails.root.join('README.rdoc'),
|
|
33
|
+
Rails.root.join('doc', 'README.md')
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
readme_paths.each do |path|
|
|
37
|
+
next unless File.exist?(path)
|
|
38
|
+
|
|
39
|
+
content = File.read(path)
|
|
40
|
+
add_knowledge_entry(
|
|
41
|
+
title: "Application Documentation",
|
|
42
|
+
content: extract_summary(content),
|
|
43
|
+
source_type: "documentation",
|
|
44
|
+
source_url: "/docs"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def index_documentation_files
|
|
50
|
+
doc_dirs = [Rails.root.join('doc'), Rails.root.join('docs')]
|
|
51
|
+
|
|
52
|
+
doc_dirs.each do |doc_dir|
|
|
53
|
+
next unless Dir.exist?(doc_dir)
|
|
54
|
+
|
|
55
|
+
Dir.glob("#{doc_dir}/**/*.md").each do |file|
|
|
56
|
+
content = File.read(file)
|
|
57
|
+
title = File.basename(file, '.md').titleize
|
|
58
|
+
|
|
59
|
+
add_knowledge_entry(
|
|
60
|
+
title: title,
|
|
61
|
+
content: extract_summary(content),
|
|
62
|
+
source_type: "documentation",
|
|
63
|
+
source_url: file.sub(Rails.root.to_s, '')
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def index_route_files
|
|
70
|
+
routes_file = Rails.root.join('config', 'routes.rb')
|
|
71
|
+
return unless File.exist?(routes_file)
|
|
72
|
+
|
|
73
|
+
content = File.read(routes_file)
|
|
74
|
+
routes_info = extract_routes_info(content)
|
|
75
|
+
|
|
76
|
+
add_knowledge_entry(
|
|
77
|
+
title: "Available Routes and Features",
|
|
78
|
+
content: routes_info,
|
|
79
|
+
source_type: "routes",
|
|
80
|
+
source_url: "/routes"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def index_model_files
|
|
85
|
+
models_dir = Rails.root.join('app', 'models')
|
|
86
|
+
return unless Dir.exist?(models_dir)
|
|
87
|
+
|
|
88
|
+
model_info = []
|
|
89
|
+
Dir.glob("#{models_dir}/**/*.rb").each do |file|
|
|
90
|
+
next if File.basename(file).start_with?('application_', 'concerns/')
|
|
91
|
+
|
|
92
|
+
content = File.read(file)
|
|
93
|
+
model_name = extract_model_name(content, file)
|
|
94
|
+
next unless model_name
|
|
95
|
+
|
|
96
|
+
description = extract_model_description(content)
|
|
97
|
+
model_info << "#{model_name}: #{description}" if description.present?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if model_info.any?
|
|
101
|
+
add_knowledge_entry(
|
|
102
|
+
title: "Application Models",
|
|
103
|
+
content: model_info.join("\n"),
|
|
104
|
+
source_type: "models",
|
|
105
|
+
source_url: "/models"
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def index_custom_content
|
|
111
|
+
# Add default service information
|
|
112
|
+
add_knowledge_entry(
|
|
113
|
+
title: "Services",
|
|
114
|
+
content: RailsChatbot.configuration.default_responses[:services],
|
|
115
|
+
source_type: "default",
|
|
116
|
+
source_url: "/services"
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def extract_summary(content)
|
|
121
|
+
# Extract first paragraph or main points
|
|
122
|
+
lines = content.split("\n")
|
|
123
|
+
summary_lines = []
|
|
124
|
+
|
|
125
|
+
lines.each do |line|
|
|
126
|
+
line = line.strip
|
|
127
|
+
next if line.empty? || line.start_with?('#', '*', '-', '=')
|
|
128
|
+
|
|
129
|
+
summary_lines << line
|
|
130
|
+
break if summary_lines.length >= 3
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
summary_lines.join(' ').truncate(500)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def extract_routes_info(content)
|
|
137
|
+
# Extract route information from routes.rb
|
|
138
|
+
routes = []
|
|
139
|
+
|
|
140
|
+
# Look for common route patterns
|
|
141
|
+
content.scan(/resources\s+:(\w+)/) do |match|
|
|
142
|
+
routes << "#{match[0].pluralize} management"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
content.scan(/get\s+['"]([^'"]+)['"]/) do |match|
|
|
146
|
+
routes << match[0]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if routes.any?
|
|
150
|
+
"Available features: #{routes.join(', ')}"
|
|
151
|
+
else
|
|
152
|
+
"Web application with various features and endpoints"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def extract_model_name(content, file)
|
|
157
|
+
match = content.match(/class\s+(\w+)/)
|
|
158
|
+
match ? match[1] : File.basename(file, '.rb').camelize
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def extract_model_description(content)
|
|
162
|
+
# Look for comments at the beginning of the class
|
|
163
|
+
lines = content.split("\n")
|
|
164
|
+
description_lines = []
|
|
165
|
+
|
|
166
|
+
in_comment = false
|
|
167
|
+
lines.each do |line|
|
|
168
|
+
line = line.strip
|
|
169
|
+
|
|
170
|
+
if line.start_with?('class')
|
|
171
|
+
break
|
|
172
|
+
elsif line.start_with?('#')
|
|
173
|
+
description_lines << line.sub(/^#\s*/, '')
|
|
174
|
+
elsif !line.empty? && description_lines.any?
|
|
175
|
+
break
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
description_lines.join(' ').truncate(200)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def add_knowledge_entry(title:, content:, source_type:, source_url: nil)
|
|
183
|
+
# Check if entry already exists
|
|
184
|
+
existing = KnowledgeBase.find_by(
|
|
185
|
+
title: title,
|
|
186
|
+
source_type: source_type,
|
|
187
|
+
source_url: source_url
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if existing
|
|
191
|
+
existing.update!(content: content, updated_at: Time.current)
|
|
192
|
+
else
|
|
193
|
+
KnowledgeBase.create!(
|
|
194
|
+
title: title,
|
|
195
|
+
content: content,
|
|
196
|
+
source_type: source_type,
|
|
197
|
+
source_url: source_url
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
rescue => e
|
|
201
|
+
Rails.logger.error "Error adding knowledge entry: #{e.message}"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def find_default_response(query)
|
|
205
|
+
query_lower = query.downcase
|
|
206
|
+
|
|
207
|
+
case query_lower
|
|
208
|
+
when /services?, what.*service?, what.*do.*do?/
|
|
209
|
+
{
|
|
210
|
+
title: "Services Information",
|
|
211
|
+
content: RailsChatbot.configuration.default_responses[:services],
|
|
212
|
+
source_type: "default",
|
|
213
|
+
source_url: "/services"
|
|
214
|
+
}
|
|
215
|
+
when /hello?, hi?, hey?/
|
|
216
|
+
{
|
|
217
|
+
title: "Greeting",
|
|
218
|
+
content: RailsChatbot.configuration.default_responses[:greeting],
|
|
219
|
+
source_type: "default",
|
|
220
|
+
source_url: "/greeting"
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
{
|
|
224
|
+
title: "General Response",
|
|
225
|
+
content: RailsChatbot.configuration.default_responses[:no_results],
|
|
226
|
+
source_type: "default",
|
|
227
|
+
source_url: "/general"
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -8,34 +8,54 @@ module RailsChatbot
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def process_message(user_message)
|
|
11
|
-
# Retrieve relevant knowledge
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# Retrieve relevant knowledge from application content
|
|
12
|
+
knowledge_results = ApplicationContentService.search_application_content(user_message)
|
|
13
|
+
|
|
15
14
|
# Get conversation history
|
|
16
15
|
messages = conversation.conversation_history
|
|
17
16
|
|
|
18
17
|
# Add the new user message
|
|
19
18
|
conversation.add_user_message(user_message)
|
|
20
19
|
|
|
20
|
+
# Format context from knowledge results
|
|
21
|
+
context = format_knowledge_context(knowledge_results)
|
|
22
|
+
|
|
21
23
|
# Generate response
|
|
22
|
-
response =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
response = if knowledge_results.any? && knowledge_results.first[:source_type] == 'default'
|
|
25
|
+
# Use default response directly
|
|
26
|
+
knowledge_results.first[:content]
|
|
27
|
+
else
|
|
28
|
+
# Generate response using LLM with context
|
|
29
|
+
llm_service.chat(
|
|
30
|
+
messages: messages,
|
|
31
|
+
context: context
|
|
32
|
+
)
|
|
33
|
+
end
|
|
26
34
|
|
|
27
35
|
# Save assistant response
|
|
28
36
|
conversation.add_assistant_message(
|
|
29
37
|
response,
|
|
30
38
|
metadata: {
|
|
31
|
-
knowledge_results:
|
|
39
|
+
knowledge_results: knowledge_results.map { |r| r.slice(:title, :source_type, :source_id) }
|
|
32
40
|
}
|
|
33
41
|
)
|
|
34
42
|
|
|
35
43
|
{
|
|
36
44
|
response: response,
|
|
37
|
-
knowledge_sources:
|
|
45
|
+
knowledge_sources: knowledge_results
|
|
38
46
|
}
|
|
39
47
|
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def format_knowledge_context(knowledge_results)
|
|
52
|
+
return "" if knowledge_results.empty?
|
|
53
|
+
|
|
54
|
+
context_parts = knowledge_results.map do |result|
|
|
55
|
+
"#{result[:title]}: #{result[:content]}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
"Relevant information:\n#{context_parts.join("\n\n")}"
|
|
59
|
+
end
|
|
40
60
|
end
|
|
41
61
|
end
|
|
@@ -127,12 +127,12 @@
|
|
|
127
127
|
</style>
|
|
128
128
|
<% end %>
|
|
129
129
|
|
|
130
|
-
<div class="chatbot-container"
|
|
130
|
+
<div class="chatbot-container">
|
|
131
131
|
<div class="chatbot-header">
|
|
132
132
|
<%= RailsChatbot.configuration.chatbot_title %>
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
|
-
<div class="chatbot-messages"
|
|
135
|
+
<div class="chatbot-messages" id="chatbot-messages">
|
|
136
136
|
<div class="message assistant">
|
|
137
137
|
<div class="message-bubble">
|
|
138
138
|
Hello! I'm your application assistant. How can I help you today?
|
|
@@ -144,14 +144,12 @@
|
|
|
144
144
|
<input
|
|
145
145
|
type="text"
|
|
146
146
|
class="chatbot-input"
|
|
147
|
+
id="chatbot-input"
|
|
147
148
|
placeholder="Type your message..."
|
|
148
|
-
data-chatbot-target="input"
|
|
149
|
-
data-action="keydown->chatbot#handleKeyDown"
|
|
150
149
|
>
|
|
151
150
|
<button
|
|
152
151
|
class="chatbot-send"
|
|
153
|
-
|
|
154
|
-
data-chatbot-target="sendButton"
|
|
152
|
+
id="chatbot-send"
|
|
155
153
|
>
|
|
156
154
|
Send
|
|
157
155
|
</button>
|
|
@@ -160,6 +158,6 @@
|
|
|
160
158
|
|
|
161
159
|
<script>
|
|
162
160
|
window.RailsChatbot = window.RailsChatbot || {};
|
|
163
|
-
window.RailsChatbot.routes = <%= rails_chatbot_routes %>;
|
|
161
|
+
window.RailsChatbot.routes = <%= @rails_chatbot_routes %>;
|
|
164
162
|
</script>
|
|
165
163
|
<%= javascript_include_tag "rails_chatbot/application" %>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- Floating Chatbot Widget -->
|
|
2
|
+
<div class="chatbot-widget">
|
|
3
|
+
<!-- Chat Icon -->
|
|
4
|
+
<div class="chatbot-icon" id="chatbot-toggle">
|
|
5
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
6
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
<path d="M8 10h.01M12 10h.01M16 10h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
</svg>
|
|
9
|
+
<span class="chatbot-badge" id="unread-badge" style="display: none;">1</span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- Chat Window -->
|
|
13
|
+
<div class="chatbot-window" id="chatbot-window">
|
|
14
|
+
<div class="chatbot-header">
|
|
15
|
+
<div class="chatbot-title">
|
|
16
|
+
<%= RailsChatbot.configuration.chatbot_title %>
|
|
17
|
+
</div>
|
|
18
|
+
<button class="chatbot-close" id="chatbot-close">
|
|
19
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
20
|
+
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
21
|
+
</svg>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="chatbot-messages" id="chatbot-messages">
|
|
26
|
+
<div class="message assistant">
|
|
27
|
+
<div class="message-bubble">
|
|
28
|
+
Hello! I'm your application assistant. How can I help you today?
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="chatbot-input-container">
|
|
34
|
+
<input
|
|
35
|
+
type="text"
|
|
36
|
+
class="chatbot-input"
|
|
37
|
+
id="chatbot-input"
|
|
38
|
+
placeholder="Type your message..."
|
|
39
|
+
>
|
|
40
|
+
<button
|
|
41
|
+
class="chatbot-send"
|
|
42
|
+
id="chatbot-send"
|
|
43
|
+
>
|
|
44
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
45
|
+
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
window.RailsChatbot = window.RailsChatbot || {};
|
|
54
|
+
window.RailsChatbot.routes = <%= rails_chatbot_routes %>;
|
|
55
|
+
</script>
|
|
56
|
+
<%= javascript_include_tag "rails_chatbot/widget" %>
|
data/lib/rails_chatbot.rb
CHANGED
|
@@ -15,7 +15,7 @@ module RailsChatbot
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
class Configuration
|
|
18
|
-
attr_accessor :openai_api_key, :openai_model, :chatbot_title, :current_user_proc, :enable_knowledge_base_indexing, :indexable_models
|
|
18
|
+
attr_accessor :openai_api_key, :openai_model, :chatbot_title, :current_user_proc, :enable_knowledge_base_indexing, :indexable_models, :enable_widget, :default_responses
|
|
19
19
|
|
|
20
20
|
def initialize
|
|
21
21
|
@openai_api_key = ENV['OPENAI_API_KEY']
|
|
@@ -24,6 +24,13 @@ module RailsChatbot
|
|
|
24
24
|
@enable_knowledge_base_indexing = true
|
|
25
25
|
@current_user_proc = nil
|
|
26
26
|
@indexable_models = nil # Array of model classes to index
|
|
27
|
+
@enable_widget = true # Enable floating widget
|
|
28
|
+
@default_responses = {
|
|
29
|
+
greeting: "Hello! I'm your application assistant. How can I help you today?",
|
|
30
|
+
services: "We are working on web-based applications using Ruby on Rails.",
|
|
31
|
+
no_results: "I don't have specific information about that, but I'm here to help with general questions about our services.",
|
|
32
|
+
error: "Sorry, I encountered an error. Please try again or contact support."
|
|
33
|
+
}
|
|
27
34
|
end
|
|
28
35
|
end
|
|
29
36
|
end
|
|
@@ -25,6 +25,35 @@ namespace :rails_chatbot do
|
|
|
25
25
|
puts "\nDone!"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
desc "Index application content for knowledge base"
|
|
29
|
+
task index_application_content: :environment do
|
|
30
|
+
puts "Indexing application content..."
|
|
31
|
+
RailsChatbot::ApplicationContentService.index_application_content
|
|
32
|
+
puts "Application content indexed successfully!"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "Add default knowledge entries"
|
|
36
|
+
task add_default_knowledge: :environment do
|
|
37
|
+
puts "Adding default knowledge entries..."
|
|
38
|
+
|
|
39
|
+
RailsChatbot::KnowledgeIndexer.add_custom_knowledge(
|
|
40
|
+
title: "Services",
|
|
41
|
+
content: RailsChatbot.configuration.default_responses[:services],
|
|
42
|
+
source_type: "default",
|
|
43
|
+
source_url: "/services"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
puts "Default knowledge entries added!"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "Setup chatbot (index content + add defaults)"
|
|
50
|
+
task setup: :environment do
|
|
51
|
+
puts "Setting up RailsChatbot..."
|
|
52
|
+
Rake::Task['rails_chatbot:index_application_content'].invoke
|
|
53
|
+
Rake::Task['rails_chatbot:add_default_knowledge'].invoke
|
|
54
|
+
puts "RailsChatbot setup complete!"
|
|
55
|
+
end
|
|
56
|
+
|
|
28
57
|
desc "Clear knowledge base"
|
|
29
58
|
task clear_knowledge_base: :environment do
|
|
30
59
|
count = RailsChatbot::KnowledgeBase.count
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_chatbot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Burraq Ur Rehman
|
|
@@ -63,6 +63,7 @@ files:
|
|
|
63
63
|
- MIT-LICENSE
|
|
64
64
|
- README.md
|
|
65
65
|
- Rakefile
|
|
66
|
+
- app/assets/config/rails_chatbot_manifest.js
|
|
66
67
|
- app/assets/stylesheets/rails_chatbot/application.css
|
|
67
68
|
- app/controllers/rails_chatbot/application_controller.rb
|
|
68
69
|
- app/controllers/rails_chatbot/chat_controller.rb
|
|
@@ -70,17 +71,20 @@ files:
|
|
|
70
71
|
- app/controllers/rails_chatbot/messages_controller.rb
|
|
71
72
|
- app/helpers/rails_chatbot/application_helper.rb
|
|
72
73
|
- app/javascript/rails_chatbot/application.js
|
|
74
|
+
- app/javascript/rails_chatbot/widget.js
|
|
73
75
|
- app/jobs/rails_chatbot/application_job.rb
|
|
74
76
|
- app/mailers/rails_chatbot/application_mailer.rb
|
|
75
77
|
- app/models/rails_chatbot/application_record.rb
|
|
76
78
|
- app/models/rails_chatbot/conversation.rb
|
|
77
79
|
- app/models/rails_chatbot/knowledge_base.rb
|
|
78
80
|
- app/models/rails_chatbot/message.rb
|
|
81
|
+
- app/services/rails_chatbot/application_content_service.rb
|
|
79
82
|
- app/services/rails_chatbot/chat_service.rb
|
|
80
83
|
- app/services/rails_chatbot/knowledge_retrieval_service.rb
|
|
81
84
|
- app/services/rails_chatbot/llm_service.rb
|
|
82
85
|
- app/views/layouts/rails_chatbot/application.html.erb
|
|
83
86
|
- app/views/rails_chatbot/chat/index.html.erb
|
|
87
|
+
- app/views/rails_chatbot/shared/_chat_widget.html.erb
|
|
84
88
|
- config/initializers/rails_chatbot.rb
|
|
85
89
|
- config/routes.rb
|
|
86
90
|
- db/migrate/001_create_rails_chatbot_conversations.rb
|