alexanderHSG commited on
Commit
3bbdf91
·
1 Parent(s): 03d8e0d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -87
app.py CHANGED
@@ -75,7 +75,7 @@ def slide_deck_storyline(storyline_prompt, nr_of_storypoints=5):
75
  """
76
 
77
  response = openai.chat.completions.create(
78
- model = "gpt-4o-mini",
79
  response_format = {"type": "json_object"},
80
  messages = [
81
  {"role": "system", "content": system_prompt},
@@ -94,18 +94,53 @@ def slide_deck_storyline(storyline_prompt, nr_of_storypoints=5):
94
 
95
  return map, storypoint_name_nested
96
 
97
- #we need this function to turn the non iterable nested list that is gr.List into a simple list.
98
- def iterator_for_gr(nested_list, i):
99
- #Initialize a variable to store the processing result
100
- storypoint_names = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- #since the gr.List is a List[List] (nested list, we need to unwrap the 0th element)
103
- for item in nested_list[0]:
104
- storypoint_names.append(item)
105
-
106
 
107
- # Return a string that combines all the processed results
108
- return str(storypoint_names[i-1])
109
 
110
 
111
 
@@ -179,18 +214,26 @@ def coordinate_simcalculation(storyline_output_storypoint_name_list):
179
  for new_id, existing_id, similarity in highest_similarities:
180
  print(f"Input STORYPOINT '{new_id}' is most similar to existing STORYPOINT '{existing_id}' with a similarity of {similarity:.2f}")
181
 
182
- HTMLoutput = construct_hmtl(highest_similarities)
183
 
184
- return HTMLoutput, highest_similarities
185
 
186
- def construct_hmtl(highest_similarities, nodes_to_show=["SLIDE_DECK", "SLIDE", "STORYPOINT"]):
 
 
 
 
187
 
188
- storypoint_ids = [existing_id for _, existing_id, _ in highest_similarities]
189
- print(storypoint_ids)
190
 
 
 
 
 
191
 
192
- # Starting with the base of the query
193
- query_parts = [
 
194
  f"WITH {storypoint_ids} AS ids",
195
  "MATCH (sp:STORYPOINT) WHERE sp.id IN ids",
196
  "WITH sp",
@@ -201,35 +244,36 @@ def construct_hmtl(highest_similarities, nodes_to_show=["SLIDE_DECK", "SLIDE", "
201
  "CALL apoc.create.vRelationship(sp_start, 'FOLLOWS', {}, sp_end) YIELD rel",
202
  "WITH sps, sp_start, rel, sp_end",
203
  "UNWIND sps AS sp"
204
- ]
205
 
206
  # Initialize the match and return parts of the query
207
- match_parts = []
208
- return_parts = []
209
 
210
- # Include virtual relationship and its nodes conditionally
211
- if "STORYPOINT" in nodes_to_show:
212
- return_parts.extend(["sp_start", "rel", "sp_end", "sp"])
213
 
214
  # Conditionally add SLIDE and SLIDE_DECK with their relationships
215
- if "SLIDE" in nodes_to_show or "SLIDE_DECK" in nodes_to_show:
216
- match_parts.append("(sp)<-[r1:ASSIGNED_TO]-(s:SLIDE)")
217
- return_parts.extend(["s", "r1"])
218
- if "SLIDE_DECK" in nodes_to_show:
219
- match_parts.append("<-[r2:CONTAINS]-(sd:SLIDE_DECK)")
220
- return_parts.extend(["sd", "r2"])
221
-
222
- # Construct the final query
223
- query = "\n".join(query_parts)
224
- if match_parts:
225
- query += "\nMATCH " + "".join(match_parts)
226
- if return_parts:
227
- query += "\nRETURN " + ", ".join(return_parts)
228
- else:
229
- query += "\nRETURN 'No nodes to show based on the selected types'"
230
 
231
 
232
 
 
233
  graphVisualHTML = f"""
234
 
235
  <head>
@@ -243,10 +287,10 @@ def construct_hmtl(highest_similarities, nodes_to_show=["SLIDE_DECK", "SLIDE", "
243
  margin: 0; /* Remove default margin */
244
  }}
245
  #viz {{
246
- width: 1200px;
247
  height: 700px;
