polylingo_chat 0.1.1 → 0.4.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 +383 -22
- data/lib/generators/polylingo_chat/install/README +72 -0
- data/lib/generators/polylingo_chat/install/install_generator.rb +43 -15
- data/lib/generators/polylingo_chat/install/templates/channels/polylingo_chat_channel.rb +1 -1
- data/lib/generators/polylingo_chat/install/templates/controllers/polylingo_chat/conversations_controller.rb +127 -0
- data/lib/generators/polylingo_chat/install/templates/controllers/polylingo_chat/messages_controller.rb +113 -0
- data/lib/generators/polylingo_chat/install/templates/create_conversations.rb +2 -2
- data/lib/generators/polylingo_chat/install/templates/create_message_translations.rb +13 -0
- data/lib/generators/polylingo_chat/install/templates/create_messages.rb +8 -4
- data/lib/generators/polylingo_chat/install/templates/create_participants.rb +8 -5
- data/lib/generators/polylingo_chat/install/templates/javascript/chat.js +5 -5
- data/lib/generators/polylingo_chat/install/templates/jobs/translate_job.rb +93 -0
- data/lib/generators/polylingo_chat/install/templates/models/conversation.rb +34 -5
- data/lib/generators/polylingo_chat/install/templates/models/message.rb +16 -8
- data/lib/generators/polylingo_chat/install/templates/models/participant.rb +19 -4
- data/lib/generators/polylingo_chat/install/templates/views/polylingo_chat/conversations/index.html.erb +56 -0
- data/lib/generators/polylingo_chat/install/templates/views/polylingo_chat/conversations/show.html.erb +141 -0
- data/lib/polylingo_chat/version.rb +1 -1
- data/lib/polylingo_chat.rb +0 -1
- metadata +11 -4
- data/lib/polylingo_chat/translate_job.rb +0 -65
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class ConversationsController < ApplicationController
|
|
3
|
+
before_action :set_conversation, only: [:show, :update, :destroy]
|
|
4
|
+
|
|
5
|
+
# GET /polylingo_chat/conversations
|
|
6
|
+
def index
|
|
7
|
+
@conversations = Conversation.all.includes(:participants, :messages)
|
|
8
|
+
|
|
9
|
+
respond_to do |format|
|
|
10
|
+
format.html # renders app/views/polylingo_chat/conversations/index.html.erb
|
|
11
|
+
format.json { render json: @conversations.map { |c| conversation_json(c) } }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# GET /polylingo_chat/conversations/:id
|
|
16
|
+
def show
|
|
17
|
+
respond_to do |format|
|
|
18
|
+
format.html # renders app/views/polylingo_chat/conversations/show.html.erb
|
|
19
|
+
format.json { render json: conversation_json(@conversation, include_messages: true) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# POST /polylingo_chat/conversations
|
|
24
|
+
def create
|
|
25
|
+
@conversation = Conversation.new(conversation_params)
|
|
26
|
+
|
|
27
|
+
if @conversation.save
|
|
28
|
+
# Add participants if provided
|
|
29
|
+
if params[:participant_ids].present?
|
|
30
|
+
add_participants_from_params(@conversation)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
respond_to do |format|
|
|
34
|
+
format.html { redirect_to polylingo_chat_conversation_path(@conversation), notice: 'Conversation created successfully.' }
|
|
35
|
+
format.json { render json: conversation_json(@conversation), status: :created }
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
respond_to do |format|
|
|
39
|
+
format.html { render :new, status: :unprocessable_entity }
|
|
40
|
+
format.json { render json: { errors: @conversation.errors.full_messages }, status: :unprocessable_entity }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# PATCH/PUT /polylingo_chat/conversations/:id
|
|
46
|
+
def update
|
|
47
|
+
if @conversation.update(conversation_params)
|
|
48
|
+
respond_to do |format|
|
|
49
|
+
format.html { redirect_to polylingo_chat_conversation_path(@conversation), notice: 'Conversation updated successfully.' }
|
|
50
|
+
format.json { render json: conversation_json(@conversation) }
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
respond_to do |format|
|
|
54
|
+
format.html { render :edit, status: :unprocessable_entity }
|
|
55
|
+
format.json { render json: { errors: @conversation.errors.full_messages }, status: :unprocessable_entity }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# DELETE /polylingo_chat/conversations/:id
|
|
61
|
+
def destroy
|
|
62
|
+
@conversation.destroy
|
|
63
|
+
|
|
64
|
+
respond_to do |format|
|
|
65
|
+
format.html { redirect_to polylingo_chat_conversations_path, notice: 'Conversation deleted successfully.' }
|
|
66
|
+
format.json { head :no_content }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def set_conversation
|
|
73
|
+
@conversation = Conversation.find(params[:id])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def conversation_params
|
|
77
|
+
params.require(:conversation).permit(:title)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_participants_from_params(conversation)
|
|
81
|
+
Array(params[:participant_ids]).each do |participant_data|
|
|
82
|
+
type = participant_data[:type] || 'User'
|
|
83
|
+
id = participant_data[:id]
|
|
84
|
+
role = participant_data[:role]
|
|
85
|
+
|
|
86
|
+
klass = type.constantize
|
|
87
|
+
record = klass.find(id)
|
|
88
|
+
conversation.add_participant(record, role: role)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def conversation_json(conversation, include_messages: false)
|
|
93
|
+
{
|
|
94
|
+
id: conversation.id,
|
|
95
|
+
title: conversation.title,
|
|
96
|
+
created_at: conversation.created_at,
|
|
97
|
+
updated_at: conversation.updated_at,
|
|
98
|
+
participants: conversation.participants.map { |p| participant_json(p) },
|
|
99
|
+
messages: include_messages ? conversation.messages.map { |m| message_json(m) } : []
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def participant_json(participant)
|
|
104
|
+
{
|
|
105
|
+
id: participant.id,
|
|
106
|
+
type: participant.participantable_type,
|
|
107
|
+
participant_id: participant.participantable_id,
|
|
108
|
+
role: participant.role,
|
|
109
|
+
name: participant.participantable.try(:name) || participant.participantable.try(:email)
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def message_json(message)
|
|
114
|
+
{
|
|
115
|
+
id: message.id,
|
|
116
|
+
body: message.body,
|
|
117
|
+
language: message.language,
|
|
118
|
+
translated_body: message.translated_body,
|
|
119
|
+
translated: message.translated,
|
|
120
|
+
sender_type: message.sender_type,
|
|
121
|
+
sender_id: message.sender_id,
|
|
122
|
+
sender_name: message.sender_name,
|
|
123
|
+
created_at: message.created_at
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class MessagesController < ApplicationController
|
|
3
|
+
before_action :set_conversation
|
|
4
|
+
|
|
5
|
+
# GET /polylingo_chat/conversations/:conversation_id/messages
|
|
6
|
+
# Optional query params:
|
|
7
|
+
# - target_language: ISO 639-1 code (e.g., 'es', 'fr', 'de')
|
|
8
|
+
# - translate: 'true' to enable on-the-fly translation
|
|
9
|
+
def index
|
|
10
|
+
@messages = @conversation.messages.order(created_at: :asc)
|
|
11
|
+
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.html # renders app/views/polylingo_chat/messages/index.html.erb
|
|
14
|
+
format.json do
|
|
15
|
+
# Check if on-the-fly translation is requested
|
|
16
|
+
if params[:translate] == 'true' && params[:target_language].present?
|
|
17
|
+
render json: @messages.map { |m| translate_message_json(m, params[:target_language]) }
|
|
18
|
+
else
|
|
19
|
+
render json: @messages.map { |m| message_json(m) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# POST /polylingo_chat/conversations/:conversation_id/messages
|
|
26
|
+
def create
|
|
27
|
+
@message = @conversation.messages.new(message_params)
|
|
28
|
+
|
|
29
|
+
# Set sender from params (polymorphic)
|
|
30
|
+
if params[:sender_type] && params[:sender_id]
|
|
31
|
+
sender_klass = params[:sender_type].constantize
|
|
32
|
+
@message.sender = sender_klass.find(params[:sender_id])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if @message.save
|
|
36
|
+
respond_to do |format|
|
|
37
|
+
format.html { redirect_to polylingo_chat_conversation_path(@conversation), notice: 'Message sent successfully.' }
|
|
38
|
+
format.json { render json: message_json(@message), status: :created }
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
respond_to do |format|
|
|
42
|
+
format.html { redirect_to polylingo_chat_conversation_path(@conversation), alert: 'Failed to send message.' }
|
|
43
|
+
format.json { render json: { errors: @message.errors.full_messages }, status: :unprocessable_entity }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def set_conversation
|
|
51
|
+
@conversation = Conversation.find(params[:conversation_id])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def message_params
|
|
55
|
+
params.require(:message).permit(:body, :language)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def message_json(message)
|
|
59
|
+
{
|
|
60
|
+
id: message.id,
|
|
61
|
+
body: message.body,
|
|
62
|
+
language: message.language,
|
|
63
|
+
translated_body: message.translated_body,
|
|
64
|
+
translated: message.translated,
|
|
65
|
+
sender_type: message.sender_type,
|
|
66
|
+
sender_id: message.sender_id,
|
|
67
|
+
sender_name: message.sender_name,
|
|
68
|
+
conversation_id: message.conversation_id,
|
|
69
|
+
created_at: message.created_at,
|
|
70
|
+
updated_at: message.updated_at
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def translate_message_json(message, target_language)
|
|
75
|
+
# Check if translation is enabled
|
|
76
|
+
unless PolylingoChat.config.api_key.present?
|
|
77
|
+
return message_json(message)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# If message is already in target language, return as is
|
|
81
|
+
if message.language == target_language
|
|
82
|
+
return message_json(message).merge(
|
|
83
|
+
translated_body: message.body,
|
|
84
|
+
target_language: target_language
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# If we have a stored translation in the target language (for default language)
|
|
89
|
+
if target_language == PolylingoChat.config.default_language && message.translated_body.present?
|
|
90
|
+
return message_json(message).merge(target_language: target_language)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Translate on-the-fly
|
|
94
|
+
begin
|
|
95
|
+
translated = PolylingoChat::Translator.translate(
|
|
96
|
+
text: message.body,
|
|
97
|
+
from: message.language || 'auto',
|
|
98
|
+
to: target_language,
|
|
99
|
+
context: nil
|
|
100
|
+
)
|
|
101
|
+
translated = translated.value if translated.respond_to?(:value)
|
|
102
|
+
|
|
103
|
+
message_json(message).merge(
|
|
104
|
+
translated_body: translated,
|
|
105
|
+
target_language: target_language
|
|
106
|
+
)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Rails.logger.error("PolylingoChat: Translation failed - #{e.message}")
|
|
109
|
+
message_json(message).merge(target_language: target_language)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreatePolylingoChatConversations < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
3
|
+
create_table :polylingo_chat_conversations do |t|
|
|
4
4
|
t.string :title
|
|
5
5
|
t.boolean :private, default: true
|
|
6
6
|
t.timestamps
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreatePolylingoChatMessageTranslations < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :polylingo_chat_message_translations do |t|
|
|
4
|
+
t.references :message, null: false, foreign_key: { to_table: :polylingo_chat_messages }
|
|
5
|
+
t.string :language, null: false
|
|
6
|
+
t.text :translated_text, null: false
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :polylingo_chat_message_translations, [:message_id, :language], unique: true, name: 'idx_message_translations_on_message_and_language'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreatePolylingoChatMessages < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
4
|
-
|
|
5
|
-
t.references :
|
|
3
|
+
create_table :polylingo_chat_messages do |t|
|
|
4
|
+
# Polymorphic sender - supports User, Vendor, Customer, etc.
|
|
5
|
+
t.references :sender, polymorphic: true, null: false
|
|
6
|
+
t.references :conversation, null: false, foreign_key: { to_table: :polylingo_chat_conversations }
|
|
6
7
|
t.text :body
|
|
7
8
|
t.string :language
|
|
8
9
|
t.text :translated_body
|
|
9
10
|
t.boolean :translated, default: false
|
|
10
11
|
t.timestamps
|
|
11
12
|
end
|
|
13
|
+
|
|
14
|
+
add_index :polylingo_chat_messages, [:sender_type, :sender_id]
|
|
15
|
+
add_index :polylingo_chat_messages, [:conversation_id, :created_at]
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreatePolylingoChatParticipants < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
4
|
-
|
|
5
|
-
t.references :
|
|
3
|
+
create_table :polylingo_chat_participants do |t|
|
|
4
|
+
# Polymorphic association - supports User, Vendor, Customer, etc.
|
|
5
|
+
t.references :participantable, polymorphic: true, null: false
|
|
6
|
+
t.references :conversation, null: false, foreign_key: { to_table: :polylingo_chat_conversations }
|
|
6
7
|
t.string :role
|
|
7
8
|
t.timestamps
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
add_index :
|
|
11
|
+
add_index :polylingo_chat_participants, [:participantable_type, :participantable_id, :conversation_id],
|
|
12
|
+
unique: true,
|
|
13
|
+
name: 'index_polylingo_participants_on_participantable_conversation'
|
|
11
14
|
end
|
|
12
15
|
end
|
|
@@ -16,17 +16,17 @@ document.addEventListener('turbo:load', () => {
|
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// Subscribe to the
|
|
20
|
-
console.log("Subscribing to
|
|
19
|
+
// Subscribe to the PolylingoChatChannel
|
|
20
|
+
console.log("Subscribing to PolylingoChatChannel...")
|
|
21
21
|
const subscription = consumer.subscriptions.create(
|
|
22
|
-
{ channel: "
|
|
22
|
+
{ channel: "PolylingoChatChannel", conversation_id: conversationId },
|
|
23
23
|
{
|
|
24
24
|
connected() {
|
|
25
|
-
console.log("✓ Connected to
|
|
25
|
+
console.log("✓ Connected to PolylingoChatChannel!")
|
|
26
26
|
},
|
|
27
27
|
|
|
28
28
|
disconnected() {
|
|
29
|
-
console.log("✗ Disconnected from
|
|
29
|
+
console.log("✗ Disconnected from PolylingoChatChannel")
|
|
30
30
|
},
|
|
31
31
|
|
|
32
32
|
received(data) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class TranslateJob < ::ApplicationJob
|
|
3
|
+
queue_as :polylingo_chat_translations
|
|
4
|
+
|
|
5
|
+
def perform(message_id)
|
|
6
|
+
# Find message using namespaced model
|
|
7
|
+
message = PolylingoChat::Message.find_by(id: message_id)
|
|
8
|
+
return unless message
|
|
9
|
+
|
|
10
|
+
conversation = message.conversation
|
|
11
|
+
return unless conversation
|
|
12
|
+
|
|
13
|
+
# Get all participants except the sender (works with polymorphic associations)
|
|
14
|
+
recipient_participants = conversation.participants.where.not(
|
|
15
|
+
participantable_type: message.sender_type,
|
|
16
|
+
participantable_id: message.sender_id
|
|
17
|
+
)
|
|
18
|
+
recipients = recipient_participants.map(&:participantable)
|
|
19
|
+
|
|
20
|
+
# Check if translation is enabled (API key present)
|
|
21
|
+
translation_enabled = PolylingoChat.config.api_key.present?
|
|
22
|
+
|
|
23
|
+
# Detect and store the source language
|
|
24
|
+
if translation_enabled
|
|
25
|
+
source_lang = PolylingoChat::Translator.detect_language(message.body)
|
|
26
|
+
message.update_column(:language, source_lang)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Store default translation (to default_language) for API consumers
|
|
30
|
+
if translation_enabled
|
|
31
|
+
default_lang = PolylingoChat.config.default_language
|
|
32
|
+
unless source_lang == default_lang
|
|
33
|
+
context = conversation.messages.order(created_at: :asc).last(20).pluck(:body).join("") rescue nil
|
|
34
|
+
default_translation = PolylingoChat::Translator.translate(
|
|
35
|
+
text: message.body,
|
|
36
|
+
from: source_lang,
|
|
37
|
+
to: default_lang,
|
|
38
|
+
context: context
|
|
39
|
+
)
|
|
40
|
+
default_translation = default_translation.value if default_translation.respond_to?(:value)
|
|
41
|
+
message.update_column(:translated_body, default_translation)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
recipients.each do |recipient|
|
|
46
|
+
if translation_enabled
|
|
47
|
+
# Translation enabled - translate message for this specific recipient
|
|
48
|
+
target_lang = recipient.preferred_language || PolylingoChat.config.default_language
|
|
49
|
+
context = conversation.messages.order(created_at: :asc).last(20).pluck(:body).join("") rescue nil
|
|
50
|
+
|
|
51
|
+
translated = PolylingoChat::Translator.translate(text: message.body, from: source_lang, to: target_lang, context: context)
|
|
52
|
+
# If translator returns a Future-like, wait
|
|
53
|
+
translated = translated.value if translated.respond_to?(:value)
|
|
54
|
+
else
|
|
55
|
+
# No API key - just use original message (chat-only mode)
|
|
56
|
+
translated = message.body
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Broadcast to recipient using ActionCable (skip if API-only)
|
|
60
|
+
begin
|
|
61
|
+
if defined?(ActionCable)
|
|
62
|
+
# Broadcast to user-specific channel
|
|
63
|
+
ActionCable.server.broadcast("polylingo_chat_recipient_#{recipient.id}", {
|
|
64
|
+
message: translated,
|
|
65
|
+
original: message.body,
|
|
66
|
+
message_id: message.id,
|
|
67
|
+
sender_id: message.sender_id,
|
|
68
|
+
sender_name: message.sender_name,
|
|
69
|
+
translated: translation_enabled
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
# Also broadcast to conversation channel for demo/group chat
|
|
73
|
+
ActionCable.server.broadcast("conversation_#{conversation.id}", {
|
|
74
|
+
message: translated,
|
|
75
|
+
original: message.body,
|
|
76
|
+
message_id: message.id,
|
|
77
|
+
sender_id: message.sender_id,
|
|
78
|
+
sender_name: message.sender_name,
|
|
79
|
+
translated: translation_enabled,
|
|
80
|
+
recipient_id: recipient.id
|
|
81
|
+
})
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
Rails.logger.error("PolylingoChat: Broadcast failed - #{e.message}")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
message.update_column(:translated, translation_enabled)
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
Rails.logger.error("PolylingoChat: Failed to process translation - #{e.message}")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class Conversation < ApplicationRecord
|
|
3
|
+
has_many :participants, class_name: "PolylingoChat::Participant", dependent: :destroy
|
|
4
|
+
has_many :messages, class_name: "PolylingoChat::Message", dependent: :destroy
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
validates :title, length: { maximum: 255 }, allow_blank: true
|
|
7
|
+
|
|
8
|
+
# Get all participantable objects (User, Vendor, Customer, etc.)
|
|
9
|
+
def participantables
|
|
10
|
+
participants.map(&:participantable)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get participants of a specific type
|
|
14
|
+
# Example: conversation.participantables_of_type(User)
|
|
15
|
+
def participantables_of_type(klass)
|
|
16
|
+
participants.where(participantable_type: klass.name).map(&:participantable)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Backward compatibility - returns all User participants
|
|
20
|
+
def users
|
|
21
|
+
participantables_of_type(User) if defined?(User)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if a record is a participant in this conversation
|
|
25
|
+
def includes_participant?(record)
|
|
26
|
+
participants.exists?(participantable: record)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add a participant to the conversation
|
|
30
|
+
def add_participant(record, role: nil)
|
|
31
|
+
participants.find_or_create_by(participantable: record) do |p|
|
|
32
|
+
p.role = role if role
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
7
36
|
end
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class Message < ApplicationRecord
|
|
3
|
+
belongs_to :conversation, class_name: "PolylingoChat::Conversation"
|
|
4
|
+
# Polymorphic sender - supports User, Vendor, Customer, etc.
|
|
5
|
+
belongs_to :sender, polymorphic: true
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
validates :body, presence: true
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
after_create_commit :enqueue_translation_job
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
# Helper method to get sender's name (works with any model that has a name method)
|
|
12
|
+
def sender_name
|
|
13
|
+
sender.try(:name) || sender.try(:full_name) || sender.try(:email) || "Unknown"
|
|
14
|
+
end
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def enqueue_translation_job
|
|
19
|
+
PolylingoChat::TranslateJob.perform_later(id) if PolylingoChat.config.async
|
|
20
|
+
end
|
|
13
21
|
end
|
|
14
22
|
end
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
module PolylingoChat
|
|
2
|
+
class Participant < ApplicationRecord
|
|
3
|
+
# Polymorphic association - can belong to User, Vendor, Customer, etc.
|
|
4
|
+
belongs_to :participantable, polymorphic: true
|
|
5
|
+
belongs_to :conversation, class_name: "PolylingoChat::Conversation"
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
validates :participantable_id, uniqueness: {
|
|
8
|
+
scope: [:participantable_type, :conversation_id],
|
|
9
|
+
message: "is already a participant in this conversation"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Alias for backward compatibility and convenience
|
|
13
|
+
def user
|
|
14
|
+
participantable
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def user=(value)
|
|
18
|
+
self.participantable = value
|
|
19
|
+
end
|
|
20
|
+
end
|
|
6
21
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<div class="polylingo-conversations">
|
|
2
|
+
<h1>Conversations</h1>
|
|
3
|
+
|
|
4
|
+
<div class="conversations-list">
|
|
5
|
+
<%% if @conversations.any? %>
|
|
6
|
+
<%% @conversations.each do |conversation| %>
|
|
7
|
+
<div class="conversation-item">
|
|
8
|
+
<h3><%%= link_to conversation.title || "Conversation ##{conversation.id}", polylingo_chat_conversation_path(conversation) %></h3>
|
|
9
|
+
<p>
|
|
10
|
+
<strong>Participants:</strong>
|
|
11
|
+
<%%= conversation.participantables.map { |p| p.try(:name) || p.try(:email) }.join(', ') %>
|
|
12
|
+
</p>
|
|
13
|
+
<p>
|
|
14
|
+
<strong>Messages:</strong> <%%= conversation.messages.count %>
|
|
15
|
+
</p>
|
|
16
|
+
<small>Created <%%= time_ago_in_words(conversation.created_at) %> ago</small>
|
|
17
|
+
</div>
|
|
18
|
+
<%% end %>
|
|
19
|
+
<%% else %>
|
|
20
|
+
<p>No conversations yet.</p>
|
|
21
|
+
<%% end %>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.polylingo-conversations {
|
|
27
|
+
max-width: 800px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
padding: 20px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.conversations-list {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: 15px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.conversation-item {
|
|
39
|
+
padding: 15px;
|
|
40
|
+
border: 1px solid #ddd;
|
|
41
|
+
border-radius: 8px;
|
|
42
|
+
background: #f9f9f9;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.conversation-item h3 {
|
|
46
|
+
margin: 0 0 10px 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.conversation-item p {
|
|
50
|
+
margin: 5px 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.conversation-item small {
|
|
54
|
+
color: #666;
|
|
55
|
+
}
|
|
56
|
+
</style>
|