langsmithrb_rails 0.1.0 → 0.1.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rspec_status +82 -0
  4. data/CHANGELOG.md +25 -0
  5. data/Gemfile +20 -0
  6. data/Gemfile.lock +321 -0
  7. data/LICENSE +21 -0
  8. data/README.md +268 -0
  9. data/Rakefile +10 -0
  10. data/langsmithrb_rails-0.1.0.gem +0 -0
  11. data/langsmithrb_rails.gemspec +45 -0
  12. data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
  13. data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
  14. data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
  15. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
  16. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
  17. data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
  18. data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
  19. data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
  20. data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
  21. data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
  22. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
  23. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
  24. data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
  25. data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
  26. data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
  27. data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
  28. data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
  29. data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
  30. data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
  31. data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
  32. data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
  33. data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
  34. data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
  35. data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
  36. data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
  37. data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
  38. data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
  39. data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
  40. data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
  41. data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
  42. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
  43. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
  44. data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
  45. data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
  46. data/lib/langsmithrb_rails/client.rb +77 -0
  47. data/lib/langsmithrb_rails/config.rb +72 -0
  48. data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
  49. data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
  50. data/lib/langsmithrb_rails/langsmith.rb +35 -0
  51. data/lib/langsmithrb_rails/railtie.rb +33 -0
  52. data/lib/langsmithrb_rails/redactor.rb +76 -0
  53. data/lib/langsmithrb_rails/version.rb +5 -0
  54. data/lib/langsmithrb_rails.rb +31 -0
  55. metadata +59 -6