248
- background-color: #f0f0f0; /* Lighter grey background for the viz div */
249
- padding: 10px; /* Adds padding inside the div */
250
  }}
251
  .heading {{
252
  font-size: 24px;
@@ -254,7 +298,7 @@ def construct_hmtl(highest_similarities, nodes_to_show=["SLIDE_DECK", "SLIDE", "
254
  margin-bottom: 20px;
255
  }}
256
  #queryCypher {{
257
- opacity: 0;
258
  }}
259
  </style>
260
  </head>
@@ -266,15 +310,10 @@ def construct_hmtl(highest_similarities, nodes_to_show=["SLIDE_DECK", "SLIDE", "
266
  <p id="queryCypher">{query}</p>
267
  </div>
268
 
269
- <div class="custom-menu" style="display: none; position: absolute; z-index: 1000; background: white; border: 1px solid #ccc; padding: 5px; box-shadow: 2px 2px 5px #888;">
270
- <ul>
271
- <li onclick="alert('Action 1')">Action 1</li>
272
- <li onclick="alert('Action 2')">Action 2</li>
273
- </ul>
274
- </div>
275
  </body>
276
  """
277
- return graphVisualHTML
278
 
279
  scripts = """
280
 
@@ -325,7 +364,7 @@ async () => {
325
  font: {
326
  color: 'black',
327
  size: 14, // Pixel size
328
- face: 'Helvetica' // Uniform font across all graph elements
329
  }
330
  }
331
  }
@@ -347,7 +386,7 @@ async () => {
347
  font: {
348
  color: 'black',
349
  size: 14, // Pixel size
350
- face: 'Helvetica' // Uniform font across all graph elements
351
  }
352
  }
353
  }
@@ -365,7 +404,7 @@ async () => {
365
  font: {
366
  color: '#2c3e50', // Dark grey color for strong contrast against light background
367
  size: 14, // Larger font size for enhanced readability
368
- face: 'Helvetica' // Modern font for a clean appearance
369
  },
370
  dashes: false, // Solid line to indicate a strong, permanent relationship
371
  }
