view hip.js @ 4:4ea09b7ce820

Added verbs and nouns and up/down arrows and space-to-autocomplete and suggestion list and escape-to-clear.
author jonathandicarlo@jonathan-dicarlos-macbook-pro.local
date Tue, 13 May 2008 13:04:25 -0700
parents a31bdc4de955
children a049cb93db46
line wrap: on
line source


function NounType( name, expectedWords ) {
    this._init( name, expectedWords );
  }
NounType.prototype = {
 _init: function( name, expectedWords ) {
    this._name = name;
    this._expectedWords = expectedWords; // an array
  },

 match: function( fragment ) {
    var suggs = this.suggest( fragment );
    // klugy!
    if ( suggs.length > 0 ) {
      return true;
    }
    return false;
  },
 
 suggest: function( fragment ) {
    // returns (ordered) array of suggestions
    var suggestions = [];
    for ( var x in this._expectedWords ) {
      word = this._expectedWords[x];
      if ( word.indexOf( fragment ) > -1 ) {
	suggestions.push( word );
	// TODO sort these in order of goodness
	// todo if fragment is multiple words, search for each of them
	// separately within the expected word.
      }
    }
    return suggestions;
  }
};

// for example....
var city = new NounType( "city", [ "new york", "los angeles", "mexico city", "sao paulo", "rio de janeiro", "buenos aires", "london", "paris", "moscow", "cairo", "lagos", "tehran", "karachi", "mumbai", "delhi", "kolkata", "jakarta", "manila", "bejing", "singapore", "shanghai", "hong kong", "seoul", "tokyo", "osaka" ] );

var language = new NounType( "language", [ "english", "chinese", "hindi", "japanese", "klingon", "esperanto", "sanskrit", "pig latin", "tagalog", "portugese" ] );

var tab = new NounType( "tab", [ "gmail", "mozilla developer connection", "xulplanet", "evilbrainjono.net", "google calendar", "humanized enso forum" ] );

var person = new NounType( "person", ["atul@mozilla.com", "aza@mozilla.com", "thunder@mozilla.com", "chris@mozilla.com", "myk@mozilla.com" ] );

var anyWord = {
  // a singleton object which can be used in place of a NounType.
 match: function( fragment ) {
    return true;
  },
 suggest: function( fragment ) {
    return [ fragment ];
  }
};

function ParsedSentence( rawText, verb, DO, DOType, modifiers ) {
  this._init( rawText, verb, DO, DOType, modifiers );
}
ParsedSentence.prototype = {
 _init: function( rawText, verb, DO, DOType, modifiers ) {

    // needs state to represent what's locked in so far.
  },

 lockInLatestWord: function( selectedCompletion ) {
    // 

  },

 getCompletions: function() {
  },

 getDescription: function() {
    // returns a string describing what the sentence will do if executed
  },

};


function Verb( name, DOLabel, DOType, modifiers ) {
  this._init( name, DOLabel, DOType, modifiers );
}
Verb.prototype = {
 _init: function( name, DOLabel, DOType, modifiers ) {
    this._name = name;
    this._DOLabel = DOLabel;
    this._DOType = DOType; // must be a NounType.
    this._modifiers = modifiers;
    // modifiers should be a dictionary
    // keys are prepositions
    // values are NounTypes.
    // example:  { "from" : City, "to" : City, "on" : Day }
  },

 getCompletions: function( words ) {
    /* words is an array of words that were space-separated.
     already determined that words[0] matches this verb;
       Everything after that is either:
       1. my direct object
       2. a preposition
       3. a noun following a preposition. 
    */
    var predicate = "";
    var completions = [];
    var suggestions = [];
    var x;
    var y;

    // TODO pull out things that might be modifiers, try using
    // the remainder as the direct object.

    for ( x = 1; x < words.length; x++ ) {
      // a horrible way of reassembling input minus verb
      predicate = predicate + words[x];
      if ( x < words.length - 1 ) {
	predicate = predicate + " ";
      }
    }

    // first approx: just try out each word as direct object
    if ( this._DOType == null ) {
      // No direct object accepted!!
      if ( predicate.length == 0 ) {
	return [this._name]; 
      } else {
	return [];
      }
    } else if ( this._DOType.match( predicate ) ){
      // direct object accepts whole input at once?
      suggestions = this._DOType.suggest( predicate );
      for( y in suggestions ) {
	completions.push( this._name + " " + suggestions[y] );
      }
      return completions;
    } else {
      // try each word as the direct object
      for ( x=1; x<words.length; x++ ) {
	if ( this._DOType.match( words[x] ) ){
	  suggestions = this._DOType.suggest( predicate );
	  for( y in suggestions ) {
	    completions.push( this._name + " " + suggestions[y] );
	  }
	}
      }
      return completions;
    }


  },

 match: function( sentence ) {
    // returns a float from 0 to 1 telling how good of a match the input
    // is to this verb.
    if ( this._name.indexOf( sentence ) == 0 ) {
      // verb starts with the sentence, i.e. you may be typing this
      // verb but haven't typed the full thing yet.
      return sentence.length / this._name.length;
    } else {
      return 0.0;
    }
  }
};