@@ -0,0 +1,85 @@
1
+ name: LangSmith Evaluations
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ main, master ]
6
+ workflow_dispatch:
7
+ inputs:
8
+ dataset:
9
+ description: 'Evaluation dataset to use'
10
+ required: true
11
+ default: 'sample'
12
+ target:
13
+ description: 'Evaluation target to use'
14
+ required: true
15
+ default: 'http'
16
+ experiment_name:
17
+ description: 'Experiment name'
18
+ required: false
19
+ default: ''
20
+
21
+ jobs:
22
+ evaluate:
23
+ name: Run LangSmith Evaluations
24
+ runs-on: ubuntu-latest
25
+
26
+ env:
27
+ RAILS_ENV: test
28
+ LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
29
+ LANGSMITH_PROJECT: ${{ secrets.LANGSMITH_PROJECT || 'ci-evaluations' }}
30
+ LANGSMITH_EVAL_THRESHOLD: 0.7
31
+ LANGSMITH_EVAL_TARGET_URL: ${{ secrets.LANGSMITH_EVAL_TARGET_URL || 'http://localhost:3000/api/v1/evaluate' }}
32
+
33
+ steps:
34
+ - name: Checkout code
35
+ uses: actions/checkout@v3
36
+
37
+ - name: Set up Ruby
38
+ uses: ruby/setup-ruby@v1
39
+ with:
40
+ ruby-version: '3.2'
41
+ bundler-cache: true
42
+
43
+ - name: Setup database
44
+ run: |
45
+ bundle exec rails db:create
46
+ bundle exec rails db:schema:load
47
+
48
+ - name: Set experiment name
49
+ run: |
50
+ if [ -z "${{ github.event.inputs.experiment_name }}" ]; then
51
+ echo "EXPERIMENT_NAME=pr_${{ github.event.pull_request.number || github.run_id }}" >> $GITHUB_ENV
52
+ else
53
+ echo "EXPERIMENT_NAME=${{ github.event.inputs.experiment_name }}" >> $GITHUB_ENV
54
+ fi
55
+
56
+ - name: Run evaluations
57
+ run: |
58
+ bundle exec rails langsmith:eval[${{ github.event.inputs.dataset || 'sample' }},${{ github.event.inputs.target || 'http' }},${{ env.EXPERIMENT_NAME }}]
59
+
60
+ - name: Generate evaluation summary
61
+ run: |
62
+ ruby script/ci/langsmith_export_summary.rb > evaluation_summary.md
63
+
64
+ - name: Upload evaluation results
65
+ uses: actions/upload-artifact@v3
66
+ with:
67
+ name: langsmith-evaluation-results
68
+ path: |
69
+ tmp/langsmith/last_eval.json
70
+ evaluation_summary.md
71
+
72
+ - name: Comment on PR
73
+ if: github.event_name == 'pull_request'
74
+ uses: actions/github-script@v6
75
+ with:
76
+ github-token: ${{ secrets.GITHUB_TOKEN }}
77
+ script: |
78
+ const fs = require('fs');
79
+ const summary = fs.readFileSync('evaluation_summary.md', 'utf8');
80
+ github.rest.issues.createComment({
81
+ issue_number: context.issue.number,
82
+ owner: context.repo.owner,
83
+ repo: context.repo.repo,
84
+ body: summary
85
+ });
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ # Path to the evaluation results
8
+ results_path = File.join(Dir.pwd, "tmp/langsmith/last_eval.json")
9
+
10
+ unless File.exist?(results_path)
11
+ puts "## LangSmith Evaluation Results"
12
+ puts
13
+ puts "⚠️ No evaluation results found. Please run evaluations first."
14
+ exit 0
15
+ end
16
+
17
+ # Load the results
18
+ results = JSON.parse(File.read(results_path))
19
+
20
+ # Generate summary
21
+ puts "## LangSmith Evaluation Results"
22
+ puts
23
+ puts "### Summary"
24
+ puts
25
+ puts "- **Dataset**: #{results['dataset']}"
26
+ puts "- **Target**: #{results['target']}"
27
+ puts "- **Experiment**: #{results['experiment']}"
28
+ puts "- **Timestamp**: #{results['timestamp']}"
29
+ puts "- **Passed**: #{results['summary']['passed']}/#{results['summary']['total']} (#{(results['summary']['passed'].to_f / results['summary']['total'] * 100).round(1)}%)"
30
+ puts "- **Average Score**: #{(results['summary']['avg_score'] * 100).round(1)}%"
31
+ puts
32
+
33
+ # Determine overall status
34
+ threshold = ENV["LANGSMITH_EVAL_THRESHOLD"] ? ENV["LANGSMITH_EVAL_THRESHOLD"].to_f : 0.7
35
+ threshold_percent = (threshold * 100).round(1)
36
+
37
+ if results['summary']['avg_score'] >= threshold
38
+ puts "✅ **PASSED**: Score exceeds threshold of #{threshold_percent}%"
39
+ else
40
+ puts "❌ **FAILED**: Score below threshold of #{threshold_percent}%"
41
+ end
42
+
43
+ puts
44
+
45
+ # Show top 5 items with lowest scores
46
+ puts "### Lowest Scoring Items"
47
+ puts
48
+
49
+ items_with_scores = results['items'].map do |item|
50
+ total_score = 0.0
51
+ item['checks'].each do |_name, check|
52
+ total_score += check['score']
53
+ end
54
+ avg_score = item['checks'].empty? ? 0.0 : (total_score / item['checks'].size)
55
+
56
+ [item['id'], avg_score, item]
57
+ end
58
+
59
+ items_with_scores.sort_by! { |_, score, _| score }
60
+
61
+ puts "| ID | Score | Input | Status |"
62
+ puts "| --- | --- | --- | --- |"
63
+
64
+ items_with_scores.first(5).each do |id, score, item|
65
+ input_summary = item['input'].map { |k, v| "#{k}: #{v.to_s[0..30]}..." }.join(", ")
66
+ status = item['passed'] ? "✅" : "❌"
67
+ puts "| #{id} | #{(score * 100).round(1)}% | #{input_summary} | #{status} |"
68
+ end
69
+
70
+ puts
71
+
72
+ # Link to full results
73
+ puts "### Detailed Results"
74
+ puts
75
+ puts "Full results are available as an artifact in the GitHub Actions workflow."
76
+ puts
77
+ puts "To view the results locally, run:"
78
+ puts
79
+ puts "```bash"
80
+ puts "cat tmp/langsmith/last_eval.json | jq"
81
+ puts "```"
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module Generators
5
+ # Generator for adding a demo LLM application with LangSmith tracing
6
+ class DemoGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Adds a demo LLM application with LangSmith tracing"
10
+
11
+ class_option :provider, type: :string, default: "openai",
12
+ desc: "LLM provider to use (openai, anthropic, or mock)"
13
+
14
+ def check_dependencies
15
+ # Check if the required gems are installed
16
+ unless options[:provider] == "mock"
17
+ gem_name = options[:provider] == "anthropic" ? "anthropic" : "ruby-openai"
18
+
19
+ begin
20
+ require options[:provider] == "anthropic" ? "anthropic" : "openai"
21
+ rescue LoadError
22
+ say_status :error, "#{gem_name} gem is required for this generator", :red
23
+ say "Please add the following to your Gemfile and run bundle install:", :yellow
24
+ say "gem '#{gem_name}'", :yellow
25
+ exit 1
26
+ end
27
+ end
28
+ end
29
+
30
+ def create_service
31
+ template "llm_service.rb", "app/services/llm_service.rb"
32
+ end
33
+
34
+ def create_controller
35
+ template "chat_controller.rb", "app/controllers/chat_controller.rb"
36
+ end
37
+
38
+ def create_views
39
+ empty_directory "app/views/chat"
40
+ template "index.html.erb", "app/views/chat/index.html.erb"
41
+ end
42
+
43
+ def create_model
44
+ template "chat_message.rb", "app/models/chat_message.rb"
45
+ end
46
+
47
+ def create_migration
48
+ migration_template "create_chat_messages.rb", "db/migrate/create_chat_messages.rb"
49
+ end
50
+
51
+ def update_routes
52
+ route "resources :chat, only: [:index, :create]"
53
+ end
54
+
55
+ def create_javascript
56
+ empty_directory "app/javascript/controllers"
57
+ template "chat_controller.js", "app/javascript/controllers/chat_controller.js"
58
+ end
59
+
60
+ def display_post_install_message
61
+ say "\n"
62
+ say "LangSmith demo application has been added to your Rails application! 🎉", :green
63
+ say "\n"
64
+ say "This adds:", :yellow
65
+ say " 1. LLM service with LangSmith tracing", :yellow
66
+ say " 2. Chat controller and views", :yellow
67
+ say " 3. Chat message model and migration", :yellow
68
+ say "\n"
69
+ say "To set up the demo:", :yellow
70
+ say " 1. Run the migration: bin/rails db:migrate", :yellow
71
+ say " 2. Configure your LLM provider API key in your environment", :yellow
72
+ say " 3. Start your Rails server: bin/rails server", :yellow
73
+ say " 4. Visit http://localhost:3000/chat", :yellow
74
+ say "\n"
75
+ say "For OpenAI, set OPENAI_API_KEY in your environment", :yellow
76
+ say "For Anthropic, set ANTHROPIC_API_KEY in your environment", :yellow
77
+ say "\n"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,88 @@
1
+ // Chat controller for Stimulus.js
2
+ import { Controller } from "@hotwired/stimulus"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["input", "submitButton"]
6
+
7
+ connect() {
8
+ // Scroll to bottom of messages on load
9
+ this.scrollToBottom()
10
+
11
+ // Setup keyboard shortcuts
12
+ this.inputTarget.addEventListener("keydown", this.handleKeyDown.bind(this))
13
+ }
14
+
15
+ // Handle form submission
16
+ async submit(event) {
17
+ event.preventDefault()
18
+
19
+ const message = this.inputTarget.value.trim()
20
+ if (!message) return
21
+
22
+ // Disable input and button during submission
23
+ this.inputTarget.disabled = true
24
+ this.submitButton.disabled = true
25
+ this.submitButton.textContent = "Sending..."
26
+
27
+ try {
28
+ const form = event.target
29
+ const formData = new FormData(form)
30
+
31
+ const response = await fetch(form.action, {
32
+ method: form.method,
33
+ body: formData,
34
+ headers: {
35
+ "Accept": "application/json",
36
+ "X-Requested-With": "XMLHttpRequest"
37
+ }
38
+ })
39
+
40
+ if (response.ok) {
41
+ const data = await response.json()
42
+
43
+ // Add messages to the chat
44
+ const messagesContainer = document.getElementById("chat-messages")
45
+ messagesContainer.insertAdjacentHTML("beforeend", data.user_message)
46
+
47
+ // Clear input
48
+ this.inputTarget.value = ""
49
+
50
+ // Scroll to bottom
51
+ this.scrollToBottom()
52
+
53
+ // Add assistant message with a slight delay for better UX
54
+ setTimeout(() => {
55
+ messagesContainer.insertAdjacentHTML("beforeend", data.assistant_message)
56
+ this.scrollToBottom()
57
+ }, 500)
58
+ } else {
59
+ const errorData = await response.json()
60
+ alert(`Error: ${errorData.error || "Something went wrong"}`)
61
+ }
62
+ } catch (error) {
63
+ console.error("Error submitting message:", error)
64
+ alert("Failed to send message. Please try again.")
65
+ } finally {
66
+ // Re-enable input and button
67
+ this.inputTarget.disabled = false
68
+ this.submitButton.disabled = false
69
+ this.submitButton.textContent = "Send"
70
+ this.inputTarget.focus()
71
+ }
72
+ }
73
+
74
+ // Handle keyboard shortcuts
75
+ handleKeyDown(event) {
76
+ // Submit on Enter (without Shift)
77
+ if (event.key === "Enter" && !event.shiftKey) {
78
+ event.preventDefault()
79
+ this.submitButton.click()
80
+ }
81
+ }
82
+
83
+ // Scroll to the bottom of the messages container
84
+ scrollToBottom() {
85
+ const messagesContainer = document.getElementById("chat-messages")
86
+ messagesContainer.scrollTop = messagesContainer.scrollHeight
87
+ }
88
+ }
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Controller for the chat demo
4
+ class ChatController < ApplicationController
5
+ include LangsmithrbRails::TracedController
6
+
7
+ # GET /chat
8
+ def index
9
+ @messages = ChatMessage.order(created_at: :asc).last(10)
10
+ @message = ChatMessage.new
11
+ end
12
+
13
+ # POST /chat
14
+ def create
15
+ # Create trace for the entire request
16
+ langsmith_trace("chat_request", run_type: "chain") do |run|
17
+ @message = ChatMessage.new(message_params)
18
+ @message.is_user = true
19
+
20
+ if @message.save
21
+ # Record the user message in the trace
22
+ run.inputs = { user_message: @message.content }
23
+
24
+ # Generate a response
25
+ llm_service = LlmService.new
26
+ context = ChatMessage.order(created_at: :asc).last(5).map do |msg|
27
+ { is_user: msg.is_user, content: msg.content }
28
+ end
29
+
30
+ response = llm_service.generate(@message.content, context)
31
+
32
+ # Create the assistant message
33
+ @assistant_message = ChatMessage.create!(
34
+ content: response,
35
+ is_user: false
36
+ )
37
+
38
+ # Record the assistant response in the trace
39
+ run.outputs = { assistant_message: @assistant_message.content }
40
+
41
+ # Respond with both messages for AJAX updates
42
+ render json: {
43
+ user_message: render_to_string(partial: "message", locals: { message: @message }),
44
+ assistant_message: render_to_string(partial: "message", locals: { message: @assistant_message })
45
+ }
46
+ else
47
+ render json: { error: @message.errors.full_messages.join(", ") }, status: :unprocessable_entity
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Only allow a list of trusted parameters through
55
+ def message_params
56
+ params.require(:chat_message).permit(:content)
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Model for storing chat messages
4
+ class ChatMessage < ApplicationRecord
5
+ # Validations
6
+ validates :content, presence: true
7
+
8
+ # Scopes
9
+ scope :user_messages, -> { where(is_user: true) }
10
+ scope :assistant_messages, -> { where(is_user: false) }
11
+ scope :recent, -> { order(created_at: :desc) }
12
+
13
+ # Get the conversation history as an array of messages
14
+ # @param limit [Integer] Maximum number of messages to return
15
+ # @return [Array<Hash>] Array of messages with role and content
16
+ def self.conversation_history(limit = 10)
17
+ recent.limit(limit).map do |message|
18
+ {
19
+ role: message.is_user? ? "user" : "assistant",
20
+ content: message.content
21
+ }
22
+ end.reverse
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration for creating the chat_messages table
4
+ class CreateChatMessages < ActiveRecord::Migration[7.0]
5
+ def change
6
+ create_table :chat_messages do |t|
7
+ t.text :content, null: false
8
+ t.boolean :is_user, default: true
9
+ t.string :metadata
10
+ t.string :langsmith_run_id
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :chat_messages, :is_user
16
+ add_index :chat_messages, :langsmith_run_id
17
+ add_index :chat_messages, :created_at
18
+ end
19
+ end
@@ -0,0 +1,180 @@
1
+ <%# Chat demo index page %>
2
+ <div class="chat-container" data-controller="chat">
3
+ <div class="chat-header">
4
+ <h1>LangSmith Rails Demo</h1>
5
+ <p class="subtitle">A simple chat interface with LangSmith tracing</p>
6
+ </div>
7
+
8
+ <div class="chat-messages" id="chat-messages">
9
+ <% if @messages.empty? %>
10
+ <div class="welcome-message">
11
+ <h2>Welcome to the LangSmith Rails Demo!</h2>
12
+ <p>This is a simple chat interface that demonstrates LangSmith tracing in a Rails application.</p>
13
+ <p>Every message you send will be traced with LangSmith, allowing you to monitor and debug your LLM interactions.</p>
14
+ <p>Try asking a question below to get started!</p>
15
+ </div>
16
+ <% else %>
17
+ <% @messages.each do |message| %>
18
+ <%= render partial: "message", locals: { message: message } %>
19
+ <% end %>
20
+ <% end %>
21
+ </div>
22
+
23
+ <div class="chat-form">
24
+ <%= form_with(model: @message, url: chat_index_path, data: { action: "submit->chat#submit" }) do |form| %>
25
+ <div class="input-group">
26
+ <%= form.text_area :content, placeholder: "Type your message...", class: "chat-input", data: { chat_target: "input" } %>
27
+ <button type="submit" class="send-button" data-chat-target="submitButton">
28
+ Send
29
+ </button>
30
+ </div>
31
+ <% end %>
32
+ </div>
33
+
34
+ <div class="chat-footer">
35
+ <p>
36
+ Powered by LangSmith Rails
37
+ <% if ENV["LANGSMITH_PROJECT"].present? %>
38
+ | Project: <%= ENV["LANGSMITH_PROJECT"] %>
39
+ <% end %>
40
+ </p>
41
+ </div>
42
+ </div>
43
+
44
+ <%# Partial for rendering a message %>
45
+ <% content_for :partials do %>
46
+ <script type="text/html" id="message-template">
47
+ <div class="message <%= message.is_user? ? 'user-message' : 'assistant-message' %>">
48
+ <div class="message-content">
49
+ <%= message.content %>
50
+ </div>
51
+ <div class="message-meta">
52
+ <span class="message-time"><%= message.created_at.strftime("%H:%M") %></span>
53
+ <span class="message-role"><%= message.is_user? ? "You" : "Assistant" %></span>
54
+ </div>
55
+ </div>
56
+ </script>
57
+ <% end %>
58
+
59
+ <style>
60
+ .chat-container {
61
+ max-width: 800px;
62
+ margin: 0 auto;
63
+ padding: 20px;
64
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
65
+ }
66
+
67
+ .chat-header {
68
+ text-align: center;
69
+ margin-bottom: 20px;
70
+ padding-bottom: 10px;
71
+ border-bottom: 1px solid #eaeaea;
72
+ }
73
+
74
+ .chat-header h1 {
75
+ margin-bottom: 5px;
76
+ color: #333;
77
+ }
78
+
79
+ .subtitle {
80
+ color: #666;
81
+ margin-top: 0;
82
+ }
83
+
84
+ .chat-messages {
85
+ height: 500px;
86
+ overflow-y: auto;
87
+ padding: 10px;
88
+ background-color: #f9f9f9;
89
+ border-radius: 8px;
90
+ margin-bottom: 20px;
91
+ }
92
+
93
+ .welcome-message {
94
+ text-align: center;
95
+ padding: 40px 20px;
96
+ color: #666;
97
+ }
98
+
99
+ .welcome-message h2 {
100
+ color: #333;
101
+ margin-bottom: 15px;
102
+ }
103
+
104
+ .message {
105
+ margin-bottom: 15px;
106
+ padding: 10px 15px;
107
+ border-radius: 8px;
108
+ max-width: 80%;
109
+ position: relative;
110
+ }
111
+
112
+ .user-message {
113
+ background-color: #e1f5fe;
114
+ margin-left: auto;
115
+ border-bottom-right-radius: 0;
116
+ }
117
+
118
+ .assistant-message {
119
+ background-color: #f0f0f0;
120
+ margin-right: auto;
121
+ border-bottom-left-radius: 0;
122
+ }
123
+
124
+ .message-content {
125
+ white-space: pre-wrap;
126
+ }
127
+
128
+ .message-meta {
129
+ font-size: 0.8em;
130
+ color: #999;
131
+ margin-top: 5px;
132
+ text-align: right;
133
+ }
134
+
135
+ .chat-form {
136
+ margin-bottom: 20px;
137
+ }
138
+
139
+ .input-group {
140
+ display: flex;
141
+ gap: 10px;
142
+ }
143
+
144
+ .chat-input {
145
+ flex-grow: 1;
146
+ padding: 10px;
147
+ border: 1px solid #ddd;
148
+ border-radius: 4px;
149
+ resize: none;
150
+ height: 60px;
151
+ font-family: inherit;
152
+ }
153
+
154
+ .send-button {
155
+ padding: 10px 20px;
156
+ background-color: #2196f3;
157
+ color: white;
158
+ border: none;
159
+ border-radius: 4px;
160
+ cursor: pointer;
161
+ transition: background-color 0.2s;
162
+ }
163
+
164
+ .send-button:hover {
165
+ background-color: #0b7dda;
166
+ }
167
+
168
+ .send-button:disabled {
169
+ background-color: #cccccc;
170
+ cursor: not-allowed;
171
+ }
172
+
173
+ .chat-footer {
174
+ text-align: center;
175
+ color: #999;
176
+ font-size: 0.8em;
177
+ padding-top: 10px;
178
+ border-top: 1px solid #eaeaea;
179
+ }
180
+ </style>