Commit 1c7fdbb8 authored by Professor Chaos's avatar Professor Chaos

add like/dislike classifier

parent a6e1e452
# IltapetoApi.ArticlesApi
All URIs are relative to *https://iltapeto-221419.appspot.com/iltapeto/1.0.0*
Method | HTTP request | Description
------------- | ------------- | -------------
[**updateArticles**](ArticlesApi.md#updateArticles) | **GET** /articles/update | Update application article data
<a name="updateArticles"></a>
# **updateArticles**
> updateArticles(opts)
Update application article data
Updates application article dataset
### Example
```javascript
var IltapetoApi = require('iltapeto_api');
var apiInstance = new IltapetoApi.ArticlesApi();
var opts = {
'limit': 10 // Number | Max number of articles to return
};
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully.');
}
};
apiInstance.updateArticles(opts, callback);
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**limit** | **Number**| Max number of articles to return | [optional] [default to 10]
### Return type
null (empty response body)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
# IltapetoApi.PreferenceResult
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**articles** | [**[PreferredItem]**](PreferredItem.md) | array of articles sorted by estimated user preference |
# IltapetoApi.PreferredItem
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**articleId** | **String** | Article id |
**headline** | **String** | Article title |
**lead** | **String** | Article subtitle | [optional]
**image** | **String** | Article feature image | [optional]
**preferenceValue** | **Number** | Computed value for user preference, max &#x3D; 1.0 (highly likely user prefers article), mid &#x3D; 0.0 (preference unknown), min &#x3D; -1.0 (highly unlikely user preffers article) | [optional]
# IltapetoApi.UserId
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**userId** | **Number** | |
# IltapetoApi.UserPreferenceRecord
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**userId** | **Number** | Current user id |
**articleId** | **String** | Article Id |
**likeValue** | **Boolean** | set &#x60;true&#x60; to like; set &#x60;false&#x60; to dislike |
# IltapetoApi.UsersApi
All URIs are relative to *https://iltapeto-221419.appspot.com/iltapeto/1.0.0*
Method | HTTP request | Description
------------- | ------------- | -------------
[**addArticlePreference**](UsersApi.md#addArticlePreference) | **POST** /user/preference | Set article preference as liked or disliked
[**getNewUserId**](UsersApi.md#getNewUserId) | **GET** /user/create | Create new application user
[**getRecommendedArticles**](UsersApi.md#getRecommendedArticles) | **GET** /user/articles | Get list of latest articles sorted by user preference value
[**retrainUserModel**](UsersApi.md#retrainUserModel) | **POST** /user/retrain | Retrain user model
<a name="addArticlePreference"></a>
# **addArticlePreference**
> addArticlePreference(opts)
Set article preference as liked or disliked
Set article as liked or disliked by specific user
### Example
```javascript
var IltapetoApi = require('iltapeto_api');
var apiInstance = new IltapetoApi.UsersApi();
var opts = {
'articlePreferenceItem': new IltapetoApi.UserPreferenceRecord() // UserPreferenceRecord | Article being rated
};
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully.');
}
};
apiInstance.addArticlePreference(opts, callback);
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**articlePreferenceItem** | [**UserPreferenceRecord**](UserPreferenceRecord.md)| Article being rated | [optional]
### Return type
null (empty response body)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
<a name="getNewUserId"></a>
# **getNewUserId**
> UserId getNewUserId()
Create new application user
Get id for new user
### Example
```javascript
var IltapetoApi = require('iltapeto_api');
var apiInstance = new IltapetoApi.UsersApi();
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully. Returned data: ' + data);
}
};
apiInstance.getNewUserId(callback);
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**UserId**](UserId.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
<a name="getRecommendedArticles"></a>
# **getRecommendedArticles**
> PreferenceResult getRecommendedArticles(userId, opts)
Get list of latest articles sorted by user preference value
Get list of recommended articles for specific user
### Example
```javascript
var IltapetoApi = require('iltapeto_api');
var apiInstance = new IltapetoApi.UsersApi();
var userId = 789; // Number | Id of current user
var opts = {
'limit': 10 // Number | Max number of articles to return
};
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully. Returned data: ' + data);
}
};
apiInstance.getRecommendedArticles(userId, opts, callback);
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**userId** | **Number**| Id of current user |
**limit** | **Number**| Max number of articles to return | [optional] [default to 10]
### Return type
[**PreferenceResult**](PreferenceResult.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
<a name="retrainUserModel"></a>
# **retrainUserModel**
> retrainUserModel(userId)
Retrain user model
Request retraining user classification model
### Example
```javascript
var IltapetoApi = require('iltapeto_api');
var apiInstance = new IltapetoApi.UsersApi();
var userId = 789; // Number | Id of current user
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully.');
}
};
apiInstance.retrainUserModel(userId, callback);
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**userId** | **Number**| Id of current user |
### Return type
null (empty response body)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-81731558-13', 'auto');
ga('set', 'checkProtocolTask', null);
ga('set', 'transport', 'beacon');
......@@ -18,8 +18,8 @@
"release": "git add . && git commit && standard-version"
},
"dependencies": {
"bayes-classifier": "^0.0.5",
"dexie": "^2.0.4"
"material-design-lite": "^1.3.0",
"superagent": "3.5.2"
},
"devDependencies": {
"cws-publish": "^1.0.13",
......@@ -42,12 +42,22 @@
"name": "background",
"src": "./src/background.js"
},
{
"name": "popup",
"src": "./src/content/popup.js"
},
{
"name": "content_end",
"src": "./src/onend.js"
}
],
"scss_bundles": [
{
"src": [
"./src/**/popup.scss"
],
"name": "popup.css"
},
{
"name": "preload.css",
"src": "./src/**/preload.scss"
......@@ -60,6 +70,10 @@
]
}
],
"copyAsIs": [
"./node_modules/material-design-lite/material.min.js",
"./assets/ga.js"
],
"locales_list": [
"fi"
]
......
import {ContextMenu} from 'pm-components';
import Dexie from 'dexie';
import Analyzer from './content/analyzer';
import Db from './content/db';
import Launcher from './content/Launcher';
import Storage from './content/storage';
import Api from './content/Api';
(() => new ContextMenu())();
(() => new Analyzer(new Db(Dexie)))();
(() => new Launcher())();
class DeclarativeContent {
constructor(PageStateMatcher, Actions) {
window.chrome.runtime.onInstalled.addListener(
() => {
window.chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
window.chrome.declarativeContent.onPageChanged.addRules([{
conditions: [
new window.chrome.declarativeContent
.PageStateMatcher(PageStateMatcher)
],
actions: Actions
}]);
});
});
}
}
Storage.get([Storage.keys.userId, Storage.keys.lastTrain], data => {
const userId = parseInt(data[Storage.keys.userId], 0);
const lastTrain = parseInt(data[Storage.keys.lastTrain], 0);
const currentTime = new Date().getTime();
const retrainLimit = 1; // 24 * 60 * 60 * 1000
(() => new DeclarativeContent({pageUrl: {hostEquals: 'www.iltalehti.fi'}},
[new window.chrome.declarativeContent.ShowPageAction()]))();
if (userId && (!lastTrain || lastTrain < currentTime - retrainLimit)) {
console.log('retraining model....');
Api.retrain(userId);
Storage.save(Storage.keys.lastTrain, currentTime);
}
});
const BASEPATH = 'https://iltapeto-221419.appspot.com/iltapeto/1.0.0/';
const ENDPOINTS = {
CREATEUSER: 'user/create',
SETPREFERENCE: 'user/preference',
RETRAIN: 'user/retrain?userId={USERID}',
ARTICLES: 'user/articles?userId={USERID}&limit={LIMIT}'
};
class Api {
static __uri(endpoint, userId, limit) {
return (BASEPATH + endpoint)
.replace('{USERID}', userId)
.replace('{LIMIT}', limit);
}
static newUser() {
return new Promise(resolve => {
Api.http('GET', Api.__uri(ENDPOINTS.CREATEUSER), null, (resp) => {
resolve(Api.tryParseJson(resp).userId);
});
});
}
static getArticles(userId, limit) {
return new Promise(resolve => {
Api.http('GET', Api.__uri(ENDPOINTS.ARTICLES, userId, limit), null, (resp) => {
resolve(Api.tryParseJson(resp).articles);
});
});
}
static setPreference(model) {
return new Promise(resolve => {
Api.http('POST', Api.__uri(ENDPOINTS.SETPREFERENCE), model, (resp, status) => {
console.log(status);
resolve(status === 200 || status === 201);
});
});
}
static retrain(userId) {
return new Promise(resolve => {
Api.http('POST', Api.__uri(ENDPOINTS.RETRAIN, userId), null, (resp, status) => {
console.log(status);
resolve(status === 200 || status === 201);
});
});
}
/**
* @description Make POST/GET request
* @param {String} method - enum: { POST, GET }
* @param {String} url - endpoint address
* @param {String} data - encoded data to post (if any)
* @param {function} callback - response callback
*/
static http(method, url, data, callback) {
let xhr = new window.XMLHttpRequest();
if (data) data = JSON.stringify(data);
xhr.open(method, url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onerror = (err) => callback(err);
xhr.onload = () => callback(xhr.response, xhr.status);
xhr.send(data);
};
/**
* @description Try parse string to JSON
* @param {String} text - text to parse
* @returns {{}} - Object; empty object if parse fails
*/
static tryParseJson(text) {
try {
return JSON.parse(text);
} catch (e) {
return {};
}
}
}
export default Api;
/**
* @class
* @classdesc Launch drawing board when browserAction is clicked
*/
class Launcher {
constructor() {
window.chrome.browserAction.onClicked.addListener(Launcher.launchBoard);
}
/**
* @description This function launches the drawing board.
*/
static launchBoard() {
let popup = window.chrome.extension.getURL('popup.html');
window.chrome.tabs.create({url: popup, active: true});
}
}
export default Launcher;
let BayesClassifier = require('bayes-classifier');
let classifier = new BayesClassifier();
class Analyzer {
constructor(db) {
this.db = db;
window.chrome.runtime.onMessage.addListener((r, s, c) => this.messageListener(r, s, c));
this.train();
}
messageListener(request, sender, sendResponse) {
if (request.setLike) {
const {url, like, text} = request.setLike;
this.db.setValue(url, like, text);
}
if (request.getLike) {
const text = request.getLike;
if (!text) return false;
let resp = {
classify: classifier.classify(text),
classifications: classifier.getClassifications(text)
};
sendResponse(resp);
}
return true;
}
train() {
this.db.likes().then(likes => {
this.db.dislikes().then(dislikes => {
const llikes = likes.map(l => {
return l.text;
});
const dlikes = dislikes.map(l => {
return l.text;
});
// console.log('likes ' + JSON.stringify(llikes), 'dislikes ' + JSON.stringify(dlikes));
classifier.addDocuments(llikes, 'positive');
classifier.addDocuments(dlikes, 'negative');
classifier.train();
});
});
}
}
export default Analyzer;
export default class DB {
constructor(Dexie) {
this.db = new Dexie('db');
this.db.version(1).stores({
likes: 'id,like,text'
});
}
likes() {
const {db} = this;
return new Promise(function (resolve, reject) {
db.likes.where('like').equals(1).toArray().then(resolve);
});
};
dislikes() {
const {db} = this;
return new Promise(function (resolve, reject) {
db.likes.where('like').equals(0).toArray().then(resolve);
});
};
static normalize(text) {
return !text || text.replace(/\n/, ' ');
}
setValue(url, likeValue, text) {
this.db.likes.put({id: url, like: likeValue ? 1 : 0, text: DB.normalize(text)});
};
}
// class DeclarativeContent {
// constructor(PageStateMatcher, Actions) {
// window.chrome.runtime.onInstalled.addListener(
// () => {
// window.chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
// window.chrome.declarativeContent.onPageChanged.addRules([{
// conditions: [
// new window.chrome.declarativeContent
// .PageStateMatcher(PageStateMatcher)
// ],
// actions: Actions
// }]);
// });
// });
// }
// }
//
// (() => new DeclarativeContent({pageUrl: {hostEquals: 'www.iltalehti.fi'}},
// [new window.chrome.declarativeContent.ShowPageAction()]))();
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.red-amber.min.css"/>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700|Material+Icons" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="./css/popup.css">
<script src='ga.js'></script>
</head>
<body class="app">
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
<!-- title bar -->
<header class="mdl-layout__header titlebar">
<div class="mdl-layout__header-row">
<span class="mdl-layout-title" data-text="popup_header">Uutisia Juuri Sinulle</span>
<div class="mdl-layout-spacer"></div>
<nav class="mdl-navigation">
<button id="close" class="mdl-button mdl-js-button mdl-js-ripple-effect">
Iltalehti
</button>
</nav>
</div>
</header>
<!-- progress -->
<div class="mdl-progress mdl-js-progress mdl-progress__indeterminate" id="progress-bar"></div>
<main class="mdl-layout__content">
<div class="page-content">
<div class="intro">
Nämä uusimmat artikkelit on listattu tykkäysjärjestyksessä. Valitse `Tykkää` tai `En Tykkää`
opettaaksesi tekoälyä tunnistamaan tarkemmin millaisista artikkeleista tykkäät.
</div>
<!-- articles -->
<div id="articles">
<div class="card-wide mdl-card mdl-shadow--2dp">
<div class="mdl-card__media"></div>
<div class="mdl-card__title">
<h2 class="mdl-card__title-text"></h2>
</div>
<div class="mdl-card__supporting-text"></div>
<div class="rate">
<button class="like mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect">
Tykkään
</button>
<button class="dislike mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect">
En tykkää
</button>
</div>
</div>
</div>
</div>
<div class="read-more">
<a href="http://www.iltalehti.fi">
<button class="mdl-button mdl-button--colored mdl-js-button
mdl-js-ripple-effect mdl-button--raised mdl-button--colored">
Lisää Uutisia →
</button>
</a>
</div>
</main>
<script src="material.min.js"></script>
<script src="popup.js"></script>
</div>
</body>
</html>
import Storage from './storage';
import Api from './Api';
let articleTemplate, articleList, userId;
class Popup {
constructor() {
Popup.bindActions();
articleList = document.getElementById('articles');
articleTemplate = articleList.children[0].outerHTML;
articleList.innerHTML = '';
Popup.toggleProgress(true);
Storage.get([Storage.keys.userId], data => {
userId = parseInt(data[Storage.keys.userId], 0);
if (!userId) {
Api.newUser().then(uid => {
userId = parseInt(uid, 0);
Storage.save(Storage.keys.userId, userId);
Popup.getArticles();
});
} else {
Popup.getArticles();
}
});
}
static getArticles() {
Api.getArticles(userId, 51).then(Popup.displayArticleList);
}
/**
* @description Bind click actions
*/
static bindActions() {
/** @ignore */
document.getElementById('close').onclick = () => {
window.close();