polylingo_chat 0.1.0 → 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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +383 -22
  3. data/lib/generators/polylingo_chat/install/README +72 -0
  4. data/lib/generators/polylingo_chat/install/install_generator.rb +44 -16
  5. data/lib/generators/polylingo_chat/install/templates/channels/polylingo_chat_channel.rb +1 -1
  6. data/lib/generators/polylingo_chat/install/templates/controllers/polylingo_chat/conversations_controller.rb +127 -0
  7. data/lib/generators/polylingo_chat/install/templates/controllers/polylingo_chat/messages_controller.rb +113 -0
  8. data/lib/generators/polylingo_chat/install/templates/create_conversations.rb +2 -2
  9. data/lib/generators/polylingo_chat/install/templates/create_message_translations.rb +13 -0
  10. data/lib/generators/polylingo_chat/install/templates/create_messages.rb +8 -4
  11. data/lib/generators/polylingo_chat/install/templates/create_participants.rb +8 -5
  12. data/lib/generators/polylingo_chat/install/templates/javascript/chat.js +5 -5
  13. data/lib/generators/polylingo_chat/install/templates/jobs/translate_job.rb +93 -0
  14. data/lib/generators/polylingo_chat/install/templates/models/conversation.rb +34 -5
  15. data/lib/generators/polylingo_chat/install/templates/models/message.rb +16 -8
  16. data/lib/generators/polylingo_chat/install/templates/models/participant.rb +19 -4
  17. data/lib/generators/polylingo_chat/install/templates/views/polylingo_chat/conversations/index.html.erb +56 -0
  18. data/lib/generators/polylingo_chat/install/templates/views/polylingo_chat/conversations/show.html.erb +141 -0
  19. data/lib/polylingo_chat/translator/anthropic_client.rb +2 -2
  20. data/lib/polylingo_chat/translator/gemini_client.rb +2 -2
  21. data/lib/polylingo_chat/translator/openai_client.rb +2 -2
  22. data/lib/polylingo_chat/version.rb +1 -1
  23. data/lib/polylingo_chat.rb +0 -1
  24. metadata +17 -29
  25. data/lib/generators/polylingo_chat/install_generator.rb +0 -38
  26. data/lib/generators/polylingo_chat/templates/INSTALL_README.md +0 -124
  27. data/lib/generators/polylingo_chat/templates/chat_channel_example.js +0 -18
  28. data/lib/generators/polylingo_chat/templates/create_polyglot_conversations.rb +0 -9
  29. data/lib/generators/polylingo_chat/templates/create_polyglot_messages.rb +0 -13
  30. data/lib/generators/polylingo_chat/templates/create_polyglot_participants.rb +0 -12
  31. data/lib/generators/polylingo_chat/templates/models/conversation.rb +0 -7
  32. data/lib/generators/polylingo_chat/templates/models/message.rb +0 -14
  33. data/lib/generators/polylingo_chat/templates/models/participant.rb +0 -6
  34. data/lib/generators/polylingo_chat/templates/polyglot.rb +0 -53
  35. data/lib/generators/polylingo_chat/templates/polylingo_chat_channel.rb +0 -19
  36. data/lib/polylingo_chat/translate_job.rb +0 -63
@@ -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 CreateConversations < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
1
+ class CreatePolylingoChatConversations < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- create_table :conversations do |t|
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 CreateMessages < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
1
+ class CreatePolylingoChatMessages < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
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
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 CreateParticipants < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
1
+ class CreatePolylingoChatParticipants < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
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
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 :participants, [:user_id, :conversation_id], unique: true
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 PolylinguoChatChannel
20
- console.log("Subscribing to PolylinguoChatChannel...")
19
+ // Subscribe to the PolylingoChatChannel
20
+ console.log("Subscribing to PolylingoChatChannel...")
21
21
  const subscription = consumer.subscriptions.create(
22
- { channel: "PolylinguoChatChannel", conversation_id: conversationId },
22
+ { channel: "PolylingoChatChannel", conversation_id: conversationId },
23
23
  {
24
24
  connected() {
25
- console.log("✓ Connected to PolylinguoChatChannel!")
25
+ console.log("✓ Connected to PolylingoChatChannel!")
26
26
  },
27
27
 
28
28
  disconnected() {
29
- console.log("✗ Disconnected from PolylinguoChatChannel")
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
- class Conversation < ApplicationRecord
2
- has_many :participants, dependent: :destroy
3
- has_many :users, through: :participants
4
- has_many :messages, dependent: :destroy
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
- validates :title, length: { maximum: 255 }, allow_blank: true
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
- class Message < ApplicationRecord
2
- belongs_to :conversation
3
- belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
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
- validates :body, presence: true
7
+ validates :body, presence: true
6
8
 
7
- after_create_commit :enqueue_translation_job
9
+ after_create_commit :enqueue_translation_job
8
10
 
9
- private
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
- def enqueue_translation_job
12
- PolylingoChat::TranslateJob.perform_later(id) if PolylingoChat.config.async
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
- class Participant < ApplicationRecord
2
- belongs_to :user
3
- belongs_to :conversation
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
- validates :user_id, uniqueness: { scope: :conversation_id }
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>