Class: RailsMarkup::Annotation

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/rails_markup/annotation.rb

Constant Summary collapse

INTENTS =
%w[fix change question approve].freeze
SEVERITIES =
%w[suggestion important blocking].freeze
STATUSES =
%w[pending acknowledged resolved dismissed].freeze
BROWSER_ATTRIBUTES =
%w[content intent severity selected_text target page_url].freeze
BROWSER_METADATA_KEYS =
%w[tool url localId sessionId screenshot].freeze
CLIENT_UUID_PATTERN =
/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/
CLIENT_UUID_INPUT_PATTERN =
/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
LEGACY_SESSION_PATTERN =
/\Arm-[0-9a-f]{16}\z/i
LEGACY_CLIENT_ID_LIMIT =
256
LEGACY_UUID_NAMESPACE =
"265e7cf0-8be6-5e21-8f31-a582cfde8646"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.distinct_authorsObject



90
91
92
93
94
95
96
# File 'app/models/rails_markup/annotation.rb', line 90

def self.distinct_authors
  if connection.adapter_name.downcase.include?("postgres")
    where("metadata->>'author' IS NOT NULL").distinct.pluck(Arel.sql("metadata->>'author'")).compact.sort
  else
    all.filter_map(&:author_name).uniq.sort
  end
end

.legacy_client_uuid(session_id:, legacy_client_id:) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'app/models/rails_markup/annotation.rb', line 48

def self.legacy_client_uuid(session_id:, legacy_client_id:)
  session_id = session_id.to_s
  legacy_client_id = legacy_client_id.to_s
  unless LEGACY_SESSION_PATTERN.match?(session_id) && legacy_client_id.present? && legacy_client_id.bytesize <= LEGACY_CLIENT_ID_LIMIT
    raise ArgumentError, "legacy client identity requires a valid session and client id"
  end

  namespace = [LEGACY_UUID_NAMESPACE.delete("-")].pack("H*")
  bytes = Digest::SHA1.digest(namespace + "#{session_id}\0#{legacy_client_id}").bytes.first(16)
  bytes[6] = (bytes[6] & 0x0f) | 0x50
  bytes[8] = (bytes[8] & 0x3f) | 0x80
  hex = bytes.pack("C*").unpack1("H*")
  "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
end

.normalize_client_uuid(value) ⇒ Object



44
45
46
# File 'app/models/rails_markup/annotation.rb', line 44

def self.normalize_client_uuid(value)
  value.downcase if value.is_a?(String) && CLIENT_UUID_INPUT_PATTERN.match?(value)
end

.valid_client_uuid?(value) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
# File 'app/models/rails_markup/annotation.rb', line 40

def self.valid_client_uuid?(value)
  value.is_a?(String) && CLIENT_UUID_PATTERN.match?(value)
end

Instance Method Details

#acknowledge!Object



109
110
111
112
113
# File 'app/models/rails_markup/annotation.rb', line 109

def acknowledge!
  raise "Cannot acknowledge a #{status} annotation" unless status == "pending"

  update!(status: "acknowledged")
end

#add_reply!(message:, role: "agent") ⇒ Object



133
134
135
136
# File 'app/models/rails_markup/annotation.rb', line 133

def add_reply!(message:, role: "agent")
  add_thread_entry(role: role, message: message)
  save!
end

#apply_browser_state(attributes, dirty_fields: []) ⇒ Object



102
103
104
105
106
107
# File 'app/models/rails_markup/annotation.rb', line 102

def apply_browser_state(attributes, dirty_fields: [])
  assign_attributes(attributes.slice(*BROWSER_ATTRIBUTES))
  self. = ( || {}).merge(attributes.fetch("metadata", {}).slice(*BROWSER_METADATA_KEYS))
  self.status = attributes["status"] if dirty_fields.include?("status")
  self
end

#as_api_jsonObject



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'app/models/rails_markup/annotation.rb', line 138

def as_api_json
  {
    id: id.to_s,
    clientId: client_uuid,
    userId: user_id,
    authorName: author_name,
    content: content,
    intent: intent,
    severity: severity,
    status: status,
    selectedText: selected_text,
    pageUrl: page_url,
    target: target,
    metadata: ,
    thread: thread,
    createdAt: created_at&.iso8601,
    updatedAt: updated_at&.iso8601
  }
end

#author_nameObject



98
99
100
# File 'app/models/rails_markup/annotation.rb', line 98

def author_name
  &.dig("author")
end

#dismiss!(reason: nil) ⇒ Object



124
125
126
127
128
129
130
131
# File 'app/models/rails_markup/annotation.rb', line 124

def dismiss!(reason: nil)
  raise "Cannot dismiss a #{status} annotation" unless status.in?(%w[pending acknowledged])

  transaction do
    add_thread_entry(role: "agent", message: reason) if reason.present?
    update!(status: "dismissed")
  end
end

#resolve!(summary: nil) ⇒ Object



115
116
117
118
119
120
121
122
# File 'app/models/rails_markup/annotation.rb', line 115

def resolve!(summary: nil)
  raise "Cannot resolve a #{status} annotation" unless status.in?(%w[pending acknowledged])

  transaction do
    add_thread_entry(role: "agent", message: summary) if summary.present?
    update!(status: "resolved")
  end
end