ashwath-vaithina-ibm commited on
Commit
6c43dbf
·
verified ·
1 Parent(s): 61f1b91

add multiturn conversation

Browse files
Files changed (1) hide show
  1. static/demo/index.html +783 -432
static/demo/index.html CHANGED
@@ -1,453 +1,804 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <title>Responsible Prompting Demo</title>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="stylesheet" href="https://unpkg.com/carbon-components/css/carbon-components.min.css">
8
- <script type="text/javascript" src="static/demo/js/d3.v7.min.js"></script>
9
- <script type="text/javascript" src="static/demo/js/jquery-3.7.1.min.js"></script>
10
- <!-- <script type="text/javascript" src="js/d3.v7.min.js"></script>
11
- <script type="text/javascript" src="js/jquery-3.7.1.min.js"></script> -->
12
- <style type="text/css">
13
- div.tooltip {
14
- position: absolute;
15
- text-align: left;
16
- padding: 0.5em;
17
- width: 20em;
18
- min-height: 5em;
19
- background: #fff;
20
- color: #000;
21
- border: 1px solid #000;
22
- border-radius: 5px;
23
- pointer-events: none;
24
- font-size: inherit;
25
- font-family: inherit;
26
  }
27
- </style>
28
- </head>
29
- <body>
30
- <div class="bx--grid">
31
- <div class="bx--row">
32
- <div class="bx--col-lg-10 bx--offset-lg-1">
33
- <h1 class="bx--type-expressive-heading-03" style="margin-top: 1em;">Responsible Prompting</h1>
34
- <p class="bx--type-body-long-01"><br>Please provide a prompt that would be sent to an LLM. Recommendations are performed in prompting-time, before content generation. The recommendations consider a curated dataset of values and prompts sentences. The recommendations are based on the similarity between the input sentences and our prompt sentences dataset.<br><br></p>
35
-
36
- <div class="bx--tabs">
37
- <ul class="bx--tabs__nav">
38
- <li class="bx--tabs__nav-item bx--tabs__nav-item--selected" id="tab-1" data-target="#tab-content-1">
39
- <a class="bx--tabs__nav-link" href="#" onclick="$('#tab-content-1').toggle();$('#tab-1').toggleClass('bx--tabs__nav-item--selected');$('#tab-content-2').toggle();$('#tab-2').toggleClass('bx--tabs__nav-item--selected');">Prompt</a>
40
- </li>
41
- <li class="bx--tabs__nav-item" data-target="#tab-content-2" id="tab-2">
42
- <a class="bx--tabs__nav-link" href="#" onclick="$('#tab-content-1').toggle();$('#tab-1').toggleClass('bx--tabs__nav-item--selected');$('#tab-content-2').toggle();$('#tab-2').toggleClass('bx--tabs__nav-item--selected');renderGraph($('#prompt').data('recommendations'))">Graph</a>
43
- </li>
44
- </ul>
45
- </div>
46
-
47
- <form class="bx--form" id="demo">
48
- <div class="bx--tab-content">
49
- <div id="tab-content-1" class="bx--tab-content__section">
50
- <div class="bx--form-item">
51
- <textarea id="prompt" name="prompt" class="bx--text-area" rows="15" placeholder="Enter your prompt">Act as a professional designer with 20 years of experience creating and testing UX interfaces and landing sites for a variety of IT applications. We are in need of more people and an increased budget to be able to keep up with clients' needs. What kind of evidence should I gather to support my demands to gain more resources?</textarea>
52
- <div id="recommendation" style="min-height: 3em;" class="bx--form__helper-text"></div>
53
- </div>
54
- </div>
55
- <div id="tab-content-2" class="bx--tab-content__section bx--tab-content--hidden" style="display: none;">
56
- <div class="bx--form-item" id="graph" style="background: #efefef"></div>
57
- </div>
58
- </div>
59
- <div class="bx--form-item" style="margin-bottom: 2em;">
60
- <button id="generate" type="submit" class="bx--btn bx--btn--primary">Generate</button>
61
- </div>
62
- <div class="bx--form-item">
63
- <label for="outcome" id="outcome-label" class="bx--label">Generated text</label>
64
- <textarea id="outcome" name="outcome" class="bx--text-area" rows="12" placeholder=""></textarea>
65
- </div>
66
- </form>
67
- </div>
68
- </div>
69
- </div>
70
- <script src="./js/jquery-3.7.1.min.js"></script>
71
- <script lang="javascript">
72
- const width = 600;
73
- const height = 200;
74
- const marginTop = 30;
75
- const marginRight = 30;
76
- const marginBottom = 30;
77
- const marginLeft = 30;
78
- const nodeRadius = 3;
79
-
80
- const svg = d3.select("#graph").append("svg")
81
- .attr("viewBox", [0, 0, width, height])
82
- .attr("style", "max-width: 100%; height: auto; font: 8px sans-serif;");
83
-
84
- var tooltip = d3.select("body").append("div")
85
- .attr("class", "tooltip")
86
- .style("opacity", 0);
87
-
88
- const renderGraph = (recommendations) => {
89
- if( !recommendations ){
90
- return ;
91
- }
92
- svg.selectAll("*").remove(); // Clearing previous plots
93
- var graphData = {nodes: [], edges: []}
94
- var i = j = 0;
95
- if( recommendations['input'].length > 0 ){
96
- graphData.nodes.push({
97
- id: 0,
98
- x: Number(recommendations['input'][0].x),
99
- y: Number(recommendations['input'][0].y),
100
- text: recommendations['input'][0].sentence,
101
- label: 'S' + (i+1),
102
- type: 'input'
103
- })
104
- for( i = 1; i < recommendations['input'].length; i++ ){
105
- graphData.nodes.push({
106
- id: i,
107
- x: Number(recommendations['input'][i].x),
108
- y: Number(recommendations['input'][i].y),
109
- text: recommendations['input'][i].sentence,
110
- label: 'S' + (i+1),
111
- type: 'input'
112
- });
113
- graphData.edges.push({ source: (i-1), target: i, type: 'input' });
114
- }
115
- }
116
 
117
- // Adding nodes & edges for inclusion recommendations
118
- if( recommendations['add'].length > 0 ){
119
- for( j = 0; j < recommendations['add'].length; j++ ){
120
- graphData.nodes.push({
121
- id: i + j + 1,
122
- x: Number(recommendations['add'][j].x),
123
- y: Number(recommendations['add'][j].y),
124
- text: recommendations['add'][j].prompt,
125
- label: recommendations['add'][j].value,
126
- type: 'add'
127
- });
128
- graphData.edges.push({ source: (i-1), target: (i+j+1), type: 'add' });
129
- }
130
- }
131
 
132
- // Adding nodes & edges for removal recommendations
133
- if( recommendations['remove'].length > 0 ){
134
- // Showing only the first removal recommendation
135
- graphData.nodes.push({
136
- id: i + j,
137
- x: Number(recommendations['remove'][0].x),
138
- y: Number(recommendations['remove'][0].y),
139
- text: recommendations['remove'][0].closest_harmful_sentence,
140
- label: recommendations['remove'][0].value,
141
- type: 'remove'
142
- });
143
- }
 
 
 
 
 
 
 
144
 
145
- // Convert edge references to actual node objects
146
- graphData.edges = graphData.edges.map(edge => ({
147
- source: graphData.nodes.find(n => n.id === edge.source),
148
- target: graphData.nodes.find(n => n.id === edge.target)
149
- }));
150
-
151
- const { nodes, edges } = graphData;
152
-
153
- // Prepare the ranges of values for the axes
154
- const xDomain = d3.extent(nodes, d => d.x);
155
- const yDomain = d3.extent(nodes, d => d.y);
156
- const xPadding = 2
157
- const yPadding = 2
158
-
159
- // Prepare the scales for positional encoding.
160
- const x = d3.scaleLinear()
161
- .domain([xDomain[0]-xPadding,xDomain[1]+xPadding]).nice()
162
- .range([marginLeft, width - marginRight]);
163
-
164
- const y = d3.scaleLinear()
165
- .domain([yDomain[0]-yPadding,yDomain[1]+yPadding]).nice()
166
- .range([height - marginBottom, marginTop]);
167
-
168
- // Create the axes.
169
- svg.append("g")
170
- .attr("transform", `translate(0,${height - marginBottom})`)
171
- .call(d3.axisBottom(x).ticks(width / 80))
172
- .call(g => g.select(".domain").remove())
173
- .call(g => g.append("text")
174
- .attr("x", width)
175
- .attr("y", marginBottom - 4)
176
- .attr("fill", "currentColor")
177
- .attr("text-anchor", "end")
178
- .text("Semantic dimension 1"));
179
-
180
- svg.append("g")
181
- .attr("transform", `translate(${marginLeft},0)`)
182
- .call(d3.axisLeft(y))
183
- .call(g => g.select(".domain").remove())
184
- .call(g => g.append("text")
185
- .attr("x", -marginLeft)
186
- .attr("y", 10)
187
- .attr("fill", "currentColor")
188
- .attr("text-anchor", "start")
189
- .text("Semantic dimension 2"));
190
-
191
- // Create the grid.
192
- svg.append("g")
193
- .attr("stroke", "#cccccc")
194
- .attr("stroke-opacity", 0.5)
195
- .call(g => g.append("g")
196
- .selectAll("line")
197
- .data(x.ticks())
198
- .join("line")
199
- .attr("x1", d => 0.5 + x(d))
200
- .attr("x2", d => 0.5 + x(d))
201
- .attr("y1", marginTop)
202
- .attr("y2", height - marginBottom))
203
- .call(g => g.append("g")
204
- .selectAll("line")
205
- .data(y.ticks())
206
- .join("line")
207
- .attr("y1", d => 0.5 + y(d))
208
- .attr("y2", d => 0.5 + y(d))
209
- .attr("x1", marginLeft)
210
- .attr("x2", width - marginRight));
211
-
212
- // Add a layer of dots.
213
- svg.append("g")
214
- .attr("stroke-width", 2.5)
215
- .attr("stroke-opacity", 0.5)
216
- .attr("fill", "none")
217
- .selectAll("circle")
218
- .data(nodes)
219
- .join("circle")
220
- .attr("stroke", d => d.type == "add" ? "green" : d.type == "input"? "#666" : "red" )
221
- .attr("cx", d => x(d.x))
222
- .attr("cy", d => y(d.y))
223
- .attr("r", nodeRadius);
224
-
225
- // Add a layer of labels.
226
- svg.append("g")
227
- .attr("font-family", "sans-serif")
228
- .attr("text-opacity", 0.5)
229
- .attr("font-size", 8)
230
- .selectAll("text")
231
- .data(nodes)
232
- .join("text")
233
- .attr("dy", "0.35em")
234
- .attr("x", d => x(d.x)+5)
235
- .attr("y", d => y(d.y))
236
- .text(d => d.label)
237
- .on('mousemove', function (d, i) {
238
- d3.select(this).transition()
239
- .duration("50")
240
- .attr("text-opacity", 1.0)
241
- .attr("stroke", "white")
242
- .attr("stroke-width", 3)
243
- .style("paint-order", "stroke fill")
244
- .attr("fill", d => d.type == "add" ? "green" : d.type == "input"? "black" : "red" )
245
- tooltip.transition()
246
- .duration(50)
247
- .style("opacity", 1);
248
- tooltip.html( "<strong>" + i.label + ":</strong><br>" + i.text )
249
- .style("left", (event.pageX + 10) + "px")
250
- .style("top", (event.pageY + 10) + "px");
251
- ;
252
- })
253
- .on('mouseout', function (d, i) {
254
- d3.select(this).transition()
255
- .duration("50")
256
- .attr("text-opacity", 0.5)
257
- .attr("stroke-width", 0)
258
- .style("paint-order", "stroke fill")
259
- .attr("fill", "black")
260
- tooltip.transition()
261
- .duration(50)
262
- .style("opacity", 0);
263
- });
264
-
265
- // Adding edges
266
- svg.append("g")
267
- .selectAll("line")
268
- .data(edges)
269
- .join("line")
270
- .attr("stroke", "#666")
271
- .attr("stroke-opacity", 0.5)
272
- .attr("x1", d => x(d.source.x)+(d.source.x<d.target.x?1.3*nodeRadius:nodeRadius*-1.3))
273
- .attr("y1", d => y(d.source.y))
274
- .attr("x2", d => x(d.target.x)+(d.source.x>d.target.x?1.3*nodeRadius:nodeRadius*-1.3))
275
- .attr("y2", d => y(d.target.y))
276
- .style("stroke-dasharray", d => d.target.type == "add" ? "3,3" : "");
277
-
278
- };
279
-
280
- // ------------------------------------------------
281
-
282
- // Init state
283
- if( $( "#prompt" ).val() == "" ){
284
- $( "#generate" ).attr( "disabled", true ) ;
285
- }
286
- var last_request = Date.now() - 60 * 60 * 1000 ;
287
- var last_prompt = $( "#prompt" ).val().trim() ;
288
-
289
- // Add recommendations to the prompt
290
- function add_recommendation( p ){
291
- preview_add_recommendation( p, "hide" )
292
- $( "#prompt" ).val( $( "#prompt" ).val() + " " + p ) ;
293
- $( "#recommendation" ).html( "" ) ;
294
- $( "#prompt" ).trigger( "keyup" ) ; // Looking for recommendations after accepting a recommendation
295
- }
296
 
