@miso.ai/doggoganger 0.9.0-beta.8 → 0.9.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.
@@ -23,6 +23,7 @@ jobs:
23
23
  - run: git config --global user.name "${{ github.actor }}"
24
24
  - run: git config --global user.email "github-action-${{ github.actor }}@users.noreply.github.com"
25
25
  - run: npm run version ${{ steps.version.outputs.version }}
26
+ - run: npm run build
26
27
  - run: npm publish
27
28
  if: "!github.event.release.prerelease"
28
29
  env:
package/bin/data.js ADDED
@@ -0,0 +1,15 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, resolve } from 'path';
3
+ import { writeFileSync, readFileSync } from 'fs';
4
+ import yaml from 'js-yaml';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ const WORDS_YAML = resolve(__dirname, '../data/words.yaml');
9
+ const WORDS_JS = resolve(__dirname, '../src/data/words.js');
10
+
11
+ const words = yaml.load(readFileSync(WORDS_YAML, 'utf8'));
12
+ writeFileSync(WORDS_JS, `
13
+ // genereated by bin/data.js
14
+ export default ${JSON.stringify(words, null, 2)};
15
+ `);
package/cli/index.js CHANGED
@@ -10,6 +10,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  const SRC_DIR = join(__dirname, '../src');
11
11
 
12
12
  let { watch, ...argv } = yargs(hideBin(process.argv))
13
+ .option('verbose', {
14
+ alias: 'v',
15
+ describe: 'Be verbose',
16
+ type: 'boolean',
17
+ })
13
18
  .option('port', {
14
19
  alias: 'p',
15
20
  describe: 'Port of mock API endpoint',
@@ -21,10 +26,6 @@ let { watch, ...argv } = yargs(hideBin(process.argv))
21
26
  describe: 'Watch files for changes',
22
27
  type: 'boolean',
23
28
  })
24
- .option('answer-format', {
25
- describe: 'Answer field format in answers API response',
26
- type: 'string',
27
- })
28
29
  .option('serve', {
29
30
  alias: 's',
30
31
  describe: 'Serve static files as well',
@@ -32,8 +33,8 @@ let { watch, ...argv } = yargs(hideBin(process.argv))
32
33
  })
33
34
  .argv;
34
35
 
35
- const { port, serve, ['answer-format']: answerFormat } = argv;
36
- argv = { port, serve, answerFormat };
36
+ const { verbose, port, serve } = argv;
37
+ argv = { verbose, port, serve };
37
38
 
