Julien Chaumond commited on
Commit
2d2e027
1 Parent(s): 30358b4
front/assets/iconmonstr-github-1.svg ADDED
front/assets/iconmonstr-media-control-55.svg ADDED
front/assets/iconmonstr-medium-1.svg ADDED
front/assets/iconmonstr-share-11.svg ADDED
front/index.html ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Hugging Face – Convo</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
7
+ <link rel="stylesheet" href="/dist/style.css">
8
+ <link href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono|IBM+Plex+Sans:400,700" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <div class="header">
12
+ <div class="header-container">
13
+ <div class="title">
14
+ 🦄 How to build a State-of-the-Art Conversational AI with Transfer Learning
15
+ </div>
16
+ <div class="links">
17
+ <a target="_blank" href="https://medium.com/@Thomwolf/2d818ac26313"><img src="/assets/iconmonstr-medium-1.svg"></a>
18
+ <a target="_blank" href="https://github.com/huggingface/transfer-learning-conv-ai"><img src="/assets/iconmonstr-github-1.svg"></a>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="subheader">
24
+ <div class="container">
25
+ <div class="box box0">
26
+ <div class="clearfix">
27
+ <div class="title">Random personality</div>
28
+ <div class="links">
29
+ <a href="#" class="js-shuffle">Shuffle <img src="/assets/iconmonstr-media-control-55.svg"></a>
30
+ <a href="#" class="js-share" >Share <img src="/assets/iconmonstr-share-11.svg"></a>
31
+ </div>
32
+ </div>
33
+ <div class="persona"><!-- p --></div>
34
+ </div>
35
+ <div class="box box1">
36
+ <div class="clearfix">
37
+ <div class="title"><!--Attention: How it works-->Start chatting</div>
38
+ </div>
39
+ <div class="description">
40
+ The machine learning model created a comprehensive consistent persona based on
41
+ these few lines of bio. You can now chat with this persona below.
42
+ </div>
43
+ <div class="gauge">
44
+ <div class="gauge-el-wrapper"><div class="gauge-el"><span class="legend">Decoder settings: Low</span></div></div>
45
+ <div class="gauge-el-wrapper"><div class="gauge-el"><span class="legend">Medium</span></div></div>
46
+ <div class="gauge-el-wrapper"><div class="gauge-el"><span class="legend">High</span></div></div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="container">
53
+ <div class="chat-container">
54
+ <div class="messages-gradient"></div>
55
+ <div class="placeholder-start">
56
+ <div class="inner">
57
+ Start chatting with this model, or tweak the decoder settings in the bottom-left corner.
58
+ </div>
59
+ </div>
60
+ <div class="messages"></div>
61
+ </div>
62
+ <div class="chat-input">
63
+ <div class="input-wrapper">
64
+ <form class="js-form">
65
+ <div class="input-message-wrapper">
66
+ <input class="input-message" type="text" name="q" placeholder="Type a message..." autocomplete="off" autofocus>
67
+ </div>
68
+ <button class="input-button">Send</button>
69
+ </form>
70
+ </div>
71
+ </div>
72
+ <div class="chat-suggestion">
73
+ Suggestion: <span class="js-loading">Loading…</span> <a class="js-suggestion hide" href="#">Why are you so stressed out?</a>
74
+ </div>
75
+ </div>
76
+
77
+
78
+ <div class="decoder-settings">
79
+ <div class="title">Decoder settings</div>
80
+ <div class="setting">
81
+ <div class="desc">
82
+ <span>Top-k</span><span class="js-val"></span>
83
+ </div>
84
+ <input class="slider" type="range" min="0" max="1000" step="1" value="0">
85
+ </div>
86
+ <div class="setting">
87
+ <div class="desc">
88
+ <span>Top-p</span><span class="js-val"></span>
89
+ </div>
90
+ <input class="slider" type="range" min="0" max="1" step="any" value="0.9">
91
+ </div>
92
+ <div class="setting">
93
+ <div class="desc">
94
+ <span>Temperature</span><span class="js-val"></span>
95
+ </div>
96
+ <input class="slider" type="range" min="0" max="100" step="any" value="0.7">
97
+ </div>
98
+ </div>
99
+
100
+ <script>/* //u */</script>
101
+ <script src="/dist/script.js"></script>
102
+ <script>
103
+ (function() {
104
+ if (window.location.hostname === 'localhost') {
105
+ c.log('[livereload]', 'on');
106
+ const s = document.createElement('script');
107
+ s.setAttribute('src', '//localhost:35729/livereload.js');
108
+ document.body.appendChild(s);
109
+ }
110
+ })();
111
+ </script>
112
+ <script>
113
+ if (! ['localhost', 'huggingface.test'].includes(window.location.hostname)) {
114
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
115
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
116
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
117
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
118
+ ga('create', 'UA-83738774-2', 'auto');
119
+ ga('send', 'pageview');
120
+ }
121
+ </script>
122
+ </body>
123
+ </html>
front/js-src/Api.ts ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ interface Message {
3
+ incoming: boolean;
4
+ content: string;
5
+ }
6
+
7
+ interface MessageOutput {
8
+ text: string;
9
+ attention: [string, number][][]; /// <- [persona, context, user message]
10
+ }
11
+
12
+ class Api {
13
+
14
+ static ENDPOINT = `http://pouet.eastus.cloudapp.azure.com:8080`;
15
+ static shared = new Api();
16
+
17
+ path(p: string): string {
18
+ return `${Api.ENDPOINT}/${p}`;
19
+ }
20
+
21
+ async getShuffle(): Promise<{slug: string, text: string}> {
22
+ const response = await fetch('/shuffle');
23
+ return response.json();
24
+ }
25
+
26
+ private async postMessage(
27
+ user: string,
28
+ query: { text: string } | { completion: string },
29
+ params: {
30
+ context?: Message[];
31
+ persona?: string;
32
+ top_k?: number; /// int between 0 and 1k
33
+ top_p?: number; /// float between 0 and 1
34
+ temperature?: number; /// float between 0 and 100
35
+ }
36
+ ): Promise<MessageOutput> {
37
+ const qs = new URLSearchParams(query);
38
+ const path = this.path(`messages/${user}?${ qs.toString() }`);
39
+
40
+ const response = await fetch(path, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify(params),
44
+ });
45
+ return await response.json() as MessageOutput;
46
+ }
47
+
48
+ /**
49
+ * Demo-specific helpers
50
+ */
51
+ async postWithSettings(
52
+ query: { text: string } | { completion: string },
53
+ params: {
54
+ context?: Message[];
55
+ persona?: string;
56
+ }
57
+ ): Promise<MessageOutput> {
58
+ /// Retrieve all settings params then launch the request.
59
+ const top_k = Number(
60
+ document.querySelector('.decoder-settings .setting:nth-child(2) .js-val')!.textContent
61
+ );
62
+ const top_p = Number(
63
+ document.querySelector('.decoder-settings .setting:nth-child(3) .js-val')!.textContent
64
+ );
65
+ const temperature = Number(
66
+ document.querySelector('.decoder-settings .setting:nth-child(4) .js-val')!.textContent
67
+ );
68
+ return this.postMessage(`toto`, query, { ...params, top_k, top_p, temperature });
69
+ }
70
+ }
71
+
front/js-src/Markup.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ class Markup {
4
+ private static scrollToBottom(opts: { animate?: boolean } = {}) {
5
+ App.messagesRoot.scrollTop = App.messagesRoot.scrollHeight;
6
+ }
7
+
8
+ private static messageMarkup(m: Message): string {
9
+ const incomingStr = m.incoming ? 'incoming' : 'outgoing';
10
+ return `<div class="message ${incomingStr}">
11
+ <div class="message-inner">${Utils.escape(m.content)}</div>
12
+ </div>`;
13
+ }
14
+
15
+ static append(m: Message) {
16
+ const s = this.messageMarkup(m);
17
+ App.messagesRoot.insertAdjacentHTML('beforeend', s);
18
+ this.scrollToBottom();
19
+ }
20
+
21
+ /**
22
+ * Bucketize a float into a level
23
+ * according to a set of thresholds.
24
+ */
25
+ static attentionThreshold(att: number): number {
26
+ const thresholds = [2, 4.5, 10, 30];
27
+ for (const [i, x] of thresholds.entries()) {
28
+ if (x > att) {
29
+ return i;
30
+ }
31
+ }
32
+ return thresholds.length;
33
+ }
34
+ }
front/js-src/controller.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const App = {
2
+ persona: {
3
+ slug: "",
4
+ text: "",
5
+ },
6
+ messages: [] as Message[],
7
+ messagesRoot: document.querySelector('div.messages') as HTMLDivElement,
8
+ divPersona: document.querySelector('div.persona') as HTMLDivElement,
9
+ linkSuggest: document.querySelector('.chat-suggestion .js-suggestion') as HTMLLinkElement,
10
+ };
11
+
12
+ document.addEventListener('DOMContentLoaded', () => {
13
+ /**
14
+ * Persona
15
+ * - in simple mode, we just re-format the text slightly to make it prettier.
16
+ * - in attention mode, we tokenize and add spans around every token.
17
+ */
18
+ const simplePersona = () => {
19
+ const text = App.persona.text;
20
+ const html = text.split('.').map(x => Utils.capitalize(x)).join(`.<br>`);
21
+ App.divPersona.innerHTML = html;
22
+ };
23
+ const tokenizePersona = () => {
24
+ const text = App.persona.text;
25
+ const tokens = text.split(/\b/).filter(x => ! /\s/.test(x));
26
+ let html = ``;
27
+ for (let [i, tok] of tokens.entries()) {
28
+ if (i === 0) {
29
+ html += `<span data-idx="${i}">${ Utils.capitalize(tok) }</span>`;
30
+ } else if (i > 0 && tokens[i-1] === '.') {
31
+ html += `<br><span data-idx="${i}">${ Utils.capitalize(tok) }</span>`;
32
+ } else if (/[.,']/.test(tok)) {
33
+ html += `<span data-idx="${i}">${ tok }</span>`;
34
+ } else {
35
+ html += ` <span data-idx="${i}">${ tok }</span>`;
36
+ }
37
+ }
38
+ App.divPersona.innerHTML = html;
39
+ };
40
+ App.persona = (<any>window).PERSONA_ATLOAD;
41
+ // ^^ tokenizePersona();
42
+ simplePersona();
43
+ document.querySelector('.js-shuffle')!.addEventListener('click', async (evt) => {
44
+ evt.preventDefault();
45
+ App.persona = await Api.shared.getShuffle();
46
+ tokenizePersona();
47
+ history.replaceState(null, "", `/persona/${App.persona.slug}`);
48
+ /// Also reset messages.
49
+ App.messages = [];
50
+ App.messagesRoot.innerHTML = "";
51
+ loadSuggestion();
52
+ });
53
+ document.querySelector('.js-share')!.addEventListener('click', async (evt) => {
54
+ evt.preventDefault();
55
+ history.replaceState(null, "", `/persona/${App.persona.slug}`);
56
+ const text = `Chat with me: ${ App.divPersona.innerText.replace(/\n/g, " ") }`;
57
+ window.open(`https://twitter.com/share?url=${ encodeURIComponent(window.location.href) }&text=${ encodeURIComponent(text) }`);
58
+ });
59
+
60
+ /**
61
+ * Settings
62
+ */
63
+ const handleSliderChange = (slider: HTMLInputElement) => {
64
+ const div = slider.parentNode as HTMLDivElement;
65
+ const spanVal = div.querySelector('.js-val') as HTMLSpanElement;
66
+ const value = Number.isInteger(slider.valueAsNumber)
67
+ ? slider.valueAsNumber
68
+ : Number(slider.valueAsNumber.toFixed(2))
69
+ ;
70
+ spanVal.innerText = value.toString();
71
+ const min = Number(slider.getAttribute('min'));
72
+ const max = Number(slider.getAttribute('max'));
73
+ if (value < min + (max - min) / 3) {
74
+ spanVal.className = "js-val green";
75
+ } else if (value < min + 2 * (max - min) / 3) {
76
+ spanVal.className = "js-val orange";
77
+ } else {
78
+ spanVal.className = "js-val red";
79
+ }
80
+ };
81
+ const sliders = Array.from(
82
+ document.querySelectorAll('.decoder-settings input.slider')
83
+ ) as HTMLInputElement[];
84
+ for (const slider of sliders) {
85
+ handleSliderChange(slider);
86
+ slider.addEventListener('input', () => {
87
+ handleSliderChange(slider);
88
+ });
89
+ }
90
+
91
+ const gauge = document.querySelector('div.gauge') as HTMLDivElement;
92
+ gauge.addEventListener('click', () => {
93
+ gauge.classList.toggle('active');
94
+ });
95
+
96
+ /**
97
+ * Chat input
98
+ */
99
+ const form = document.querySelector('form.js-form') as HTMLFormElement;
100
+ const input = document.querySelector('input.input-message') as HTMLInputElement;
101
+ form.addEventListener('submit', async (evt) => {
102
+ evt.preventDefault();
103
+ document.querySelector<HTMLDivElement>('.placeholder-start')!.style.display = 'none';
104
+ const content = input.value;
105
+ if (content.trim() === "") {
106
+ c.debug(`Empty input`);
107
+ return ;
108
+ }
109
+ input.value = "";
110
+ const um: Message = {
111
+ incoming: false,
112
+ content: content,
113
+ };
114
+ Markup.append(um);
115
+ const o = await Api.shared.postWithSettings({ text: content }, {
116
+ context: App.messages,
117
+ persona: App.persona.text,
118
+ });
119
+ c.log(o);
120
+ // c.log(o.attention[0]);
121
+ // c.log(o.attention[1]);
122
+ // c.log(o.attention[2]);
123
+ App.messages.push(um);
124
+ /// ^^ not before the API call because it shouldn't be included in context.
125
+ /**
126
+ * Visualize Attention
127
+ */
128
+ // for (const [idx, [token, att]] of o.attention[0].entries()) {
129
+ // if (att === 0) {
130
+ // continue;
131
+ // }
132
+ // const span = document.querySelector(`.persona span[data-idx="${idx}"]`);
133
+ // if (!span) {
134
+ // c.error(`span not found`);
135
+ // return ;
136
+ // }
137
+ // span.className = `attention-level level${ Markup.attentionThreshold(att) }`;
138
+ // }
139
+ //////
140
+ const m: Message = {
141
+ incoming: true,
142
+ content: o.text,
143
+ };
144
+ App.messages.push(m);
145
+ Markup.append(m);
146
+ /// Finally, launch an auto-complete:
147
+ loadSuggestion();
148
+ });
149
+
150
+
151
+ /**
152
+ * Suggestion box
153
+ */
154
+ const loadSuggestion = async () => {
155
+ const spanLoading = document.querySelector('.chat-suggestion .js-loading') as HTMLSpanElement;
156
+ spanLoading.classList.remove('hide');
157
+ App.linkSuggest.classList.add('hide');
158
+ const o = await Api.shared.postWithSettings({ completion: "" }, {
159
+ context: App.messages,
160
+ persona: App.persona.text,
161
+ });
162
+ c.log(o);
163
+ App.linkSuggest.innerText = o.text;
164
+ spanLoading.classList.add('hide');
165
+ App.linkSuggest.classList.remove('hide');
166
+ };
167
+ loadSuggestion();
168
+ App.linkSuggest.addEventListener('click', async (evt) => {
169
+ evt.preventDefault();
170
+ App.linkSuggest.classList.add('hide');
171
+ input.value = App.linkSuggest.innerText;
172
+ await Utils.delay(500);
173
+ const sendBtn = form.querySelector('button.input-button') as HTMLButtonElement;
174
+ sendBtn.click();
175
+ /// ^^ do not do `form.submit()` as we want to trigger our handler.
176
+ });
177
+ });
front/js-src/lib/Log.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ const c = console;
front/js-src/lib/Utils.ts ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ class Utils {
3
+ private static escapeMap = {
4
+ /// From underscore.js
5
+ '&': '&amp;',
6
+ '<': '&lt;',
7
+ '>': '&gt;',
8
+ '"': '&quot;',
9
+ "'": '&#x27;',
10
+ '`': '&#x60;'
11
+ };
12
+
13
+ /**
14
+ * Escape a message's content for insertion into html.
15
+ */
16
+ static escape(s: string): string {
17
+ let x = s;
18
+ for (const [k, v] of Object.entries(this.escapeMap)) {
19
+ x = x.replace(new RegExp(k, 'g'), v);
20
+ }
21
+ return x.replace(/\n/g, '<br>');
22
+ }
23
+
24
+ /**
25
+ * Opposite of escape.
26
+ */
27
+ static unescape(s: string): string {
28
+ let x = s.replace(/<br>/g, '\n');
29
+ for (const [k, v] of Object.entries(this.escapeMap)) {
30
+ x = x.replace(new RegExp(v, 'g'), k);
31
+ }
32
+ return x;
33
+ }
34
+
35
+ /**
36
+ * "Real" modulo (always >= 0), not remainder.
37
+ */
38
+ static mod(a: number, n: number): number {
39
+ return ((a % n) + n) % n;
40
+ }
41
+
42
+ /**
43
+ * Noop object with arbitrary number of nested attributes that are also noop.
44
+ */
45
+ static deepNoop() {
46
+ const noop = new Proxy(() => {}, {
47
+ get: () => noop
48
+ });
49
+ return noop;
50
+ }
51
+
52
+ /**
53
+ * Capitalize
54
+ */
55
+ static capitalize(s: string): string {
56
+ return s.charAt(0).toUpperCase() + s.slice(1);
57
+ }
58
+
59
+ /**
60
+ * Returns a promise that will resolve after the specified time
61
+ * @param ms Number of ms to wait
62
+ */
63
+ static delay(ms: number): Promise<void> {
64
+ return new Promise((resolve, reject) => {
65
+ setTimeout(() => resolve(), ms);
66
+ });
67
+ }
68
+ }
69
+
front/less/mixins/bfc.less ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .bfc {
2
+ overflow: hidden;
3
+ }
front/less/mixins/clearfix.less ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .clearfix {
2
+ &:before,
3
+ &:after {
4
+ content: " ";
5
+ display: table;
6
+ }
7
+ &:after {
8
+ clear: both;
9
+ }
10
+ }
front/less/mixins/user-select.less ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .user-select(@select) {
2
+ -webkit-user-select: @select;
3
+ -moz-user-select: @select;
4
+ -ms-user-select: @select; // IE10+
5
+ user-select: @select;
6
+ }
front/less/style.less ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import (less) "../node_modules/normalize.css/normalize.css";
2
+
3
+ @import "mixins/bfc.less";
4
+ @import "mixins/clearfix.less";
5
+ @import "mixins/user-select.less";
6
+
7
+ @import "variables.less";
8
+
9
+ @blueText: #3B48F6;
10
+ @containerWidth: 600px;
11
+
12
+
13
+ .clearfix {
14
+ .clearfix();
15
+ }
16
+
17
+ .hide { display: none !important; }
18
+
19
+ body {
20
+ font-family: @fontSans;
21
+ font-size: 16px;
22
+ line-height: 1.4;
23
+ color: #333;
24
+ -webkit-font-smoothing: antialiased;
25
+ }
26
+
27
+ input {
28
+ color: #333;
29
+ }
30
+ a:link {
31
+ color: @blueText;
32
+ }
33
+ button.input-button {
34
+ color: @blueText;
35
+ outline: none;
36
+ background: transparent;
37
+ border: none;
38
+ height: 32px;
39
+ cursor: pointer;
40
+ .user-select(none);
41
+ }
42
+
43
+ .container {
44
+ margin-left: auto;
45
+ margin-right: auto;
46
+ max-width: @containerWidth;
47
+ }
48
+
49
+ div.header {
50
+ background-image: linear-gradient(270deg, #BEFF00 0%, #FFCF00 100%);
51
+ padding: 16px 0;
52
+ .header-container {
53
+ .clearfix();
54
+ max-width: 80%;
55
+ margin: auto;
56
+ .title {
57
+ font-weight: bold;
58
+ font-size: 20px;
59
+ line-height: 24px;
60
+ float: left;
61
+ }
62
+ .links {
63
+ float: right;
64
+ img {
65
+ margin-left: 14px;
66
+ }
67
+ a:hover {
68
+ opacity: 0.8;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ div.subheader {
75
+ background-color: #F6F6F6;
76
+ padding-top: 10px;
77
+ padding-bottom: 2px;
78
+ .box {
79
+ .clearfix();
80
+ margin: 18px 0;
81
+ }
82
+ .box1 {
83
+ margin-bottom: 8px;
84
+ }
85
+ .title {
86
+ float: left;
87
+ font-weight: bold;
88
+ margin-bottom: 10px;
89
+ }
90
+ div.links {
91
+ float: left;
92
+ a {
93
+ margin-left: 28px;
94
+ text-decoration: none;
95
+ color: @blueText;
96
+ img {
97
+ vertical-align: sub;
98
+ height: 20px;
99
+ margin-left: 4px;
100
+ }
101
+ &:hover {
102
+ opacity: 0.8;
103
+ }
104
+ }
105
+ }
106
+ .gauge {
107
+ .clearfix();
108
+ padding: 14px 0;
109
+ cursor: pointer;
110
+ .gauge-el-wrapper {
111
+ position: relative;
112
+ box-sizing: border-box;
113
+ float: left;
114
+ width: 33.33%;
115
+ padding-right: 6px;
116
+ &:nth-child(1) .gauge-el {
117
+ background-image: linear-gradient(90deg, #FF2300 0%, #FF7E00 100%);
118
+ }
119
+ &:nth-child(2) .gauge-el {
120
+ background-color: orange;
121
+ }
122
+ &:nth-child(3) .gauge-el {
123
+ background-image: linear-gradient(90deg, #FFCF00 0%, #BEFF00 100%);
124
+ }
125
+ }
126
+ .gauge-el {
127
+ height: 5px;
128
+ border-radius: 10px;
129
+ background-color: red;
130
+ }
131
+ .legend {
132
+ font-size: 11px;
133
+ font-weight: bold;
134
+ position: absolute;
135
+ transition: all 0.2s ease;
136
+ top: 0px;
137
+ opacity: 0;
138
+ }
139
+ &:hover, &.active {
140
+ .legend {
141
+ top: 10px;
142
+ opacity: 1;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ div.decoder-settings {
149
+ background-color: #ececec;
150
+ position: fixed;
151
+ bottom: 0;
152
+ width: 140px;
153
+ padding: 2px 6px;
154
+ .title {
155
+ font-size: 11px;
156
+ }
157
+ .setting {
158
+ .desc {
159
+ font-size: 11px;
160
+ font-weight: bold;
161
+ }
162
+ .js-val {
163
+ margin-left: 3px;
164
+ &.green { color: rgb(109, 144, 6); }
165
+ &.orange { color: #FF7E00; }
166
+ &.red { color: red; }
167
+ }
168
+ }
169
+ .slider {
170
+ width: 100%;
171
+ }
172
+ }
173
+
174
+ div.chat-container {
175
+ position: relative;
176
+ .messages-gradient {
177
+ position: absolute;
178
+ top: 0;
179
+ left: 0;
180
+ right: 0;
181
+ height: 30px;
182
+ background-image: linear-gradient(180deg, #FFFFFF 0%, rgba(255,255,255,0.00) 100%);
183
+ }
184
+ .placeholder-start {
185
+ position: absolute;
186
+ top: 100px;
187
+ left: 0; right: 0;
188
+ .inner {
189
+ width: 260px;
190
+ margin: auto;
191
+ text-align: center;
192
+ font-size: 14px;
193
+ color: #777777;
194
+ }
195
+ }
196
+ .messages {
197
+ height: 200px;
198
+ overflow: auto;
199
+ .message {
200
+ .clearfix();
201
+ margin-top: 12px;
202
+ &.incoming .message-inner {
203
+ background-color: white;
204
+ border: 1px solid #ccc;
205
+ float: left;
206
+ }
207
+ &.outgoing .message-inner {
208
+ background-color: #f1f0f0;
209
+ float: right;
210
+ }
211
+ }
212
+ .message-inner {
213
+ border-radius: 14px;
214
+ padding: 6px 14px;
215
+ &.typing-indicator {
216
+ padding: 8px;
217
+ }
218
+ img.typing-indicator {
219
+ width: 32px;
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ div.chat-input {
226
+ margin-top: 20px;
227
+ padding: 12px 20px;
228
+ border: 1px solid #a7a7a7;
229
+ border-radius: 6px;
230
+ box-shadow: 0px 2px 4px 1px rgba(0, 0, 0, 0.2);
231
+ .input-wrapper {
232
+ .clearfix();
233
+ position: relative;
234
+ width: 100%;
235
+ .input-message-wrapper {
236
+ position: absolute;
237
+ left: 0;
238
+ right: 50px;
239
+ input.input-message {
240
+ width: 100%;
241
+ outline: none;
242
+ border: none;
243
+ height: 32px;
244
+ padding: 0;
245
+ background-color: transparent;
246
+ }
247
+ }
248
+ button.input-button {
249
+ float: right;
250
+ }
251
+ }
252
+ }
253
+ div.chat-suggestion {
254
+ margin-top: 8px;
255
+ margin-bottom: 10px;
256
+ font-size: 14px;
257
+ color: #777777;
258
+ .js-loading {
259
+ font-size: 12px;
260
+ }
261
+ }
262
+
263
+
264
+ span.attention-level {
265
+ background-color: rgba(0, 0, 0, 0.4);
266
+ &.level0 { background-color: #FF2300; }
267
+ &.level1 { background-color: #FF7E00; }
268
+ &.level2 { background-color: orange; }
269
+ &.level3 { background-color: #FFCF00; }
270
+ &.level4 { background-color: #BEFF00; }
271
+ }
front/less/variables.less ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+
2
+ @fontSans: 'IBM Plex Sans', sans-serif;
3
+ @fontMonospace: 'IBM Plex Mono', monospace;
front/less/zGeneral.less ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Filename starts with a z to keep things in order.
2
+
3
+ @import "variables.less";
4
+
5
+ code, pre, div.pre {
6
+ background-color: rgba(0,0,0,0.04);
7
+ border-radius: 3px;
8
+ padding: 2px 6px;
9
+ font-family: @fontMonospace;
10
+ font-size: 12px;
11
+ }
12
+
13
+ div.pre {
14
+ span.value {
15
+ white-space: pre;
16
+ color: #840a6e;
17
+ &.string { color: #000; }
18
+ &.boolean { color: #0086b3; }
19
+ &.number { color: #40a070; }
20
+ }
21
+ }
front/package-lock.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "foobar-front",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 1,
5
+ "requires": true,
6
+ "dependencies": {
7
+ "normalize.css": {
8
+ "version": "8.0.1",
9
+ "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
10
+ "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
11
+ }
12
+ }
13
+ }
front/package.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "foobar-front",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "normalize.css": "^8.0.1"
8
+ },
9
+ "devDependencies": {},
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "author": "",
14
+ "license": "ISC"
15
+ }
front/tsconfig.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2018",
4
+ "outFile": "dist/script.js",
5
+ "sourceMap": false,
6
+ "strictNullChecks": true,
7
+ "strictBindCallApply": true,
8
+ "lib": ["dom", "es6", "es2016", "es2017", "es2018", "esnext"]
9
+ }
10
+ }