297
- // Preview for add recommendation
298
- function preview_add_recommendation( p, flag ){
299
- if( flag == "show" ){
300
- $( "#prompt" ).val( $( "#prompt" ).val() + " " + p ) ;
301
- }
302
- else{
303
- $( "#prompt" ).val( $( "#prompt" ).val().replace( " " + p, "" ) ) ;
304
- }
305
- }
 
 
 
 
 
306
 
307
- // Remove adversarial sentences from prompt
308
- function remove_recommendation( p ){
309
- $( "#prompt" ).val( $( "#prompt" ).val().replace( p, "" ) ) ;
310
- $( "#prompt" ).val( $( "#prompt" ).val().replace( " ", " " ) ) ;
311
- $( "#recommendation" ).html( "" ) ;
312
- $( "#prompt" ).trigger( "keyup" ) ; // Looking for recommendations after accepting a recommendation
313
- }
 
 
 
 
 
314
 
315
- // Preview for add recommendation
316
- function preview_remove_recommendation( p, flag ){
317
- if( flag == "show" ){
318
- $( "#prompt" ).data( "previous_prompt", $( "#prompt" ).val() ) ;
319
- $( "#prompt" ).val( $( "#prompt" ).val().replace( p, "" ) ) ;
320
- }
321
- else{
322
- $( "#prompt" ).val( $( "#prompt" ).data( "previous_prompt" ) ) ;
323
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
- // Listening to changes performed on the prompt input field
327
- $( "#prompt" ).on( "keyup", function( e ){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
- // Updating the generate button state based on prompt length
330
- if( $( "#prompt" ).val().length > 0 ){
331
- $( "#generate" ).removeAttr( "disabled" ) ;
332
- }
333
- else{
334
- $( "#generate" ).attr( "disabled", true ) ;
335
- }
336
 
337
- // Minimum timeout between the requests
338
- if( Date.now() - last_request > 500 && last_prompt != $( "#prompt" ).val().trim() ){
339
- last_request = Date.now() ;
340
- last_prompt = $( "#prompt" ).val().trim() ;
341
- // Getting the last typed char
342
- var last_char = $( "#prompt" ).val().trim().slice( -1 ) ;
343
-
344
- // Triggering the API request when ending of a sentence is found, e.g., .?!
345
- if( last_char == "." || last_char == "?" || last_char == "!" ){
346
- $( "#recommendation" ).html( 'Requesting recommendations: <div class="bx--tag bx--tag--gray bx--tag--deletable">...</div>' ) ;
347
- var api_url = "/recommend?prompt="
348
- // var api_url = "/recommend_local?prompt="
349
- var p = $( "#prompt" ).val() ;
350
-
351
- // API request
352
- $.getJSON( api_url + encodeURI( p ), function( data ) {
353
- $( "#recommendation" ).html( "Recommendations: " ) ;
354
-
355
- // Looking first for removal recommendations
356
- // if( data["remove"].length > 0 && data["remove"][0].similarity > 0.5 ){
357
- if( data["remove"].length > 0 ){
358
- for( var i = 0; i < data["remove"].length ; i++ ){
359
- // $( "#prompt" ).html( $( "#prompt" ).html().replace( data["remove"][i].sentence, '<span style="text-decoration: underline red">' + data["remove"][i].sentence + '</span>' ) ) ;
360
- var sentence = data["remove"][i].sentence.replaceAll( "'", "\\'" ) ;
361
- var sentence_entity = data["remove"][i].sentence.replaceAll( "'", "&apos;" ) ;
362
- var sentence_value = data["remove"][i].value.replaceAll( "'", "\\'" ) ;
363
- $( "#recommendation" ).append( '<div class="bx--tag bx--tag--red bx--tag--deletable" style="cursor: pointer;" onmouseover="preview_remove_recommendation(\''+ sentence + '\', \'show\')" onmouseout="preview_remove_recommendation(\''+ sentence + '\', \'hide\')" onclick="remove_recommendation(\''+ sentence + '\')">x ' + sentence_value + '</div>' ) ;
364
- break ; // Showing only once removal recommendation at a time
365
- }
366
- }
367
-
368
- // else if( data["add"].length > 0 ){ // After the removal recommendations are dealt with, then we show recommendations for inclusion
369
- if( data["add"].length > 0 ){ // Think Demo UX
370
- for( var i = 0; i < data["add"].length; i++ ){
371
- if( $( "#prompt" ).val().indexOf( data["add"][i].prompt ) == -1 ){
372
- // Adding only recommendations that are not present in the prompt
373
- var sentence = data["add"][i].prompt.replaceAll( "'", "\\'" ) ;
374
- var sentence_entity = data["add"][i].prompt.replaceAll( "'", "&apos;" ) ;
375
- var sentence_value = data["add"][i].value.replaceAll( "'", "\\'" ) ;
376
- $( "#recommendation" ).append( '<div class="bx--tag bx--tag--green" style="cursor: pointer;" onmouseover="preview_add_recommendation(\''+ sentence + '\', \'show\')" onmouseout="preview_add_recommendation(\''+ sentence + '\', \'hide\')" onclick="add_recommendation(\''+ sentence + '\')">+ ' + sentence_value + '</div>' ) ;
377
- }
378
- }
379
- }
380
-
381
- // User status message about recommendations found
382
- if( data["add"].length == 0 && data["remove"].length == 0 ){
383
- $( "#recommendation" ).html( "No recommendations found." ) ;
384
- }
385
-
386
- $("#prompt").data( "recommendations", data );
387
- // renderGraph(data);
388
- });
389
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
- });
392
 
393
- // Generation request
394
- $( "#demo" ).on( "submit", function(e){ // Hugging Face
395
- $( "#generate" ).toggleClass( "bx--btn--disabled" ) ;
396
- ( function loading_animation(){
397
- if( $( "#outcome" ).attr( "placeholder" ) == "" ){
398
- $( "#outcome" ).attr( "placeholder", "Requesting content." ) ;
399
- }
400
- else if( $( "#outcome" ).attr( "placeholder" ).length < 21 ){
401
- $( "#outcome" ).attr( "placeholder", $( "#outcome" ).attr( "placeholder") + "." ) ;
402
- }
403
- else{
404
- $( "#outcome" ).attr( "placeholder", "Requesting content." ) ;
405
- }
406
- setTimeout( loading_animation, 500 );
407
- } )()
408
-
409
- $.ajax({
410
- url: encodeURI("/demo_inference?prompt=" + $("#prompt").val()),
411
- dataType: 'json',
412
- success: function(data){
413
-
414
- console.log("Inference response")
415
- console.log(data)
416
- // Resetting the status of the button
417
- $( "#generate" ).toggleClass( "bx--btn--disabled" ) ;
418
-
419
- // Clearing the previous timeout
420
- if( $( "#demo" ).data( "timeoutId" ) != "" ){
421
- clearTimeout( $( "#demo" ).data( "timeoutId" ) );
422
- $( "#demo" ).data( "timeoutId", "" ) ;
423
- }
424
-
425
- out = data.content.split("");
426
- model_id = data.model_id;
427
- temperature = data.temperature
428
- max_new_tokens = data.max_new_tokens
429
-
430
- $( "#outcome" ).append( "\n\n+ ------------------------------------\n| Model: " + model_id + "\n| Temperature: " + temperature + "\n| Max new tokens: " + max_new_tokens + "\n+ ------------------------------------\n\n" ) ;
431
- // Animating the generated output
432
- ( function typing_animation(){
433
- $( "#outcome" ).append( out.shift() ) ;
434
- $( "#outcome" ).scrollTop( $( "#outcome" )[0].scrollHeight ) ;
435
- if( out.length > 0 ) {
436
- const timeoutId = setTimeout( typing_animation, 5 );
437
- $( "#demo" ).data( "timeoutId", timeoutId ) ;
438
- }
439
- } )()
440
  },
441
- error: function(data) {
442
- out = data.responseJSON.error.message
443
- $( "#outcome" ).val(out);
444
  }
445
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
- // Returning false so the form keeps user in the same page
448
- return false;
449
- });
450
- </script>
451
- <!-- <script src="https://unpkg.com/carbon-components/scripts/carbon-components.min.js"></script> -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  </body>
453
- </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Responsible Prompting Multi‐Turn Chat + Graph</title>
7
+
8
+ <!-- Carbon CSS (for tabs and tags) -->
9
+ <link
10
+ rel="stylesheet"
11
+ href="https://unpkg.com/carbon-components/css/carbon-components.min.css"
12
+ />
13
+
14
+ <style>
15
+ /* ---------- Overall Layout ---------- */
16
+ html,
17
+ body {
18
+ margin: 0;
19
+ padding: 0;
20
+ height: 100vh;
21
+ font-family: "IBM Plex Sans", sans-serif;
22
+ }
23
+ body {
24
+ display: flex;
25
+ flex-direction: column;
26
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ /* Container for the header (title + tabs) */
29
+ .header-container {
30
+ background: white;
31
+ padding: 1rem;
32
+ border-bottom: 1px solid #ddd;
33
+ }
 
 
 
 
 
 
 
 
34
 
35
+ .bx--tabs__nav {
36
+ display: flex;
37
+ list-style: none;
38
+ padding: 0;
39
+ margin: 0;
40
+ border-bottom: 2px solid #c6c6c6;
41
+ }
42
+ .bx--tabs__nav-item {
43
+ margin-right: 1rem;
44
+ padding: 0.5rem 0.75rem;
45
+ cursor: pointer;
46
+ font-size: 1rem;
47
+ color: #393939;
48
+ border-bottom: 2px solid transparent;
49
+ }
50
+ .bx--tabs__nav-item--selected {
51
+ color: #0f62fe;
52
+ border-bottom: 2px solid #0f62fe;
53
+ }
54
 
55
+ /* The “content” area below header: either chat or graph */
56
+ .tab-content {
57
+ flex: 1;
58
+ display: flex;
59
+ flex-direction: column;
60
+ background: #f4f4f4;
61
+ overflow: hidden;
62
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ /* When Chat is selected: show its container; Graph is hidden */
65
+ #chat-content {
66
+ display: flex;
67
+ flex-direction: column;
68
+ flex: 1;
69
+ overflow: hidden;
70
+ }
71
+ #graph-content {
72
+ display: none;
73
+ flex: 1;
74
+ overflow: auto;
75
+ background: #fff;
76
+ padding: 1rem;
77
+ }
78
 
79
+ /* ---------- Intro Paragraph ---------- */
80
+ .intro {
81
+ background: white;
82
+ padding: 1rem;
83
+ border-bottom: 1px solid #ddd;
84
+ font-size: 0.95rem;
85
+ line-height: 1.5;
86
+ color: #393939;
87
+ }
88
+ .intro p {
89
+ margin: 0.5rem 0;
90
+ }
91
 
92
+ /* ---------- Chat Area ---------- */
93
+ .chat-container {
94
+ flex: 1;
95
+ overflow-y: auto;
96
+ padding: 0.5rem 1rem; /* reduced top padding to avoid overlap */
97
+ display: flex;
98
+ flex-direction: column;
99
+ }
100
+
101
+ .message {
102
+ max-width: 70%;
103
+ margin-bottom: 0.75rem;
104
+ padding: 0.5rem 0.75rem;
105
+ border-radius: 0.5rem;
106
+ line-height: 1.4;
107
+ word-wrap: break-word;
108
+ white-space: pre-wrap;
109
+ }
110
+ .message.user {
111
+ background-color: #0f62fe;
112
+ color: white;
113
+ align-self: flex-end;
114
+ }
115
+ .message.assistant {
116
+ background-color: #e0e0e0;
117
+ color: #161616;
118
+ align-self: flex-start;
119
+ }
120
+
121
+ .recs-container {
122
+ display: flex;
123
+ flex-wrap: wrap;
124
+ margin-bottom: 0.75rem;
125
+ margin-right: 0.25rem;
126
+ }
127
+ .recs-item {
128
+ font-size: 0.85rem;
129
+ padding: 0.25rem 0.5rem;
130
+ border-radius: 0.25rem;
131
+ margin-right: 0.25rem;
132
+ margin-bottom: 0.25rem;
133
+ color: white;
134
+ }
135
+ .recs-item.add {
136
+ background-color: #24a148; /* green */
137
+ }
138
+ .recs-item.remove {
139
+ background-color: #da1e28; /* red */
140
+ }
141
+
142
+ /* ---------- Input Area ---------- */
143
+ .input-area {
144
+ border-top: 1px solid #ddd;
145
+ padding: 0.5rem 1rem;
146
+ display: flex;
147
+ align-items: flex-end;
148
+ background: white;
149
+ }
150
+
151
+ #userInput {
152
+ flex: 1;
153
+ resize: none;
154
+ font-size: 1rem;
155
+ padding: 0.5rem;
156
+ border: 1px solid #c6c6c6;
157
+ border-radius: 0.375rem;
158
+ font-family: inherit;
159
+ line-height: 1.4;
160
+ }
161
+
162
+ #recommendation {
163
+ margin-top: 0.25rem;
164
+ font-size: 0.9rem;
165
+ color: #393939;
166
+ display: flex;
167
+ flex-wrap: wrap;
168
+ }
169
+ #recommendation .bx--tag {
170
+ margin-right: 0.25rem;
171
+ margin-bottom: 0.25rem;
172
+ cursor: pointer;
173
+ }
174
+
175
+ #sendBtn {
176
+ margin-left: 0.5rem;
177
+ margin-bottom: 0.25rem;
178
+ }
179
+
180
+ /* ---------- Graph Area ---------- */
181
+ #graph {
182
+ background: #efefef;
183
+ position: relative;
184
+ }
185
+ .tooltip {
186
+ position: absolute;
187
+ text-align: left;
188
+ padding: 0.5em;
189
+ width: 20em;
190
+ min-height: 5em;
191
+ background: #fff;
192
+ color: #000;
193
+ border: 1px solid #000;
194
+ border-radius: 5px;
195
+ pointer-events: none;
196
+ font-size: inherit;
197
+ font-family: inherit;
198
+ }
199
+ </style>
200
+ </head>
201
+ <body>
202
+ <!-- ===== Header: Title + Carbon Tabs ===== -->
203
+ <div class="header-container">
204
+ <h1 class="bx--type-expressive-heading-03">Responsible Prompting</h1>
205
+ <div class="intro">
206
+ <p>
207
+ Please provide a prompt that would be sent to an LLM. Recommendations are performed in prompting-time, before content generation. The recommendations consider a curated dataset of values and prompt-sentences. They are based on the similarity between your input and that dataset.
208
+ </p>
209
+ </div>
210
+ <ul class="bx--tabs__nav">
211
+ <li id="tab-chat" class="bx--tabs__nav-item bx--tabs__nav-item--selected">
212
+ Chat
213
+ </li>
214
+ <li id="tab-graph" class="bx--tabs__nav-item">
215
+ Graph
216
+ </li>
217
+ </ul>
218
+ </div>
219
+
220
+ <!-- ===== Tab Content: Chat or Graph ===== -->
221
+ <div class="tab-content">
222
+ <!-- Chat Content -->
223
+ <div id="chat-content">
224
+ <!-- Intro paragraph about responsible prompting -->
225
+
226
+
227
+ <!-- Chat container (messages appear here) -->
228
+ <div id="chat" class="chat-container"></div>
229
+
230
+ <!-- Input area -->
231
+ <div class="input-area">
232
+ <div style="flex: 1; display: flex; flex-direction: column;">
233
+ <textarea
234
+ id="userInput"
235
+ rows="4"
236
+ placeholder="Enter your prompt"
237
+ >Act as a professional designer with 20 years of experience creating and testing UX interfaces and landing sites for a variety of IT applications. We are in need of more people and an increased budget to be able to keep up with clients' needs. What kind of evidence should I gather to support my demands to gain more resources?</textarea>
238
+ <div id="recommendation"></div>
239
+ </div>
240
+ <button id="sendBtn" class="bx--btn bx--btn--primary" disabled>
241
+ Send
242
+ </button>
243
+ </div>
244
+ </div>
245
+
246
+ <!-- Graph Content -->
247
+ <div id="graph-content">
248
+ <div id="graph"></div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- Dependencies -->
253
+ <script type="text/javascript" src="static/demo/js/d3.v7.min.js"></script>
254
+ <script type="text/javascript" src="static/demo/js/jquery-3.7.1.min.js"></script>
255
+
256
+ <script>
257
+ // =============================
258
+ // 1. State & Dimensions for D3
259
+ // =============================
260
+ let lastRecommendations = null; // stores latest /recommend response
261
+
262
+ const width = 600;
263
+ const height = 300;
264
+ const marginTop = 30;
265
+ const marginRight = 30;
266
+ const marginBottom = 30;
267
+ const marginLeft = 30;
268
+ const nodeRadius = 3;
269
+
270
+ // Create SVG once
271
+ const svg = d3
272
+ .select("#graph")
273
+ .append("svg")
274
+ .attr("viewBox", `0 0 ${width} ${height}`)
275
+ .attr("style", "max-width: 100%; height: auto; font: 8px sans-serif;");
276
+
277
+ const tooltip = d3
278
+ .select("body")
279
+ .append("div")
280
+ .attr("class", "tooltip")
281
+ .style("opacity", 0);
282
+
283
+ function renderGraph(recommendations) {
284
+ if (!recommendations) {
285
+ svg.selectAll("*").remove();
286
+ svg
287
+ .append("text")
288
+ .attr("x", width / 2)
289
+ .attr("y", height / 2)
290
+ .attr("text-anchor", "middle")
291
+ .attr("fill", "#666")
292
+ .text("No recommendation data available");
293
+ return;
294
+ }
295
+
296
+ const rec = recommendations;
297
+ const graphData = { nodes: [], edges: [] };
298
+ let i = 0,
299
+ j = 0;
300
+
301
+ // Input sentences
302
+ if (rec.input && rec.input.length > 0) {
303
+ graphData.nodes.push({
304
+ id: 0,
305
+ x: Number(rec.input[0].x),
306
+ y: Number(rec.input[0].y),
307
+ text: rec.input[0].sentence,
308
+ label: "S1",
309
+ type: "input",
310
+ });
311
+ for (i = 1; i < rec.input.length; i++) {
312
+ graphData.nodes.push({
313
+ id: i,
314
+ x: Number(rec.input[i].x),
315
+ y: Number(rec.input[i].y),
316
+ text: rec.input[i].sentence,
317
+ label: `S${i + 1}`,
318
+ type: "input",
319
+ });
320
+ graphData.edges.push({ source: i - 1, target: i, type: "input" });
321
  }
322
+ }
323
+
324
+ // “Add” recommendations
325
+ if (rec.add && rec.add.length > 0) {
326
+ for (j = 0; j < rec.add.length; j++) {
327
+ graphData.nodes.push({
328
+ id: i + j,
329
+ x: Number(rec.add[j].x),
330
+ y: Number(rec.add[j].y),
331
+ text: rec.add[j].prompt,
332
+ label: rec.add[j].value,
333
+ type: "add",
334
+ });
335
+ graphData.edges.push({
336
+ source: i - 1,
337
+ target: i + j,
338
+ type: "add",
339
+ });
340
+ }
341
+ }
342
+
343
+ // “Remove” recommendation (first only)
344
+ if (rec.remove && rec.remove.length > 0) {
345
+ graphData.nodes.push({
346
+ id: i + j,
347
+ x: Number(rec.remove[0].x),
348
+ y: Number(rec.remove[0].y),
349
+ text: rec.remove[0].closest_harmful_sentence,
350
+ label: rec.remove[0].value,
351
+ type: "remove",
352
+ });
353
+ }
354
+
355
+ graphData.edges = graphData.edges.map((e) => ({
356
+ source: graphData.nodes.find((n) => n.id === e.source),
357
+ target: graphData.nodes.find((n) => n.id === e.target),
358
+ type: e.type,
359
+ }));
360
+
361
+ const { nodes, edges } = graphData;
362
+ if (nodes.length === 0) {
363
+ svg.selectAll("*").remove();
364
+ svg
365
+ .append("text")
366
+ .attr("x", width / 2)
367
+ .attr("y", height / 2)
368
+ .attr("text-anchor", "middle")
369
+ .attr("fill", "#666")
370
+ .text("No nodes to display");
371
+ return;
372
+ }
373
+
374
+ const xDomain = d3.extent(nodes, (d) => d.x);
375
+ const yDomain = d3.extent(nodes, (d) => d.y);
376
+ const xPadding = 2;
377
+ const yPadding = 2;
378
+
379
+ const xScale = d3
380
+ .scaleLinear()
381
+ .domain([xDomain[0] - xPadding, xDomain[1] + xPadding])
382
+ .nice()
383
+ .range([marginLeft, width - marginRight]);
384
+
385
+ const yScale = d3
386
+ .scaleLinear()
387
+ .domain([yDomain[0] - yPadding, yDomain[1] + yPadding])
388
+ .nice()
389
+ .range([height - marginBottom, marginTop]);
390
+
391
+ svg.selectAll("*").remove();
392
+
393
+ // X axis
394
+ svg
395
+ .append("g")
396
+ .attr("transform", `translate(0, ${height - marginBottom})`)
397
+ .call(d3.axisBottom(xScale).ticks(width / 80))
398
+ .call((g) => g.select(".domain").remove())
399
+ .call((g) =>
400
+ g
401
+ .append("text")
402
+ .attr("x", width)
403
+ .attr("y", marginBottom - 4)
404
+ .attr("fill", "currentColor")
405
+ .attr("text-anchor", "end")
406
+ .text("Semantic dimension 1")
407
+ );
408
+
409
+ // Y axis
410
+ svg
411
+ .append("g")
412
+ .attr("transform", `translate(${marginLeft}, 0)`)
413
+ .call(d3.axisLeft(yScale))
414
+ .call((g) => g.select(".domain").remove())
415
+ .call((g) =>
416
+ g
417
+ .append("text")
418
+ .attr("x", -marginLeft)
419
+ .attr("y", 10)
420
+ .attr("fill", "currentColor")
421
+ .attr("text-anchor", "start")
422
+ .text("Semantic dimension 2")
423
+ );
424
+
425
+ // Grid
426
+ svg
427
+ .append("g")
428
+ .attr("stroke", "#cccccc")
429
+ .attr("stroke-opacity", 0.5)
430
+ .call((g) =>
431
+ g
432
+ .append("g")
433
+ .selectAll("line")
434
+ .data(xScale.ticks())
435
+ .join("line")
436
+ .attr("x1", (d) => 0.5 + xScale(d))
437
+ .attr("x2", (d) => 0.5 + xScale(d))
438
+ .attr("y1", marginTop)
439
+ .attr("y2", height - marginBottom)
440
+ )
441
+ .call((g) =>
442
+ g
443
+ .append("g")
444
+ .selectAll("line")
445
+ .data(yScale.ticks())
446
+ .join("line")
447
+ .attr("y1", (d) => 0.5 + yScale(d))
448
+ .attr("y2", (d) => 0.5 + yScale(d))
449
+ .attr("x1", marginLeft)
450
+ .attr("x2", width - marginRight)
451
+ );
452
+
453
+ // Edges
454
+ svg
455
+ .append("g")
456
+ .selectAll("line")
457
+ .data(edges)
458
+ .join("line")
459
+ .attr("stroke", "#666")
460
+ .attr("stroke-opacity", 0.5)
461
+ .attr(
462
+ "x1",
463
+ (d) =>
464
+ xScale(d.source.x) +
465
+ (d.source.x < d.target.x ? 1.3 * nodeRadius : -1.3 * nodeRadius)
466
+ )
467
+ .attr("y1", (d) => yScale(d.source.y))
468
+ .attr(
469
+ "x2",
470
+ (d) =>
471
+ xScale(d.target.x) +
472
+ (d.source.x > d.target.x ? 1.3 * nodeRadius : -1.3 * nodeRadius)
473
+ )
474
+ .attr("y2", (d) => yScale(d.target.y))
475
+ .style("stroke-dasharray", (d) =>
476
+ d.target.type === "add" ? "3,3" : ""
477
+ );
478
+
479
+ // Nodes
480
+ svg
481
+ .append("g")
482
+ .attr("stroke-width", 2.5)
483
+ .attr("stroke-opacity", 0.5)
484
+ .attr("fill", "none")
485
+ .selectAll("circle")
486
+ .data(nodes)
487
+ .join("circle")
488
+ .attr("stroke", (d) =>
489
+ d.type === "add" ? "green" : d.type === "input" ? "#666" : "red"
490
+ )
491
+ .attr("cx", (d) => xScale(d.x))
492
+ .attr("cy", (d) => yScale(d.y))
493
+ .attr("r", nodeRadius);
494
+
495
+ // Labels
496
+ svg
497
+ .append("g")
498
+ .attr("font-family", "sans-serif")
499
+ .attr("text-opacity", 0.5)
500
+ .attr("font-size", 8)
501
+ .selectAll("text")
502
+ .data(nodes)
503
+ .join("text")
504
+ .attr("dy", "0.35em")
505
+ .attr("x", (d) => xScale(d.x) + 5)
506
+ .attr("y", (d) => yScale(d.y))
507
+ .text((d) => d.label)
508
+ .on("mousemove", function (event, d) {
509
+ d3.select(this)
510
+ .transition()
511
+ .duration(50)
512
+ .attr("text-opacity", 1.0)
513
+ .attr("stroke", "white")
514
+ .attr("stroke-width", 3)
515
+ .style("paint-order", "stroke fill")
516
+ .attr(
517
+ "fill",
518
+ d.type === "add"
519
+ ? "green"
520
+ : d.type === "input"
521
+ ? "black"
522
+ : "red"
523
+ );
524
+ tooltip.transition().duration(50).style("opacity", 1);
525
+ tooltip
526
+ .html(`<strong>${d.label}:</strong><br/>${d.text}`)
527
+ .style("left", event.pageX + 10 + "px")
528
+ .style("top", event.pageY + 10 + "px");
529
+ })
530
+ .on("mouseout", function () {
531
+ d3.select(this)
532
+ .transition()
533
+ .duration(50)
534
+ .attr("text-opacity", 0.5)
535
+ .attr("stroke-width", 0)
536
+ .style("paint-order", "fill")
537
+ .attr("fill", "black");
538
+ tooltip.transition().duration(50).style("opacity", 0);
539
+ });
540
+ }
541
 
542
+ // Initially show “no data” message
543
+ renderGraph(null);
544
+
545
+ // =============================
546
+ // 2. Multi‐Turn Chat + Recommendation Logic
547
+ // =============================
548
+ const conversation = []; // stores { role, content, recs? }
549
+ let currentRecs = [];
550
+ let debounceId = null;
551
+
552
+ function appendUserTurn(turn) {
553
+ const bubble = $("<div>")
554
+ .addClass("message user")
555
+ .text(turn.content);
556
+ $("#chat").append(bubble);
557
+
558
+ if (turn.recs && turn.recs.length > 0) {
559
+ const container = $("<div>").addClass("recs-container");
560
+ turn.recs.forEach((r) => {
561
+ const item = $("<div>")
562
+ .addClass("recs-item")
563
+ .addClass(r.type === "add" ? "add" : "remove")
564
+ .text(r.value);
565
+ container.append(item);
566
+ });
567
+ container.css("align-self", "flex-end");
568
+ $("#chat").append(container);
569
+ }
570
+ $("#chat").scrollTop($("#chat")[0].scrollHeight);
571
+ }
572
 
573
+ function appendAssistantTurn(text) {
574
+ const bubble = $("<div>")
575
+ .addClass("message assistant")
576
+ .text(text);
577
+ $("#chat").append(bubble);
578
+ $("#chat").scrollTop($("#chat")[0].scrollHeight);
579
+ }
580
 
581
+ $("#userInput").on("input", function () {
582
+ const txt = $(this).val();
583
+ const trimmed = txt.trim();
584
+
585
+ if (trimmed.length > 0) {
586
+ $("#sendBtn").removeAttr("disabled");
587
+ } else {
588
+ $("#sendBtn").attr("disabled", true);
589
+ $("#recommendation").empty();
590
+ }
591
+
592
+ clearTimeout(debounceId);
593
+
594
+ if (txt.length > 0 && /[.?!]$/.test(txt)) {
595
+ debounceId = setTimeout(() => {
596
+ $("#recommendation").html(
597
+ 'Checking recommendations… <div class="bx--tag bx--tag--gray bx--tag--deletable">…</div>'
598
+ );
599
+ $.getJSON("/recommend?prompt=" + encodeURIComponent(txt), (data) => {
600
+ $("#recommendation").empty();
601
+ lastRecommendations = data;
602
+
603
+ if (data.remove && data.remove.length > 0) {
604
+ const rec = data.remove[0];
605
+ const sentence = rec.sentence.replaceAll("'", "\\'");
606
+ const valueEscaped = rec.value.replaceAll("'", "\\'");
607
+ const $tag = $(`
608
+ <div class="bx--tag bx--tag--red bx--tag--deletable">
609
+ ✕ ${valueEscaped}
610
+ </div>
611
+ `);
612
+
613
+ $tag.hover(
614
+ () => {
615
+ const cur = $("#userInput").val();
616
+ $("#userInput").data("prevText", cur);
617
+ $("#userInput").val(cur.replace(rec.sentence, ""));
618
+ },
619
+ () => {
620
+ const prev = $("#userInput").data("prevText") || txt;
621
+ $("#userInput").val(prev);
 
 
 
 
 
 
 
 
 
 
 
622
  }
623
+ );
624
+
625
+ $tag.click(() => {
626
+ const updated = $("#userInput")
627
+ .val()
628
+ .replace(rec.sentence, "")
629
+ .replace(/ {2,}/g, " ")
630
+ .trim();
631
+ $("#userInput").val(updated);
632
+ currentRecs.push({ type: "remove", value: rec.value });
633
+ $("#recommendation").empty();
634
+ $("#userInput").trigger("input");
635
+ });
636
+
637
+ $("#recommendation").append($tag);
638
  }
 
639
 
640
+ if (data.add && data.add.length > 0) {
641
+ data.add.forEach((rec) => {
642
+ if (!$("#userInput").val().includes(rec.prompt)) {
643
+ const promptEscaped = rec.prompt.replaceAll("'", "\\'");
644
+ const valueEscaped = rec.value.replaceAll("'", "\\'");
645
+ const $tag = $(`
646
+ <div class="bx--tag bx--tag--green">
647
+ + ${valueEscaped}
648
+ </div>
649
+ `);
650
+
651
+ $tag.hover(
652
+ () => {
653
+ const cur = $("#userInput").val();
654
+ $("#userInput").data("prevAdd", cur);
655
+ $("#userInput").val((cur + " " + rec.prompt).trim());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  },
657
+ () => {
658
+ const prev = $("#userInput").data("prevAdd") || txt;
659
+ $("#userInput").val(prev);
660
  }
661
+ );
662
+
663
+ $tag.click(() => {
664
+ const base =
665
+ $("#userInput").data("prevAdd") ||
666
+ $("#userInput").val().trim();
667
+ $("#userInput").val((base + " " + rec.prompt).trim());
668
+ currentRecs.push({ type: "add", value: rec.value });
669
+ $("#recommendation").empty();
670
+ $("#userInput").trigger("input");
671
+ });
672
+
673
+ $("#recommendation").append($tag);
674
+ }
675
+ });
676
+ }
677
 
678
+ if (
679
+ (!data.add || data.add.length === 0) &&
680
+ (!data.remove || data.remove.length === 0)
681
+ ) {
682
+ $("#recommendation").text("No recommendations found.");
683
+ }
684
+ });
685
+ }, 500);
686
+ } else {
687
+ $("#recommendation").empty();
688
+ lastRecommendations = null;
689
+ }
690
+ });
691
+
692
+ $("#sendBtn").on("click", function () {
693
+ const rawText = $("#userInput").val();
694
+ const userText = rawText.trim();
695
+ if (!userText) return;
696
+
697
+ const thisTurn = {
698
+ role: "user",
699
+ content: userText,
700
+ recs: currentRecs.slice(),
701
+ };
702
+ conversation.push(thisTurn);
703
+ appendUserTurn(thisTurn);
704
+
705
+ $("#userInput").val("");
706
+ $("#recommendation").empty();
707
+ $("#sendBtn").attr("disabled", true);
708
+ currentRecs = [];
709
+
710
+ const typingBubble = $("<div>")
711
+ .addClass("message assistant")
712
+ .attr("id", "typing")
713
+ .text("Assistant is typing…");
714
+ $("#chat").append(typingBubble);
715
+ $("#chat").scrollTop($("#chat")[0].scrollHeight);
716
+
717
+ let fullPrompt = "";
718
+ conversation.forEach((turn) => {
719
+ if (turn.role === "user") {
720
+ fullPrompt += `User: ${turn.content}\n`;
721
+ } else {
722
+ fullPrompt += `Assistant: ${turn.content}\n`;
723
+ }
724
+ });
725
+ fullPrompt += "Assistant: ";
726
+
727
+ $.ajax({
728
+ url: "/demo_inference?prompt=" + encodeURIComponent(fullPrompt),
729
+ dataType: "json",
730
+ success: function (data) {
731
+ $("#typing").remove();
732
+ const generated = data.content;
733
+ const modelId = data.model_id;
734
+ const temp = data.temperature;
735
+ const maxTokens = data.max_new_tokens;
736
+
737
+ let toType = `Model: ${modelId} · Temperature: ${temp} · Max tokens: ${maxTokens}\n\n`;
738
+ toType += generated;
739
+
740
+ const assistantBubble = $("<div>")
741
+ .addClass("message assistant")
742
+ .attr("id", "assistantBubble")
743
+ .text("");
744
+ $("#chat").append(assistantBubble);
745
+ $("#chat").scrollTop($("#chat")[0].scrollHeight);
746
+
747
+ const chars = toType.split("");
748
+ let idx = 0;
749
+ function typeNext() {
750
+ if (idx < chars.length) {
751
+ const cur = $("#assistantBubble").text();
752
+ $("#assistantBubble").text(cur + chars[idx]);
753
+ idx++;
754
+ $("#chat").scrollTop($("#chat")[0].scrollHeight);
755
+ setTimeout(typeNext, 10);
756
+ } else {
757
+ $("#assistantBubble").removeAttr("id");
758
+ conversation.push({
759
+ role: "assistant",
760
+ content: generated,
761
+ });
762
+ }
763
+ }
764
+ typeNext();
765
+ },
766
+ error: function (xhr) {
767
+ $("#typing").remove();
768
+ const err =
769
+ xhr.responseJSON?.error?.message ||
770
+ "Unknown error from inference.";
771
+ appendAssistantTurn(`Error: ${err}`);
772
+ },
773
+ });
774
+ });
775
+
776
+ $("#userInput").on("keydown", function (e) {
777
+ if (e.key === "Enter" && !e.shiftKey) {
778
+ e.preventDefault();
779
+ if (!$("#sendBtn").attr("disabled")) {
780
+ $("#sendBtn").click();
781
+ }
782
+ }
783
+ });
784
+
785
+ // =============================
786
+ // 3. Tab Switching (Chat <-> Graph)
787
+ // =============================
788
+ $("#tab-chat").on("click", function () {
789
+ $(this).addClass("bx--tabs__nav-item--selected");
790
+ $("#tab-graph").removeClass("bx--tabs__nav-item--selected");
791
+ $("#chat-content").show();
792
+ $("#graph-content").hide();
793
+ });
794
+
795
+ $("#tab-graph").on("click", function () {
796
+ $(this).addClass("bx--tabs__nav-item--selected");
797
+ $("#tab-chat").removeClass("bx--tabs__nav-item--selected");
798
+ $("#chat-content").hide();
799
+ $("#graph-content").show();
800
+ renderGraph(lastRecommendations);
801
+ });
802
+ </script>
803
  </body>
804
+ </html>