38
39
  if (watch) {
39
40
  const exec = `node ${resolve(__dirname, 'server.js')} ${JSON.stringify(JSON.stringify(argv))}`;
@@ -0,0 +1 @@
1
+ !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).doggoganger=n()}(this,(function(){"use strict";function t(t,n){return null!=n&&n>t?t+Math.floor(Math.random()*(n-t)):t}function n(){return Math.random().toString(36).substring(2,10)}function e(t){return`https://picsum.photos/seed/${Math.floor(1e3*Math.random())}/${Array.isArray(t)?t.length>1?`${t[0]}/${t[1]}`:""+t[0]:""+t}`}function r(t,n){return void 0!==n?Math.ceil(t*n):t}function o(){return 2*Math.random()-1}var s=["lorem","ipsum","dolor","sit","amet","consectetur","adipiscing","elit","curabitur","vel","hendrerit","libero","eleifend","blandit","nunc","ornare","odio","ut","orci","gravida","imperdiet","nullam","purus","lacinia","a","pretium","quis","congue","praesent","sagittis","laoreet","auctor","mauris","non","velit","eros","dictum","proin","accumsan","sapien","nec","massa","volutpat","venenatis","sed","eu","molestie","lacus","quisque","porttitor","ligula","dui","mollis","tempus","at","magna","vestibulum","turpis","ac","diam","tincidunt","id","condimentum","enim","sodales","in","hac","habitasse","platea","dictumst","aenean","neque","fusce","augue","leo","eget","semper","mattis","tortor","scelerisque","nulla","interdum","tellus","malesuada","rhoncus","porta","sem","aliquet","et","nam","suspendisse","potenti","vivamus","luctus","fringilla","erat","donec","justo","vehicula","ultricies","varius","ante","primis","faucibus","ultrices","posuere","cubilia","curae","etiam","cursus","aliquam","quam","dapibus","nisl","feugiat","egestas","class","aptent","taciti","sociosqu","ad","litora","torquent","per","conubia","nostra","inceptos","himenaeos","phasellus","nibh","pulvinar","vitae","urna","iaculis","lobortis","nisi","viverra","arcu","morbi","pellentesque","metus","commodo","ut","facilisis","felis","tristique","ullamcorper","placerat","aenean","convallis","sollicitudin","integer","rutrum","duis","est","etiam","bibendum","donec","pharetra","vulputate","maecenas","mi","fermentum","consequat","suscipit","aliquam","habitant","senectus","netus","fames","quisque","euismod","curabitur","lectus","elementum","tempor","risus","cras"];function i({decorates:n=[],output:e="string",size:r,min:o,max:i,...a}={}){let c=function(n=[5,10]){const e="number"==typeof n?n:t(...n);return function*(t){let n=0;for(let r of t){if(n++>=e)break;yield r}}}(r||[o,i])(function*({words:t=s,fixedStarts:n=0}={}){const e=t.length;for(let r=0;;r++)yield t[n>r?r:Math.floor(Math.random()*e)]}(a));for(const t of n)c=u(t)(c);return u(e)(c)}const a={string:function({separator:t=" "}={}){return n=>[...n].join(t)},array:function(){return t=>[...t]},title:function({}={}){return function*(t){for(let n of t)yield c(n)}},description:function({wordsPerSentence:t={avg:24,std:5,min:1},punctuation:n="."}={}){return function*(e){let r,o=0;for(let s of e)r&&(yield r),r=s,0===o&&(r=c(r),o=l(t)),0==--o&&(r+=n);r&&(r.endsWith(n)||(r+=n),yield r)}},multiline:function({wordsPerLine:t={avg:10,std:3,min:1}}={}){return n=>{let e=l(t),r="";for(let o of n)r&&(0==e--?(r+="\n",e=l(t)):r+=" "),r+=o;return r}}};function u(t){switch(typeof t){case"string":return a[t]();case"function":return t;case"object":if(Array.isArray(t)){const[n,e={}]=t;return a[n](e)}}throw Error("Unrecognized decorator/output form: "+t)}function c(t){return t[0].toUpperCase()+t.substring(1)}function l(t){if("number"==typeof t)return Math.round(n);let{avg:n,std:e,min:r,max:s}=t;void 0===e&&(e=n/4);let i=(o()+o()+o())*e+n;return void 0!==r&&(i=Math.max(r,i)),void 0!==s&&(i=Math.min(s,i)),Math.round(i)}const f="// module\nimport { a, b } from './module';\nimport * as module from './module';\nimport module from './module';\nexport default module;\nexport const a = 0;\nexport * from './module';\nexport * as module from './module';\n\n// variables\nlet a = 10;\nconst b = 20;\n\n// function declaration\nfunction sum(x, y) {\n return x + y;\n}\n\n// generator function\nfunction* iterator() {\n yield 0;\n yield 1;\n}\n\n// arrow function\nconst multiply = (x, y) => x * y;\n\n// class\nclass Person {\n constructor(name, age) {\n this.name = name;\n this.age = age;\n }\n\n greet() {\n console.log('Hello, my name is Miso.');\n }\n}\n\n// primitive\nconst str = 'Hello, world!';\nconst num = 10.99;\n\n// object\nconst person = new Person('John', 30);\nperson.greet();\n\n// object literal\nconst object = {\n name: 'John',\n [x]: 10,\n ...props,\n};\n\n// array literal\nconst arr = [1, 2, 3, 4, 5, ...props];\n\n// regexp literal\nconst regexp = /\\w+/g;\n\n// operators\nconst sum = a + b;\nconst product = a * b;\nconst negation = -a;\nconst max = a > b ? a : b;\n\n// flow control\nlet i = 9;\nfor (const n of arr) {\n if (n > i) {\n console.log(n);\n }\n i++;\n}\n\n// async/await\n(async () => {\n const result = await asyncFunction();\n})();\n\n// try/catch\ntry {\n} catch (e) {\n}\n\n// destructuring\nconst { name, age, ...rest } = person;\nconst [ x, y, ...rest ] = arr;\n\n// template literals\nconsole.log(`The sum of ${a} and ${b} is ${sum(a, b)}.`);";var d=Object.freeze({__proto__:null,javascript:function(){return f},js:function(){return f}});function p({features:n,blocks:r=[8,12],sampling:o=1}={}){let s=[];return[s,n]=function(t=[]){const n=new Set,e=[];for(const r of t)if(r.startsWith("lang-")){let t=r.slice(5);"javascript"===t&&(t="js"),n.add(t)}else e.push(r);return[[...n],e]}(n),function(t,n){const e=[];for(const r of t)1>n&&Math.random()>=n||e.push(r());return e}([()=>m({features:n}),()=>g({features:n}),...s.map((t=>()=>h({lang:t,features:n}))),...s.length?[]:[()=>h({features:n})],()=>g({features:n}),()=>function({columns:n=[2,4],rows:e=[2,8]}){n=t(...n),e=t(...e);const r=[...$({size:1},n-1),{size:[3,8]}],o=i({size:n,output:"array"}),s=r.map((()=>"---")),a=[o,s];for(let t=0;e-1>t;t++)a.push(r.map((({size:t})=>i({size:t}))));return a.map(x).join("\n")}({features:n}),()=>function({url:t,imageSize:n=[400,250],...r}={}){return t=t||e(n),`![${function({size:t=[1,3],content:n}={}){return n||i({size:t})}(r)}](${t})`}(),()=>g({features:n}),()=>"*-_".charAt(t(0,2)).repeat(t(3,6)),()=>m({features:n}),()=>g({features:n}),()=>w({features:n}),()=>g({features:n})],o).join("\n\n")}function m({level:n=[1,6],size:e=[1,8],content:r}){const o=r||i({size:e});return`${"#".repeat(t(...n))} ${o}`}function h({lang:n,content:e,size:r,fenceChar:o="`"}){return"random"===o&&(o="`~".charAt(t(0,1))),e=e||function({lang:t,size:n=[10,30]}){return t&&d[t]?d[t]():i({output:"multiline",size:n})}({lang:n,size:r}),`${o.repeat(3)}${n||""}\n${e}\n${o.repeat(3)}`}function g({size:t=[20,50]}){return i({size:t,decorates:["description",z()]})}const _=["ordered","bullet","task"];function w({features:n,type:e="random",count:r=[1,8],size:o=[5,15]}){r="number"==typeof r?r:t(...r);const s="random"===e?_[Math.floor(3*Math.random())]:e,i=[];for(;r>0;){const a=t(1,r);let u=g({features:n,size:o});a>1&&(u+="\n"+w({features:n,type:e,count:a-1,size:o})),i.push(M(s,u)),r-=a}return i.join("\n")}const y={"code-span":()=>["`","`"],"emphasis-1":()=>$(b(1),2),"emphasis-2":()=>$(b(2),2),"emphasis-3":()=>$(b(3),2),strikethrough:()=>["~","~"],link:({url:t="https://miso.ai"}={})=>["[",`](${t})`]};function b(n=[1,3]){return n="number"==typeof n?n:t(...n),"_*".charAt(t(0,1)).repeat(n)}const q=Object.keys(y),v=new Set(q);function z({features:n=q,size:e=[1,3],rest:r=[0,8]}={}){n=n.filter((t=>v.has(t)));const o=function(t){for(let n=t.length-1;n>0;n--){const e=Math.floor(Math.random()*(n+1));[t[n],t[e]]=[t[e],t[n]]}return t}([...n]);let s=o.length-1;const i=()=>"number"==typeof r?r:t(...r);return function*(r){let a,u,c=i();for(const l of r)if(u&&(yield u),0===c)if(a)u=`${l}${a}`,a=void 0,c=i();else{const[r,i]=y[0>s?n[t(0,n.length-1)]:o[s--]]();u=`${r}${l}`,a=i,c="number"==typeof e?e:t(...e)}else u=l,c--;u&&(yield void 0!==a?`${u}${a}`:u)}}function M(t,n){const[e,r]=n.split("\n",2),o=`${function(t,n="random"){switch(t){case"ordered":return"1.";case"bullet":return"-";case"task":return`- [${("random"===n?.5>Math.random():n)?"x":" "}]`;default:throw Error("unknown list item type: "+t)}}(t)} ${e}`;return r?o+"\n"+function(t,n){return n.split("\n").map((n=>" ".repeat(t)+n)).join("\n")}("ordered"===t?3:2,r):o}function x(t){return`| ${t.join(" | ")} |`}function $(t,n){const e=[];for(let r=0;n>r;r++)e.push("function"==typeof t?t():t);return e}function j({size:t=300}={}){return e(t)}function S({size:t=[1,3]}={}){return i({size:t,decorates:["title"],output:"array"})}function k({size:t=[1,4]}={}){return i({size:t,output:"array"})}function O({size:t=[2,6]}={}){return i({size:t,decorates:["title"]})}function A({size:t=[10,20],...n}={}){return i({size:t,decorates:[Object.keys(n).length?["description",n]:"description"]})}function T(){return Math.floor(1e4*Math.random())/100}function*C({rows:t,...n}={}){for(let e=0;t>e;e++)yield F({...n,index:e})}function F({}={}){const e=n(),r=function(n,e){const r=t(...e),o=[];for(let t=0;r>t;t++)o.push(n());return o}(T,[1,2]);return r.sort(),{product_id:e,authors:S(),categories:[],tags:k(),title:O(),description:A(),cover_image:j(),url:"/products/"+e,sale_price:r[0],original_price:r[r.length-1],rating:Math.floor(500*Math.random())/100+1,availability:Math.random()>.3?"IN_STOCK":"OUT_OF_STOCK"}}function*D({rows:t,...n}={}){for(let e=0;t>e;e++)yield L({...n,index:e})}function L({}={}){const t=n();return{product_id:t,authors:S(),categories:[],tags:k(),title:O({size:[4,10]}),snippet:A({size:[20,40]}),cover_image:j(),url:"/products/"+t}}function*P({rows:t,...n}={}){for(let e=0;t>e;e++)yield U({...n,index:e})}function U({}={}){return A({size:[4,8],punctuation:"?"})}function W({question:t,parent_question_id:n,timestamp:e=Date.now()},{answerFormat:o="markdown",answerSampling:s,answerLanguages:a=[]}={}){const u="10000000-1000-4000-8000-100000000000".replace(/[018]/g,(t=>(t^16*Math.random()>>t/4).toString(16))),c=function(t){const n=new Date(t).toISOString();return n.endsWith("Z")?n.slice(0,-1):n}(e),l=void 0!==s?Math.max(0,Math.min(1,s)):void 0,f=function({format:t,sampling:n,features:e}){return"markdown"===t?p({sampling:n,features:e}):i({min:r(50,n),max:r(50,n),decorates:["description"]})}({format:o,sampling:l,features:a.length?a.map((t=>"lang-"+t)):void 0}),d=[...D({rows:E(6,8,l)})];return{question:t,question_id:u,...n&&{parent_question_id:n},datetime:c,answer:f,sources:[...D({rows:E(4,6,l)})],related_resources:d,followup_questions:[...P({rows:E(3,6)})]}}function E(n,e,o){return t(r(n,o),r(e,o))}const H=[{name:"fetch",duration:1.5,text:"Checking the question and fetching results... "},{name:"verify",duration:1.5,text:"Verifying results... "},{name:"generate",duration:1.5,text:"Generating answer... "}];class I{constructor(t={}){this._options=t,this._answers=new Map}questions(t,n={}){return new J(t,{...this._options,...n})}}class J{constructor({question:t,parent_question_id:n},{answerFormat:e,answerSampling:r,answerLanguages:o,...s}={}){this._options=Object.freeze(s);const i=this.timestamp=Date.now();this._data=W({question:t,parent_question_id:n,timestamp:i},{answerFormat:e,answerSampling:r,answerLanguages:o})}get question_id(){return this._data.question_id}get(){const t=(Date.now()-this.timestamp)*(this._options.speedRate||1)/1e3,[n,e,r]=this._answer(t),o=this._sources(t,r),s=this._related_resources(t,r),i=this._followup_questions(t,r),{question_id:a,question:u,datetime:c,parent_question_id:l}=this._data;return{answer:e,answer_stage:n,datetime:c,finished:r,parent_question_id:l,question:u,question_id:a,sources:o,related_resources:s,followup_questions:i}}_answer(t){for(const n of H)if(0>(t-=n.duration))return[n.name,n.text,!1];const{answer:n}=this._data,e=Math.floor(100*t),r=e>=n.length;return["result",r?n:n.slice(0,e),r]}_sources(t,n){const{sources:e}=this._data;if(n)return e;const{length:r}=e;return e.slice(0,Math.floor(r*t/3))}_related_resources(t,n){const{related_resources:e}=this._data;if(n)return e;const{length:r}=e;return e.slice(0,Math.floor(r*t/3))}_followup_questions(t,n){const{followup_questions:e}=this._data;if(n||!e)return e;const{length:r}=e;return e.slice(0,Math.floor(r*t/3))}}class K{constructor(t){this._options=t}search({rows:t=5}){return{products:[...C({rows:t})]}}}class G{constructor(t){this._options=t}user_to_products({rows:t=5}){return{products:[...C({rows:t})]}}product_to_products({rows:t=5}){return{products:[...C({rows:t})]}}}class N{constructor(t){this._options=t}upload(t){return[]}}class R{constructor(t){this._options=t}upload(t){return[]}ids(){const t=[];for(let n=0;5e3>n;n++)t.push(V(n));return{ids:t}}}function V(t){return`${10>t?"p_000":100>t?"p_00":1e3>t?"p_0":"p_"}${t}`}class Z{constructor(t){this.ask=new I(t),this.search=new K(t),this.recommendation=new G(t),this.interactions=new N(t),this.products=new R(t)}}return{buildApi:(...t)=>new Z(...t)}}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miso.ai/doggoganger",
3
- "version": "0.9.0-beta.8",
3
+ "version": "0.9.0",
4
4
  "description": "A dummy miso endpoint for demo and testing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,12 @@
10
10
  "access": "public"
11
11
  },
12
12
  "scripts": {
13
- "start": "nodemon src/index.js",
13
+ "build:data": "node ./bin/data.js",
14
+ "build:js": "rollup -c",
15
+ "build": "npm run build:data && npm run build:js",
16
+ "start:js": "rollup -w -c rollup.config.dev.mjs",
17
+ "start": "nodemon cli/index.js",
18
+ "dev": "npm run start -- -- -v",
14
19
  "version": "node ./bin/version.js"
15
20
  },
16
21
  "repository": {
@@ -32,8 +37,13 @@
32
37
  "koa": "^2.14.1",
33
38
  "koa-body": "^6.0.1",
34
39
  "koa-static": "^5.0.0",
35
- "nodemon": "^2.0.15",
36
- "uuid": "^8.3.2",
40
+ "nodemon": "^2.0.22",
37
41
  "yargs": "^17.5.1"
42
+ },
43
+ "devDependencies": {
44
+ "@rollup/plugin-terser": "^0.4.3",
45
+ "rollup": "^3.23.0",
46
+ "rollup-plugin-livereload": "^2.0.5",
47
+ "rollup-plugin-serve": "^2.0.2"
38
48
  }
39
49
  }
