flow_chat 0.5.1 → 0.6.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 +50 -5
- data/app/controllers/demo_controller.rb +101 -0
- data/app/flow_chat/demo_restaurant_flow.rb +889 -0
- data/config/routes_demo.rb +59 -0
- data/lib/flow_chat/base_processor.rb +1 -1
- data/lib/flow_chat/config.rb +3 -0
- data/lib/flow_chat/interrupt.rb +6 -5
- data/lib/flow_chat/prompt.rb +91 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +4 -4
- data/lib/flow_chat/ussd/app.rb +2 -2
- data/lib/flow_chat/ussd/gateway/nalo.rb +4 -4
- data/lib/flow_chat/ussd/middleware/executor.rb +2 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +35 -18
- data/lib/flow_chat/ussd/renderer.rb +28 -3
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +3 -3
- data/lib/flow_chat/whatsapp/client.rb +22 -15
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +36 -12
- data/lib/flow_chat/whatsapp/middleware/executor.rb +3 -3
- data/lib/flow_chat/whatsapp/renderer.rb +191 -0
- data/lib/flow_chat.rb +1 -0
- metadata +6 -3
- data/lib/flow_chat/ussd/prompt.rb +0 -102
- data/lib/flow_chat/whatsapp/prompt.rb +0 -247
@@ -0,0 +1,191 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Whatsapp
|
3
|
+
class Renderer
|
4
|
+
attr_reader :message, :choices, :media
|
5
|
+
|
6
|
+
def initialize(message, choices: nil, media: nil)
|
7
|
+
@message = message
|
8
|
+
@choices = choices
|
9
|
+
@media = media
|
10
|
+
end
|
11
|
+
|
12
|
+
def render
|
13
|
+
if media && choices
|
14
|
+
build_selection_message_with_media
|
15
|
+
elsif media
|
16
|
+
build_media_message
|
17
|
+
elsif choices
|
18
|
+
build_selection_message
|
19
|
+
else
|
20
|
+
build_text_message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_text_message
|
27
|
+
[:text, message, {}]
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_media_message
|
31
|
+
media_type = media[:type] || :image
|
32
|
+
url = media[:url] || media[:path]
|
33
|
+
filename = media[:filename]
|
34
|
+
|
35
|
+
case media_type.to_sym
|
36
|
+
when :image
|
37
|
+
[:media_image, "", {url: url, caption: message}]
|
38
|
+
when :document
|
39
|
+
[:media_document, "", {url: url, caption: message, filename: filename}]
|
40
|
+
when :audio
|
41
|
+
[:media_audio, "", {url: url, caption: message}]
|
42
|
+
when :video
|
43
|
+
[:media_video, "", {url: url, caption: message}]
|
44
|
+
when :sticker
|
45
|
+
[:media_sticker, "", {url: url}] # Stickers don't support captions
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Unsupported media type: #{media_type}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_selection_message
|
52
|
+
# Determine the best way to present choices
|
53
|
+
if choices.is_a?(Array)
|
54
|
+
# Convert array to hash with index-based keys
|
55
|
+
choice_hash = choices.each_with_index.to_h { |choice, index| [index.to_s, choice] }
|
56
|
+
build_interactive_message(choice_hash)
|
57
|
+
elsif choices.is_a?(Hash)
|
58
|
+
build_interactive_message(choices)
|
59
|
+
else
|
60
|
+
raise ArgumentError, "choices must be an Array or Hash"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_selection_message_with_media
|
65
|
+
# Convert array to hash with index-based keys if needed, same as build_selection_message
|
66
|
+
if choices.is_a?(Array)
|
67
|
+
choice_hash = choices.each_with_index.to_h { |choice, index| [index.to_s, choice] }
|
68
|
+
build_buttons_message_with_media(choice_hash)
|
69
|
+
elsif choices.is_a?(Hash)
|
70
|
+
build_buttons_message_with_media(choices)
|
71
|
+
else
|
72
|
+
raise ArgumentError, "choices must be an Array or Hash"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_interactive_message(choice_hash)
|
77
|
+
if choice_hash.length <= 3
|
78
|
+
# Use buttons for 3 or fewer choices
|
79
|
+
build_buttons_message(choice_hash)
|
80
|
+
else
|
81
|
+
# Use list for more than 3 choices
|
82
|
+
build_list_message(choice_hash)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_buttons_message(choices)
|
87
|
+
buttons = choices.map do |key, value|
|
88
|
+
{
|
89
|
+
id: key.to_s,
|
90
|
+
title: truncate_text(value.to_s, 20) # WhatsApp button titles have a 20 character limit
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
[:interactive_buttons, message, {buttons: buttons}]
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_buttons_message_with_media(choices)
|
98
|
+
buttons = choices.map do |key, value|
|
99
|
+
{
|
100
|
+
id: key.to_s,
|
101
|
+
title: truncate_text(value.to_s, 20) # WhatsApp button titles have a 20 character limit
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
# Build media header
|
106
|
+
header = build_media_header
|
107
|
+
|
108
|
+
[:interactive_buttons, message, {buttons: buttons, header: header}]
|
109
|
+
end
|
110
|
+
|
111
|
+
def build_media_header
|
112
|
+
media_type = media[:type] || :image
|
113
|
+
url = media[:url] || media[:path]
|
114
|
+
filename = media[:filename]
|
115
|
+
|
116
|
+
case media_type.to_sym
|
117
|
+
when :image
|
118
|
+
{
|
119
|
+
type: "image",
|
120
|
+
image: {link: url}
|
121
|
+
}
|
122
|
+
when :video
|
123
|
+
{
|
124
|
+
type: "video",
|
125
|
+
video: {link: url}
|
126
|
+
}
|
127
|
+
when :document
|
128
|
+
header_doc = {link: url}
|
129
|
+
header_doc[:filename] = filename if filename
|
130
|
+
{
|
131
|
+
type: "document",
|
132
|
+
document: header_doc
|
133
|
+
}
|
134
|
+
when :text
|
135
|
+
{
|
136
|
+
type: "text",
|
137
|
+
text: url # For text headers, url contains the text
|
138
|
+
}
|
139
|
+
else
|
140
|
+
raise ArgumentError, "Unsupported header media type: #{media_type}. Supported types for button headers: image, video, document, text"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_list_message(choices)
|
145
|
+
items = choices.map do |key, value|
|
146
|
+
original_text = value.to_s
|
147
|
+
truncated_title = truncate_text(original_text, 24)
|
148
|
+
|
149
|
+
# If title was truncated, put full text in description (up to 72 chars)
|
150
|
+
description = if original_text.length > 24
|
151
|
+
truncate_text(original_text, 72)
|
152
|
+
end
|
153
|
+
|
154
|
+
{
|
155
|
+
id: key.to_s,
|
156
|
+
title: truncated_title,
|
157
|
+
description: description
|
158
|
+
}.compact
|
159
|
+
end
|
160
|
+
|
161
|
+
# If 10 or fewer items, use single section
|
162
|
+
sections = if items.length <= 10
|
163
|
+
[
|
164
|
+
{
|
165
|
+
title: "Options",
|
166
|
+
rows: items
|
167
|
+
}
|
168
|
+
]
|
169
|
+
else
|
170
|
+
# Paginate into multiple sections (max 10 items per section)
|
171
|
+
items.each_slice(10).with_index.map do |section_items, index|
|
172
|
+
start_num = (index * 10) + 1
|
173
|
+
end_num = start_num + section_items.length - 1
|
174
|
+
|
175
|
+
{
|
176
|
+
title: "#{start_num}-#{end_num}",
|
177
|
+
rows: section_items
|
178
|
+
}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
[:interactive_list, message, {sections: sections}]
|
183
|
+
end
|
184
|
+
|
185
|
+
def truncate_text(text, length)
|
186
|
+
return text if text.length <= length
|
187
|
+
text[0, length - 3] + "..."
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
data/lib/flow_chat.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flow_chat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Froelich
|
@@ -96,8 +96,11 @@ files:
|
|
96
96
|
- README.md
|
97
97
|
- Rakefile
|
98
98
|
- SECURITY.md
|
99
|
+
- app/controllers/demo_controller.rb
|
100
|
+
- app/flow_chat/demo_restaurant_flow.rb
|
99
101
|
- bin/console
|
100
102
|
- bin/setup
|
103
|
+
- config/routes_demo.rb
|
101
104
|
- examples/initializer.rb
|
102
105
|
- examples/media_prompts_examples.rb
|
103
106
|
- examples/multi_tenant_whatsapp_controller.rb
|
@@ -114,6 +117,7 @@ files:
|
|
114
117
|
- lib/flow_chat/context.rb
|
115
118
|
- lib/flow_chat/flow.rb
|
116
119
|
- lib/flow_chat/interrupt.rb
|
120
|
+
- lib/flow_chat/prompt.rb
|
117
121
|
- lib/flow_chat/session/cache_session_store.rb
|
118
122
|
- lib/flow_chat/session/middleware.rb
|
119
123
|
- lib/flow_chat/session/rails_session_store.rb
|
@@ -126,7 +130,6 @@ files:
|
|
126
130
|
- lib/flow_chat/ussd/middleware/pagination.rb
|
127
131
|
- lib/flow_chat/ussd/middleware/resumable_session.rb
|
128
132
|
- lib/flow_chat/ussd/processor.rb
|
129
|
-
- lib/flow_chat/ussd/prompt.rb
|
130
133
|
- lib/flow_chat/ussd/renderer.rb
|
131
134
|
- lib/flow_chat/version.rb
|
132
135
|
- lib/flow_chat/whatsapp/app.rb
|
@@ -135,7 +138,7 @@ files:
|
|
135
138
|
- lib/flow_chat/whatsapp/gateway/cloud_api.rb
|
136
139
|
- lib/flow_chat/whatsapp/middleware/executor.rb
|
137
140
|
- lib/flow_chat/whatsapp/processor.rb
|
138
|
-
- lib/flow_chat/whatsapp/
|
141
|
+
- lib/flow_chat/whatsapp/renderer.rb
|
139
142
|
- lib/flow_chat/whatsapp/send_job_support.rb
|
140
143
|
- lib/flow_chat/whatsapp/template_manager.rb
|
141
144
|
homepage: https://github.com/radioactive-labs/flow_chat
|
@@ -1,102 +0,0 @@
|
|
1
|
-
module FlowChat
|
2
|
-
module Ussd
|
3
|
-
class Prompt
|
4
|
-
attr_reader :user_input
|
5
|
-
|
6
|
-
def initialize(input)
|
7
|
-
@user_input = input
|
8
|
-
end
|
9
|
-
|
10
|
-
def ask(msg, choices: nil, convert: nil, validate: nil, transform: nil, media: nil)
|
11
|
-
if user_input.present?
|
12
|
-
input = user_input
|
13
|
-
input = convert.call(input) if convert.present?
|
14
|
-
validation_error = validate.call(input) if validate.present?
|
15
|
-
|
16
|
-
if validation_error.present?
|
17
|
-
# Include media URL in validation error message
|
18
|
-
original_message_with_media = build_message_with_media(msg, media)
|
19
|
-
prompt!([validation_error, original_message_with_media].join("\n\n"), choices:)
|
20
|
-
end
|
21
|
-
|
22
|
-
input = transform.call(input) if transform.present?
|
23
|
-
return input
|
24
|
-
end
|
25
|
-
|
26
|
-
# Include media URL in the message for USSD
|
27
|
-
final_message = build_message_with_media(msg, media)
|
28
|
-
prompt! final_message, choices:
|
29
|
-
end
|
30
|
-
|
31
|
-
def say(message, media: nil)
|
32
|
-
# Include media URL in the message for USSD
|
33
|
-
final_message = build_message_with_media(message, media)
|
34
|
-
terminate! final_message
|
35
|
-
end
|
36
|
-
|
37
|
-
def select(msg, choices)
|
38
|
-
choices, choices_prompt = build_select_choices choices
|
39
|
-
ask(
|
40
|
-
msg,
|
41
|
-
choices: choices_prompt,
|
42
|
-
convert: lambda { |choice| choice.to_i },
|
43
|
-
validate: lambda { |choice| "Invalid selection:" unless (1..choices.size).cover?(choice) },
|
44
|
-
transform: lambda { |choice| choices[choice - 1] }
|
45
|
-
)
|
46
|
-
end
|
47
|
-
|
48
|
-
def yes?(msg)
|
49
|
-
select(msg, ["Yes", "No"]) == "Yes"
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def build_message_with_media(message, media)
|
55
|
-
return message unless media
|
56
|
-
|
57
|
-
media_url = media[:url] || media[:path]
|
58
|
-
media_type = media[:type] || :image
|
59
|
-
|
60
|
-
# For USSD, we append the media URL to the message
|
61
|
-
media_text = case media_type.to_sym
|
62
|
-
when :image
|
63
|
-
"📷 Image: #{media_url}"
|
64
|
-
when :document
|
65
|
-
"📄 Document: #{media_url}"
|
66
|
-
when :audio
|
67
|
-
"🎵 Audio: #{media_url}"
|
68
|
-
when :video
|
69
|
-
"🎥 Video: #{media_url}"
|
70
|
-
when :sticker
|
71
|
-
"😊 Sticker: #{media_url}"
|
72
|
-
else
|
73
|
-
"📎 Media: #{media_url}"
|
74
|
-
end
|
75
|
-
|
76
|
-
# Combine message with media information
|
77
|
-
"#{message}\n\n#{media_text}"
|
78
|
-
end
|
79
|
-
|
80
|
-
def build_select_choices(choices)
|
81
|
-
case choices
|
82
|
-
when Array
|
83
|
-
choices_prompt = choices.map.with_index { |c, i| [i + 1, c] }.to_h
|
84
|
-
when Hash
|
85
|
-
choices_prompt = choices.values.map.with_index { |c, i| [i + 1, c] }.to_h
|
86
|
-
choices = choices.keys
|
87
|
-
else
|
88
|
-
raise ArgumentError, "choices must be an array or hash"
|
89
|
-
end
|
90
|
-
[choices, choices_prompt]
|
91
|
-
end
|
92
|
-
|
93
|
-
def prompt!(msg, choices:)
|
94
|
-
raise FlowChat::Interrupt::Prompt.new(msg, choices:)
|
95
|
-
end
|
96
|
-
|
97
|
-
def terminate!(msg)
|
98
|
-
raise FlowChat::Interrupt::Terminate.new(msg)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
@@ -1,247 +0,0 @@
|
|
1
|
-
module FlowChat
|
2
|
-
module Whatsapp
|
3
|
-
class Prompt
|
4
|
-
attr_reader :input
|
5
|
-
|
6
|
-
def initialize(input)
|
7
|
-
@input = input
|
8
|
-
end
|
9
|
-
|
10
|
-
def ask(message, transform: nil, validate: nil, convert: nil, media: nil)
|
11
|
-
if input.present?
|
12
|
-
begin
|
13
|
-
processed_input = process_input(input, transform, validate, convert)
|
14
|
-
return processed_input unless processed_input.nil?
|
15
|
-
rescue FlowChat::Interrupt::Prompt => validation_error
|
16
|
-
# If validation failed, include the error message with the original prompt
|
17
|
-
error_message = validation_error.prompt[1]
|
18
|
-
combined_message = "#{message}\n\n#{error_message}"
|
19
|
-
raise FlowChat::Interrupt::Prompt.new([:text, combined_message, {}])
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# Send message and wait for response, optionally with media
|
24
|
-
if media
|
25
|
-
raise FlowChat::Interrupt::Prompt.new(build_media_prompt(message, media))
|
26
|
-
else
|
27
|
-
raise FlowChat::Interrupt::Prompt.new([:text, message, {}])
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def say(message, media: nil)
|
32
|
-
if media
|
33
|
-
raise FlowChat::Interrupt::Terminate.new(build_media_prompt(message, media))
|
34
|
-
else
|
35
|
-
raise FlowChat::Interrupt::Terminate.new([:text, message, {}])
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def select(message, choices, transform: nil, validate: nil, convert: nil)
|
40
|
-
if input.present?
|
41
|
-
processed_input = process_selection(input, choices, transform, validate, convert)
|
42
|
-
return processed_input unless processed_input.nil?
|
43
|
-
end
|
44
|
-
|
45
|
-
# Validate choices
|
46
|
-
validate_choices(choices)
|
47
|
-
|
48
|
-
# Standard selection without media support
|
49
|
-
interactive_prompt = build_selection_prompt(message, choices)
|
50
|
-
raise FlowChat::Interrupt::Prompt.new(interactive_prompt)
|
51
|
-
end
|
52
|
-
|
53
|
-
def yes?(message, transform: nil, validate: nil, convert: nil)
|
54
|
-
if input.present?
|
55
|
-
processed_input = process_boolean(input, transform, validate, convert)
|
56
|
-
return processed_input unless processed_input.nil?
|
57
|
-
end
|
58
|
-
|
59
|
-
buttons = [
|
60
|
-
{id: "yes", title: "Yes"},
|
61
|
-
{id: "no", title: "No"}
|
62
|
-
]
|
63
|
-
raise FlowChat::Interrupt::Prompt.new([:interactive_buttons, message, {buttons: buttons}])
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
def build_media_prompt(message, media)
|
69
|
-
media_type = media[:type] || :image
|
70
|
-
url = media[:url] || media[:path]
|
71
|
-
filename = media[:filename]
|
72
|
-
|
73
|
-
case media_type.to_sym
|
74
|
-
when :image
|
75
|
-
[:media_image, "", {url: url, caption: message}]
|
76
|
-
when :document
|
77
|
-
[:media_document, "", {url: url, caption: message, filename: filename}]
|
78
|
-
when :audio
|
79
|
-
[:media_audio, "", {url: url, caption: message}]
|
80
|
-
when :video
|
81
|
-
[:media_video, "", {url: url, caption: message}]
|
82
|
-
when :sticker
|
83
|
-
[:media_sticker, "", {url: url}] # Stickers don't support captions
|
84
|
-
else
|
85
|
-
raise ArgumentError, "Unsupported media type: #{media_type}"
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def build_selection_prompt(message, choices)
|
90
|
-
# Determine the best way to present choices
|
91
|
-
if choices.is_a?(Array)
|
92
|
-
# Convert array to hash with index-based keys
|
93
|
-
choice_hash = choices.each_with_index.to_h { |choice, index| [index.to_s, choice] }
|
94
|
-
build_list_prompt(message, choice_hash)
|
95
|
-
elsif choices.is_a?(Hash)
|
96
|
-
if choices.length <= 3
|
97
|
-
# Use buttons for 3 or fewer choices
|
98
|
-
build_buttons_prompt(message, choices)
|
99
|
-
else
|
100
|
-
# Use list for more than 3 choices
|
101
|
-
build_list_prompt(message, choices)
|
102
|
-
end
|
103
|
-
else
|
104
|
-
raise ArgumentError, "choices must be an Array or Hash"
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def build_buttons_prompt(message, choices)
|
109
|
-
buttons = choices.map do |key, value|
|
110
|
-
{
|
111
|
-
id: key.to_s,
|
112
|
-
title: truncate_text(value.to_s, 20) # WhatsApp button titles have a 20 character limit
|
113
|
-
}
|
114
|
-
end
|
115
|
-
|
116
|
-
[:interactive_buttons, message, {buttons: buttons}]
|
117
|
-
end
|
118
|
-
|
119
|
-
def build_list_prompt(message, choices)
|
120
|
-
items = choices.map do |key, value|
|
121
|
-
original_text = value.to_s
|
122
|
-
truncated_title = truncate_text(original_text, 24)
|
123
|
-
|
124
|
-
# If title was truncated, put full text in description (up to 72 chars)
|
125
|
-
description = if original_text.length > 24
|
126
|
-
truncate_text(original_text, 72)
|
127
|
-
end
|
128
|
-
|
129
|
-
{
|
130
|
-
id: key.to_s,
|
131
|
-
title: truncated_title,
|
132
|
-
description: description
|
133
|
-
}.compact
|
134
|
-
end
|
135
|
-
|
136
|
-
# If 10 or fewer items, use single section
|
137
|
-
sections = if items.length <= 10
|
138
|
-
[
|
139
|
-
{
|
140
|
-
title: "Options",
|
141
|
-
rows: items
|
142
|
-
}
|
143
|
-
]
|
144
|
-
else
|
145
|
-
# Paginate into multiple sections (max 10 items per section)
|
146
|
-
items.each_slice(10).with_index.map do |section_items, index|
|
147
|
-
start_num = (index * 10) + 1
|
148
|
-
end_num = start_num + section_items.length - 1
|
149
|
-
|
150
|
-
{
|
151
|
-
title: "#{start_num}-#{end_num}",
|
152
|
-
rows: section_items
|
153
|
-
}
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
[:interactive_list, message, {sections: sections}]
|
158
|
-
end
|
159
|
-
|
160
|
-
def process_input(input, transform, validate, convert)
|
161
|
-
# Apply transformation
|
162
|
-
transformed_input = transform ? transform.call(input) : input
|
163
|
-
|
164
|
-
# Apply conversion first, then validation
|
165
|
-
converted_input = convert ? convert.call(transformed_input) : transformed_input
|
166
|
-
|
167
|
-
# Apply validation on converted value
|
168
|
-
if validate
|
169
|
-
error_message = validate.call(converted_input)
|
170
|
-
if error_message
|
171
|
-
raise FlowChat::Interrupt::Prompt.new([:text, error_message, {}])
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
converted_input
|
176
|
-
end
|
177
|
-
|
178
|
-
def process_selection(input, choices, transform, validate, convert)
|
179
|
-
choice_hash = choices.is_a?(Array) ?
|
180
|
-
choices.each_with_index.to_h { |choice, index| [index.to_s, choice] } :
|
181
|
-
choices
|
182
|
-
|
183
|
-
# Check if input matches a valid choice
|
184
|
-
if choice_hash.key?(input)
|
185
|
-
selected_value = choice_hash[input]
|
186
|
-
process_input(selected_value, transform, validate, convert)
|
187
|
-
elsif choice_hash.value?(input)
|
188
|
-
# Input matches a choice value directly
|
189
|
-
process_input(input, transform, validate, convert)
|
190
|
-
else
|
191
|
-
# Invalid choice
|
192
|
-
choice_list = choice_hash.map { |key, value| "#{key}: #{value}" }.join("\n")
|
193
|
-
error_message = "Invalid choice. Please select one of:\n#{choice_list}"
|
194
|
-
raise FlowChat::Interrupt::Prompt.new([:text, error_message, {}])
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def process_boolean(input, transform, validate, convert)
|
199
|
-
boolean_value = case input.to_s.downcase
|
200
|
-
when "yes", "y", "1", "true"
|
201
|
-
true
|
202
|
-
when "no", "n", "0", "false"
|
203
|
-
false
|
204
|
-
end
|
205
|
-
|
206
|
-
if boolean_value.nil?
|
207
|
-
raise FlowChat::Interrupt::Prompt.new([:text, "Please answer with Yes or No.", {}])
|
208
|
-
end
|
209
|
-
|
210
|
-
process_input(boolean_value, transform, validate, convert)
|
211
|
-
end
|
212
|
-
|
213
|
-
def validate_choices(choices)
|
214
|
-
# Check for empty choices
|
215
|
-
if choices.nil? || choices.empty?
|
216
|
-
raise ArgumentError, "choices cannot be empty"
|
217
|
-
end
|
218
|
-
|
219
|
-
choice_count = choices.length
|
220
|
-
|
221
|
-
# WhatsApp supports max 100 total items across all sections
|
222
|
-
if choice_count > 100
|
223
|
-
raise ArgumentError, "WhatsApp supports maximum 100 choice options, got #{choice_count}"
|
224
|
-
end
|
225
|
-
|
226
|
-
# Validate individual choice values
|
227
|
-
choices_to_validate = choices.is_a?(Array) ? choices : choices.values
|
228
|
-
|
229
|
-
choices_to_validate.each_with_index do |choice, index|
|
230
|
-
if choice.nil? || choice.to_s.strip.empty?
|
231
|
-
raise ArgumentError, "choice at index #{index} cannot be empty"
|
232
|
-
end
|
233
|
-
|
234
|
-
choice_text = choice.to_s
|
235
|
-
if choice_text.length > 100
|
236
|
-
raise ArgumentError, "choice '#{choice_text[0..20]}...' is too long (#{choice_text.length} chars). Maximum is 100 characters"
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def truncate_text(text, length)
|
242
|
-
return text if text.length <= length
|
243
|
-
text[0, length - 3] + "..."
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
247
|
-
end
|