@@ -380,7 +419,7 @@ async () => {
380
  font: {
381
  color: '#2c3e50', // Dark grey to maintain visibility and consistency
382
  size: 14,
383
- face: 'Helvetica'
384
  },
385
  arrows: {
386
  to: { enabled: true, scaleFactor: 1.2 } // Prominent arrow for visual emphasis
@@ -397,7 +436,7 @@ async () => {
397
  font: {
398
  color: '#2c3e50', // Dark grey to ensure readability on light backgrounds
399
  size: 14,
400
- face: 'Helvetica'
401
  },
402
  arrows: {
403
  to: { enabled: true, scaleFactor: 1.5 } // Larger arrow to denote directionality
@@ -430,20 +469,20 @@ async () => {
430
  try {
431
  viz = new NeoVis.default(config);
432
  viz.render();
433
- viz.registerOnEvent("completed", () => {
434
- viz.network.on("oncontext", function (params) {
435
- params.event.preventDefault();
436
- const customMenu = document.querySelector('.custom-menu');
437
-
438
- if (customMenu) {
439
- console.log("Displaying custom menu.");
440
- const containerRect = document.getElementById('viz').getBoundingClientRect();
441
- customMenu.style.display = 'block';
442
- customMenu.style.top = `${params.event.pageY - containerRect.top + window.scrollY}px`;
443
- customMenu.style.left = `${params.event.pageX - containerRect.left + window.scrollX}px`;
444
- }
445
- });
446
- });
447
 
448
 
449
  } catch (error) {
@@ -465,7 +504,7 @@ async () => {
465
 
466
 
467
 
468
- js_click = """
469
  <script>
470
 
471
  // Function to handle the mutations
@@ -505,6 +544,7 @@ console.log("Observer is set to monitor changes in the document body.");
505
  </script>
506
  """
507
 
 
508
  css = """
509
  #SPList {
510
  font-family: 'Arial', sans-serif;
@@ -529,29 +569,49 @@ highest_similarities_gradio_list = gr.List(type="array", interactive=False, visi
529
  nodeSelector = gr.Dropdown(label="Filter nodes", choices=["SLIDE_DECK", "SLIDE", "STORYPOINT"], value=["SLIDE_DECK", "SLIDE", "STORYPOINT"], multiselect=True, scale=1)
530
  filterBTN = gr.Button("Apply Filter")
531
 
532
- with gr.Blocks(title='Slide Inspo', js=scripts, head = js_click, theme = gr.themes.Monochrome()).queue(default_concurrency_limit=1) as demo:
533
-
534
  highest_similarities_gradio_list.render()
535
  with gr.Row():
 
 
 
 
536
  with gr.Column(scale=1):
537
- gr.Markdown("# 1. Input: 🔍")
 
 
 
 
 
 
538
  storyline_prompt = gr.Textbox(placeholder = 'Give us a topic and we will provide a storyline for you! For example: "SCRUM in Software Development"',
539
  label = 'Topic to build:',
540
  lines=5,
541
  scale = 3)
542
  nr_storypoints_to_build = gr.Number(value=5,
543
- label="How many storypoints?",
544
  scale =1)
545
  storyline_output_JSON = gr.JSON(visible=False)
546
 
547
  btn = gr.Button("Build Storyline 🦄")
548
 
549
  with gr.Column(scale=1):
550
- gr.Markdown("# 2. Storyline: 🦄")
551
- storyline_output_storypoint_name_list = gr.List(visible=True, type="array", interactive=True, label="Adapt and add Storypoints, if needed: 📝", scale=1, wrap=True, col_count=[2, "fixed"], elem_id="SPList", headers=["#SP", "Description"])
 
 
 
 
 
 
 
 
 
 
552
  #storyline_output_pretty = gr.Textbox(label="Your Storyline:", lines=13, scale=3, interactive=False)
553
  submit_button = gr.Button("⚡ Find Slides ⚡", elem_id="visGraph")
554
- submit_button.click(fn= coordinate_simcalculation, inputs=[storyline_output_storypoint_name_list], outputs=[graphVisual, highest_similarities_gradio_list]).then(js = js_click)
555
 
556
 
557
 
@@ -563,19 +623,33 @@ with gr.Blocks(title='Slide Inspo', js=scripts, head = js_click, theme = gr.them
563
  inputs = [storyline_prompt, nr_storypoints_to_build],
564
  outputs = [storyline_output_JSON, storyline_output_storypoint_name_list])
565
 
 
566
 
567
- with gr.Row():
568
- with gr.Group():
569
- with gr.Column(scale=1):
570
- nodeSelector.render()
571
- with gr.Column(scale=1):
572
- filterBTN.render()
573
- filterBTN.click(fn= construct_hmtl, inputs=[highest_similarities_gradio_list, nodeSelector], outputs=[graphVisual]).then(js = js_click)
574
-
575
- with gr.Row():
576
- graphVisual.render()
577
 
578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
 
580
 
581
 
 
75
  """
76
 
77
  response = openai.chat.completions.create(
78
+ model = "gpt-4o",
79
  response_format = {"type": "json_object"},
80
  messages = [
81
  {"role": "system", "content": system_prompt},
 
94
 
95
  return map, storypoint_name_nested
96
 
97
+ #this is a prompt that takes a filter prompt and formats an output in json to return a filter cypress query.
98
+ def custom_filtering(filter_prompt, current_cypher_query, neo4j_response):
99
+
100
+
101
+ system_prompt = f"""You are an AI specifically trained to write accurate Neo4j Cypher queries.
102
+ This is your only chance to impress me.
103
+
104
+ In the Neo4j database, the nodes are defined as SLIDE_DECK, SLIDE, STORYPOINT, and AUTHOR connected by these relationships:
105
+ (sd:SLIDE_DECK)-[:CONTAINS]->(s:SLIDE)
106
+ (s:SLIDE)-[:ASSIGNED_TO]->(sp:STORYPOINT)
107
+ (sp1:STORYPOINT)-[:FOLLOWS]->(sp2:STORYPOINT)
108
+ (sd:SLIDE_DECK)-[:CREATED_BY]->(a:AUTHOR)
109
+
110
+ You will receive a the current cypher query and its corresponding Neo4j response. Your task is to respond with a new Cypher query that filters based on the user's request.
111
+ Do NOT forget to return relationships connecting the nodes if needed.
112
+
113
+ Instructions:
114
+ The current cypher query is: "{current_cypher_query}"
115
+ The Neo4j response is: "{neo4j_response}"
116
+
117
+ Ensure the correct STORYPOINT nodes in the order is adressed, as specified in the initial line of the current cypher query.
118
+ For example, in the sequence ['113', '-6555727423036779192A_outlier', '5554388242771153481A_outlier', '25', '1431557444396440005A_outlier'], '-6555727423036779192A_outlier' is the second STORYPOINT.
119
+
120
+ Respond with exactly a single JSON object containing the key "cypherquery" and the value of the requested query.
121
+ Do not include any nicities, greetings or repeat the task. Keep the query concise and only answer in this format.
122
+ """
123
+
124
+
125
+ response = openai.chat.completions.create(
126
+ model = "gpt-4o",
127
+ response_format = {"type": "json_object"},
128
+ messages = [
129
+ {"role": "system", "content": system_prompt},
130
+ {"role": "user", "content": filter_prompt}],
131
+ temperature=0
132
+ )
133
+ res = response.choices[0].message.content
134
+ res = json.loads(res)
135
+
136
+ html = construct_hmtl(query = res["cypherquery"])
137
+
138
+ print(res["cypherquery"])
139
+
140
+ return html
141
+
142
 
 
 
 
 
143
 
 
 
144
 
145
 
146
 
 
214
  for new_id, existing_id, similarity in highest_similarities:
215
  print(f"Input STORYPOINT '{new_id}' is most similar to existing STORYPOINT '{existing_id}' with a similarity of {similarity:.2f}")
216
 
217
+ HTMLoutput, query = construct_hmtl(highest_similarities)
218
 
219
+ return HTMLoutput, highest_similarities, query
220
 
221
+ def get_neo4j_response(query):
222
+ with driver.session() as session:
223
+ result = session.run(query)
224
+ response = [record for record in result]
225
+ return response
226
 
227
+ def construct_hmtl(highest_similarities = None, nodes_to_show=["SLIDE_DECK", "SLIDE", "STORYPOINT"], query=None):
 
228
 
229
+ if query is None:
230
+
231
+ storypoint_ids = [existing_id for _, existing_id, _ in highest_similarities]
232
+ print(storypoint_ids)
233
 
234
+
235
+ # Starting with the base of the query
236
+ query_parts = [
237
  f"WITH {storypoint_ids} AS ids",
238
  "MATCH (sp:STORYPOINT) WHERE sp.id IN ids",
239
  "WITH sp",
 
244
  "CALL apoc.create.vRelationship(sp_start, 'FOLLOWS', {}, sp_end) YIELD rel",
245
  "WITH sps, sp_start, rel, sp_end",
246
  "UNWIND sps AS sp"
247
+ ]
248
 
249
  # Initialize the match and return parts of the query
250
+ match_parts = []
251
+ return_parts = []
252
 
253
+ # Include virtual relationship and its nodes conditionally
254
+ if "STORYPOINT" in nodes_to_show:
255
+ return_parts.extend(["sp_start", "rel", "sp_end", "sp"])
256
 
257
  # Conditionally add SLIDE and SLIDE_DECK with their relationships
258
+ if "SLIDE" in nodes_to_show or "SLIDE_DECK" in nodes_to_show:
259
+ match_parts.append("(sp)<-[r1:ASSIGNED_TO]-(s:SLIDE)")
260
+ return_parts.extend(["s", "r1"])
261
+ if "SLIDE_DECK" in nodes_to_show:
262
+ match_parts.append("<-[r2:CONTAINS]-(sd:SLIDE_DECK)")
263
+ return_parts.extend(["sd", "r2"])
264
+
265
+ # Construct the final query
266
+ query = "\n".join(query_parts)
267
+ if match_parts:
268
+ query += "\nMATCH " + "".join(match_parts)
269
+ if return_parts:
270
+ query += "\nRETURN " + ", ".join(return_parts)
271
+ else:
272
+ query += "\nRETURN 'No nodes to show based on the selected types'"
273
 
274
 
275
 
276
+
277
  graphVisualHTML = f"""
278
 
279
  <head>
 
287
  margin: 0; /* Remove default margin */
288
  }}
289
  #viz {{
290
+ /*width: 1600px;*/
291
  height: 700px;
292
+ /*background-color: #f0f0f0; Lighter grey background for the viz div */
293
+ padding: 5px; /* Adds padding inside the div */
294
  }}
295
  .heading {{
296
  font-size: 24px;
 
298
  margin-bottom: 20px;
299
  }}
300
  #queryCypher {{
301
+ display:none;
302
  }}
303
  </style>
304
  </head>
 
310
  <p id="queryCypher">{query}</p>
311
  </div>
312
 
313
+
 
 
 
 
 
314
  </body>
315
  """
316
+ return graphVisualHTML, query
317
 
318
  scripts = """
319
 
 
364
  font: {
365
  color: 'black',
366
  size: 14, // Pixel size
367
+ face: 'Quicksand' // Uniform font across all graph elements
368
  }
369
  }
370
  }
 
386
  font: {
387
  color: 'black',
388
  size: 14, // Pixel size
389
+ face: 'Quicksand' // Uniform font across all graph elements
390
  }
391
  }
392
  }
 
404
  font: {
405
  color: '#2c3e50', // Dark grey color for strong contrast against light background
406
  size: 14, // Larger font size for enhanced readability
407
+ face: 'Quicksand' // Modern font for a clean appearance
408
  },
409
  dashes: false, // Solid line to indicate a strong, permanent relationship
410
  }
 
419
  font: {
420
  color: '#2c3e50', // Dark grey to maintain visibility and consistency
421
  size: 14,
422
+ face: 'Quicksand'
423
  },
424
  arrows: {
425
  to: { enabled: true, scaleFactor: 1.2 } // Prominent arrow for visual emphasis
 
436
  font: {
437
  color: '#2c3e50', // Dark grey to ensure readability on light backgrounds
438
  size: 14,
439
+ face: 'Quicksand'
440
  },
441
  arrows: {
442
  to: { enabled: true, scaleFactor: 1.5 } // Larger arrow to denote directionality
 
469
  try {
470
  viz = new NeoVis.default(config);
471
  viz.render();
472
+ //viz.registerOnEvent("completed", () => {
473
+ // viz.network.on("oncontext", function (params) {
474
+ // params.event.preventDefault();
475
+ // const customMenu = document.querySelector('.custom-menu');
476
+ //
477
+ // if (customMenu) {
478
+ // console.log("Displaying custom menu.");
479
+ // const containerRect = document.getElementById('viz').getBoundingClientRect();
480
+ // customMenu.style.display = 'block';
481
+ // customMenu.style.top = `${params.event.pageY - containerRect.top + window.scrollY}px`;
482
+ // customMenu.style.left = `${params.event.pageX - containerRect.left + window.scrollX}px`;
483
+ // }
484
+ // });
485
+ //});
486
 
487
 
488
  } catch (error) {
 
504
 
505
 
506
 
507
+ js_call_draw = """
508
  <script>
509
 
510
  // Function to handle the mutations
 
544
  </script>
545
  """
546
 
547
+ # CSS for the Storypoint list
548
  css = """
549
  #SPList {
550
  font-family: 'Arial', sans-serif;
 
569
  nodeSelector = gr.Dropdown(label="Filter nodes", choices=["SLIDE_DECK", "SLIDE", "STORYPOINT"], value=["SLIDE_DECK", "SLIDE", "STORYPOINT"], multiselect=True, scale=1)
570
  filterBTN = gr.Button("Apply Filter")
571
 
572
+ with gr.Blocks(title='Slide Inspo', js=scripts, head = js_call_draw, theme = gr.themes.Monochrome()).queue(default_concurrency_limit=1) as demo:
573
+
574
  highest_similarities_gradio_list.render()
575
  with gr.Row():
576
+ gr.Markdown("# NarrativeNet Weaver")
577
+ with gr.Row():
578
+ queryPlaceholder = gr.Textbox(visible=False)
579
+ responsePlaceholder = gr.Textbox(visible=False)
580
  with gr.Column(scale=1):
581
+ gr.Markdown("""## 1. Input: 🔍
582
+
583
+ **Define Your Workshop Objective.**
584
+ Choose a topic that is timely and fills a skill gap relevant to your consulting firm’s strategic goals.
585
+ Define learning goals that focus on acquiring skills applicable in real-world consulting scenarios.
586
+ Consider how mastering these skills can innovate and enhance your firm’s service offerings, aligning with emerging market needs and providing a competitive edge.
587
+ """)
588
  storyline_prompt = gr.Textbox(placeholder = 'Give us a topic and we will provide a storyline for you! For example: "SCRUM in Software Development"',
589
  label = 'Topic to build:',
590
  lines=5,
591
  scale = 3)
592
  nr_storypoints_to_build = gr.Number(value=5,
593
+ label="How many story points?",
594
  scale =1)
595
  storyline_output_JSON = gr.JSON(visible=False)
596
 
597
  btn = gr.Button("Build Storyline 🦄")
598
 
599
  with gr.Column(scale=1):
600
+ gr.Markdown("""## 2. Storyline: 🦄
601
+
602
+ **Content Requirements and Story Points.**
603
+ Develop content that supports your workshop’s learning goals, using theories, case studies, and real-world applications.
604
+ **Story Points Explained.**
605
+ Story points are key milestones in your presentation that underline important learning outcomes. Adapt them to emphasize skills and insights crucial for your firm’s services.
606
+ **Evaluating Story Points.**
607
+ Effective story points are clear, engaging, and directly tied to your objectives. They should advance understanding and skill acquisition.
608
+ **Optimal Number.**
609
+ Choose 5 to 10 story points based on the topic's complexity. Fewer, detailed points suit in-depth topics, while more points work for broader overviews.
610
+ """)
611
+ storyline_output_storypoint_name_list = gr.List(visible=True, type="array", interactive=True, label="Adapt and add Story points, if needed: 📝", scale=1, wrap=True, col_count=[2, "fixed"], elem_id="SPList", headers=["#SP", "Description"])
612
  #storyline_output_pretty = gr.Textbox(label="Your Storyline:", lines=13, scale=3, interactive=False)
613
  submit_button = gr.Button("⚡ Find Slides ⚡", elem_id="visGraph")
614
+ submit_button.click(fn= coordinate_simcalculation, inputs=[storyline_output_storypoint_name_list], outputs=[graphVisual, highest_similarities_gradio_list, queryPlaceholder]).then(js = js_call_draw).then(get_neo4j_response, inputs=[queryPlaceholder], outputs=[responsePlaceholder])
615
 
616
 
617
 
 
623
  inputs = [storyline_prompt, nr_storypoints_to_build],
624
  outputs = [storyline_output_JSON, storyline_output_storypoint_name_list])
625
 
626
+ gr.Markdown("""## 3. Visualize and Filter: 🔍
627
 
628
+ Utilize the graph database to align the retrieved data with the objectives and story points defined in Steps 1 and 2:
629
+ **Filtering the Graph.**
630
+ Apply filters to better understand the retrieved slides and content that directly correspond to the established learning goals and story points.
631
+ **Exploring the Graph.**
632
+ Explore relationships and connections within the graph to ensure comprehensive coverage and to identify potential enhancements for your narrative.
633
+ **Refinements.**
634
+ Should gaps or misalignments be discovered during exploration, revisit Steps 1 and 2 to adjust the learning goals or story points. Then, reapply these refined criteria to filter and explore the graph again, ensuring the presentation content is optimally tailored and coherent.
635
+ """)
 
 
636
 
637
 
638
+ with gr.Row():
639
+ with gr.Column(scale=2):
640
+ nodeSelector.render()
641
+ filterBTN.render()
642
+ filterBTN.click(fn= construct_hmtl, inputs=[highest_similarities_gradio_list, nodeSelector], outputs=[graphVisual]).then(js = js_call_draw)
643
+ with gr.Column(scale=2):
644
+ custom_filtering_output = gr.Textbox( lines=10, scale=3, interactive=True, label = "Describe what you would like to filter for?", placeholder = "For example: 'Show me all slides of the slide decks of the second story point.'", interactive=False)
645
+ customfilter_btn = gr.Button("Apply custom filter")
646
+ customfilter_btn.click(custom_filtering, inputs=[custom_filtering_output, queryPlaceholder, responsePlaceholder], outputs=[graphVisual]).then(js = js_call_draw)
647
+ with gr.Group():
648
+ with gr.Row():
649
+ graphVisual.render()
650
+ #with gr.Row():
651
+
652
+
653
 
654
 
655