support_desk 0.1.3 → 0.3.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/CHANGELOG.md +126 -0
- data/README.md +1101 -22
- data/Rakefile +16 -0
- data/app/assets/stylesheets/support_desk.css +10 -0
- data/app/controllers/support_desk/tickets_controller.rb +28 -4
- data/app/helpers/support_desk/engine_helper.rb +23 -1
- data/app/views/support_desk/console/tickets/_actions.html.erb +15 -0
- data/app/views/support_desk/console/tickets/_assignment.html.erb +16 -2
- data/app/views/support_desk/console/tickets/_composer.html.erb +27 -0
- data/app/views/support_desk/console/tickets/_context_card.html.erb +33 -0
- data/app/views/support_desk/console/tickets/_draft.html.erb +114 -0
- data/app/views/support_desk/console/tickets/_message.html.erb +34 -1
- data/app/views/support_desk/console/tickets/_new_conversation_form.html.erb +91 -0
- data/app/views/support_desk/console/tickets/_ticket_row.html.erb +28 -0
- data/app/views/support_desk/console/tickets/_timeline.html.erb +68 -14
- data/app/views/support_desk/console/tickets/index.html.erb +14 -2
- data/app/views/support_desk/console/tickets/new.html.erb +25 -0
- data/app/views/support_desk/console/tickets/show.html.erb +5 -0
- data/app/views/support_desk/tickets/_human_door.html.erb +24 -0
- data/app/views/support_desk/tickets/_ticket_row.html.erb +13 -0
- data/config/console_routes.rb +1 -1
- data/config/locales/support_desk.console.en.yml +89 -0
- data/config/locales/support_desk.console.es.yml +91 -0
- data/config/locales/support_desk.en.yml +18 -0
- data/config/locales/support_desk.es.yml +29 -0
- data/config/routes.rb +7 -1
- data/lib/generators/support_desk/assistant_generator.rb +193 -0
- data/lib/generators/support_desk/console_generator.rb +1 -1
- data/lib/generators/support_desk/install_generator.rb +26 -0
- data/lib/generators/support_desk/templates/add_assistants_to_support_desk.rb.erb +236 -0
- data/lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb +79 -0
- data/lib/generators/support_desk/templates/assistant/service.rb.erb +60 -0
- data/lib/generators/support_desk/templates/assistant/turn_job.rb.erb +72 -0
- data/lib/generators/support_desk/templates/assistant/turn_job_test.rb.erb +69 -0
- data/lib/generators/support_desk/templates/console/controller.rb.erb +1 -1
- data/lib/generators/support_desk/templates/initializer.rb +63 -0
- data/lib/generators/support_desk/upgrade_generator.rb +66 -0
- data/lib/support_desk/assistant_policy.rb +213 -0
- data/lib/support_desk/brief.rb +283 -0
- data/lib/support_desk/configuration.rb +699 -3
- data/lib/support_desk/console.rb +634 -30
- data/lib/support_desk/console_engine.rb +2 -0
- data/lib/support_desk/console_routes.rb +20 -8
- data/lib/support_desk/context_card.rb +33 -1
- data/lib/support_desk/doctor.rb +195 -2
- data/lib/support_desk/engine.rb +10 -0
- data/lib/support_desk/errors.rb +42 -0
- data/lib/support_desk/events.rb +12 -5
- data/lib/support_desk/macros.rb +41 -12
- data/lib/support_desk/models/assignment.rb +3 -1
- data/lib/support_desk/models/assistant.rb +153 -0
- data/lib/support_desk/models/concerns/agent.rb +39 -4
- data/lib/support_desk/models/concerns/requester.rb +45 -12
- data/lib/support_desk/models/desk.rb +26 -3
- data/lib/support_desk/models/draft.rb +266 -0
- data/lib/support_desk/models/event.rb +15 -1
- data/lib/support_desk/models/ticket/assistance.rb +675 -0
- data/lib/support_desk/models/ticket.rb +781 -120
- data/lib/support_desk/outcome.rb +38 -0
- data/lib/support_desk/queue.rb +41 -5
- data/lib/support_desk/summary.rb +1 -1
- data/lib/support_desk/test_helpers.rb +160 -3
- data/lib/support_desk/timeline.rb +40 -11
- data/lib/support_desk/topic.rb +22 -0
- data/lib/support_desk/topic_tree.rb +7 -1
- data/lib/support_desk/transcript.rb +237 -0
- data/lib/support_desk/version.rb +1 -1
- data/lib/support_desk/wizard.rb +4 -12
- data/lib/support_desk.rb +153 -0
- data/lib/tasks/support_desk.rake +73 -0
- metadata +35 -8
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
Collapsed by default, because an agent opens a case to answer it, not to
|
|
6
6
|
audit it. `<details>` rather than a toggle controller: the browser has
|
|
7
|
-
had this for a decade and it survives a broken bundle.
|
|
7
|
+
had this for a decade and it survives a broken bundle.
|
|
8
|
+
|
|
9
|
+
Three kinds of moment now. A message and an event read as they always
|
|
10
|
+
did. A DRAFT reads greyed, because it is the one thing here that was
|
|
11
|
+
never said to anybody: a proposal somebody discarded, replaced, or sent
|
|
12
|
+
in somebody else's words. %>
|
|
8
13
|
<details class="rounded-lg border border-slate-200 bg-white p-4">
|
|
9
14
|
<summary class="cursor-pointer text-sm font-semibold"><%= t("support_desk.console.timeline.title") %></summary>
|
|
10
15
|
|
|
@@ -15,22 +20,71 @@
|
|
|
15
20
|
<%= l(entry.at, format: :short) if entry.at %>
|
|
16
21
|
</time>
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
<% actor = entry.actor %>
|
|
24
|
+
<% actor_name = if actor.nil?
|
|
25
|
+
nil
|
|
26
|
+
elsif actor.is_a?(Symbol)
|
|
27
|
+
t("support_desk.console.timeline.system")
|
|
28
|
+
else
|
|
29
|
+
actor.try(:support_agent_name) || Chats.display_name_for(actor)
|
|
30
|
+
end %>
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
<% if entry.draft? %>
|
|
33
|
+
<%# What the machine proposed, and what became of it. The event
|
|
34
|
+
beside it says who decided; this is what they decided ABOUT. %>
|
|
35
|
+
<% draft = entry.draft %>
|
|
36
|
+
<div class="min-w-0 text-slate-400">
|
|
37
|
+
<span class="font-medium">
|
|
38
|
+
<% case draft.status %>
|
|
39
|
+
<% when "sent" %>
|
|
40
|
+
<%= t("support_desk.console.draft.sent_by", name: actor_name) %>
|
|
41
|
+
<% when "rejected" %>
|
|
42
|
+
<%= t("support_desk.console.draft.rejected_by", name: actor_name) %>
|
|
43
|
+
<% when "superseded" %>
|
|
44
|
+
<%= t("support_desk.console.timeline.draft_superseded") %>
|
|
45
|
+
<% else %>
|
|
46
|
+
<%= t("support_desk.console.timeline.draft_expired") %>
|
|
47
|
+
<% end %>
|
|
27
48
|
</span>
|
|
28
|
-
|
|
49
|
+
<% if entry.body.present? %>
|
|
50
|
+
<p class="truncate"><%= truncate(entry.body, length: 120) %></p>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% if draft.rejection_reason.present? %>
|
|
53
|
+
<p class="truncate italic"><%= truncate(draft.rejection_reason, length: 120) %></p>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<% elsif (sentence = support_console_timeline_sentence(entry, actor_name)) %>
|
|
58
|
+
<%# The kinds the assistants added say the whole thing in one
|
|
59
|
+
sentence, with their own payload in it: "who did what to whom"
|
|
60
|
+
is the only reading of these that helps. %>
|
|
61
|
+
<div class="min-w-0">
|
|
62
|
+
<span class="text-slate-700"><%= sentence %></span>
|
|
63
|
+
<% if entry.kind == :escalated && entry.event.summary.present? %>
|
|
64
|
+
<%# Her hand-over note: the one piece of a machine's reasoning
|
|
65
|
+
a person actually needs, so it gets room rather than a
|
|
66
|
+
truncated line. %>
|
|
67
|
+
<p class="mt-1 whitespace-pre-wrap rounded-md bg-amber-50 p-2 text-xs text-amber-900">
|
|
68
|
+
<%= entry.event.summary %>
|
|
69
|
+
</p>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<% else %>
|
|
74
|
+
<div class="min-w-0">
|
|
75
|
+
<span class="font-medium text-slate-700">
|
|
76
|
+
<%= t("support_desk.console.timeline.kinds.#{entry.kind}", default: entry.kind.to_s.humanize) %>
|
|
77
|
+
</span>
|
|
78
|
+
|
|
79
|
+
<% if actor_name %>
|
|
80
|
+
<span class="text-slate-500">· <%= actor_name %></span>
|
|
81
|
+
<% end %>
|
|
29
82
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
<% if entry.body.present? %>
|
|
84
|
+
<p class="truncate text-slate-600"><%= truncate(entry.body, length: 120) %></p>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
34
88
|
</li>
|
|
35
89
|
<% end %>
|
|
36
90
|
</ol>
|
|
@@ -17,8 +17,20 @@
|
|
|
17
17
|
<p class="text-sm text-slate-500"><%= support_desk_record.name %></p>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
<div class="flex items-center gap-2">
|
|
21
|
+
<%# The door to writing first. Through console_tickets_path so it
|
|
22
|
+
resolves under an isolated admin engine as well as a host
|
|
23
|
+
namespace, and carrying the desk so a multi-desk console composes
|
|
24
|
+
for the desk being worked. %>
|
|
25
|
+
<% if support_conversation_offered? %>
|
|
26
|
+
<%= link_to t("support_desk.console.actions.new_conversation"),
|
|
27
|
+
console_tickets_path(:new, desk: support_desk_record.key),
|
|
28
|
+
class: "inline-flex items-center rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-900 hover:bg-slate-50" %>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<%= link_to t("support_desk.console.actions.next"), console_tickets_path(:next),
|
|
32
|
+
class: "inline-flex items-center rounded-md bg-slate-900 px-3 py-2 text-sm font-medium text-white hover:bg-slate-700" %>
|
|
33
|
+
</div>
|
|
22
34
|
</header>
|
|
23
35
|
|
|
24
36
|
<%= render "tabs", queue: @queue, scope: @scope %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# Writing first: the desk starting a conversation with somebody who hasn't
|
|
2
|
+
written to us.
|
|
3
|
+
|
|
4
|
+
It is a whole page rather than a modal on purpose — the 422 that a bad
|
|
5
|
+
email or a refused message comes back as has to render the form again
|
|
6
|
+
with everything still typed into it, and a stream refresh would throw
|
|
7
|
+
that away. %>
|
|
8
|
+
<div class="mx-auto max-w-2xl px-4 py-6 text-slate-900">
|
|
9
|
+
<header class="mb-5">
|
|
10
|
+
<h1 class="text-xl font-semibold"><%= t("support_desk.console.new_conversation.title") %></h1>
|
|
11
|
+
<p class="text-sm text-slate-500"><%= support_desk_record.name %></p>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<%# The refusal is rendered HERE rather than left to your layout: this
|
|
15
|
+
screen answers 422 with itself, and an agent has to read why on the
|
|
16
|
+
page that came back. If your layout already prints flashes, delete
|
|
17
|
+
this block when you eject the views. %>
|
|
18
|
+
<% if flash[:alert] %>
|
|
19
|
+
<p class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
|
|
20
|
+
<%= flash[:alert] %>
|
|
21
|
+
</p>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<%= render "new_conversation_form" %>
|
|
25
|
+
</div>
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
<div class="grid gap-6 lg:grid-cols-3">
|
|
34
34
|
<div class="space-y-4 lg:col-span-2">
|
|
35
35
|
<%= render "transcript", ticket: @ticket %>
|
|
36
|
+
<%# The proposal, when there is one waiting: above the composer,
|
|
37
|
+
because it is the thing on this screen that wants an answer. %>
|
|
38
|
+
<% if (draft = support_pending_draft) %>
|
|
39
|
+
<%= render "draft", ticket: @ticket, draft: draft, actions: @actions %>
|
|
40
|
+
<% end %>
|
|
36
41
|
<%= render "composer", ticket: @ticket, actions: @actions %>
|
|
37
42
|
</div>
|
|
38
43
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%# "Prefiero hablar con una persona."
|
|
2
|
+
|
|
3
|
+
The one control on this screen that is never taken away while a machine
|
|
4
|
+
is in play: a customer who wants a person gets a person, and pressing
|
|
5
|
+
this is the whole of it — no wording to get right, no phrase to guess.
|
|
6
|
+
|
|
7
|
+
Three states, and the middle one is the point. While the assistant is in
|
|
8
|
+
play the button is here. Once somebody has asked (the customer, the
|
|
9
|
+
assistant, or the sweep that notices she went quiet) the button becomes
|
|
10
|
+
the promise, because a door that stays pressable after it worked is a
|
|
11
|
+
door people press again wondering whether it did. A case nobody can
|
|
12
|
+
write to shows nothing at all: it is over, and a button that only 500s
|
|
13
|
+
is worse than silence.
|
|
14
|
+
|
|
15
|
+
`ticket.assistant_in_play?` counts HISTORY as well as configuration, so
|
|
16
|
+
the door does not vanish from a live conversation because somebody
|
|
17
|
+
edited an initializer. %>
|
|
18
|
+
<% if ticket.human_required? %>
|
|
19
|
+
<p class="support-desk-notice__title"><%= t("support_desk.thread.handled_by_humans") %></p>
|
|
20
|
+
<% elsif ticket.assistant_in_play? && !ticket.chat_locked? %>
|
|
21
|
+
<%= button_to t("support_desk.thread.human_door"),
|
|
22
|
+
support_desk_routes.request_human_ticket_path(ticket),
|
|
23
|
+
method: :post, class: "support-desk-button support-desk-button--quiet" %>
|
|
24
|
+
<% end %>
|
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
|
|
19
19
|
<span class="chats-row__subject"><%= support_ticket_state(ticket) %></span>
|
|
20
20
|
|
|
21
|
+
<%# Who is on it, when that is not simply "us". A machine answering is
|
|
22
|
+
something the person is entitled to know before they read the
|
|
23
|
+
answer, so the row says so by her disclosed name — and the moment
|
|
24
|
+
a person has been asked for, it says THAT instead, because it is
|
|
25
|
+
the only one of the two that is a promise. %>
|
|
26
|
+
<% if ticket.human_required? %>
|
|
27
|
+
<span class="chats-row__subject"><%= t("support_desk.thread.handled_by_humans") %></span>
|
|
28
|
+
<% elsif ticket.held_by_assistant? %>
|
|
29
|
+
<span class="chats-row__subject">
|
|
30
|
+
<%= t("support_desk.thread.handled_by_assistant", name: ticket.assignee.support_agent_name) %>
|
|
31
|
+
</span>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
21
34
|
<span class="chats-row__bottom">
|
|
22
35
|
<span class="chats-row__preview">
|
|
23
36
|
<%= ticket.conversation ? chats_preview_for(ticket.conversation, current_requester) : "" %>
|
data/config/console_routes.rb
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# SAME `:support_console` concern a host writes in their own routes file —
|
|
11
11
|
# the turnkey console gets no private API.
|
|
12
12
|
SupportDesk::ConsoleEngine.routes.draw do
|
|
13
|
-
resources :tickets, path: "", only: %i[index show], concerns: :support_console
|
|
13
|
+
resources :tickets, path: "", only: %i[index show new], concerns: :support_console
|
|
14
14
|
|
|
15
15
|
root to: "tickets#index"
|
|
16
16
|
end
|
|
@@ -8,19 +8,36 @@ en:
|
|
|
8
8
|
tabs_label: "Queue tabs"
|
|
9
9
|
empty: "Nothing here right now."
|
|
10
10
|
row:
|
|
11
|
+
opened_by_support: "Written by support"
|
|
11
12
|
waiting: "%{duration}"
|
|
12
13
|
unassigned: "Unassigned"
|
|
13
14
|
context:
|
|
14
15
|
title: "Context"
|
|
16
|
+
opened_by: "Opened by"
|
|
17
|
+
not_recorded: "not recorded"
|
|
18
|
+
unavailable: "account unavailable"
|
|
15
19
|
requester: "Requester"
|
|
16
20
|
open_tickets: "Open cases"
|
|
17
21
|
member_since: "Member since"
|
|
18
22
|
topic: "Topic"
|
|
19
23
|
subject_status: "Status"
|
|
20
24
|
open_subject: "Open in admin"
|
|
25
|
+
assistant: "Assistant"
|
|
26
|
+
requester_context: "About this person"
|
|
21
27
|
transcript:
|
|
22
28
|
title: "Conversation"
|
|
23
29
|
empty: "Nothing has been said yet."
|
|
30
|
+
new_conversation:
|
|
31
|
+
title: "Write as support"
|
|
32
|
+
requester: "To"
|
|
33
|
+
requester_query: "Their email"
|
|
34
|
+
requester_hint: "Set `config.find_requester` to look people up from here"
|
|
35
|
+
about: "About %{subject}"
|
|
36
|
+
topic: "Topic"
|
|
37
|
+
message: "Message"
|
|
38
|
+
send: "Send as %{desk}"
|
|
39
|
+
cancel: "Cancel"
|
|
40
|
+
signed_as: "Signed “%{agent}”."
|
|
24
41
|
composer:
|
|
25
42
|
reply: "Reply"
|
|
26
43
|
note: "Internal note"
|
|
@@ -29,6 +46,41 @@ en:
|
|
|
29
46
|
note_hint: "Notes never leave the desk."
|
|
30
47
|
closed: "This case is closed. Reopen it to answer."
|
|
31
48
|
assignee_only: "%{agent} is handling this case, and this desk only lets the assignee reply."
|
|
49
|
+
draft:
|
|
50
|
+
title: "%{name}'s proposal"
|
|
51
|
+
from: "%{name}'s proposal"
|
|
52
|
+
confidence: "declared confidence %{percent} %"
|
|
53
|
+
sources: "Sources"
|
|
54
|
+
attachments: "Attachments"
|
|
55
|
+
stale: "The conversation has changed since this was proposed. Read it before you send."
|
|
56
|
+
editing: "Editing %{name}'s proposal"
|
|
57
|
+
cancel_edit: "cancel"
|
|
58
|
+
reason_placeholder: "Why are you discarding it? (optional)"
|
|
59
|
+
sent_by: "Proposal sent by %{name}"
|
|
60
|
+
rejected_by: "Proposal discarded by %{name}"
|
|
61
|
+
# The verb the "already %{status}" refusal reads.
|
|
62
|
+
statuses:
|
|
63
|
+
pending: "proposed"
|
|
64
|
+
sent: "sent"
|
|
65
|
+
rejected: "discarded"
|
|
66
|
+
superseded: "replaced"
|
|
67
|
+
expired: "expired"
|
|
68
|
+
badges:
|
|
69
|
+
needs_human: "Needs a person"
|
|
70
|
+
pending_draft: "Proposal ready"
|
|
71
|
+
assistant_holds: "%{name} has it · %{used}/%{max} turns"
|
|
72
|
+
assistant_holds_unlimited: "%{name} has it"
|
|
73
|
+
withheld_reasons:
|
|
74
|
+
policy: "this case's policy doesn't allow it"
|
|
75
|
+
not_your_turn: "it wasn't her turn"
|
|
76
|
+
# Why a case ended up with a person. The tokens are written by the gem
|
|
77
|
+
# or by the host's own harness; these are how they read.
|
|
78
|
+
escalation_reasons:
|
|
79
|
+
max_turns: "she ran out of turns"
|
|
80
|
+
assistant_silent: "the assistant didn't answer in time"
|
|
81
|
+
requester_request: "the customer asked for one"
|
|
82
|
+
phrase: "the customer asked in the conversation"
|
|
83
|
+
hand_off_when_error: "the assistant failed"
|
|
32
84
|
assignment:
|
|
33
85
|
title: "Who has it"
|
|
34
86
|
held_by: "Held by %{agent}"
|
|
@@ -39,6 +91,16 @@ en:
|
|
|
39
91
|
timeline:
|
|
40
92
|
title: "Timeline"
|
|
41
93
|
system: "System"
|
|
94
|
+
escalated: "%{actor} handed the case to a person: %{reason}"
|
|
95
|
+
human_requested: "The customer asked for a person"
|
|
96
|
+
assistant_paused: "%{actor} paused the assistant"
|
|
97
|
+
assistant_resumed: "%{actor} resumed the assistant"
|
|
98
|
+
draft_sent: "%{actor} sent %{assistant}'s proposal"
|
|
99
|
+
draft_sent_edited: "%{actor} sent %{assistant}'s proposal (edited)"
|
|
100
|
+
draft_rejected: "%{actor} discarded %{assistant}'s proposal"
|
|
101
|
+
assistant_withheld: "%{assistant} didn't answer: %{reason}"
|
|
102
|
+
draft_superseded: "Proposal replaced"
|
|
103
|
+
draft_expired: "Proposal expired when the case closed"
|
|
42
104
|
kinds:
|
|
43
105
|
message: "Message"
|
|
44
106
|
opened: "Opened"
|
|
@@ -53,6 +115,7 @@ en:
|
|
|
53
115
|
note: "Internal note"
|
|
54
116
|
actions:
|
|
55
117
|
back: "Back to the queue"
|
|
118
|
+
new_conversation: "Write to someone"
|
|
56
119
|
next: "Next case"
|
|
57
120
|
reply: "Send"
|
|
58
121
|
note: "Save note"
|
|
@@ -63,7 +126,14 @@ en:
|
|
|
63
126
|
close: "Close"
|
|
64
127
|
reopen: "Reopen"
|
|
65
128
|
change_topic: "Refile"
|
|
129
|
+
send_draft: "Send"
|
|
130
|
+
send_edited_draft: "Send the edited proposal"
|
|
131
|
+
edit_draft: "Edit"
|
|
132
|
+
reject_draft: "Discard"
|
|
133
|
+
pause_assistant: "Pause the assistant"
|
|
134
|
+
resume_assistant: "Resume the assistant"
|
|
66
135
|
flashes:
|
|
136
|
+
message_sent: "Message sent to %{requester}."
|
|
67
137
|
replied: "Answer sent."
|
|
68
138
|
taken: "This case is yours."
|
|
69
139
|
assigned: "Assigned to %{agent}."
|
|
@@ -74,6 +144,10 @@ en:
|
|
|
74
144
|
noted: "Note saved."
|
|
75
145
|
topic_changed: "Refiled."
|
|
76
146
|
queue_empty: "Nothing is waiting for you."
|
|
147
|
+
draft_sent: "Proposal sent."
|
|
148
|
+
draft_rejected: "Proposal discarded."
|
|
149
|
+
assistant_paused: "The assistant is paused on this case."
|
|
150
|
+
assistant_resumed: "The assistant is back on this case."
|
|
77
151
|
errors:
|
|
78
152
|
forbidden: "You don't have access to the support console."
|
|
79
153
|
not_allowed: "You can't do that on this case."
|
|
@@ -90,4 +164,19 @@ en:
|
|
|
90
164
|
take_it_first: "Take this case first — this desk only lets the assignee reply."
|
|
91
165
|
held_by_somebody_else: "%{holder} is handling this case."
|
|
92
166
|
unavailable_action: "You can't do that on this case right now."
|
|
167
|
+
unknown_requester: "We can't find that person."
|
|
168
|
+
not_a_requester: "That account can't receive support messages."
|
|
169
|
+
invalid_requester: "That link doesn't point at anybody we can write to."
|
|
170
|
+
invalid_subject: "That link doesn't point at anything we can open a conversation about."
|
|
171
|
+
invalid_input: "Something in the form didn't come through. Try again."
|
|
172
|
+
no_requester_lookup: "Open this form from the person's own page: there's no way to look them up from here."
|
|
173
|
+
writing_to_yourself: "You can't open a support conversation with yourself. Ask for help as a customer instead."
|
|
174
|
+
off_duty: "You're off duty, so you can't write to anybody right now."
|
|
175
|
+
attachments_again: "If you attach files and something goes wrong, you'll have to pick them again."
|
|
176
|
+
unknown_draft: "That proposal is gone."
|
|
177
|
+
draft_already_reviewed: "That proposal was already %{status}."
|
|
178
|
+
no_pending_draft: "There is no proposal waiting on this case."
|
|
179
|
+
stale_turn: "The conversation has changed. Read it and send again."
|
|
180
|
+
assistant_not_allowed: "The assistant can’t do that on this case."
|
|
181
|
+
not_an_assistant: "That action is the assistant’s only."
|
|
93
182
|
generic: "That didn't work."
|
|
@@ -8,19 +8,36 @@ es:
|
|
|
8
8
|
tabs_label: "Pestañas de la cola"
|
|
9
9
|
empty: "Aquí no hay nada ahora mismo."
|
|
10
10
|
row:
|
|
11
|
+
opened_by_support: "Escrita por soporte"
|
|
11
12
|
waiting: "%{duration}"
|
|
12
13
|
unassigned: "Sin asignar"
|
|
13
14
|
context:
|
|
14
15
|
title: "Contexto"
|
|
16
|
+
opened_by: "Abierta por"
|
|
17
|
+
not_recorded: "sin registrar"
|
|
18
|
+
unavailable: "cuenta no disponible"
|
|
15
19
|
requester: "Cliente"
|
|
16
20
|
open_tickets: "Casos abiertos"
|
|
17
21
|
member_since: "Cliente desde"
|
|
18
22
|
topic: "Tema"
|
|
19
23
|
subject_status: "Estado"
|
|
20
24
|
open_subject: "Abrir en el admin"
|
|
25
|
+
assistant: "Asistente"
|
|
26
|
+
requester_context: "Sobre la persona"
|
|
21
27
|
transcript:
|
|
22
28
|
title: "Conversación"
|
|
23
29
|
empty: "Todavía no se ha dicho nada."
|
|
30
|
+
new_conversation:
|
|
31
|
+
title: "Escribir por soporte"
|
|
32
|
+
requester: "Para"
|
|
33
|
+
requester_query: "Email de la persona"
|
|
34
|
+
requester_hint: "Configura `config.find_requester` para buscar a alguien desde aquí"
|
|
35
|
+
about: "Sobre %{subject}"
|
|
36
|
+
topic: "Tema"
|
|
37
|
+
message: "Mensaje"
|
|
38
|
+
send: "Enviar como %{desk}"
|
|
39
|
+
cancel: "Cancelar"
|
|
40
|
+
signed_as: "Firmado «%{agent}»."
|
|
24
41
|
composer:
|
|
25
42
|
reply: "Responder"
|
|
26
43
|
note: "Nota interna"
|
|
@@ -29,6 +46,43 @@ es:
|
|
|
29
46
|
note_hint: "Las notas nunca salen del equipo."
|
|
30
47
|
closed: "Este caso está cerrado. Vuelve a abrirlo para responder."
|
|
31
48
|
assignee_only: "%{agent} lleva este caso y en esta mesa solo responde quien lo tiene."
|
|
49
|
+
draft:
|
|
50
|
+
title: "Propuesta de %{name}"
|
|
51
|
+
from: "propuesta de %{name}"
|
|
52
|
+
confidence: "confianza declarada %{percent} %"
|
|
53
|
+
sources: "Fuentes"
|
|
54
|
+
attachments: "Adjuntos"
|
|
55
|
+
stale: "La conversación ha cambiado desde esta propuesta. Léela antes de enviar."
|
|
56
|
+
editing: "Editando la propuesta de %{name}"
|
|
57
|
+
cancel_edit: "cancelar"
|
|
58
|
+
reason_placeholder: "¿Por qué la descartas? (opcional)"
|
|
59
|
+
sent_by: "Propuesta enviada por %{name}"
|
|
60
|
+
rejected_by: "Propuesta descartada por %{name}"
|
|
61
|
+
# The verb the "ya se %{status}" refusal reads. Never the English
|
|
62
|
+
# status: a Spanish desk reading "ya se sent" is the whole reason
|
|
63
|
+
# this map exists.
|
|
64
|
+
statuses:
|
|
65
|
+
pending: "propuso"
|
|
66
|
+
sent: "envió"
|
|
67
|
+
rejected: "descartó"
|
|
68
|
+
superseded: "sustituyó"
|
|
69
|
+
expired: "caducó"
|
|
70
|
+
badges:
|
|
71
|
+
needs_human: "Necesita persona"
|
|
72
|
+
pending_draft: "Propuesta lista"
|
|
73
|
+
assistant_holds: "%{name} se ocupa · %{used}/%{max} turnos"
|
|
74
|
+
assistant_holds_unlimited: "%{name} se ocupa"
|
|
75
|
+
withheld_reasons:
|
|
76
|
+
policy: "la política de este caso no se lo permite"
|
|
77
|
+
not_your_turn: "no era su turno"
|
|
78
|
+
# Por qué un caso acabó en manos de una persona. Los tokens los
|
|
79
|
+
# escribe el gem o el harness del host; aquí se leen en castellano.
|
|
80
|
+
escalation_reasons:
|
|
81
|
+
max_turns: "se quedó sin turnos"
|
|
82
|
+
assistant_silent: "el asistente no contestó a tiempo"
|
|
83
|
+
requester_request: "lo pidió el cliente"
|
|
84
|
+
phrase: "el cliente lo pidió en la conversación"
|
|
85
|
+
hand_off_when_error: "el asistente falló"
|
|
32
86
|
assignment:
|
|
33
87
|
title: "Quién lo lleva"
|
|
34
88
|
held_by: "Lo lleva %{agent}"
|
|
@@ -39,6 +93,16 @@ es:
|
|
|
39
93
|
timeline:
|
|
40
94
|
title: "Historial"
|
|
41
95
|
system: "Sistema"
|
|
96
|
+
escalated: "%{actor} pasó el caso a una persona: %{reason}"
|
|
97
|
+
human_requested: "El cliente pidió hablar con una persona"
|
|
98
|
+
assistant_paused: "%{actor} pausó el asistente"
|
|
99
|
+
assistant_resumed: "%{actor} reanudó el asistente"
|
|
100
|
+
draft_sent: "%{actor} envió la propuesta de %{assistant}"
|
|
101
|
+
draft_sent_edited: "%{actor} envió la propuesta de %{assistant} (editada)"
|
|
102
|
+
draft_rejected: "%{actor} descartó la propuesta de %{assistant}"
|
|
103
|
+
assistant_withheld: "%{assistant} no respondió: %{reason}"
|
|
104
|
+
draft_superseded: "Propuesta sustituida"
|
|
105
|
+
draft_expired: "Propuesta caducada al cerrar"
|
|
42
106
|
kinds:
|
|
43
107
|
message: "Mensaje"
|
|
44
108
|
opened: "Abierto"
|
|
@@ -53,6 +117,7 @@ es:
|
|
|
53
117
|
note: "Nota interna"
|
|
54
118
|
actions:
|
|
55
119
|
back: "Volver a la cola"
|
|
120
|
+
new_conversation: "Escribir a alguien"
|
|
56
121
|
next: "Siguiente caso"
|
|
57
122
|
reply: "Enviar"
|
|
58
123
|
note: "Guardar nota"
|
|
@@ -63,7 +128,14 @@ es:
|
|
|
63
128
|
close: "Cerrar"
|
|
64
129
|
reopen: "Reabrir"
|
|
65
130
|
change_topic: "Cambiar tema"
|
|
131
|
+
send_draft: "Enviar"
|
|
132
|
+
send_edited_draft: "Enviar propuesta editada"
|
|
133
|
+
edit_draft: "Editar"
|
|
134
|
+
reject_draft: "Descartar"
|
|
135
|
+
pause_assistant: "Pausar asistente"
|
|
136
|
+
resume_assistant: "Reanudar asistente"
|
|
66
137
|
flashes:
|
|
138
|
+
message_sent: "Mensaje enviado a %{requester}."
|
|
67
139
|
replied: "Respuesta enviada."
|
|
68
140
|
taken: "El caso es tuyo."
|
|
69
141
|
assigned: "Asignado a %{agent}."
|
|
@@ -74,6 +146,10 @@ es:
|
|
|
74
146
|
noted: "Nota guardada."
|
|
75
147
|
topic_changed: "Tema cambiado."
|
|
76
148
|
queue_empty: "No te espera nada."
|
|
149
|
+
draft_sent: "Propuesta enviada."
|
|
150
|
+
draft_rejected: "Propuesta descartada."
|
|
151
|
+
assistant_paused: "Asistente pausado en este caso."
|
|
152
|
+
assistant_resumed: "Asistente reanudado en este caso."
|
|
77
153
|
errors:
|
|
78
154
|
forbidden: "No tienes acceso a la consola de soporte."
|
|
79
155
|
not_allowed: "No puedes hacer eso en este caso."
|
|
@@ -90,4 +166,19 @@ es:
|
|
|
90
166
|
take_it_first: "Toma el caso antes de responder: en esta mesa solo responde quien lo lleva."
|
|
91
167
|
held_by_somebody_else: "Lo lleva %{holder}."
|
|
92
168
|
unavailable_action: "Ahora mismo no puedes hacer eso en este caso."
|
|
169
|
+
unknown_requester: "No encontramos a esa persona."
|
|
170
|
+
not_a_requester: "Esa cuenta no puede recibir mensajes de soporte."
|
|
171
|
+
invalid_requester: "Ese enlace no apunta a nadie a quien podamos escribir."
|
|
172
|
+
invalid_subject: "Ese enlace no apunta a nada sobre lo que podamos abrir la conversación."
|
|
173
|
+
invalid_input: "Algo en el formulario no se ha enviado bien. Vuelve a intentarlo."
|
|
174
|
+
no_requester_lookup: "Abre este formulario desde la ficha de la persona: aquí no podemos buscarla."
|
|
175
|
+
writing_to_yourself: "No puedes abrir una conversación de soporte contigo. Si necesitas ayuda, pídela como cliente."
|
|
176
|
+
off_duty: "No estás de guardia, así que no puedes escribir a nadie ahora mismo."
|
|
177
|
+
attachments_again: "Si adjuntas archivos y algo falla, tendrás que volver a seleccionarlos."
|
|
178
|
+
unknown_draft: "Esa propuesta ya no existe."
|
|
179
|
+
draft_already_reviewed: "Esa propuesta ya se %{status}."
|
|
180
|
+
no_pending_draft: "No hay ninguna propuesta pendiente en este caso."
|
|
181
|
+
stale_turn: "La conversación ha cambiado. Léela y vuelve a enviar."
|
|
182
|
+
assistant_not_allowed: "El asistente no puede hacer eso en este caso."
|
|
183
|
+
not_an_assistant: "Esa acción es solo para el asistente."
|
|
93
184
|
generic: "No ha funcionado."
|
|
@@ -3,15 +3,31 @@ en:
|
|
|
3
3
|
system:
|
|
4
4
|
assigned: "%{agent} is taking care of your request"
|
|
5
5
|
reassigned: "%{agent} is now taking care of your request"
|
|
6
|
+
assistant_disclosure: "%{name} is %{desk}’s virtual assistant. If you’d rather talk to a person, use the button in this conversation."
|
|
7
|
+
handed_off_to_humans: "I’m passing you to somebody on the team."
|
|
8
|
+
handed_off_to_humans_with_promise: "I’m passing you to somebody on the team. We usually reply in under %{reply_within}."
|
|
9
|
+
human_requested: "Somebody from the team will take it from here."
|
|
10
|
+
human_requested_with_promise: "Somebody from the team will take it from here. We usually reply in under %{reply_within}."
|
|
11
|
+
assistant:
|
|
12
|
+
disclosed_name: "%{name} · virtual assistant"
|
|
6
13
|
thread:
|
|
7
14
|
closed_notice: "This conversation is closed. Start a new one if you need anything else."
|
|
8
15
|
reopened_notice: "We reopened this conversation."
|
|
9
16
|
promise: "We usually reply in under %{time}"
|
|
17
|
+
opened_by_support: "%{desk} opened this conversation with you about “%{label}”. You can reply right here."
|
|
18
|
+
unavailable_notice: "This account can no longer send or receive messages. The conversation stays here to read."
|
|
19
|
+
human_door: "I’d rather talk to a person"
|
|
20
|
+
human_requested_notice: "Done. Somebody from the team will take it from here."
|
|
21
|
+
handled_by_assistant: "Handled by %{name}"
|
|
22
|
+
handled_by_humans: "Somebody from the team will take it from here"
|
|
23
|
+
transcript:
|
|
24
|
+
deleted: "[message deleted]"
|
|
10
25
|
notifications:
|
|
11
26
|
title: "%{desk} · new message"
|
|
12
27
|
queue:
|
|
13
28
|
tabs:
|
|
14
29
|
awaiting: "Needs a reply"
|
|
30
|
+
needs_human: "Needs a person"
|
|
15
31
|
mine: "Mine"
|
|
16
32
|
unassigned: "Unassigned"
|
|
17
33
|
open: "Open"
|
|
@@ -19,6 +35,7 @@ en:
|
|
|
19
35
|
closed: "Closed"
|
|
20
36
|
summary:
|
|
21
37
|
awaiting_reply: "awaiting reply"
|
|
38
|
+
needs_human: "needs a person"
|
|
22
39
|
awaiting_requester: "awaiting requester"
|
|
23
40
|
open: "open"
|
|
24
41
|
closed: "closed"
|
|
@@ -41,6 +58,7 @@ en:
|
|
|
41
58
|
state:
|
|
42
59
|
awaiting_reply: "We're on it"
|
|
43
60
|
answered: "We replied"
|
|
61
|
+
opened_by_support: "We wrote to you"
|
|
44
62
|
closed: "Closed"
|
|
45
63
|
doors:
|
|
46
64
|
new: "Need help?"
|
|
@@ -4,10 +4,36 @@ es:
|
|
|
4
4
|
system:
|
|
5
5
|
assigned: "%{agent} se ocupa de tu consulta"
|
|
6
6
|
reassigned: "%{agent} se ocupa ahora de tu consulta"
|
|
7
|
+
# Las tres líneas del asistente. Cada una tiene su variante con
|
|
8
|
+
# promesa: una mesa que no promete un plazo no debe inventarlo.
|
|
9
|
+
assistant_disclosure: "%{name} es el asistente virtual de %{desk}. Si prefieres hablar con una persona, usa el botón de esta conversación."
|
|
10
|
+
handed_off_to_humans: "Te paso con una persona del equipo."
|
|
11
|
+
handed_off_to_humans_with_promise: "Te paso con una persona del equipo. Normalmente contestamos en menos de %{reply_within}."
|
|
12
|
+
human_requested: "Una persona del equipo seguirá con tu consulta."
|
|
13
|
+
human_requested_with_promise: "Una persona del equipo seguirá con tu consulta. Normalmente contestamos en menos de %{reply_within}."
|
|
14
|
+
assistant:
|
|
15
|
+
# La divulgación no es una insignia que alguien pueda no ver: cuando
|
|
16
|
+
# se divulga, el nombre mismo dice qué es.
|
|
17
|
+
disclosed_name: "%{name} · asistente virtual"
|
|
7
18
|
thread:
|
|
8
19
|
closed_notice: "Esta conversación está cerrada. Abre una nueva si necesitas algo más."
|
|
9
20
|
reopened_notice: "Hemos vuelto a abrir esta conversación."
|
|
10
21
|
promise: "Normalmente respondemos en menos de %{time}"
|
|
22
|
+
# La línea con la que se abre un hilo que ha empezado el equipo: quien
|
|
23
|
+
# recibe un mensaje de un sitio al que nunca ha escrito necesita saber
|
|
24
|
+
# qué es esto.
|
|
25
|
+
opened_by_support: "%{desk} ha abierto esta conversación contigo sobre «%{label}». Puedes responder aquí mismo."
|
|
26
|
+
unavailable_notice: "Esta cuenta ya no puede enviar ni recibir mensajes. La conversación se queda aquí para consultarla."
|
|
27
|
+
human_door: "Prefiero hablar con una persona"
|
|
28
|
+
human_requested_notice: "Hecho. Una persona del equipo seguirá con tu consulta."
|
|
29
|
+
handled_by_assistant: "Atendida por %{name}"
|
|
30
|
+
handled_by_humans: "Una persona del equipo seguirá con tu consulta"
|
|
31
|
+
transcript:
|
|
32
|
+
# Un mensaje borrado sigue siendo un turno. Quien lea la conversación
|
|
33
|
+
# después, una persona o el modelo al que el host se la pasa, tiene
|
|
34
|
+
# que ver que ahí se dijo algo y se retiró: un hueco sin marcar se lee
|
|
35
|
+
# como que nunca pasó nada.
|
|
36
|
+
deleted: "[mensaje eliminado]"
|
|
11
37
|
notifications:
|
|
12
38
|
# Generic on purpose: una pantalla de bloqueo no debe contar de qué va
|
|
13
39
|
# el caso de nadie. El detalle va en el cuerpo.
|
|
@@ -15,6 +41,7 @@ es:
|
|
|
15
41
|
queue:
|
|
16
42
|
tabs:
|
|
17
43
|
awaiting: "Pendientes"
|
|
44
|
+
needs_human: "Necesitan persona"
|
|
18
45
|
mine: "Mi cola"
|
|
19
46
|
unassigned: "Sin asignar"
|
|
20
47
|
open: "Abiertos"
|
|
@@ -22,6 +49,7 @@ es:
|
|
|
22
49
|
closed: "Cerrados"
|
|
23
50
|
summary:
|
|
24
51
|
awaiting_reply: "esperando respuesta"
|
|
52
|
+
needs_human: "necesita una persona"
|
|
25
53
|
awaiting_requester: "esperando al cliente"
|
|
26
54
|
open: "abierto"
|
|
27
55
|
closed: "cerrado"
|
|
@@ -45,6 +73,7 @@ es:
|
|
|
45
73
|
state:
|
|
46
74
|
awaiting_reply: "Estamos con ello"
|
|
47
75
|
answered: "Te hemos contestado"
|
|
76
|
+
opened_by_support: "Te hemos escrito"
|
|
48
77
|
closed: "Cerrada"
|
|
49
78
|
# The ways in, from anywhere in the host app.
|
|
50
79
|
doors:
|
data/config/routes.rb
CHANGED
|
@@ -18,7 +18,13 @@ SupportDesk::Engine.routes.draw do
|
|
|
18
18
|
# `show` redirects into the chats conversation — a stable URL for a case,
|
|
19
19
|
# for emails and notifications, that never has to know where chats is
|
|
20
20
|
# mounted.
|
|
21
|
-
resources :tickets, only: %i[create show]
|
|
21
|
+
resources :tickets, only: %i[create show] do
|
|
22
|
+
# The door out of a machine. It is a POST on the CASE rather than
|
|
23
|
+
# anything to do with chats, because it is a fact about the case: from
|
|
24
|
+
# here on a person is expected, whatever the assistant would have done
|
|
25
|
+
# next.
|
|
26
|
+
post :request_human, on: :member
|
|
27
|
+
end
|
|
22
28
|
|
|
23
29
|
root to: "tickets#index"
|
|
24
30
|
end
|