@@ -0,0 +1,3 @@
1
+ import { config } from './rollup.util.mjs';
2
+
3
+ export default config('dev');
@@ -0,0 +1,3 @@
1
+ import { config } from './rollup.util.mjs';
2
+
3
+ export default config('prod');
@@ -0,0 +1,42 @@
1
+ import terser from '@rollup/plugin-terser';
2
+ import serve from 'rollup-plugin-serve';
3
+ import livereload from 'rollup-plugin-livereload';
4
+
5
+ export function config(env) {
6
+ const prod = env === 'prod';
7
+ let plugins;
8
+ if (prod) {
9
+ plugins = [
10
+ terser({
11
+ compress: {
12
+ pure_getters: true,
13
+ unsafe: true,
14
+ unsafe_comps: true,
15
+ warnings: false
16
+ },
17
+ }),
18
+ ];
19
+ } else {
20
+ plugins = [
21
+ serve({
22
+ port: 10098,
23
+ }),
24
+ livereload({
25
+ delay: 500,
26
+ watch: 'dist',
27
+ }),
28
+ ];
29
+ }
30
+ return {
31
+ input: `src/browser.js`,
32
+ output: {
33
+ file: prod ? `dist/umd/doggoganger-browser.min.js` : `dist/umd/doggoganger-browser.js`,
34
+ format: 'umd',
35
+ name: 'doggoganger',
36
+ exports: 'default',
37
+ indent: !prod,
38
+ },
39
+ watch: !prod,
40
+ plugins,
41
+ };
42
+ }
package/src/api/ask.js CHANGED
@@ -1,8 +1,4 @@
1
- import Router from '@koa/router';
2
- import { v4 as uuid } from 'uuid';
3
- import { lorem, md, articles, utils } from '../data/index.js';
4
-
5
- const { randomInt } = utils;
1
+ import { answer } from '../data/index.js';
6
2
 
