voice_control 0.1.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 +7 -0
- data/CHANGELOG.md +55 -0
- data/CONTRIBUTING.md +56 -0
- data/LICENSE +21 -0
- data/README.md +343 -0
- data/SECURITY.md +15 -0
- data/app/controllers/voice_control/assets_controller.rb +20 -0
- data/app/controllers/voice_control/commands_controller.rb +77 -0
- data/assets/widget.css +346 -0
- data/assets/widget.js +1910 -0
- data/config/routes.rb +7 -0
- data/docs/browser-actions.md +112 -0
- data/docs/commands.md +113 -0
- data/docs/configuration.md +40 -0
- data/docs/demo.md +39 -0
- data/docs/deployment.md +103 -0
- data/docs/integration.md +79 -0
- data/examples/react.jsx +27 -0
- data/lib/generators/voice_control/install/install_generator.rb +21 -0
- data/lib/generators/voice_control/install/templates/voice_control.rb +72 -0
- data/lib/voice_control/argument.rb +59 -0
- data/lib/voice_control/browser_actions.rb +210 -0
- data/lib/voice_control/command.rb +51 -0
- data/lib/voice_control/configuration.rb +71 -0
- data/lib/voice_control/conversation.rb +217 -0
- data/lib/voice_control/engine.rb +11 -0
- data/lib/voice_control/jev.rb +60 -0
- data/lib/voice_control/result.rb +59 -0
- data/lib/voice_control/version.rb +3 -0
- data/lib/voice_control/widget_helper.rb +20 -0
- data/lib/voice_control.rb +29 -0
- metadata +153 -0
data/config/routes.rb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
VoiceControl::Engine.routes.draw do
|
|
2
|
+
get "commands", to: "commands#index"
|
|
3
|
+
post "interpret", to: "commands#create"
|
|
4
|
+
post "execute", to: "commands#execute"
|
|
5
|
+
get "widget.js", to: "assets#javascript", as: :javascript
|
|
6
|
+
get "widget.css", to: "assets#stylesheet", as: :stylesheet
|
|
7
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
[← README](../README.md)
|
|
2
|
+
|
|
3
|
+
# Click buttons and fill fields
|
|
4
|
+
|
|
5
|
+
Enable automatic browser actions with one option:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
VoiceControl.configure do |config|
|
|
9
|
+
config.browser_actions = true
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
No per-element Ruby commands are needed. Before each new command, the widget discovers the current document's visible, enabled buttons, local links, editable text and number fields, textareas, native checkboxes and radio buttons, and native single-select dropdowns. It sends their labels, IDs, names, element types, and temporary references to the server. Jev chooses the action and target alongside your Ruby vocabulary. Elements without IDs work too. Help includes these controls under **On this page** and refreshes automatically after Turbo navigation, browser history changes, and DOM updates such as React renders. Refreshes are debounced while the widget is visible; reopening help also discovers the current controls. Discovered labels omit emoji, decorative symbols, and icon markup while preserving words and readable punctuation. Controls must have readable text or a label: `data-voice-control-label`, an accessible/associated label, image alt text, a button caption, placeholder, or title. Unlabeled and decoration-only controls are excluded; HTML IDs/names and invented “Control 28” labels are never used as fallback names. Add `aria-label="Open menu"` to make an icon-only button discoverable. The dummy app enables this option and has no hardcoded commands for its buttons or fields.
|
|
14
|
+
|
|
15
|
+
### Exclude pages or controls
|
|
16
|
+
|
|
17
|
+
Place this in the page's `<head>` to disable all automatic browser actions on that page, including submit, scrolling, and history:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<meta name="voice-control-browser-actions" content="off">
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Ruby commands remain available, including explicit `Result.click`/`fill` commands. Remove the tag to use the application's `browser_actions` setting again; `content="on"` cannot enable discovery when the initializer disables it. Keep the tag specific to the current page: Turbo merges the document head, so use `data-turbo-temporary` on page-specific tags if they should be removed before the next Turbo render:
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<meta name="voice-control-browser-actions" content="off" data-turbo-temporary>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
To exclude a button, form, or entire region instead:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<button data-voice-control-ignore>Delete account</button>
|
|
33
|
+
<section data-voice-control-ignore>
|
|
34
|
+
<label>Private note <textarea></textarea></label>
|
|
35
|
+
</section>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`data-voice-control-ignore` is presence-based: even `data-voice-control-ignore="false"` excludes the element. Remove the attribute to include it again. Descendants and controls associated with an ignored form via `form="..."` are excluded. Excluded controls also reject explicit selector actions. Prefer ignoring the whole form when none of its fields or submission should be voice-accessible.
|
|
39
|
+
|
|
40
|
+
Changes to these policies refresh help and suggestions while the widget is open. Dynamic execution rechecks the live policy, so adding an exclusion while Jev is responding prevents the pending action from touching that control. Already-sent server work cannot be undone. These are discovery controls, not access control; the underlying endpoints must still authorize every request.
|
|
41
|
+
|
|
42
|
+
### Supported commands
|
|
43
|
+
|
|
44
|
+
Try “click Save settings,” “focus workspace name,” or “enter Studio North into workspace name.” Text entry accepts quoted values, `fill … with …`, `set … to …`, and `enter/type … into …`. When no value is supplied, the widget asks a follow-up. Jev selects the control; deterministic extraction or the follow-up supplies the text.
|
|
45
|
+
|
|
46
|
+
An exact **“click <label>”** match uses the discovered page control directly, ignoring case and extra whitespace. It bypasses Jev so an explicit button/link name cannot be replaced with an unrelated model guess. Repeated links with the same readable label (ignoring case), destination, and navigation behavior appear as one action. Matching labels with different destinations or behaviors, and separate buttons, still ask you to choose; other phrasing uses the configured interpreter. Authorization, signed tickets, and stale-control checks apply to both paths.
|
|
47
|
+
|
|
48
|
+
Field entry accepts **fill**, **enter**, **type**, and **set** as aliases. Try “enter token balance with 500”, “enter 500 into token balance”, or “set token balance to 500”. Say “enter token balance” without a value to get a follow-up question. Submit the form separately with “click Save user”.
|
|
49
|
+
|
|
50
|
+
Click an editable field, then open the widget and say **“enter 500”** or **“type Hello”** to replace the selected field’s value without naming it. The widget remembers the field while you use its command box. Help shows **Enter into selected field (field label)** only when an eligible field is selected; clicking that action asks for a value. Selecting another page control or navigating clears the selection, and stale or unsupported targets are rejected.
|
|
51
|
+
|
|
52
|
+
Native dropdowns support **“select Premium from Plan”**, **“select Inactive”**, and **“set Plan to Premium”**. Say “select Plan” for a follow-up choice. Only enabled options can be selected. Option labels and opaque references are sent, never their underlying values or current selection. Dropdown catalogs must have distinct readable labels, at most 100 options, and fit within 4KB. Custom comboboxes and multi-selects are not included.
|
|
53
|
+
|
|
54
|
+
The widget shows **Editing: field name** and up to three clickable suggestions from the current page. Suggestions prioritize fields and save buttons, refresh after navigation, and use the normal command execution flow.
|
|
55
|
+
|
|
56
|
+
Say **“check notifications”**, **“enable notifications”**, or **“turn on notifications”** to check a checkbox; **“uncheck notifications”**, **“disable notifications”**, or **“turn off notifications”** to clear it. Repeating a command keeps the requested state. Radio buttons support **“choose Inactive”** or **“select Inactive”** and use the browser's normal group selection. These controls emit native click/input/change events. Try the checkbox on Settings and status radios on a user edit page, then save the form to persist your changes.
|
|
57
|
+
|
|
58
|
+
Say **“clear Token balance”** to empty a text or number field, or **“clear this field”** after selecting it. Clearing uses the same native events and Undo as filling. Passwords, read-only fields, and dropdowns are excluded. A brief green outline marks the affected control after an action or Undo, without changing the host page's styles. Reduced-motion preferences are respected.
|
|
59
|
+
|
|
60
|
+
Say **“scroll down”**, **“scroll up”**, **“back to top”**, or **“scroll to the bottom”** to move around the document. Visible HTML headings are discovered as section destinations: **“show the Billing section”** or **“scroll to Billing”** scrolls to a heading named Billing. Scrolling uses the same signed command flow, rejects a changed page, and preserves unsaved Undo. Nested scroll containers are not targeted by the directional commands.
|
|
61
|
+
|
|
62
|
+
Say **“go back”** or **“go forward”** to use the current tab's native browser history. These commands appear in help when browser actions are enabled and use the same authorization, signed tickets, and page checks. Turbo and client-side routers receive normal history navigation. As with the browser's own buttons, nothing happens if there is no corresponding history entry; a history entry may point outside the current app.
|
|
63
|
+
|
|
64
|
+
Say **“submit”** or **“submit this form”** to submit the form containing the last field or button you selected. The widget remembers that form while you type a command. Without a selection, it uses the only visible form; with multiple forms, select a field first. Native validation and submit events still run, including Turbo and React handlers, and the first submit button's name/value are preserved. Hidden, disabled, ignored, changed, and external-destination forms are rejected. A standalone input needs a native form to support submission. Submission clears that form's unsaved Undo when a submit event fires; it does not add an undo for saved data.
|
|
65
|
+
|
|
66
|
+
Help search accepts partial labels and small spelling mistakes such as **“notifcations”**. This matching runs locally and filters help only; it does not change how Jev selects or executes commands.
|
|
67
|
+
|
|
68
|
+
Native **date** and **time** fields support fill, focus, clear, selected-field entry, and Undo. Try **“set due date to October 1”**, **“set due date to 2026-10-01”**, or **“set start time to 2:30 PM”**. English month names use the browser's current year when omitted. Times accept 24-hour `14:30` or AM/PM notation. Invalid calendar dates, times, and values outside the field's min/max/step leave the old value intact. The Orders demo page includes unsaved practice fields. Date-time, month, week, and custom date-picker controls are not included.
|
|
69
|
+
|
|
70
|
+
Generic table-row actions such as **Edit** or **Delete** gain context from a visible row header or the first identity cell: **“edit Alex Morgan”** can distinguish it from another row's Edit button. The widget prefers primary link/strong text and excludes secondary small text, ignored/hidden content, and input values. Explicit `aria-label`, `aria-labelledby`, and `data-voice-control-label` names take precedence. A changed identity invalidates an in-flight action. If a row has no usable identity, supply an explicit accessible label.
|
|
71
|
+
|
|
72
|
+
Say **“undo that”** or click **Undo** to restore the last VoiceControl field fill, dropdown selection, or checkbox change. Undo is one level, local to the current page, and dispatches the same input/change events as editing. It expires after a conflicting manual edit, form submission, navigation, radio selection, or another completed non-field action. It does not reverse saved database changes or radio-group changes. Previous values stay only in widget memory and are never sent to the backend.
|
|
73
|
+
|
|
74
|
+
### Debug mode
|
|
75
|
+
|
|
76
|
+
Enable diagnostics in your initializer and restart Rails:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
config.debug = true
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Debug is off by default. When enabled, expand **Command details** below the widget input to inspect the latest phrase, matched command and description, selection source, confidence, confidence threshold, readable candidates, available command count, HTTP status, server interpretation/execution times in milliseconds, and outcome. **Jev result** shows the provider's validated action answer as JSON: choice, confidence, and probabilities for offered commands (including `none`). Unexpected provider metadata is excluded. The result remains available after execution and in **Copy details**. Exact label matches show `exact_browser_label` as their source and do not call Jev; their confidence is a deterministic match score. Help selections also bypass interpretation and therefore have no confidence score. Expected rejection messages appear here too; internal exceptions, credentials, execution tickets, and page context are excluded. The current phrase is displayed locally and replaced by the next command; no history is stored. Existing widget authorization still applies. The local dummy app enables debug so you can try it immediately.
|
|
83
|
+
|
|
84
|
+
Use **Copy details** to copy exactly the displayed report to your clipboard for a bug report. It includes the displayed phrase; review it before sharing. If clipboard access is unavailable, the widget offers manual copying. Nothing is uploaded or sent automatically.
|
|
85
|
+
|
|
86
|
+
Discovery excludes password, file, hidden, disabled, read-only, and unsupported input types, plus the widget itself. Current field values, HTML, and link destinations are not sent. Button captions count as labels. Add `data-voice-control-ignore` to a control or subtree to exclude it, or `data-voice-control-label="Save profile"` to give a control a clearer name. Labels are sent to Jev when enabled, so exclude sections whose labels contain private information. Discovery is limited to 200 eligible controls and a 128KB request; larger pages report an error rather than silently omitting controls. A text field produces three commands (fill, focus, clear), so a large form can exceed Jev's limit before reaching 200 controls: Jev accepts at most 255 choices per request, so at most 254 commands (your vocabulary plus discovered page actions) are matched at once. Beyond that, the commands with the longest descriptions/aliases are skipped for that request and a warning listing them is written to the Rails log. Skipped commands still work through help and exact click labels. Keep large pages under the limit with `pages:` scopes and `data-voice-control-ignore`. Iframes and other shadow roots are not scanned.
|
|
87
|
+
|
|
88
|
+
Actions stay bound to the original DOM element and page snapshot across follow-ups. Removed, replaced, relabeled, hidden, disabled, or changed-page controls are rejected. Jev returns a choice from the discovered controls, never generated JavaScript or a CSS selector. Normal server authorization and signed execution tickets apply; the underlying app remains responsible for authorizing any requests its controls initiate. This feature is off by default.
|
|
89
|
+
|
|
90
|
+
You can also register explicit browser actions in the Ruby vocabulary when you want fixed targets:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
config.group "Page controls" do
|
|
94
|
+
config.command :fill_name, description: "Fill the workspace name field" do
|
|
95
|
+
argument :value, :string, prompt: "What name should I enter?"
|
|
96
|
+
execute { |args, _context| VoiceControl::Result.fill("#workspace-name", args[:value]) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
config.command :save_settings, description: "Click the Save settings button" do
|
|
100
|
+
execute { |_args, _context| VoiceControl::Result.click("#save-settings") }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For explicit commands, add an `extract` callback to accept a value in the initial command or let the widget ask a follow-up.
|
|
106
|
+
|
|
107
|
+
Explicit selectors must match exactly one visible, enabled element. Click supports buttons (including `role="button"`), checkboxes, radio buttons, and same-origin HTTP links; fill and focus support editable text/number inputs and textareas. Missing, ambiguous, hidden, disabled, and read-only targets produce feedback. A page URL change while the request is in progress cancels the browser action.
|
|
108
|
+
|
|
109
|
+
Filling replaces the value and dispatches bubbling `input` and `change` events using the native value setter for React listeners. Clicking uses the element's normal click handler and browser form validation. For custom comboboxes, multi-select controls, or other JavaScript behavior, return a named `Result.event` and handle it in your app.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
Debug probabilities show readable command descriptions alongside their internal IDs, so duplicate labels remain distinguishable. IDs such as `voice_control_browser_click_e28` identify controls for execution; they are not spoken command names.
|
data/docs/commands.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[← README](../README.md)
|
|
2
|
+
|
|
3
|
+
# Define your vocabulary
|
|
4
|
+
|
|
5
|
+
Define commands inside `VoiceControl.configure`. Keys are unique, lower-case identifiers; `none` is reserved. Groups organize help and give Jev context.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
config.group "User management" do
|
|
9
|
+
config.command :grant_credits,
|
|
10
|
+
description: "Give a user credits",
|
|
11
|
+
aliases: ["add credits", "top up credits"],
|
|
12
|
+
examples: ["give user 42 100 credits"],
|
|
13
|
+
visible: -> { current_user.admin? },
|
|
14
|
+
authorize: ->(args, _context) { policy(User.find(args[:user_id])).grant_credits? } do
|
|
15
|
+
|
|
16
|
+
argument :user_id, :integer,
|
|
17
|
+
extract: ->(text, _context) { text[/\buser (\d+)\b/i, 1] },
|
|
18
|
+
validate: ->(id) { id.positive? },
|
|
19
|
+
prompt: "Which user ID?"
|
|
20
|
+
|
|
21
|
+
argument :amount, :integer,
|
|
22
|
+
extract: ->(text, _context) { text[/\b(\d+) credits\b/i, 1] },
|
|
23
|
+
validate: ->(amount) { amount.between?(1, 1000) },
|
|
24
|
+
prompt: "How many credits?"
|
|
25
|
+
|
|
26
|
+
execute do |args, _context|
|
|
27
|
+
GrantCredits.call(user_id: args[:user_id], amount: args[:amount])
|
|
28
|
+
VoiceControl::Result.message("Credits added.")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Your action owns transactions and domain invariants. Jev receives descriptions and aliases, never your Ruby callbacks, and cannot create a new action or URL.
|
|
35
|
+
|
|
36
|
+
Callbacks run in the request controller, where `current_user`, `policy`, and `main_app` route helpers are available. `args` has symbol keys; context has string keys. `visible` runs during discovery and interpretation; `authorize` receives complete, validated arguments and runs before issuing a ticket and again before execution. Both default to allowing the command **within the overall access gate**. Supply record-level authorization for actions on a target record.
|
|
37
|
+
|
|
38
|
+
### Page scopes
|
|
39
|
+
|
|
40
|
+
Add `pages:` to a command to restrict its availability:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
config.command :settings_help, description: "Explain workspace settings",
|
|
44
|
+
pages: "/settings", aliases: ["help with settings"] do
|
|
45
|
+
execute { |_args, _context| VoiceControl::Result.message("Change the workspace fields, then say submit.") }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
config.command :user_help, description: "Explain the user form",
|
|
49
|
+
pages: ["/users/new", %r{\A/users/\d+/edit\z}] do
|
|
50
|
+
execute { |_args, _context| VoiceControl::Result.message("Enter a name, email and plan, then save.") }
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`pages:` accepts one exact local path, one Ruby `Regexp`, or a nonempty array of either. Omit it (or use `nil`) for global availability. Exact paths are case-sensitive and include trailing slashes; do not include a query string, fragment, or host. Anchor regexes with `\A` and `\z` when you want a whole-path match. Missing or invalid paths hide scoped commands.
|
|
55
|
+
|
|
56
|
+
The widget sends the current pathname to help and interpretation. Scope filtering happens before Jev matching and explicit command selection, and uses the original pathname again when executing a signed ticket. Changing scope configuration can invalidate a pending command. Navigating away cancels follow-ups, and stale help responses are discarded. This routing path is stored separately from the context passed through `config.context`, so removing URL context for privacy does not disable scopes.
|
|
57
|
+
|
|
58
|
+
Scopes are **not authorization**: a client can forge its path, and the server cannot verify the tab's actual location. Keep record/tenant permission checks in `authorize:`. `visible:` still applies on every page. Custom API clients send `context: { path: "/settings" }` to interpretation and `?path=/settings` to the catalog endpoint. Follow-ups may omit the path to retain their signed original path; supplying a different path rejects the follow-up. Custom routers can call [`window.VoiceControl.refresh()`](integration.md) after a route change.
|
|
59
|
+
|
|
60
|
+
### Arguments
|
|
61
|
+
|
|
62
|
+
| Type | Accepted values |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `:string` | Nonempty text, up to 2,000 characters |
|
|
65
|
+
| `:integer` | Whole decimal digits, with optional sign; validated before execution |
|
|
66
|
+
| `:decimal` | Plain decimal input; returned as a decimal string to preserve precision |
|
|
67
|
+
| `:boolean` | `yes/no`, `true/false`, `on/off` |
|
|
68
|
+
| `:enum` | One configured value, matched without case sensitivity (spaces and underscores are interchangeable) |
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
argument :plan, :enum, values: %w[free premium premium_plus]
|
|
72
|
+
argument :enabled, :boolean, default: false
|
|
73
|
+
argument :note, :string, required: false
|
|
74
|
+
argument :user_id, :integer,
|
|
75
|
+
default: ->(_text, context) { context["user_id"] }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Arguments are required by default. `extract` gets `(transcript, context)`; a `nil` result falls back to `default`, which can be a value or a callback with the same signature. `validate` receives the coerced value. An invalid or missing required value prompts for that argument. Follow-ups accept the value itself, for example `42` or `premium`.
|
|
79
|
+
|
|
80
|
+
Jev routes intent; it does not extract arbitrary text. Define extractors for values that should be collected from the first utterance. Without one, the widget asks for the value. Decimal callbacks can use `BigDecimal(args[:amount])` when performing arithmetic.
|
|
81
|
+
|
|
82
|
+
### Results
|
|
83
|
+
|
|
84
|
+
Return one of:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
VoiceControl::Result.message("Done.")
|
|
88
|
+
VoiceControl::Result.reload # Reload the current page after saving changes
|
|
89
|
+
VoiceControl::Result.navigate(main_app.users_path) # Local paths only
|
|
90
|
+
VoiceControl::Result.event("workspace:refresh", { user_id: args[:user_id] })
|
|
91
|
+
VoiceControl::Result.click("#save-settings")
|
|
92
|
+
VoiceControl::Result.fill("#workspace-name", args[:value])
|
|
93
|
+
VoiceControl::Result.focus("#workspace-name")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Every helper accepts optional `notify:` text for a five-second completion notice:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
VoiceControl::Result.message("Preferences saved.", notify: "Saved successfully.")
|
|
100
|
+
VoiceControl::Result.reload(notify: "Your plan is now Premium.")
|
|
101
|
+
VoiceControl::Result.navigate(main_app.users_path, notify: "User saved.")
|
|
102
|
+
VoiceControl::Result.event("workspace:refresh", { user_id: args[:user_id] }, notify: "Workspace updated.")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Return a notified result only after the action succeeds. Notifications accept 1–200 characters, render as plain text, and are absent by default. Failed browser actions do not show a success notice. Event notices mean the event was dispatched, not that an asynchronous listener finished. During navigation, the notice is briefly stored in this tab's `sessionStorage` until its five-second expiry; avoid secrets in notification text.
|
|
106
|
+
|
|
107
|
+
Navigation uses Turbo when available, otherwise a full-page visit. `reload` performs a full browser reload of the current URL, preserving its query and fragment and fetching fresh page state. Return it after a successful change that needs the whole page updated, such as changing an account plan. Events are dispatched on `window`:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
window.addEventListener("workspace:refresh", (event) => {
|
|
111
|
+
refreshUser(event.detail.user_id)
|
|
112
|
+
})
|
|
113
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[← README](../README.md)
|
|
2
|
+
|
|
3
|
+
# Configuration reference
|
|
4
|
+
|
|
5
|
+
Set options in `config/initializers/voice_control.rb` inside `VoiceControl.configure`. Restart Rails after editing the vocabulary or initializer. The DSL is process configuration, not automatically reloaded application code.
|
|
6
|
+
|
|
7
|
+
| Option | Default | Purpose |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `api_key` | `nil` | Jev key string or zero-argument callable; server only. Required for free-form Jev matching. |
|
|
10
|
+
| `model` | `"jev-latest"` | Provider model name. |
|
|
11
|
+
| `parent_controller` | `"ApplicationController"` | Host controller class name; includes its authentication/callbacks. |
|
|
12
|
+
| `authorize` | `-> { false }` | Overall widget and endpoint access, in controller context. |
|
|
13
|
+
| `identity` | `current_user&.id` when available | Binds tickets to the authenticated identity, in addition to the session. Override for other auth systems. |
|
|
14
|
+
| `context` | Pass supplied context through | Sanitize/allowlist context before interpretation. Receives a hash with string keys. |
|
|
15
|
+
| `browser_actions` | `false` | Discover supported page controls and enable dynamic actions. Pages can opt out with the [meta tag or DOM exclusions](browser-actions.md#exclude-pages-or-controls). |
|
|
16
|
+
| `debug` | `false` | Show latest diagnostics, including the validated Jev action answer. |
|
|
17
|
+
| `confidence_threshold` | `0.35` | Lower-confidence matches ask which candidate to use. Calibrate for your vocabulary. |
|
|
18
|
+
| `execution_store` | `Rails.cache`, development-only NullStore fallback | Zero-argument callable returning an atomic cache. [Production requirements](deployment.md). |
|
|
19
|
+
| `on_error` | Log error class and command key | Callable receiving `(exception, details)`; details contains `command` and `controller`. |
|
|
20
|
+
| `interpreter` | Jev | Custom callable with `transcript:`, `context:`, `commands:`. See below. |
|
|
21
|
+
| `keyboard_shortcut` | `"mod+shift+u"` | Open/close; `nil` disables it. `mod` is Cmd on macOS, Ctrl elsewhere. |
|
|
22
|
+
| `push_to_talk_shortcut` | `"mod+shift+space"` | Hold/release speech; `nil` disables it. |
|
|
23
|
+
| `speech_language` | `"en-US"` | Browser speech recognition language as a BCP 47 tag, e.g. `"uk-UA"`. Widget text and built-in page-action phrases stay English; give commands aliases in your language. |
|
|
24
|
+
| `idle_timeout` | `120_000` | Milliseconds before the open widget closes for inactivity; use a positive number. |
|
|
25
|
+
| `request_timeout` | `30_000` | Browser request deadline in milliseconds, including response parsing; integer from 1,000 to 300,000. No automatic retries. |
|
|
26
|
+
| `widget_position` | `:bottom_right` | `:bottom_right` or `:bottom_left`. |
|
|
27
|
+
| `launcher_size` | `:normal` | `:small` uses a 44px button/20px icon; `:normal` uses 56px/24px. Panel width is unchanged. |
|
|
28
|
+
|
|
29
|
+
A custom interpreter chooses from the supplied authorized catalog and returns symbol keys:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
config.interpreter = lambda do |transcript:, context:, commands:|
|
|
33
|
+
match = commands.find { |command| command.aliases.include?(transcript.downcase) }
|
|
34
|
+
{ command: match&.key, confidence: match ? 1.0 : 0.0, candidates: [] }
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is an intentionally small exact-alias example, not a natural-language parser. Return `command: nil` for no match. `confidence` is numeric from 0 to 1; `candidates` is an array of existing command keys used for disambiguation. A unique exact browser click and an explicit help selection bypass the interpreter. No Ruby action should run inside the interpreter.
|
|
39
|
+
|
|
40
|
+
The bundled demo's local interpreter is for examples and tests; it is not a runtime dependency or an automatic production fallback when Jev fails.
|
data/docs/demo.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[← README](../README.md)
|
|
2
|
+
|
|
3
|
+
# Try the demo
|
|
4
|
+
|
|
5
|
+
From this gem's directory:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bundle install
|
|
9
|
+
bin/demo
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open **http://127.0.0.1:4100**. Set `PORT=4200` to use another port.
|
|
13
|
+
|
|
14
|
+
Explore the demo pages, Ruby commands, and automatically discovered page controls. Try:
|
|
15
|
+
|
|
16
|
+
| Say or type | Demonstrates |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `open users` | Turbo navigation |
|
|
19
|
+
| `give user 42 100 tokens` | Persist a token grant and update the balance in the UI |
|
|
20
|
+
| `change my plan to premium` | Update the demo account plan shown in the sidebar and Your plan card |
|
|
21
|
+
| `change my plan` | Choose Free, Premium, or Premium Plus in a follow-up |
|
|
22
|
+
| `show announcement` | A string follow-up |
|
|
23
|
+
| `set discount` | Decimal validation |
|
|
24
|
+
| `change notifications` | A boolean follow-up |
|
|
25
|
+
| `click the Try voice_control button` | Discover and click the button on Overview to open a demo modal |
|
|
26
|
+
| `fill workspace name` | Fill a field on Settings after a follow-up |
|
|
27
|
+
| `fill token balance with 500` | Fill the numeric field on a user edit page; then `click Save user` to persist it |
|
|
28
|
+
| `click Save settings` | Save the Settings form to SQLite |
|
|
29
|
+
| `summarize workspace` | Settings-only Ruby command; shows workspace details and a completion notice without reloading |
|
|
30
|
+
|
|
31
|
+
The dummy app automatically uses **Jev** when `JEV_API_KEY` or `TYPESAFE_API_KEY` is set in the server environment (`JEV_API_KEY` takes precedence). Start it with your key:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
JEV_API_KEY=your-key bin/demo
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The page shows **Jev interpreter** in this mode. Try free-form commands such as “take me to the report page.” The key stays on the server; interpretation sends your command and page context to Jev. Restart the demo after changing the key.
|
|
38
|
+
|
|
39
|
+
Without a key, the demo uses a **local, deterministic interpreter** that understands the listed phrases and aliases. Automated tests always boot in local mode and stub Jev requests. The dummy app runs as a fictional administrator and is intended for localhost only. Users support create, view, edit, and delete. Settings, account plans, and token grants persist in `test/dummy/storage/development.sqlite3`, including after a restart. `bin/demo` prepares the SQLite database and seeds 24 example users on first launch. Tests use a separate database with transactional fixtures. Active Record and SQLite are demo/development dependencies only; the gem itself still requires no database or migrations.
|
data/docs/deployment.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[← README](../README.md)
|
|
2
|
+
|
|
3
|
+
# Execution and errors
|
|
4
|
+
|
|
5
|
+
The widget shows **“Understanding…”** while preparing or interpreting a command, then **“Running…”** after it receives a ticket and requests execution. Follow-up questions and ambiguous choices remain in the interpretation stage until a command is ready.
|
|
6
|
+
|
|
7
|
+
Interpretation never runs an action. It issues an expiring, session-bound ticket; the widget submits it automatically. The execution endpoint validates arguments and permissions again before invoking your block. Tickets also bind to `current_user.id` when available. For another authentication system:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
config.identity = -> { current_account.id }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Duplicate submissions are rejected using an atomic cache write. **Use a shared cache across Rails processes**, such as Redis or Memcached, with `write(..., unless_exist: true, expires_in: ...)` support:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
config.execution_store = -> { Rails.cache }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The dummy app uses MemoryStore for its single process. No database is required, but this short-lived execution cache is required for actions. Cache eviction or a cache reset can remove replay protection; business-critical actions should additionally use your domain's durable idempotency controls. Tickets expire after ten minutes. Failed executions remain claimed because an external side effect might already have occurred. There are no automatic retries of actions or Jev calls.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
config.on_error = ->(exception, details) { ErrorTracker.capture(exception, extra: details.except(:controller)) }
|
|
23
|
+
config.confidence_threshold = 0.35
|
|
24
|
+
config.model = "jev-latest"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Unexpected errors produce a generic message in the widget. The callback receives the exception and command context. Jev has bounded connection/read/write timeouts of 3/15/5 seconds. Missing credentials fail gracefully; help and direct command selection still work.
|
|
28
|
+
|
|
29
|
+
## Production configuration
|
|
30
|
+
|
|
31
|
+
A minimal deployment uses your existing authentication, cookie session/CSRF middleware, a server-side Jev key, and a shared cache. The gem does not create users, roles, database tables, or infrastructure.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# config/environments/production.rb
|
|
35
|
+
# Add the redis gem to your application's Gemfile if it is not already installed.
|
|
36
|
+
config.cache_store = :redis_cache_store, {
|
|
37
|
+
url: ENV.fetch("REDIS_URL"),
|
|
38
|
+
namespace: "my-app-cache"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# Inside VoiceControl.configure in your initializer:
|
|
44
|
+
config.execution_store = -> { Rails.cache }
|
|
45
|
+
config.debug = false
|
|
46
|
+
config.authorize = -> { current_user&.admin? }
|
|
47
|
+
config.identity = -> { current_user&.id }
|
|
48
|
+
config.context = lambda do |client|
|
|
49
|
+
{ "path" => client["path"], "area" => client["area"] }.compact
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use a cache shared by every web process and avoid evicting replay keys before their 11-minute TTL. Keep your existing cache configuration if it already meets these requirements. The default execution store uses Rails.cache; only development gets a single-process MemoryStore fallback when Rails.cache is a NullStore. Tests, staging, and production do not get that fallback. NullStore and failed cache claims prevent execution. They are not a substitute for durable idempotency on money or other business-critical operations.
|
|
54
|
+
|
|
55
|
+
Start with a small command vocabulary. Explicit record/tenant policies belong in each mutation's `authorize` callback; a UI label, model confidence, supplied URL, or hidden button is never an authorization boundary. Exact browser clicks bypass model matching, but still use the same ticket/access checks. The default confidence threshold is 0.35; evaluate it against your own vocabulary before rollout. Increasing it (for example, `config.confidence_threshold = 0.75`) asks for disambiguation more often. Neither value guarantees that a model inferred the right intent.
|
|
56
|
+
|
|
57
|
+
The gem does not impose per-user/provider rate limits. Apply your application's existing Rails rate limiter or Rack middleware to `/voice_control/interpret` and `/voice_control/execute`, keyed to your authenticated identity. Account for these requests in provider cost and latency monitoring. `on_error` handles unexpected failures; expected authorization/validation rejections are HTTP responses.
|
|
58
|
+
|
|
59
|
+
### Privacy and logs
|
|
60
|
+
|
|
61
|
+
The default URL context includes its query and fragment. Filter context before it reaches Jev when a URL can contain reset tokens, emails, or other private data. `data-voice-control-ignore` prevents a control/subtree from being discovered; label attributes and nearby row identities can otherwise contain application data. Debug is for trusted users and includes the current transcript. Never share copied diagnostics without checking their contents.
|
|
62
|
+
|
|
63
|
+
Rails request logs may include payloads even though voice_control has no history database. Filter them in your app:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# config/initializers/filter_parameter_logging.rb
|
|
67
|
+
Rails.application.config.filter_parameters += %i[
|
|
68
|
+
transcript continuation ticket browser_page context
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Signed tickets provide integrity, not encryption: the issuing browser can read their state. Do not put secrets in context or argument defaults. The browser's speech service has separate data handling from Jev; review both for your application.
|
|
73
|
+
|
|
74
|
+
### CSP and network requirements
|
|
75
|
+
|
|
76
|
+
Allow the engine's same-origin JavaScript and CSS in your CSP (`script-src` / `style-src`) and same-origin Fetch requests in `connect-src`. The Rails helper passes the Rails script nonce. Styles load through a link inside the shadow root. No `unsafe-eval` is used. Strict Trusted Types policies need their own integration check because the widget creates its static markup with `innerHTML`.
|
|
77
|
+
|
|
78
|
+
Only the Rails server calls Jev at `https://api.typesafe.ai/v1/systemone`. Do not expose the API key to JavaScript or add the provider to browser `connect-src`. Browser speech recognition may separately require access to the browser vendor's service. Keep asset endpoints public, command endpoints authenticated, and reverse-proxy access logs free of command bodies.
|
|
79
|
+
|
|
80
|
+
### Cancellation and failures
|
|
81
|
+
|
|
82
|
+
Closing or canceling during interpretation prevents a later response from triggering execution. Once `/execute` has been sent, aborting the browser request cannot roll back server work. Check the result before issuing the command again. A claimed ticket remains consumed even if the action fails. There is no automatic action retry or database rollback supplied by the gem.
|
|
83
|
+
|
|
84
|
+
### Troubleshooting
|
|
85
|
+
|
|
86
|
+
| Symptom | Check |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| No widget | Authorization defaults to false. Confirm the signed-in account, helper in the active layout, and public asset responses. |
|
|
89
|
+
| Works through help, but typed phrases fail | The Jev key, server egress, provider response, and debug matching details. |
|
|
90
|
+
| Session changed / HTTP 422 | Current CSRF meta tag, cookies, inherited controller callbacks, and expired tickets. Reload after signing in/out. |
|
|
91
|
+
| HTTP 403 | Overall access, command visibility, record policy, and changed user identity. |
|
|
92
|
+
| Commands refuse to execute with caching disabled | Development uses a local fallback; elsewhere configure a shared atomic execution store. |
|
|
93
|
+
| Too many page controls | Dynamic discovery has a 200-control bound. Exclude irrelevant sections with `data-voice-control-ignore`, or use the page-wide `voice-control-browser-actions` meta tag. |
|
|
94
|
+
| Free-form phrases miss commands on a large page | Jev matches at most 254 commands per request; the longest are skipped and listed in a `VoiceControl:` log warning. Scope commands with `pages:` or exclude page sections. |
|
|
95
|
+
| A scoped command is missing | Check `pages:` against `location.pathname`, including case/trailing slash. Scope uses the original client path even when `config.context` filters URL data. |
|
|
96
|
+
| This command is too large | Shorten the transcript/context or narrow the discovered controls. Signed state is capped at 32KB. |
|
|
97
|
+
| Control/form changed | Restart the command after navigation, DOM replacement, relabeling, or form destination changes. |
|
|
98
|
+
| Wrong model match | Inspect Jev JSON, make descriptions/aliases distinct, reduce overlapping commands, or raise the threshold. Use exact click labels for page controls. |
|
|
99
|
+
| Voice unavailable | Use typing; check HTTPS, browser support, and microphone permission. |
|
|
100
|
+
|
|
101
|
+
### Release checks
|
|
102
|
+
|
|
103
|
+
Before publishing a public release, verify the repository links are accessible to your intended audience, establish a private security-reporting channel, and test real microphone input in each supported browser. A screenshot or short recording can then demonstrate navigation, a follow-up, a form edit, and Undo without claiming support that was not tested. The current source checkout has not been published to RubyGems.
|
data/docs/integration.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[← README](../README.md)
|
|
2
|
+
|
|
3
|
+
# Browser context and React
|
|
4
|
+
|
|
5
|
+
The widget automatically sends `url: location.href` (including query strings and fragments) and `path: location.pathname` with each command. These are read at submission time, including after Turbo or React navigation. The backend passes this context to Jev and your action callbacks; no separate page-information command is needed. Follow-ups and execution keep the initial command’s context.
|
|
6
|
+
|
|
7
|
+
Add context once the widget has connected:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
window.VoiceControl.setContext(() => ({
|
|
11
|
+
area: "admin",
|
|
12
|
+
user_id: currentUserIdFromYourPage
|
|
13
|
+
}))
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Custom context can override defaults. For example, use `url: location.origin + location.pathname` in `setContext` to omit query strings and fragments.
|
|
17
|
+
|
|
18
|
+
Restrict context on the server if needed:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
config.context = ->(client_context) { client_context.slice("url", "path", "area", "user_id") }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Browser context is **untrusted input**. It can select a candidate record, but permission checks must use the current authenticated user and that actual record. Do not send credentials, private page contents, or sensitive query parameters. Configured context and the current transcript are sent to Jev for interpretation; continuation state is signed, not encrypted, and visible to that browser.
|
|
25
|
+
|
|
26
|
+
For React, mount the web component outside the changing route subtree and load `/voice_control/widget.js` once. Keep a current Rails CSRF meta tag in the document:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
<voice-control-widget
|
|
30
|
+
id="voice-control-widget"
|
|
31
|
+
data-endpoint="/voice_control"
|
|
32
|
+
data-shortcut="mod+shift+u"
|
|
33
|
+
data-idle-timeout="120000"
|
|
34
|
+
data-request-timeout="30000"
|
|
35
|
+
data-browser-actions="true"
|
|
36
|
+
/>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Connect your router after the component mounts:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
window.VoiceControl.configure({ navigate: (path) => navigate(path) })
|
|
43
|
+
window.VoiceControl.setContext(() => ({ area: "workspace" }))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`window.VoiceControl.open()` and `.close()` are also available. Call `window.VoiceControl.refresh()` after your custom router changes the URL to refresh page-scoped help and dynamic controls and clear pending follow-ups. Turbo and browser history events do this automatically, even with dynamic browser actions disabled. In React, call it from an effect depending on your router's current location; await `customElements.whenDefined("voice-control-widget")` first if the script may still be loading.
|
|
47
|
+
|
|
48
|
+
Avoid replacing the widget during route changes. Removing it stops recognition and releases its listeners. Turbo layouts use the same `id` and `data-turbo-permanent`; the Rails helper sets both.
|
|
49
|
+
|
|
50
|
+
## Listening and keyboard controls
|
|
51
|
+
|
|
52
|
+
The floating button or keyboard shortcut opens the widget and starts browser speech recognition. Recognition uses `config.speech_language` (default `en-US`); widget text, built-in page phrases, and spoken follow-ups such as “two” or “yes” remain English. Typing remains available when recognition is unsupported or denied. Closing the widget stops listening; two minutes of inactivity closes it automatically. No conversation history is displayed or persisted by the gem.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
config.keyboard_shortcut = "mod+shift+u" # ⌘ on macOS, Ctrl elsewhere
|
|
56
|
+
config.keyboard_shortcut = "ctrl+alt+l" # An alternative
|
|
57
|
+
config.keyboard_shortcut = nil # Disable it
|
|
58
|
+
config.idle_timeout = 120_000 # Milliseconds
|
|
59
|
+
config.push_to_talk_shortcut = "mod+shift+space" # Hold to speak, release to submit
|
|
60
|
+
config.widget_position = :bottom_right # Or :bottom_left
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Push-to-talk:** hold Cmd+Shift+Space on macOS or Ctrl+Shift+Space elsewhere while the page has focus. It opens the widget and listens for that hold only; releasing submits finalized speech once. Closing, leaving the tab, losing window focus, or microphone failure cancels the recording. If speech is still incomplete, its draft stays in the input for review instead of executing. Set `push_to_talk_shortcut = nil` to disable it or use another modifier/key combination if the browser/OS reserves this one. The ordinary microphone toggle still supports continuous listening.
|
|
64
|
+
|
|
65
|
+
**Keyboard help:** opening help focuses search. Use ↓/↑ to move through matching command buttons and Enter to run the focused command. Enter from search runs the first match; ↑ from the first command returns to search. Focus survives catalog refreshes when the same command remains available. Escape closes the widget.
|
|
66
|
+
|
|
67
|
+
Say `close voice control` or `stop listening` to close it. Say `cancel` to abandon a pending follow-up. Choose ambiguous commands by clicking or saying `one`, `two`, or `three`. Help lists only visible commands; clicking a command runs it or asks for its missing arguments.
|
|
68
|
+
|
|
69
|
+
Only final speech results are interpreted. Recognition is paused while a request runs, so interim transcripts cannot fire a partially spoken amount. Turbo navigation preserves the widget. Full page loads, command-triggered reloads, and back/forward cache restoration restore the open/listening state from a short-lived sessionStorage flag, provided the idle window has not expired. Leaving a page releases its microphone. Closing clears that flag; a fresh visit does not start listening. Browsers can still require a fresh gesture to restart the microphone. Browser speech recognition may use the browser vendor's remote service and is not guaranteed to work offline.
|
|
70
|
+
|
|
71
|
+
Requests have a 30-second deadline including response parsing. Set `config.request_timeout` (milliseconds, 1,000–300,000) to change it. A timeout releases the busy state and aborts the browser request. It cannot cancel work already running on the server: execution timeouts ask the user to check the page before trying again. VoiceControl never automatically retries a timed-out action.
|
|
72
|
+
|
|
73
|
+
## Browser support
|
|
74
|
+
|
|
75
|
+
Typed commands require modern browsers with Custom Elements, Shadow DOM, Fetch, and `crypto.randomUUID`. Use HTTPS in deployed applications; localhost is suitable for development. Voice depends on the browser's `SpeechRecognition` or `webkitSpeechRecognition` implementation and microphone permission. It is not supported uniformly across browsers and may use a remote speech service; see the [MDN compatibility notes](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition). Typing and command help remain available without voice.
|
|
76
|
+
|
|
77
|
+
Test the actual browsers and devices you intend to support. CI runs browser regressions in Chromium, Firefox, and WebKit, including real Turbo navigation. Simulated speech events do not certify microphone hardware, transcription quality, mobile behavior, or offline recognition. React hooks are provided, but a complete React application needs its own end-to-end check.
|
|
78
|
+
|
|
79
|
+
The React example assumes the script has loaded and the custom element has upgraded before calling `window.VoiceControl`. Use `customElements.whenDefined("voice-control-widget")` when script loading can race your component mount. Keep one widget per document and place it outside route content. Native input events support conventional controlled inputs; custom controls should use named application events.
|
data/examples/react.jsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
// Load /voice_control/widget.js and Rails CSRF meta tags in your HTML shell first.
|
|
4
|
+
export function VoiceControlCommands({ navigate, userId, pathname }) {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
let mounted = true;
|
|
7
|
+
customElements.whenDefined("voice-control-widget").then(() => {
|
|
8
|
+
if (!mounted) return;
|
|
9
|
+
window.VoiceControl.configure({ navigate });
|
|
10
|
+
window.VoiceControl.setContext(() => ({ area: "admin", user_id: userId }));
|
|
11
|
+
window.VoiceControl.refresh();
|
|
12
|
+
});
|
|
13
|
+
return () => {
|
|
14
|
+
mounted = false;
|
|
15
|
+
};
|
|
16
|
+
}, [navigate, userId, pathname]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<voice-control-widget
|
|
20
|
+
id="voice-control-widget"
|
|
21
|
+
data-endpoint="/voice_control"
|
|
22
|
+
data-shortcut="mod+shift+u"
|
|
23
|
+
data-idle-timeout="120000"
|
|
24
|
+
data-browser-actions="true"
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module VoiceControl
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def create_initializer
|
|
9
|
+
template "voice_control.rb", "config/initializers/voice_control.rb"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mount_engine
|
|
13
|
+
route 'mount VoiceControl::Engine => "/voice_control", as: :voice_control'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def explain_widget
|
|
17
|
+
say 'Add <%= voice_control_widget %> before </body> in your layout. Configure authorization before using it.'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|