var fly = new Verb( "fly", null, null, { "from": city, "to": city } );
var define = new Verb( "define", "word", anyWord, {} );
var google = new Verb( "google", "word", anyWord, {} );
var go = new Verb( "go", "tab", tab, {} );
var close = new Verb( "close", null, null, {} );
var translate = new Verb( "translate", "text", anyWord, { "from": language, "to": language } );
var nuke = new Verb( "nuke", "city", city, {} );
var open = new Verb( "open", "url", anyWord, {} );
var email = new Verb( "email", "text", anyWord, { "to": person, "subject": anyWord } );
var encrypt = new Verb( "encrypt", "text", anyWord, { "for": person } );
var wiki = new Verb( "wikipedia", "word", anyWord, { "language": language } );

var verbs = [ fly, define, google, go, close, open, translate, email, nuke, encrypt, wiki ];

/* Initial state: no verb determined.
   After each keypress, update verb suggestion list.
   After first spacebar: lock in top verb from suggestion list.  Create
   parsedSentence object based on verb.  change state to sentence completion.
   Non-keystroke spaces after that: 
   spacebar sends the lock-in-last-word message to lockedInSentence.

   todo: add responder for arrow keys to hilight suggestions
   and escape to clear text.
*/


function QuerySource() {
    this._init( );
  }
QuerySource.prototype = {
 _init: function( ) {
    this._lockedInSentence = null;
    this._hilitedSuggestion = 0;
    this._suggestionList = [];
  },
 
 updateSuggestionList: function( query ) {
    this._suggestionList = [];
    var completions = [];
    var words = query.split( " " );
    for ( var x in verbs ) {
      var verb = verbs[x];
      if ( verb.match( words[0] ) ) {
	completions = verb.getCompletions( words );
	this._suggestionList = this._suggestionList.concat( completions );
      }
    } // TODO sort in order of match quality
    this._hilitedSuggestion = 0;
  },

 getSuggestions : function() {
    return this._suggestionList;
  },

 indicationDown: function( ) {
    this._hilitedSuggestion ++;
    if ( this._hilitedSuggestion > this._suggestionList.length ) {
      this._hilitedSuggestion = 0;
      }
  },

 indicationUp: function() {
    this._hilitedSuggestion --;
    if ( this._hilitedSuggestion < 0 ) {
      this._hilitedSuggestion = this._suggestionList.length;
      }
  },

 getHilitedSuggestion: function() {
    return this._hilitedSuggestion - 1; // because 0 means no hilite
    // and the suggestion list starts at 1... fencepost!
  },

 autocomplete: function( query ) {
    var hilited = this.getHilitedSuggestion();
    if ( hilited > -1 ) {
      var newText = this._suggestionList[ hilited ] + " ";
    } else {
      newText = query;
    }
    return newText;
  },

 clear: function() {
    this._suggestionList = [];
    this._hilitedSuggestion = 0;
    lockedInSentence = null;
  }
};

var gQs = new QuerySource();

function makeSuggestionHtml( tagName, list, hilitedNumber ) {
  var result = "";
  var openingTag = "";
  var closingTag = "</" + tagName + ">";

  for (var i = 0; i < list.length; i++) {
    if ( i == hilitedNumber ) {
      openingTag = "<" + tagName + " class=\"hilited\">";
    } else {
      openingTag = "<" + tagName + ">";
    }
    result += (openingTag + list[i] + closingTag );
  }
  return result;
}

function updateDisplay( ) {
   var suggestions = gQs.getSuggestions();
   var hilitedSuggestion = gQs.getHilitedSuggestion();
   var ac = $("#autocomplete-popup");
   ac.html( makeSuggestionHtml( "div", suggestions, hilitedSuggestion ) );
   ac.show();    
}

function searchBoxQuery( event ) {
  // TODO: if the event is an 'esc' key, clear the input field.
  // If the event is an 'up arrow' or 'down arrow' key, change the
  // indication.
  
  // key is event.which
  // esc is 27
  // up arrow is 38
  // down arrow is 40
  // enter is 13
  // space is 32
  switch( event.which ) {
  case 27: //esc
    event.target.value = "";
    gQs.clear();
    break;
  case 38: // up arrow
    gQs.indicationUp();
    break;
  case 40: // down arrow
    gQs.indicationDown();
    break;
  case 13: // enter
    gQs.execute();
    break;
  case 32: // spacebar
    event.target.value = gQs.autocomplete( event.target.value );
    break;
  default:
    gQs.updateSuggestionList( event.target.value );
    break;
    // todo: delete key "unlocks" if you delete past a space?
  }

  updateDisplay();

}

$(document).ready( function() {
    $("#search-box").focus();
    $("#search-box").keyup( searchBoxQuery );
    $("#autocomplete-popup").css(
        "width",
        $("#search-box").css("width")
    );
});