polylingo_chat 0.1.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 +7 -0
- data/README.md +432 -0
- data/lib/generators/polylingo_chat/install/install_generator.rb +107 -0
- data/lib/generators/polylingo_chat/install/templates/README +48 -0
- data/lib/generators/polylingo_chat/install/templates/channels/application_cable/channel.rb +4 -0
- data/lib/generators/polylingo_chat/install/templates/channels/application_cable/connection.rb +30 -0
- data/lib/generators/polylingo_chat/install/templates/channels/polylingo_chat_channel.rb +15 -0
- data/lib/generators/polylingo_chat/install/templates/create_conversations.rb +9 -0
- data/lib/generators/polylingo_chat/install/templates/create_messages.rb +13 -0
- data/lib/generators/polylingo_chat/install/templates/create_participants.rb +12 -0
- data/lib/generators/polylingo_chat/install/templates/initializer.rb +24 -0
- data/lib/generators/polylingo_chat/install/templates/javascript/channels/consumer.js +15 -0
- data/lib/generators/polylingo_chat/install/templates/javascript/channels/index.js +2 -0
- data/lib/generators/polylingo_chat/install/templates/javascript/chat.js +86 -0
- data/lib/generators/polylingo_chat/install/templates/models/conversation.rb +7 -0
- data/lib/generators/polylingo_chat/install/templates/models/message.rb +14 -0
- data/lib/generators/polylingo_chat/install/templates/models/participant.rb +6 -0
- data/lib/generators/polylingo_chat/install_generator.rb +38 -0
- data/lib/generators/polylingo_chat/templates/INSTALL_README.md +124 -0
- data/lib/generators/polylingo_chat/templates/chat_channel_example.js +18 -0
- data/lib/generators/polylingo_chat/templates/create_polyglot_conversations.rb +9 -0
- data/lib/generators/polylingo_chat/templates/create_polyglot_messages.rb +13 -0
- data/lib/generators/polylingo_chat/templates/create_polyglot_participants.rb +12 -0
- data/lib/generators/polylingo_chat/templates/models/conversation.rb +7 -0
- data/lib/generators/polylingo_chat/templates/models/message.rb +14 -0
- data/lib/generators/polylingo_chat/templates/models/participant.rb +6 -0
- data/lib/generators/polylingo_chat/templates/polyglot.rb +53 -0
- data/lib/generators/polylingo_chat/templates/polylingo_chat_channel.rb +19 -0
- data/lib/polylingo_chat/config.rb +26 -0
- data/lib/polylingo_chat/engine.rb +10 -0
- data/lib/polylingo_chat/railtie.rb +14 -0
- data/lib/polylingo_chat/realtime.rb +8 -0
- data/lib/polylingo_chat/translate_job.rb +63 -0
- data/lib/polylingo_chat/translator/anthropic_client.rb +85 -0
- data/lib/polylingo_chat/translator/base.rb +13 -0
- data/lib/polylingo_chat/translator/gemini_client.rb +88 -0
- data/lib/polylingo_chat/translator/openai_client.rb +92 -0
- data/lib/polylingo_chat/translator.rb +40 -0
- data/lib/polylingo_chat/version.rb +3 -0
- data/lib/polylingo_chat.rb +11 -0
- metadata +138 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
PolylingoChat.configure do |config|
|
|
2
|
+
# API key for AI translation service
|
|
3
|
+
# Leave nil to use chat-only mode (no translation)
|
|
4
|
+
config.api_key = nil
|
|
5
|
+
|
|
6
|
+
# AI provider: :openai, :anthropic, or :gemini
|
|
7
|
+
config.provider = :anthropic
|
|
8
|
+
|
|
9
|
+
# Model to use for translation
|
|
10
|
+
# OpenAI: 'gpt-4-turbo' or 'gpt-3.5-turbo'
|
|
11
|
+
# Anthropic: 'claude-3-5-sonnet-20241022' or 'claude-3-haiku-20240307'
|
|
12
|
+
# Gemini: 'gemini-1.5-pro' or 'gemini-1.5-flash'
|
|
13
|
+
config.model = 'claude-3-5-sonnet-20241022'
|
|
14
|
+
|
|
15
|
+
# Default language for users without a preferred language
|
|
16
|
+
config.default_language = 'en'
|
|
17
|
+
|
|
18
|
+
# Enable async job processing (requires ActiveJob)
|
|
19
|
+
config.async = true
|
|
20
|
+
|
|
21
|
+
# Queue adapter (e.g., :solid_queue, :sidekiq, :async)
|
|
22
|
+
# Set to match your app's ActiveJob queue adapter
|
|
23
|
+
# config.queue_adapter = :solid_queue
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Action Cable provides the framework to deal with WebSockets in Rails.
|
|
2
|
+
// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
|
|
3
|
+
|
|
4
|
+
import { createConsumer } from "@rails/actioncable"
|
|
5
|
+
|
|
6
|
+
// Pass user_id as a query parameter for authentication
|
|
7
|
+
const getWebSocketURL = () => {
|
|
8
|
+
const userId = window.currentUserId
|
|
9
|
+
if (userId) {
|
|
10
|
+
return `/cable?user_id=${userId}`
|
|
11
|
+
}
|
|
12
|
+
return "/cable"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default createConsumer(getWebSocketURL())
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import consumer from "channels/consumer"
|
|
2
|
+
|
|
3
|
+
console.log("Chat.js loaded!")
|
|
4
|
+
|
|
5
|
+
document.addEventListener('turbo:load', () => {
|
|
6
|
+
console.log("Turbo loaded!")
|
|
7
|
+
|
|
8
|
+
const conversationId = window.conversationId
|
|
9
|
+
const currentUserId = window.currentUserId
|
|
10
|
+
|
|
11
|
+
console.log("Conversation ID:", conversationId)
|
|
12
|
+
console.log("Current User ID:", currentUserId)
|
|
13
|
+
|
|
14
|
+
if (!conversationId) {
|
|
15
|
+
console.log("No conversation ID found, skipping ActionCable subscription")
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Subscribe to the PolylinguoChatChannel
|
|
20
|
+
console.log("Subscribing to PolylinguoChatChannel...")
|
|
21
|
+
const subscription = consumer.subscriptions.create(
|
|
22
|
+
{ channel: "PolylinguoChatChannel", conversation_id: conversationId },
|
|
23
|
+
{
|
|
24
|
+
connected() {
|
|
25
|
+
console.log("✓ Connected to PolylinguoChatChannel!")
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
disconnected() {
|
|
29
|
+
console.log("✗ Disconnected from PolylinguoChatChannel")
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
received(data) {
|
|
33
|
+
console.log("✓ Received message:", data)
|
|
34
|
+
|
|
35
|
+
const messagesContainer = document.getElementById('messages')
|
|
36
|
+
if (!messagesContainer) return
|
|
37
|
+
|
|
38
|
+
// Check if message already exists in DOM (prevent duplicates)
|
|
39
|
+
const existingMessage = messagesContainer.querySelector(`[data-message-id="${data.message_id}"]`)
|
|
40
|
+
if (existingMessage) {
|
|
41
|
+
console.log("Message already exists, skipping:", data.message_id)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add new message
|
|
46
|
+
const messageHtml = createMessageElement(data)
|
|
47
|
+
messagesContainer.insertAdjacentHTML('beforeend', messageHtml)
|
|
48
|
+
|
|
49
|
+
// Auto-scroll to bottom
|
|
50
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Auto-scroll messages to bottom on page load
|
|
56
|
+
const messagesContainer = document.getElementById('messages')
|
|
57
|
+
if (messagesContainer) {
|
|
58
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createMessageElement(data) {
|
|
62
|
+
const isCurrentUser = data.sender_id === currentUserId
|
|
63
|
+
const alignment = isCurrentUser ? 'justify-end' : 'justify-start'
|
|
64
|
+
const bgColor = isCurrentUser ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'
|
|
65
|
+
|
|
66
|
+
const translatedBadge = data.translated ? '<p class="text-xs mt-1 opacity-75">✓ Translated</p>' : ''
|
|
67
|
+
const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
|
68
|
+
|
|
69
|
+
return `
|
|
70
|
+
<div class="message flex ${alignment}" data-message-id="${data.message_id}">
|
|
71
|
+
<div class="${bgColor} rounded-lg px-4 py-2 max-w-xs">
|
|
72
|
+
<p class="text-xs font-semibold mb-1">${escapeHtml(data.sender_name || 'Unknown')}</p>
|
|
73
|
+
<p class="text-sm">${escapeHtml(data.message)}</p>
|
|
74
|
+
${translatedBadge}
|
|
75
|
+
<p class="text-xs mt-1 opacity-75">${time}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function escapeHtml(text) {
|
|
82
|
+
const div = document.createElement('div')
|
|
83
|
+
div.textContent = text
|
|
84
|
+
return div.innerHTML
|
|
85
|
+
}
|
|
86
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class Message < ApplicationRecord
|
|
2
|
+
belongs_to :conversation
|
|
3
|
+
belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
|
|
4
|
+
|
|
5
|
+
validates :body, presence: true
|
|
6
|
+
|
|
7
|
+
after_create_commit :enqueue_translation_job
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def enqueue_translation_job
|
|
12
|
+
PolylingoChat::TranslateJob.perform_later(id) if PolylingoChat.config.async
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
module PolylingoChat
|
|
3
|
+
module Generators
|
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
|
5
|
+
source_root File.expand_path('templates', __dir__)
|
|
6
|
+
|
|
7
|
+
def copy_initializer
|
|
8
|
+
template 'polylingo_chat.rb', 'config/initializers/polylingo_chat.rb'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def copy_channel
|
|
12
|
+
template 'polylingo_chat_chat_channel.rb', 'app/channels/polylingo_chat_chat_channel.rb'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def copy_js_example
|
|
16
|
+
template 'chat_channel_example.js', 'app/javascript/channels/polylingo_chat_chat_channel.js'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_models
|
|
20
|
+
template 'models/conversation.rb', 'app/models/conversation.rb'
|
|
21
|
+
template 'models/participant.rb', 'app/models/participant.rb'
|
|
22
|
+
template 'models/message.rb', 'app/models/message.rb'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def copy_migrations
|
|
26
|
+
migration_template 'create_polylingo_chat_conversations.rb', 'db/migrate/create_polylingo_chat_conversations.rb'
|
|
27
|
+
migration_template 'create_polylingo_chat_participants.rb', 'db/migrate/create_polylingo_chat_participants.rb'
|
|
28
|
+
migration_template 'create_polylingo_chat_messages.rb', 'db/migrate/create_polylingo_chat_messages.rb'
|
|
29
|
+
rescue => e
|
|
30
|
+
say_status('warning', 'Skipping migrations: ensure you add your own Message/Conversation models or run generator again with --migrate', :yellow)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show_readme
|
|
34
|
+
readme 'INSTALL_README.md' if behavior == :invoke
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
## PolylingoChat Installation Complete!
|
|
2
|
+
|
|
3
|
+
### Next Steps:
|
|
4
|
+
|
|
5
|
+
**PolylingoChat can work in TWO modes:**
|
|
6
|
+
1. **Chat-Only Mode:** Real-time chat without translation (no API key needed)
|
|
7
|
+
2. **With Translation:** AI-powered translation (API key required)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
### 1. Run migrations
|
|
12
|
+
```bash
|
|
13
|
+
bin/rails db:migrate
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 2. Configure ActiveJob
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# config/application.rb or config/environments/production.rb
|
|
20
|
+
config.active_job.queue_adapter = :sidekiq # or :solid_queue, :delayed_job, :async
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 3. Install your background job processor
|
|
24
|
+
|
|
25
|
+
**Sidekiq:**
|
|
26
|
+
```ruby
|
|
27
|
+
# Gemfile
|
|
28
|
+
gem 'sidekiq'
|
|
29
|
+
```
|
|
30
|
+
Then: `bundle install && bundle exec sidekiq`
|
|
31
|
+
|
|
32
|
+
**Solid Queue:**
|
|
33
|
+
```ruby
|
|
34
|
+
# Gemfile
|
|
35
|
+
gem 'solid_queue'
|
|
36
|
+
```
|
|
37
|
+
Then: `bundle install && bin/rails solid_queue:install && bin/rails db:migrate && bin/rails solid_queue:start`
|
|
38
|
+
|
|
39
|
+
**Delayed Job:**
|
|
40
|
+
```ruby
|
|
41
|
+
# Gemfile
|
|
42
|
+
gem 'delayed_job_active_record'
|
|
43
|
+
```
|
|
44
|
+
Then: `bundle install && bin/rails generate delayed_job:active_record && bin/rails db:migrate && bin/rails jobs:work`
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 4. Configure PolylingoChat
|
|
49
|
+
|
|
50
|
+
Edit `config/initializers/polylingo_chat.rb`:
|
|
51
|
+
|
|
52
|
+
**Option A: Chat-Only (No Translation)**
|
|
53
|
+
```ruby
|
|
54
|
+
PolylingoChat.configure do |config|
|
|
55
|
+
# Leave api_key as nil for chat-only mode
|
|
56
|
+
config.api_key = nil
|
|
57
|
+
config.queue_adapter = :sidekiq
|
|
58
|
+
config.async = true
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Option B: With AI Translation**
|
|
63
|
+
```ruby
|
|
64
|
+
PolylingoChat.configure do |config|
|
|
65
|
+
# Enable translation
|
|
66
|
+
config.provider = :openai # or :anthropic, :gemini
|
|
67
|
+
config.api_key = ENV['POLYGLOT_API_KEY']
|
|
68
|
+
config.model = 'gpt-4o-mini'
|
|
69
|
+
|
|
70
|
+
config.queue_adapter = :sidekiq
|
|
71
|
+
config.default_language = 'en'
|
|
72
|
+
config.cache_store = Rails.cache
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 5. (Optional) Add preferred_language to User
|
|
79
|
+
|
|
80
|
+
**Only needed if using translation:**
|
|
81
|
+
```bash
|
|
82
|
+
bin/rails generate migration AddPreferredLanguageToUsers preferred_language:string
|
|
83
|
+
bin/rails db:migrate
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 6. Get API Key (Optional)
|
|
89
|
+
|
|
90
|
+
**Only if you want translation:**
|
|
91
|
+
- OpenAI: https://platform.openai.com/api-keys
|
|
92
|
+
- Anthropic: https://console.anthropic.com/
|
|
93
|
+
- Gemini: https://ai.google.dev/
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# .env
|
|
97
|
+
POLYGLOT_API_KEY=your-key-here
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Testing
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
conversation = Conversation.create!(title: "Test")
|
|
106
|
+
Participant.create!(conversation: conversation, user: user1)
|
|
107
|
+
Participant.create!(conversation: conversation, user: user2)
|
|
108
|
+
|
|
109
|
+
Message.create!(
|
|
110
|
+
conversation: conversation,
|
|
111
|
+
sender: user1,
|
|
112
|
+
body: "Hello!"
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- **Without API key:** Message sent as-is
|
|
117
|
+
- **With API key:** Message translated to each user's language
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### Need Help?
|
|
122
|
+
|
|
123
|
+
- 📖 Documentation: https://github.com/shoaibmalik786/polylingo_chat
|
|
124
|
+
- 🐛 Issues: https://github.com/shoaibmalik786/polylingo_chat/issues
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import consumer from "@rails/actioncable"
|
|
2
|
+
|
|
3
|
+
const cable = consumer.createConsumer();
|
|
4
|
+
|
|
5
|
+
export default function subscribe(conversationId, handleReceive) {
|
|
6
|
+
const subscription = cable.subscriptions.create({ channel: 'PolylinguoChatChannel', conversation_id: conversationId }, {
|
|
7
|
+
connected() {},
|
|
8
|
+
disconnected() {},
|
|
9
|
+
received(data) {
|
|
10
|
+
handleReceive(data)
|
|
11
|
+
},
|
|
12
|
+
sendMessage(text, conversationId) {
|
|
13
|
+
this.perform('receive', { message: text, conversation_id: conversationId });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return subscription;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreatePolylingoChatMessages < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :messages do |t|
|
|
4
|
+
t.references :sender, null: false, foreign_key: { to_table: :users }
|
|
5
|
+
t.references :conversation, null: false, foreign_key: true
|
|
6
|
+
t.text :body
|
|
7
|
+
t.string :language
|
|
8
|
+
t.text :translated_body
|
|
9
|
+
t.boolean :translated, default: false
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreatePolylingoChatParticipants < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :participants do |t|
|
|
4
|
+
t.references :user, null: false, foreign_key: true
|
|
5
|
+
t.references :conversation, null: false, foreign_key: true
|
|
6
|
+
t.string :role
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
add_index :participants, [:user_id, :conversation_id], unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class Message < ApplicationRecord
|
|
2
|
+
belongs_to :conversation
|
|
3
|
+
belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
|
|
4
|
+
|
|
5
|
+
validates :body, presence: true
|
|
6
|
+
|
|
7
|
+
after_create_commit :enqueue_translation_job
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def enqueue_translation_job
|
|
12
|
+
PolylingoChat::TranslateJob.perform_later(id) if PolylingoChat.config.async
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# PolylingoChat Configuration
|
|
2
|
+
# For more information, see: https://github.com/shoaibmalik786/polylingo_chat
|
|
3
|
+
|
|
4
|
+
PolylingoChat.configure do |config|
|
|
5
|
+
# ========================================
|
|
6
|
+
# AI Provider Configuration (OPTIONAL)
|
|
7
|
+
# ========================================
|
|
8
|
+
|
|
9
|
+
# Translation is OPTIONAL! If you don't set an API key, PolylingoChat works as a
|
|
10
|
+
# real-time chat engine without translation (messages sent as-is).
|
|
11
|
+
|
|
12
|
+
# To enable translation, choose your AI provider: :openai, :anthropic, or :gemini
|
|
13
|
+
config.provider = :openai
|
|
14
|
+
|
|
15
|
+
# Set your API key (leave nil to use chat-only mode without translation)
|
|
16
|
+
config.api_key = ENV['POLYGLOT_API_KEY'] # or set to nil for chat-only mode
|
|
17
|
+
|
|
18
|
+
# Choose your model based on provider:
|
|
19
|
+
# OpenAI: 'gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'
|
|
20
|
+
# Anthropic: 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'
|
|
21
|
+
# Gemini: 'gemini-1.5-flash', 'gemini-1.5-pro'
|
|
22
|
+
config.model = 'gpt-4o-mini'
|
|
23
|
+
|
|
24
|
+
# ========================================
|
|
25
|
+
# Background Job Configuration
|
|
26
|
+
# ========================================
|
|
27
|
+
|
|
28
|
+
# Specify which ActiveJob adapter you're using (OPTIONAL, for documentation purposes)
|
|
29
|
+
# You still need to configure ActiveJob in your Rails app as normal
|
|
30
|
+
# Options: :sidekiq, :solid_queue, :delayed_job, :async, :inline, etc.
|
|
31
|
+
#
|
|
32
|
+
# Example ActiveJob configuration (in config/application.rb or config/environments/*.rb):
|
|
33
|
+
# config.active_job.queue_adapter = :sidekiq
|
|
34
|
+
#
|
|
35
|
+
# Then tell PolylingoChat which adapter you're using:
|
|
36
|
+
config.queue_adapter = :sidekiq # Change this to match your ActiveJob setup
|
|
37
|
+
|
|
38
|
+
# Enable/disable async processing
|
|
39
|
+
config.async = true
|
|
40
|
+
|
|
41
|
+
# ========================================
|
|
42
|
+
# Translation Configuration
|
|
43
|
+
# ========================================
|
|
44
|
+
|
|
45
|
+
# Default language for translations (ISO 639-1 code)
|
|
46
|
+
config.default_language = 'en'
|
|
47
|
+
|
|
48
|
+
# Enable caching for translations (recommended for production)
|
|
49
|
+
config.cache_store = Rails.cache
|
|
50
|
+
|
|
51
|
+
# Request timeout in seconds
|
|
52
|
+
config.timeout = 15
|
|
53
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class PolylinguoChatChannel < ApplicationCable::Channel
|
|
2
|
+
def subscribed
|
|
3
|
+
# stream for the current user
|
|
4
|
+
stream_from "polylingo_chat_recipient_#{current_user.id}"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def receive(data)
|
|
8
|
+
# data: { message: 'Hola', conversation_id: 1 }
|
|
9
|
+
message_text = data['message']
|
|
10
|
+
conversation = Conversation.find(data['conversation_id'])
|
|
11
|
+
|
|
12
|
+
msg = Message.create!(conversation: conversation, sender: current_user, body: message_text, language: current_user.preferred_language)
|
|
13
|
+
# enqueue translation job
|
|
14
|
+
PolylingoChat::TranslateJob.perform_async(msg.id)
|
|
15
|
+
|
|
16
|
+
# broadcast original to sender's stream if you want
|
|
17
|
+
ActionCable.server.broadcast("polylingo_chat_recipient_#{current_user.id}", { message: message_text, original: message_text, sender_id: current_user.id, message_id: msg.id, original:true })
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class Config
|
|
3
|
+
attr_accessor :provider, :api_key, :model, :default_language, :cache_store, :async, :timeout, :queue_adapter
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@provider = :openai
|
|
7
|
+
@api_key = ENV['POLYGLOT_API_KEY']
|
|
8
|
+
@model = 'gpt-4o-mini'
|
|
9
|
+
@default_language = 'en'
|
|
10
|
+
@cache_store = nil
|
|
11
|
+
@async = true
|
|
12
|
+
@timeout = 15
|
|
13
|
+
@queue_adapter = nil # Optional: Specify which ActiveJob adapter you're using
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.configure
|
|
18
|
+
@config ||= Config.new
|
|
19
|
+
yield @config if block_given?
|
|
20
|
+
@config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.config
|
|
24
|
+
@config ||= Config.new
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'rails/railtie'
|
|
2
|
+
|
|
3
|
+
module PolylingoChat
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
# PolylingoChat Railtie - currently just for initialization hooks
|
|
6
|
+
# The queue_adapter config is informational - users configure ActiveJob themselves
|
|
7
|
+
|
|
8
|
+
initializer 'polylingo_chat.log_configuration', after: :load_config_initializers do
|
|
9
|
+
if PolylingoChat.config.queue_adapter
|
|
10
|
+
Rails.logger.info "PolylingoChat: Using #{PolylingoChat.config.queue_adapter} for background jobs"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class TranslateJob < ActiveJob::Base
|
|
3
|
+
queue_as :polylingo_chat_translations
|
|
4
|
+
|
|
5
|
+
def perform(message_id)
|
|
6
|
+
# Host app must implement Message model with associations matching spec
|
|
7
|
+
message = ::Message.find_by(id: message_id)
|
|
8
|
+
return unless message
|
|
9
|
+
|
|
10
|
+
conversation = message.conversation
|
|
11
|
+
return unless conversation
|
|
12
|
+
|
|
13
|
+
recipients = conversation.users.where.not(id: message.sender_id)
|
|
14
|
+
|
|
15
|
+
# Check if translation is enabled (API key present)
|
|
16
|
+
translation_enabled = PolylingoChat.config.api_key.present?
|
|
17
|
+
|
|
18
|
+
recipients.each do |recipient|
|
|
19
|
+
if translation_enabled
|
|
20
|
+
# Translation enabled - translate message
|
|
21
|
+
target_lang = recipient.preferred_language || PolylingoChat.config.default_language
|
|
22
|
+
source_lang = PolylingoChat::Translator.detect_language(message.body)
|
|
23
|
+
context = conversation.messages.order(created_at: :asc).last(20).pluck(:body).join("") rescue nil
|
|
24
|
+
|
|
25
|
+
translated = PolylingoChat::Translator.translate(text: message.body, from: source_lang, to: target_lang, context: context)
|
|
26
|
+
# If translator returns a Future-like, wait
|
|
27
|
+
translated = translated.value if translated.respond_to?(:value)
|
|
28
|
+
else
|
|
29
|
+
# No API key - just use original message (chat-only mode)
|
|
30
|
+
translated = message.body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Broadcast to recipient using ActionCable
|
|
34
|
+
begin
|
|
35
|
+
# Broadcast to user-specific channel
|
|
36
|
+
ActionCable.server.broadcast("polylingo_chat_recipient_#{recipient.id}", {
|
|
37
|
+
message: translated,
|
|
38
|
+
original: message.body,
|
|
39
|
+
message_id: message.id,
|
|
40
|
+
sender_id: message.sender_id,
|
|
41
|
+
sender_name: message.sender.name,
|
|
42
|
+
translated: translation_enabled
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
# Also broadcast to conversation channel for demo/group chat
|
|
46
|
+
ActionCable.server.broadcast("conversation_#{conversation.id}", {
|
|
47
|
+
message: translated,
|
|
48
|
+
original: message.body,
|
|
49
|
+
message_id: message.id,
|
|
50
|
+
sender_id: message.sender_id,
|
|
51
|
+
sender_name: message.sender.name,
|
|
52
|
+
translated: translation_enabled,
|
|
53
|
+
recipient_id: recipient.id
|
|
54
|
+
})
|
|
55
|
+
rescue => e
|
|
56
|
+
# ignore broadcast errors
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
message.update(translated: translation_enabled) rescue nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|