7
3
  const CPS = 100;
8
4
  const ITEMS_LOADING_TIME = 3;
@@ -24,67 +20,51 @@ const STAGES = [
24
20
  },
25
21
  ];
26
22
 
27
- function formatDatetime(timestamp) {
28
- const str = new Date(timestamp).toISOString();
29
- return str.endsWith('Z') ? str.slice(0, -1) : str;
30
- }
23
+ export default class Ask {
31
24
 
32
- function answer(format) {
33
- switch (format) {
34
- case 'markdown':
35
- return md.markdown({});
36
- case 'plaintext':
37
- default:
38
- return lorem.lorem({
39
- min: 50,
40
- max: 100,
41
- decorates: ['description'],
42
- });
25
+ constructor(options = {}) {
26
+ this._options = options;
27
+ this._answers = new Map();
43
28
  }
44
- }
45
29
 
46
- const answers = new Map();
47
-
48
- class Answer {
30
+ questions(payload, options = {}) {
31
+ return new Answer(payload, { ...this._options, ...options });
32
+ }
49
33
 
50
- constructor(question, previous_question_id, { answerFormat = 'markdown' } = {}) {
51
- this.question_id = uuid();
52
- this.question = question;
53
- this.previous_answer_id = previous_question_id;
54
- this.timestamp = Date.now();
55
- this.datetime = formatDatetime(this.timestamp);
34
+ }
56
35
 
57
- this.answer = answer(answerFormat);
58
- this.relatedResources = [...articles({ rows: randomInt(6, 8) })];
59
- this.sources = [...articles({ rows: randomInt(4, 6) })];
36
+ class Answer {
60
37
 
61
- this.previous_question_id = previous_question_id && answers.get(previous_question_id) || undefined;
62
- answers.set(this.question_id, this);
38
+ constructor({ question, parent_question_id }, { answerFormat, answerSampling, answerLanguages, ...options } = {}) {
39
+ this._options = Object.freeze(options);
40
+ const timestamp = this.timestamp = Date.now();
41
+ this._data = answer({ question, parent_question_id, timestamp }, { answerFormat, answerSampling, answerLanguages });
63
42
  }
64
43
 
65
- get id() {
66
- return this.question_id;
44
+ get question_id() {
45
+ return this._data.question_id;
67
46
  }
68
47
 
69
48
  get() {
70
49
  const now = Date.now();
71
- const elapsed = (now - this.timestamp) / 1000;
50
+ const elapsed = (now - this.timestamp) * (this._options.speedRate || 1) / 1000;
72
51
  const [answer_stage, answer, finished] = this._answer(elapsed);
73
- const sources = this._sources(elapsed);
74
- const related_resources = this._relatedResources(elapsed);
75
- const { question_id, question, datetime, previous_question_id } = this;
52
+ const sources = this._sources(elapsed, finished);
53
+ const related_resources = this._related_resources(elapsed, finished);
54
+ const followup_questions = this._followup_questions(elapsed, finished);
55
+ const { question_id, question, datetime, parent_question_id } = this._data;
76
56
 
77
57
  return {
78
- affiliation: undefined,
79
58
  answer,
80
59
  answer_stage,
81
60
  datetime,
82
61
  finished,
83
- previous_question_id,
62
+ parent_question_id,
84
63
  question,
85
64
  question_id,
86
- related_resources,
87
65
  sources,
66
+ related_resources,
67
+ followup_questions,
88
68
  };
89
69
  }
90
70
 
@@ -95,52 +75,41 @@ class Answer {
95
75
  return [stage.name, stage.text, false];
96
76
  }
97
77
  }
78
+ const { answer } = this._data;
98
79
  const length = Math.floor(elapsed * CPS);
99
- const finished = length >= this.answer.length;
100
- const text = finished ? this.answer : this.answer.slice(0, length);
80
+ const finished = length >= answer.length;
81
+ const text = finished ? answer : answer.slice(0, length);
101
82
  return ['result', text, finished];
102
83
  }
103
84
 
104
- _sources(elapsed) {
105
- const { sources } = this;
85
+ _sources(elapsed, finished) {
86
+ const { sources } = this._data;
87
+ if (finished) {
88
+ return sources;
89
+ }
106
90
  const { length } = sources;
107
91
  const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
108
92
  return sources.slice(0, loaded);
109
93
  }
110
94
 
111
- _relatedResources(elapsed) {
112
- const { relatedResources } = this;
113
- const { length } = relatedResources;
95
+ _related_resources(elapsed, finished) {
96
+ const { related_resources } = this._data;
97
+ if (finished) {
98
+ return related_resources;
99
+ }
100
+ const { length } = related_resources;
114
101
  const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
115
- return relatedResources.slice(0, loaded);
102
+ return related_resources.slice(0, loaded);
116
103
  }
117
104
 
118
- }
119
-
120
- export default function({ answerFormat }) {
121
- const options = Object.freeze({ answerFormat });
122
- const router = new Router();
123
-
124
- router.post('/questions', (ctx) => {
125
- const { question, previous_answer_id } = JSON.parse(ctx.request.body);
126
- const answerFormat = ctx.get('x-answer-format') || options.answerFormat;
127
- const answer = new Answer(question, previous_answer_id, { answerFormat });
128
- const data = {
129
- question_id: answer.id,
130
- };
131
- ctx.body = JSON.stringify({ data });
132
- });
133
-
134
- router.get('/questions/:id/answer', (ctx) => {
135
- const { id } = ctx.params;
136
- const answer = answers.get(id);
137
- if (!answer) {
138
- ctx.status = 404;
139
- } else {
140
- const data = answer.get();
141
- ctx.body = JSON.stringify({ data });
105
+ _followup_questions(elapsed, finished) {
106
+ const { followup_questions } = this._data;
107
+ if (finished || !followup_questions) {
108
+ return followup_questions;
142
109
  }
143
- });
110
+ const { length } = followup_questions;
111
+ const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
112
+ return followup_questions.slice(0, loaded);
113
+ }
144
114
 
145
- return router;
146
115
  }
package/src/api/index.js CHANGED
@@ -1,20 +1,17 @@
1
- import Router from '@koa/router';
2
- import ask from './ask.js';
3
- import recommendation from './recommendation.js';
4
- import search from './search.js';
5
- import interactions from './interactions.js';
1
+ import Ask from './ask.js';
2
+ import Search from './search.js';
3
+ import Recommendation from './recommendation.js';
4
+ import Interactions from './interactions.js';
5
+ import Products from './products.js';
6
6
 
7
- function use(router, path, middleware) {
8
- router.use(path, middleware.routes(), middleware.allowedMethods());
9
- }
10
-
11
- export default function(options) {
12
- const router = new Router();
7
+ export default class Api {
13
8
 
14
- use(router, '/ask', ask(options));
15
- use(router, '/recommendation', recommendation);
16
- use(router, '/search', search);
17
- use(router, '/interactions', interactions);
9
+ constructor(options) {
10
+ this.ask = new Ask(options);
11
+ this.search = new Search(options);
12
+ this.recommendation = new Recommendation(options);
13
+ this.interactions = new Interactions(options);
14
+ this.products = new Products(options);
15
+ }
18
16
 
19
- return router;
20
- };
17
+ }
@@ -1,9 +1,13 @@
1
- import Router from '@koa/router';
1
+ export default class Interactions {
2
2
 
3
- const router = new Router();
3
+ constructor(options) {
4
+ this._options = options;
5
+ }
4
6
 
5
- router.post('/', (ctx) => {
6
- ctx.body = { took: 1, errors: false, data: [] };
7
- });
7
+ upload(records) {
8
+ return [];
9
+ }
8
10
 
9
- export default router;
11
+ // TODO: delete
12
+
13
+ }
@@ -0,0 +1,28 @@
1
+ export default class Products {
2
+
3
+ constructor(options) {
4
+ this._options = options;
5
+ }
6
+
7
+ upload(records) {
8
+ return [];
9
+ }
10
+
11
+ ids() {
12
+ const ids = [];
13
+ for (let i = 0; i < 5000; i++) {
14
+ ids.push(mockProductId(i));
15
+ }
16
+ return {
17
+ ids,
18
+ };
19
+ }
20
+
21
+ // TODO: delete (batch)
22
+
23
+ }
24
+
25
+ function mockProductId(i) {
26
+ const prefix = i < 10 ? 'p_000' : i < 100 ? 'p_00' : i < 1000 ? 'p_0' : 'p_';
27
+ return `${prefix}${i}`;
28
+ }
@@ -1,9 +1,21 @@
1
- import Router from '@koa/router';
2
- import { products } from './handlers.js';
1
+ import { products } from '../data/index.js';
3
2
 
4
- const router = new Router();
3
+ export default class Recommendation {
5
4
 
6
- router.post('/user_to_products', products);
7
- router.post('/product_to_products', products);
5
+ constructor(options) {
6
+ this._options = options;
7
+ }
8
8
 
9
- export default router;
9
+ user_to_products({ rows = 5 }) {
10
+ return {
11
+ products: [...products({ rows })],
12
+ };
13
+ }
14
+
15
+ product_to_products({ rows = 5 }) {
16
+ return {
17
+ products: [...products({ rows })],
18
+ };
19
+ }
20
+
21
+ }
package/src/api/search.js CHANGED
@@ -1,8 +1,15 @@
1
- import Router from '@koa/router';
2
- import { products } from './handlers.js';
1
+ import { products } from '../data/index.js';
3
2
 
4
- const router = new Router();
3
+ export default class Search {
5
4
 
6
- router.post('/search', products);
5
+ constructor(options) {
6
+ this._options = options;
7
+ }
7
8
 
8
- export default router;
9
+ search({ rows = 5 }) {
10
+ return {
11
+ products: [...products({ rows })],
12
+ };
13
+ }
14
+
15
+ }
package/src/browser.js ADDED
@@ -0,0 +1,5 @@
1
+ import Api from './api/index.js';
2
+
3
+ export default {
4
+ buildApi: (...args) => new Api(...args),
5
+ };
@@ -0,0 +1,33 @@
1
+ import { randomInt, formatDatetime, sample, uuid } from './utils.js';
2
+ import * as fields from './fields.js';
3
+ import { articles } from './articles.js';
4
+ import { questions } from './questions.js';
5
+
6
+ export function answer({ question, parent_question_id, timestamp = Date.now() }, { answerFormat = 'markdown', answerSampling, answerLanguages = [] } = {}) {
7
+
8
+ const question_id = uuid();
9
+ const datetime = formatDatetime(timestamp);
10
+
11
+ const sampling = answerSampling !== undefined ? Math.max(0, Math.min(1, answerSampling)) : undefined;
12
+ const features = answerLanguages.length ? answerLanguages.map(language => `lang-${language}`) : undefined;
13
+
14
+ const answer = fields.answer({ format: answerFormat, sampling, features });
15
+ const related_resources = [...articles({ rows: sampleRandomInt(6, 8, sampling) })];
16
+ const sources = [...articles({ rows: sampleRandomInt(4, 6, sampling) })];
17
+ const followup_questions = [...questions({ rows: sampleRandomInt(3, 6) })];
18
+
19
+ return {
20
+ question,
21
+ question_id,
22
+ ...(parent_question_id && { parent_question_id }),
23
+ datetime,
24
+ answer,
25
+ sources,
26
+ related_resources,
27
+ followup_questions,
28
+ };
29
+ }
30
+
31
+ function sampleRandomInt(min, max, sampling) {
32
+ return randomInt(sample(min, sampling), sample(max, sampling));
33
+ }
@@ -16,7 +16,7 @@ function article({} = {}) {
16
16
  categories: [],
17
17
  tags: fields.tags(),
18
18
  title: fields.title({ size: [4, 10] }),
19
- description: fields.description({ size: [20, 40] }),
19
+ snippet: fields.description({ size: [20, 40] }),
20
20
  //html,
21
21
  cover_image: fields.image(),
22
22
  url: `/products/${id}`,
@@ -1,5 +1,6 @@
1
1
  import * as lorem from './lorem.js';
2
- import { imageUrl } from './utils.js';
2
+ import { imageUrl, sample } from './utils.js';
3
+ import * as md from './markdown/index.js';
3
4
 
4
5
  export function image({ size = 300 } = {}) {
5
6
  return imageUrl(size);
@@ -29,10 +30,11 @@ export function title({ size = [2, 6] } = {}) {
29
30
  });
30
31
  }
31
32
 
32
- export function description({ size = [10, 20] } = {}) {
33
+ export function description({ size = [10, 20], ...options } = {}) {
34
+ const decorator = Object.keys(options).length ? ['description', options] : 'description';
33
35
  return lorem.lorem({
34
36
  size,
35
- decorates: ['description'],
37
+ decorates: [decorator],
36
38
  });
37
39
  }
38
40
 
@@ -47,3 +49,17 @@ export function price() {
47
49
  export function rating() {
48
50
  return Math.floor(Math.random() * 500) / 100 + 1;
49
51
  }
52
+
53
+ export function answer({ format, sampling, features }) {
54
+ switch (format) {
55
+ case 'markdown':
56
+ return md.markdown({ sampling, features });
57
+ case 'plaintext':
58
+ default:
59
+ return lorem.lorem({
60
+ min: sample(50, sampling),
61
+ max: sample(50, sampling),
62
+ decorates: ['description'],
63
+ });
64
+ }
65
+ }
package/src/data/index.js CHANGED
@@ -2,4 +2,6 @@ export * as md from './markdown/index.js';
2
2
  export * as lorem from './lorem.js';
3
3
  export * from './products.js';
4
4
  export * from './articles.js';
5
+ export * from './questions.js';
6
+ export * from './answers.js';
5
7
  export * as utils from './utils.js';
package/src/data/lorem.js CHANGED
@@ -1,11 +1,5 @@
1
- import { readFileSync } from 'fs';
2
- import { fileURLToPath } from 'url';
3
- import { dirname, resolve } from 'path';
4
- import yaml from 'js-yaml';
5
- import { randomInt } from './utils.js';
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const DEFAULT_WORDS = yaml.load(readFileSync(resolve(__dirname, './words.yaml'), 'utf8'));
1
+ import { randomInt, gaussRandom } from './utils.js';
2
+ import DEFAULT_WORDS from './words.js';
9
3
 
10
4
  export function lorem({ decorates = [], output = 'string', size, min, max, ...options } = {}) {
11
5
  let iterator = limit(size || [min, max])(base(options));
@@ -24,7 +18,18 @@ const FNS = {
24
18
  }
25
19
 
26
20
  function lookup(fn) {
27
- return typeof fn === 'string' ? FNS[fn]() : fn;
21
+ switch (typeof fn) {
22
+ case 'string':
23
+ return FNS[fn]();
24
+ case 'function':
25
+ return fn;
26
+ case 'object':
27
+ if (Array.isArray(fn)) {
28
+ const [name, options = {}] = fn;
29
+ return FNS[name](options);
30
+ }
31
+ }
32
+ throw new Error(`Unrecognized decorator/output form: ${fn}`);
28
33
  }
29
34
 
30
35
  // base //
@@ -97,6 +102,7 @@ export function description({
97
102
  std: 5,
98
103
  min: 1,
99
104
  },
105
+ punctuation = '.',
100
106
  } = {}) {
101
107
  return function *(iterator) {
102
108
  let word;
@@ -111,12 +117,12 @@ export function description({
111
117
  slen = gaussMS(wordsPerSentence);
112
118
  }
113
119
  if (--slen === 0) {
114
- word += '.';
120
+ word += punctuation;
115
121
  }
116
122
  }
117
123
  if (word) {
118
- if (!word.endsWith('.')) {
119
- word += '.';
124
+ if (!word.endsWith(punctuation)) {
125
+ word += punctuation;
120
126
  }
121
127
  yield word;
122
128
  }
@@ -146,11 +152,3 @@ function gaussMS(args) {
146
152
  }
147
153
  return Math.round(n);
148
154
  }
149
-
150
- function gaussRandom() {
151
- return uniformRandom() + uniformRandom() + uniformRandom();
152
- }
153
-
154
- function uniformRandom() {
155
- return Math.random() * 2 - 1;
156
- }