FEATURE: smart date support for AI helper (#1044)
* FEATURE: smart date support for AI helper This feature allows conversion of human typed in dates and times to smart "Discourse" timezone friendly dates. * fix specs and lint * lint * address feedback * add specs
This commit is contained in:
parent
f9f89adac5
commit
11d0f60f1e
|
@ -233,6 +233,7 @@ en:
|
|||
custom_prompt: "Custom Prompt"
|
||||
explain: "Explain"
|
||||
illustrate_post: "Illustrate Post"
|
||||
replace_dates: "Smart dates"
|
||||
painter:
|
||||
attribution:
|
||||
stable_diffusion_xl: "Image by Stable Diffusion XL"
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# frozen_string_literal: true
|
||||
CompletionPrompt.seed do |cp|
|
||||
cp.id = -301
|
||||
|
@ -217,3 +215,55 @@ CompletionPrompt.seed do |cp|
|
|||
examples: [["<input>Hello my favourite colour is red</input>", "<output>en-GB</output>"]],
|
||||
}
|
||||
end
|
||||
|
||||
CompletionPrompt.seed do |cp|
|
||||
cp.id = -310
|
||||
cp.name = "replace_dates"
|
||||
cp.prompt_type = CompletionPrompt.prompt_types[:diff]
|
||||
cp.temperature = 0
|
||||
cp.stop_sequences = ["\n</output>"]
|
||||
cp.messages = {
|
||||
insts: <<~TEXT,
|
||||
You are a date and time formatter for Discourse posts. Convert natural language time references into date placeholders.
|
||||
Do not modify any markdown, code blocks, or existing date formats.
|
||||
|
||||
Here's the temporal context:
|
||||
{{temporal_context}}
|
||||
|
||||
Available date placeholder formats:
|
||||
- Simple day without time: {{date:1}} for tomorrow, {{date:7}} for a week from today
|
||||
- Specific time: {{datetime:2pm+1}} for 2 PM tomorrow
|
||||
- Time range: {{datetime:2pm+1:4pm+1}} for tomorrow 2 PM to 4 PM
|
||||
|
||||
You will find the text between <input></input> XML tags.
|
||||
Return the text with dates converted between <output></output> XML tags.
|
||||
TEXT
|
||||
examples: [
|
||||
[
|
||||
"<input>The meeting is at 2pm tomorrow</input>",
|
||||
"<output>The meeting is at {{datetime:2pm+1}}</output>",
|
||||
],
|
||||
["<input>Due in 3 days</input>", "<output>Due {{date:3}}</output>"],
|
||||
[
|
||||
"<input>Meeting next Tuesday at 2pm</input>",
|
||||
"<output>Meeting {{next_week:tuesday-2pm}}</output>",
|
||||
],
|
||||
[
|
||||
"<input>Meeting from 2pm to 4pm tomorrow</input>",
|
||||
"<output>Meeting {{datetime:2pm+1:4pm+1}}</output>",
|
||||
],
|
||||
[
|
||||
"<input>Meeting notes for tomorrow:
|
||||
* Action items in `config.rb`
|
||||
* Review PR #1234
|
||||
* Deadline is 5pm
|
||||
* Check [this link](https://example.com)</input>",
|
||||
"<output>Meeting notes for {{date:1}}:
|
||||
* Action items in `config.rb`
|
||||
* Review PR #1234
|
||||
* Deadline is {{datetime:5pm+1}}
|
||||
* Check [this link](https://example.com)</output>",
|
||||
],
|
||||
],
|
||||
}
|
||||
end
|
||||
|
|
|
@ -60,7 +60,7 @@ module DiscourseAi
|
|||
|
||||
def custom_locale_instructions(user = nil, force_default_locale)
|
||||
locale = SiteSetting.default_locale
|
||||
locale = user.effective_locale if !force_default_locale
|
||||
locale = user.effective_locale if !force_default_locale && user
|
||||
locale_hash = LocaleSiteSetting.language_names[locale]
|
||||
|
||||
if locale != "en" && locale_hash
|
||||
|
@ -71,7 +71,7 @@ module DiscourseAi
|
|||
end
|
||||
end
|
||||
|
||||
def localize_prompt!(prompt, user = nil, force_default_locale)
|
||||
def localize_prompt!(prompt, user = nil, force_default_locale = false)
|
||||
locale_instructions = custom_locale_instructions(user, force_default_locale)
|
||||
if locale_instructions
|
||||
prompt.messages[0][:content] = prompt.messages[0][:content] + locale_instructions
|
||||
|
@ -89,6 +89,29 @@ module DiscourseAi
|
|||
"#{locale_hash["name"]}",
|
||||
)
|
||||
end
|
||||
|
||||
if user && prompt.messages[0][:content].include?("{{temporal_context}}")
|
||||
timezone = user.user_option.timezone || "UTC"
|
||||
current_time = Time.now.in_time_zone(timezone)
|
||||
|
||||
temporal_context = {
|
||||
utc_date_time: current_time.iso8601,
|
||||
local_time: current_time.strftime("%H:%M"),
|
||||
user: {
|
||||
timezone: timezone,
|
||||
weekday: current_time.strftime("%A"),
|
||||
},
|
||||
}
|
||||
|
||||
prompt.messages[0][:content] = prompt.messages[0][:content].gsub(
|
||||
"{{temporal_context}}",
|
||||
temporal_context.to_json,
|
||||
)
|
||||
|
||||
prompt.messages.each do |message|
|
||||
message[:content] = DateFormatter.process_date_placeholders(message[:content], user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def generate_prompt(completion_prompt, input, user, force_default_locale = false, &block)
|
||||
|
@ -206,6 +229,8 @@ module DiscourseAi
|
|||
"question"
|
||||
when "illustrate_post"
|
||||
"images"
|
||||
when "replace_dates"
|
||||
"calendar-days"
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
@ -233,6 +258,8 @@ module DiscourseAi
|
|||
%w[post]
|
||||
when "illustrate_post"
|
||||
%w[composer]
|
||||
when "replace_dates"
|
||||
%w[composer]
|
||||
else
|
||||
%w[]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiHelper
|
||||
class DateFormatter
|
||||
DAYS_OF_WEEK = {
|
||||
"monday" => 1,
|
||||
"tuesday" => 2,
|
||||
"wednesday" => 3,
|
||||
"thursday" => 4,
|
||||
"friday" => 5,
|
||||
"saturday" => 6,
|
||||
"sunday" => 0,
|
||||
}
|
||||
|
||||
class << self
|
||||
def process_date_placeholders(text, user)
|
||||
return text if !text.include?("{{")
|
||||
|
||||
timezone = user.user_option.timezone || "UTC"
|
||||
reference_time = Time.now.in_time_zone(timezone)
|
||||
|
||||
text.gsub(
|
||||
/\{\{(date_time_offset_minutes|date_offset_days|datetime|date|next_week):([^}]+)\}\}/,
|
||||
) do |match|
|
||||
type = $1
|
||||
value = $2
|
||||
|
||||
case type
|
||||
when "datetime"
|
||||
if value.include?(":")
|
||||
# Handle range like "2pm+1:3pm+2"
|
||||
start_str, end_str = value.split(":")
|
||||
format_datetime_range(
|
||||
parse_time_with_offset(start_str, reference_time),
|
||||
parse_time_with_offset(end_str, reference_time),
|
||||
timezone,
|
||||
)
|
||||
else
|
||||
# Handle single time like "2pm+1" or "10pm"
|
||||
format_date_time(parse_time_with_offset(value, reference_time), timezone)
|
||||
end
|
||||
when "next_week"
|
||||
if value.include?(":")
|
||||
# Handle range like "tuesday-1pm:tuesday-3pm"
|
||||
start_str, end_str = value.split(":")
|
||||
start_time = parse_next_week(start_str, reference_time)
|
||||
end_time = parse_next_week(end_str, reference_time)
|
||||
format_datetime_range(start_time, end_time, timezone)
|
||||
else
|
||||
# Handle single time like "tuesday-1pm" or just "tuesday"
|
||||
time = parse_next_week(value, reference_time)
|
||||
value.include?("-") ? format_date_time(time, timezone) : format_date(time, timezone)
|
||||
end
|
||||
when "date"
|
||||
format_date(reference_time + value.to_i.days, timezone)
|
||||
when "date_time_offset_minutes"
|
||||
if value.include?(":")
|
||||
start_offset, end_offset = value.split(":").map(&:to_i)
|
||||
format_datetime_range(
|
||||
reference_time + start_offset.minutes,
|
||||
reference_time + end_offset.minutes,
|
||||
timezone,
|
||||
)
|
||||
else
|
||||
format_date_time(reference_time + value.to_i.minutes, timezone)
|
||||
end
|
||||
when "date_offset_days"
|
||||
if value.include?(":")
|
||||
start_offset, end_offset = value.split(":").map(&:to_i)
|
||||
format_date_range(
|
||||
reference_time + start_offset.days,
|
||||
reference_time + end_offset.days,
|
||||
timezone,
|
||||
)
|
||||
else
|
||||
format_date(reference_time + value.to_i.days, timezone)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_next_week(str, reference_time)
|
||||
if str.include?("-")
|
||||
# Handle day with time like "tuesday-1pm"
|
||||
day, time = str.split("-")
|
||||
target_date = get_next_week_day(day.downcase, reference_time)
|
||||
parse_time(time, target_date)
|
||||
else
|
||||
# Just the day
|
||||
get_next_week_day(str.downcase, reference_time)
|
||||
end
|
||||
end
|
||||
|
||||
def get_next_week_day(day, reference_time)
|
||||
raise ArgumentError unless DAYS_OF_WEEK.key?(day)
|
||||
|
||||
target_date = reference_time + 1.week
|
||||
days_ahead = DAYS_OF_WEEK[day] - target_date.wday
|
||||
days_ahead += 7 if days_ahead < 0
|
||||
target_date + days_ahead.days
|
||||
end
|
||||
|
||||
def parse_time_with_offset(time_str, reference_time)
|
||||
if time_str.include?("+")
|
||||
time_part, days = time_str.split("+")
|
||||
parse_time(time_part, reference_time + days.to_i.days)
|
||||
else
|
||||
parse_time(time_str, reference_time)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_time(time_str, reference_time)
|
||||
hour = time_str.to_i
|
||||
if time_str.downcase.include?("pm") && hour != 12
|
||||
hour += 12
|
||||
elsif time_str.downcase.include?("am") && hour == 12
|
||||
hour = 0
|
||||
end
|
||||
|
||||
reference_time.change(hour: hour, min: 0, sec: 0)
|
||||
end
|
||||
|
||||
def format_date(time, timezone)
|
||||
"[date=#{time.strftime("%Y-%m-%d")} timezone=\"#{timezone}\"]"
|
||||
end
|
||||
|
||||
def format_date_time(time, timezone)
|
||||
"[date=#{time.strftime("%Y-%m-%d")} time=#{time.strftime("%H:%M:%S")} timezone=\"#{timezone}\"]"
|
||||
end
|
||||
|
||||
def format_date_range(start_time, end_time, timezone)
|
||||
"[date-range from=#{start_time.strftime("%Y-%m-%d")} to=#{end_time.strftime("%Y-%m-%d")} timezone=\"#{timezone}\"]"
|
||||
end
|
||||
|
||||
def format_datetime_range(start_time, end_time, timezone)
|
||||
"[date-range from=#{start_time.strftime("%Y-%m-%dT%H:%M:%S")} to=#{end_time.strftime("%Y-%m-%dT%H:%M:%S")} timezone=\"#{timezone}\"]"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -48,7 +48,7 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
|||
it "returns all available prompts" do
|
||||
prompts = subject.available_prompts(user)
|
||||
|
||||
expect(prompts.length).to eq(7)
|
||||
expect(prompts.length).to eq(8)
|
||||
expect(prompts.map { |p| p[:name] }).to contain_exactly(
|
||||
"translate",
|
||||
"generate_titles",
|
||||
|
@ -57,19 +57,21 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
|||
"custom_prompt",
|
||||
"explain",
|
||||
"detect_text_locale",
|
||||
"replace_dates",
|
||||
)
|
||||
end
|
||||
|
||||
it "returns all prompts to be shown in the composer" do
|
||||
prompts = subject.available_prompts(user)
|
||||
filtered_prompts = prompts.select { |prompt| prompt[:location].include?("composer") }
|
||||
expect(filtered_prompts.length).to eq(5)
|
||||
expect(filtered_prompts.length).to eq(6)
|
||||
expect(filtered_prompts.map { |p| p[:name] }).to contain_exactly(
|
||||
"translate",
|
||||
"generate_titles",
|
||||
"proofread",
|
||||
"markdown_table",
|
||||
"custom_prompt",
|
||||
"replace_dates",
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -99,7 +101,7 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
|||
it "returns the illustrate_post prompt in the list of all prompts" do
|
||||
prompts = subject.available_prompts(user)
|
||||
|
||||
expect(prompts.length).to eq(8)
|
||||
expect(prompts.length).to eq(9)
|
||||
expect(prompts.map { |p| p[:name] }).to contain_exactly(
|
||||
"translate",
|
||||
"generate_titles",
|
||||
|
@ -109,6 +111,7 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
|||
"explain",
|
||||
"illustrate_post",
|
||||
"detect_text_locale",
|
||||
"replace_dates",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -138,6 +141,45 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
|||
|
||||
expect(prompt.messages[0][:content].strip).to eq("This is a English (US) test")
|
||||
end
|
||||
|
||||
context "with temporal context" do
|
||||
let(:prompt) do
|
||||
CompletionPrompt.new(
|
||||
messages: {
|
||||
insts: "Current context: {{temporal_context}}",
|
||||
},
|
||||
).messages_with_input("test")
|
||||
end
|
||||
|
||||
it "replaces temporal context with timezone information" do
|
||||
timezone = "America/New_York"
|
||||
user.user_option.update!(timezone: timezone)
|
||||
freeze_time "2024-01-01 12:00:00"
|
||||
|
||||
subject.localize_prompt!(prompt, user)
|
||||
|
||||
content = prompt.messages[0][:content]
|
||||
|
||||
expect(content).to include(%("timezone":"America/New_York"))
|
||||
end
|
||||
|
||||
it "uses UTC as default timezone when user timezone is not set" do
|
||||
user.user_option.update!(timezone: nil)
|
||||
|
||||
freeze_time "2024-01-01 12:00:00" do
|
||||
subject.localize_prompt!(prompt, user)
|
||||
|
||||
parsed_context = JSON.parse(prompt.messages[0][:content].match(/context: (.+)$/)[1])
|
||||
expect(parsed_context["user"]["timezone"]).to eq("UTC")
|
||||
end
|
||||
end
|
||||
|
||||
it "does not replace temporal context when user is nil" do
|
||||
prompt_content = prompt.messages[0][:content].dup
|
||||
subject.localize_prompt!(prompt, nil)
|
||||
expect(prompt.messages[0][:content]).to eq(prompt_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#generate_and_send_prompt" do
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiHelper::DateFormatter do
|
||||
fab!(:user)
|
||||
|
||||
# Reference time is Tuesday Jan 16th, 2024 at 2:30 PM Sydney time
|
||||
let(:sydney_reference) { DateTime.parse("2024-01-16 14:30:00 +11:00") }
|
||||
|
||||
describe ".process_date_placeholders" do
|
||||
describe "with Sydney timezone" do
|
||||
before do
|
||||
user.user_option.update!(timezone: "Australia/Sydney")
|
||||
freeze_time(sydney_reference)
|
||||
end
|
||||
|
||||
describe "date_time_offset_minutes" do
|
||||
it "handles minute offsets" do
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting in {{date_time_offset_minutes:30}}",
|
||||
user,
|
||||
),
|
||||
).to eq("Meeting in [date=2024-01-16 time=15:00:00 timezone=\"Australia/Sydney\"]")
|
||||
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting in {{date_time_offset_minutes:90}}",
|
||||
user,
|
||||
),
|
||||
).to eq("Meeting in [date=2024-01-16 time=16:00:00 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
|
||||
it "handles minute ranges" do
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting {{date_time_offset_minutes:60:180}}",
|
||||
user,
|
||||
),
|
||||
).to eq(
|
||||
"Meeting [date-range from=2024-01-16T15:30:00 to=2024-01-16T17:30:00 timezone=\"Australia/Sydney\"]",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "date_offset_days" do
|
||||
it "handles day offsets" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Due {{date_offset_days:1}}", user),
|
||||
).to eq("Due [date=2024-01-17 timezone=\"Australia/Sydney\"]")
|
||||
|
||||
expect(
|
||||
described_class.process_date_placeholders("Due {{date_offset_days:7}}", user),
|
||||
).to eq("Due [date=2024-01-23 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
|
||||
it "handles day ranges" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Event {{date_offset_days:1:3}}", user),
|
||||
).to eq("Event [date-range from=2024-01-17 to=2024-01-19 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
end
|
||||
|
||||
describe "datetime" do
|
||||
it "handles absolute times today" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting at {{datetime:2pm}}", user),
|
||||
).to eq("Meeting at [date=2024-01-16 time=14:00:00 timezone=\"Australia/Sydney\"]")
|
||||
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting at {{datetime:10pm}}", user),
|
||||
).to eq("Meeting at [date=2024-01-16 time=22:00:00 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
|
||||
it "handles absolute times with day offset" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{datetime:2pm+1}}", user),
|
||||
).to eq("Meeting [date=2024-01-17 time=14:00:00 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
|
||||
it "handles time ranges" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{datetime:2pm:4pm}}", user),
|
||||
).to eq(
|
||||
"Meeting [date-range from=2024-01-16T14:00:00 to=2024-01-16T16:00:00 timezone=\"Australia/Sydney\"]",
|
||||
)
|
||||
end
|
||||
|
||||
it "handles time ranges with day offsets" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{datetime:2pm+1:4pm+1}}", user),
|
||||
).to eq(
|
||||
"Meeting [date-range from=2024-01-17T14:00:00 to=2024-01-17T16:00:00 timezone=\"Australia/Sydney\"]",
|
||||
)
|
||||
end
|
||||
|
||||
it "handles 12-hour time edge cases" do
|
||||
expect(described_class.process_date_placeholders("At {{datetime:12am}}", user)).to eq(
|
||||
"At [date=2024-01-16 time=00:00:00 timezone=\"Australia/Sydney\"]",
|
||||
)
|
||||
|
||||
expect(described_class.process_date_placeholders("At {{datetime:12pm}}", user)).to eq(
|
||||
"At [date=2024-01-16 time=12:00:00 timezone=\"Australia/Sydney\"]",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "next_week" do
|
||||
it "handles next week days" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{next_week:tuesday}}", user),
|
||||
).to eq("Meeting [date=2024-01-23 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
|
||||
it "handles next week with specific times" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{next_week:tuesday-2pm}}", user),
|
||||
).to eq("Meeting [date=2024-01-23 time=14:00:00 timezone=\"Australia/Sydney\"]")
|
||||
end
|
||||
|
||||
it "handles next week time ranges" do
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting {{next_week:tuesday-1pm:tuesday-3pm}}",
|
||||
user,
|
||||
),
|
||||
).to eq(
|
||||
"Meeting [date-range from=2024-01-23T13:00:00 to=2024-01-23T15:00:00 timezone=\"Australia/Sydney\"]",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with Los Angeles timezone" do
|
||||
before do
|
||||
user.user_option.update!(timezone: "America/Los_Angeles")
|
||||
# Still freeze at Sydney time, but formatter should work in LA time
|
||||
freeze_time(sydney_reference)
|
||||
end
|
||||
|
||||
it "handles current time conversions" do
|
||||
# When it's 2:30 PM Tuesday in Sydney
|
||||
# It's 7:30 PM Monday in LA
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting {{date_time_offset_minutes:30}}",
|
||||
user,
|
||||
),
|
||||
).to eq("Meeting [date=2024-01-15 time=20:00:00 timezone=\"America/Los_Angeles\"]")
|
||||
end
|
||||
|
||||
it "handles absolute times" do
|
||||
expect(described_class.process_date_placeholders("Meeting {{datetime:2pm}}", user)).to eq(
|
||||
"Meeting [date=2024-01-15 time=14:00:00 timezone=\"America/Los_Angeles\"]",
|
||||
)
|
||||
end
|
||||
|
||||
describe "next_week" do
|
||||
it "handles next week days in LA time" do
|
||||
# From Monday night in LA (Tuesday in Sydney)
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{next_week:tuesday}}", user),
|
||||
).to eq("Meeting [date=2024-01-23 timezone=\"America/Los_Angeles\"]")
|
||||
end
|
||||
|
||||
it "handles next week with specific times in LA" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{next_week:tuesday-2pm}}", user),
|
||||
).to eq("Meeting [date=2024-01-23 time=14:00:00 timezone=\"America/Los_Angeles\"]")
|
||||
end
|
||||
|
||||
it "handles next week time ranges in LA" do
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting {{next_week:tuesday-1pm:tuesday-3pm}}",
|
||||
user,
|
||||
),
|
||||
).to eq(
|
||||
"Meeting [date-range from=2024-01-23T13:00:00 to=2024-01-23T15:00:00 timezone=\"America/Los_Angeles\"]",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "handles day transitions across timezones" do
|
||||
expect(described_class.process_date_placeholders("Due {{date_offset_days:1}}", user)).to eq(
|
||||
"Due [date=2024-01-16 timezone=\"America/Los_Angeles\"]",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with UTC timezone" do
|
||||
before do
|
||||
user.user_option.update!(timezone: nil) # defaults to UTC
|
||||
freeze_time(sydney_reference)
|
||||
end
|
||||
|
||||
it "defaults to UTC for users without timezone" do
|
||||
# When it's 2:30 PM in Sydney
|
||||
# It's 3:30 AM in UTC
|
||||
expect(
|
||||
described_class.process_date_placeholders(
|
||||
"Meeting {{date_time_offset_minutes:30}}",
|
||||
user,
|
||||
),
|
||||
).to eq("Meeting [date=2024-01-16 time=04:00:00 timezone=\"UTC\"]")
|
||||
end
|
||||
|
||||
describe "next_week" do
|
||||
it "handles next week calculations in UTC" do
|
||||
expect(
|
||||
described_class.process_date_placeholders("Meeting {{next_week:tuesday-2pm}}", user),
|
||||
).to eq("Meeting [date=2024-01-23 time=14:00:00 timezone=\"UTC\"]")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "error handling" do
|
||||
before do
|
||||
user.user_option.update!(timezone: "Australia/Sydney")
|
||||
freeze_time(sydney_reference)
|
||||
end
|
||||
|
||||
it "raises on invalid day name" do
|
||||
expect {
|
||||
described_class.process_date_placeholders("Meeting {{next_week:notaday}}", user)
|
||||
}.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "raises on invalid time format" do
|
||||
expect {
|
||||
described_class.process_date_placeholders("Meeting {{datetime:25pm}}", user)
|
||||
}.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "mixed formats" do
|
||||
before do
|
||||
user.user_option.update!(timezone: "Australia/Sydney")
|
||||
freeze_time(sydney_reference)
|
||||
end
|
||||
|
||||
it "handles multiple different formats in the same text" do
|
||||
input = [
|
||||
"Meeting {{datetime:2pm+1}},",
|
||||
"duration {{date_time_offset_minutes:60:180}},",
|
||||
"repeats until {{date_offset_days:7}}",
|
||||
"with sessions {{next_week:tuesday-1pm:tuesday-3pm}}",
|
||||
].join(" ")
|
||||
|
||||
expected = [
|
||||
"Meeting [date=2024-01-17 time=14:00:00 timezone=\"Australia/Sydney\"],",
|
||||
"duration [date-range from=2024-01-16T15:30:00 to=2024-01-16T17:30:00 timezone=\"Australia/Sydney\"],",
|
||||
"repeats until [date=2024-01-23 timezone=\"Australia/Sydney\"]",
|
||||
"with sessions [date-range from=2024-01-23T13:00:00 to=2024-01-23T15:00:00 timezone=\"Australia/Sydney\"]",
|
||||
].join(" ")
|
||||
|
||||
expect(described_class.process_date_placeholders(input, user)).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,7 +19,7 @@ describe Plugin::Instance do
|
|||
|
||||
it "returns the available prompts" do
|
||||
expect(serializer.ai_helper_prompts).to be_present
|
||||
expect(serializer.ai_helper_prompts.object.count).to eq(7)
|
||||
expect(serializer.ai_helper_prompts.object.count).to eq(8)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue