aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/DictationManager.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util/DictationManager.ts')
-rw-r--r--src/client/util/DictationManager.ts158
1 files changed, 156 insertions, 2 deletions
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index b58bdb6c7..b0866a826 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -1,15 +1,58 @@
+import { SelectionManager } from "./SelectionManager";
+import { DocumentView } from "../views/nodes/DocumentView";
+import { undoBatch } from "./UndoManager";
+import * as converter from "words-to-numbers";
+import { Doc } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
+import { Docs, DocumentType } from "../documents/Documents";
+import { CollectionViewType } from "../views/collections/CollectionBaseView";
+import { Cast, CastCtor } from "../../new_fields/Types";
+import { listSpec } from "../../new_fields/Schema";
+import { AudioField, ImageField } from "../../new_fields/URLField";
+import { HistogramField } from "../northstar/dash-fields/HistogramField";
+
namespace CORE {
export interface IWindow extends Window {
webkitSpeechRecognition: any;
}
}
+const ConstructorMap = new Map<DocumentType, CastCtor>([
+ [DocumentType.COL, listSpec(Doc)],
+ [DocumentType.AUDIO, AudioField],
+ [DocumentType.IMG, ImageField],
+ [DocumentType.HIST, HistogramField],
+ [DocumentType.IMPORT, listSpec(Doc)]
+]);
+
+const tryCast = (view: DocumentView, type: DocumentType) => {
+ let ctor = ConstructorMap.get(type);
+ if (!ctor) {
+ return false;
+ }
+ return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined;
+};
+
+const validate = (target: DocumentView, types: DocumentType[]) => {
+ for (let type of types) {
+ if (tryCast(target, type)) {
+ return true;
+ }
+ }
+ return false;
+};
+
const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
+export type IndependentAction = (target: DocumentView) => any | Promise<any>;
+export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise<any>;
+export type RegistrationEntry = { action: IndependentAction, restrictTo?: DocumentType[] };
+export type ActionPredicate = (target: DocumentView) => boolean;
+export type RegexEntry = { expression: RegExp, action: DependentAction, restrictTo?: DocumentType[] };
export default class DictationManager {
public static Instance = new DictationManager();
- private isListening = false;
private recognizer: any;
+ private isListening = false;
constructor() {
this.recognizer = new webkitSpeechRecognition();
@@ -19,8 +62,12 @@ export default class DictationManager {
finish = (handler: any, data: any) => {
handler(data);
- this.isListening = false;
+ this.stop();
+ }
+
+ stop = () => {
this.recognizer.stop();
+ this.isListening = false;
}
listen = () => {
@@ -33,7 +80,114 @@ export default class DictationManager {
this.recognizer.onresult = (e: any) => this.finish(resolve, e.results[0][0].transcript);
this.recognizer.onerror = (e: any) => this.finish(reject, e);
});
+ }
+
+ public interpretNumber = (number: string) => {
+ let initial = parseInt(number);
+ if (!isNaN(initial)) {
+ return initial;
+ }
+ let converted = converter.wordsToNumbers(number, { fuzzy: true });
+ if (converted === null) {
+ return NaN;
+ }
+ return typeof converted === "string" ? parseInt(converted) : converted;
+ }
+ @undoBatch
+ public execute = async (phrase: string) => {
+ let targets = SelectionManager.SelectedDocuments();
+ if (!targets || !targets.length) {
+ return;
+ }
+
+ let entry = RegisteredCommands.Independent.get(phrase);
+ if (entry) {
+ let success = false;
+ for (let target of targets) {
+ if (!entry.restrictTo || validate(target, entry.restrictTo)) {
+ await entry.action(target);
+ success = true;
+ }
+ }
+ return success;
+ }
+
+ for (let entry of RegisteredCommands.Dependent) {
+ let regex = entry.expression;
+ let matches = regex.exec(phrase);
+ regex.lastIndex = 0;
+ if (matches !== null) {
+ let success = false;
+ for (let target of targets) {
+ if (!entry.restrictTo || validate(target, entry.restrictTo)) {
+ await entry.action(target, matches);
+ success = true;
+ }
+ }
+ return success;
+ }
+ }
+
+ return false;
}
+}
+
+export namespace RegisteredCommands {
+
+ export const Independent = new Map<string, RegistrationEntry>([
+
+ ["clear", {
+ action: (target: DocumentView) => {
+ Doc.GetProto(target.props.Document).data = new List();
+ },
+ restrictTo: [DocumentType.COL]
+ }],
+
+ ["open fields", {
+ action: (target: DocumentView) => {
+ let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
+ target.props.addDocTab(kvp, target.dataDoc, "onRight");
+ }
+ }]
+
+ ]);
+
+ export const Dependent = new Array<RegexEntry>(
+
+ {
+ expression: /create (\w+) documents of type (image|nested collection)/g,
+ action: (target: DocumentView, matches: RegExpExecArray) => {
+ let count = DictationManager.Instance.interpretNumber(matches[1]);
+ let what = matches[2];
+ let dataDoc = Doc.GetProto(target.props.Document);
+ let fieldKey = "data";
+ for (let i = 0; i < count; i++) {
+ let created: Doc | undefined;
+ switch (what) {
+ case "image":
+ created = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg");
+ break;
+ case "nested collection":
+ created = Docs.Create.FreeformDocument([], {});
+ break;
+ }
+ created && Doc.AddDocToList(dataDoc, fieldKey, created);
+ }
+ },
+ restrictTo: [DocumentType.COL]
+ },
+
+ {
+ expression: /view as (freeform|stacking|masonry|schema|tree)/g,
+ action: (target: DocumentView, matches: RegExpExecArray) => {
+ let mode = CollectionViewType.ValueOf(matches[1]);
+ mode && (target.props.Document.viewType = mode);
+ },
+ restrictTo: [DocumentType.COL]
+ }
+
+ );
+
} \ No newline at end of file