mbCrypto commited on
Commit
4167694
1 Parent(s): 394a27e

Upload 3 files

Browse files
Files changed (3) hide show
  1. main.py +391 -0
  2. requirements.txt +2 -0
  3. ui.py +142 -0
main.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import random
3
+ from typing import Callable, Optional, Tuple
4
+
5
+
6
+ def initialize_population(services: dict, users: dict, population_size: int) -> list:
7
+ """
8
+ Initialize the population of assignment solutions for the genetic algorithm.
9
+
10
+ Args:
11
+ services (dict): A dictionary containing service constraints.
12
+ users (dict): A dictionary containing user preferences and constraints.
13
+ population_size (int): The number of assignment solutions to generate.
14
+
15
+ Returns:
16
+ list: A list of generated assignment solutions.
17
+ """
18
+ population = []
19
+
20
+ # Generate population_size number of assignment solutions
21
+ for _ in range(population_size):
22
+ assignment_solution = {}
23
+
24
+ for service in services.keys():
25
+ # Randomly assign users to each service, while considering user preferences and constraints
26
+ assigned_users = []
27
+ for user, user_info in users.items():
28
+ # Check if user cannot be assigned to this service
29
+ if service not in user_info["cannot_assign"]:
30
+ # Assign user to service based on their preference
31
+ if service in user_info["preferences"]:
32
+ assigned_users.append(user)
33
+ # Assign user to service with a small probability if not in their preferences
34
+ elif random.random() < 0.1:
35
+ assigned_users.append(user)
36
+
37
+ # Shuffle the list of assigned users to create random assignments
38
+ random.shuffle(assigned_users)
39
+ assignment_solution[service] = assigned_users
40
+
41
+ # Add the generated assignment solution to the population
42
+ population.append(assignment_solution)
43
+
44
+ return population
45
+
46
+
47
+ def calculate_fitness(population: list, services: dict, users: dict, fitness_fn: Optional[Callable] = None) -> list:
48
+ """
49
+ Calculate the fitness of each assignment solution in the population.
50
+
51
+ Args:
52
+ population (list): A list of assignment solutions.
53
+ services (dict): A dictionary containing service constraints.
54
+ users (dict): A dictionary containing user preferences and constraints.
55
+ fitness_fn (Optional[Callable]): An optional custom fitness function.
56
+
57
+ Returns:
58
+ list: A list of fitness scores for each assignment solution in the population.
59
+ """
60
+ if not fitness_fn:
61
+ fitness_fn = default_fitness_function
62
+
63
+ fitness_scores = []
64
+
65
+ # Calculate the fitness score for each assignment solution in the population
66
+ for assignment_solution in population:
67
+ fitness_score = fitness_fn(assignment_solution, services, users)
68
+ fitness_scores.append(fitness_score)
69
+
70
+ return fitness_scores
71
+
72
+
73
+ def default_fitness_function(assignment_solution: dict, services: dict, users: dict) -> float:
74
+ """
75
+ Calculate the fitness of an assignment solution based on the criteria described in the problem statement,
76
+ including user preferences and cannot_assign constraints.
77
+
78
+ Args:
79
+ assignment_solution (dict): An assignment solution to evaluate.
80
+ services (dict): A dictionary containing service constraints.
81
+ users (dict): A dictionary containing user preferences and constraints.
82
+
83
+ Returns:
84
+ float: The fitness score of the given assignment solution.
85
+ """
86
+ fitness = 0
87
+
88
+ for service, assigned_users in assignment_solution.items():
89
+ service_info = services[service]
90
+ num_assigned_users = len(assigned_users)
91
+
92
+ # Bonus for solutions that assign users near the recommended value
93
+ if service_info["min"] <= num_assigned_users <= service_info["max"]:
94
+ fitness += abs(num_assigned_users - service_info["rec"])
95
+
96
+ # Punish solutions that assign users below the minimum value
97
+ elif num_assigned_users < service_info["min"]:
98
+ fitness -= (service_info["min"] - num_assigned_users) * service_info["priority"]
99
+
100
+ # Punish solutions that assign users above the maximum value
101
+ else: # num_assigned_users > service_info["max"]:
102
+ fitness -= (num_assigned_users - service_info["max"]) * service_info["priority"]
103
+
104
+ # Punish solutions that assign users to their cannot_assign services
105
+ for user in assigned_users:
106
+ if service in users[user]["cannot_assign"]:
107
+ fitness -= 100 * service_info["priority"]
108
+
109
+ # Bonus solutions that assign users to their preferred services
110
+ for user, user_info in users.items():
111
+ if service in user_info["preferences"] and user in assigned_users:
112
+ fitness += 10
113
+
114
+ return -fitness
115
+
116
+
117
+ def selection(fitness_scores: list) -> Tuple[int, int]:
118
+ """
119
+ Select two parent solutions from the population based on their fitness scores.
120
+
121
+ Args:
122
+ fitness_scores (list): A list of fitness scores for each assignment solution in the population.
123
+
124
+ Returns:
125
+ Tuple[int, int]: The indices of the two selected parent solutions in the population.
126
+ """
127
+ # Calculate the total fitness of the population
128
+ total_fitness = sum(fitness_scores)
129
+
130
+ # Calculate the relative fitness of each solution
131
+ relative_fitness = [f / total_fitness for f in fitness_scores]
132
+
133
+ # Select the first parent using roulette wheel selection
134
+ parent1_index = -1
135
+ r = random.random()
136
+ accumulator = 0
137
+ for i, rf in enumerate(relative_fitness):
138
+ accumulator += rf
139
+ if accumulator >= r:
140
+ parent1_index = i
141
+ break
142
+
143
+ # Select the second parent using roulette wheel selection, ensuring it's different from the first parent
144
+ parent2_index = -1
145
+ while parent2_index == -1 or parent2_index == parent1_index:
146
+ r = random.random()
147
+ accumulator = 0
148
+ for i, rf in enumerate(relative_fitness):
149
+ accumulator += rf
150
+ if accumulator >= r:
151
+ parent2_index = i
152
+ break
153
+
154
+ return parent1_index, parent2_index
155
+
156
+
157
+ def crossover(parent1: dict, parent2: dict) -> dict:
158
+ """
159
+ Combine two parent assignment solutions to create a child solution.
160
+
161
+ Args:
162
+ parent1 (dict): The first parent assignment solution.
163
+ parent2 (dict): The second parent assignment solution.
164
+
165
+ Returns:
166
+ dict: The child assignment solution created by combining the parents.
167
+ """
168
+ child_solution = {}
169
+
170
+ # Iterate over the services in the parents
171
+ for service in parent1.keys():
172
+ # Create two sets of users assigned to the current service in parent1 and parent2
173
+ assigned_users_parent1 = set(parent1[service])
174
+ assigned_users_parent2 = set(parent2[service])
175
+
176
+ # Perform set union to combine users assigned in both parents
177
+ combined_assigned_users = assigned_users_parent1 | assigned_users_parent2
178
+
179
+ # Randomly assign each user from the combined set to the child solution
180
+ child_assigned_users = []
181
+ for user in combined_assigned_users:
182
+ if random.random() < 0.5:
183
+ child_assigned_users.append(user)
184
+
185
+ child_solution[service] = child_assigned_users
186
+
187
+ return child_solution
188
+
189
+
190
+ def mutation(solution: dict, users: dict, mutation_rate: float = 0.01) -> dict:
191
+ """
192
+ Mutate an assignment solution by randomly reassigning users to services.
193
+
194
+ Args:
195
+ solution (dict): The assignment solution to mutate.
196
+ users (dict): A dictionary containing user preferences and constraints.
197
+ mutation_rate (float): The probability of mutation for each user in the solution (default: 0.01).
198
+
199
+ Returns:
200
+ dict: The mutated assignment solution.
201
+ """
202
+ mutated_solution = copy.deepcopy(solution)
203
+
204
+ # Iterate over the services in the solution
205
+ for service, assigned_users in mutated_solution.items():
206
+ for user in assigned_users:
207
+ # Check if the user should be mutated based on the mutation rate
208
+ if random.random() < mutation_rate:
209
+ # Remove the user from the current service
210
+ assigned_users.remove(user)
211
+
212
+ # Find a new service for the user while considering their cannot_assign constraints
213
+ new_service = service
214
+ while new_service == service or new_service in users[user]["cannot_assign"]:
215
+ new_service = random.choice(list(mutated_solution.keys()))
216
+
217
+ # Assign the user to the new service
218
+ mutated_solution[new_service].append(user)
219
+
220
+ return mutated_solution
221
+
222
+
223
+ def report_generation(generation: int, fitness_scores: list, best_solution: dict, services: dict, users: dict) -> None:
224
+ """
225
+ Print a report of the genetic algorithm's progress for the current generation.
226
+
227
+ Args:
228
+ generation (int): The current generation number.
229
+ fitness_scores (list): The fitness scores for the current population.
230
+ best_solution (dict): The best assignment solution found so far.
231
+ services (dict): The input services dictionary.
232
+ users (dict): The input users dictionary.
233
+ """
234
+ best_fitness = min(fitness_scores)
235
+ worst_fitness = max(fitness_scores)
236
+ avg_fitness = sum(fitness_scores) / len(fitness_scores)
237
+ generation_errors = polish_errors(calculate_errors(best_solution, services, users))
238
+
239
+ print(f"Generation {generation}:")
240
+ print(f" Best fitness: {best_fitness}")
241
+ print(f" Worst fitness: {worst_fitness}")
242
+ print(f" Average fitness: {avg_fitness}")
243
+ print(f" Best solution so far: {best_solution}")
244
+ print(f" Errors so far: {generation_errors}")
245
+
246
+
247
+ def calculate_errors(solution: dict, services: dict, users: dict) -> dict:
248
+ """
249
+ Calculate the errors in the assignment solution based on the user and service constraints.
250
+
251
+ Args:
252
+ solution (dict): The assignment solution to analyze.
253
+ services (dict): The input services dictionary.
254
+ users (dict): The input users dictionary.
255
+
256
+ Returns:
257
+ dict: A dictionary containing the errors for each user and service in the assignment solution.
258
+ """
259
+ errors = {"users": {}, "services": {}}
260
+
261
+ # Analyze user errors
262
+ for user, user_data in users.items():
263
+ errors["users"][user] = {"unmet_max_assignments": False, "unmet_preference": [], "unmet_cannot_assign": []}
264
+
265
+ user_assignments = [service for service, assigned_users in solution.items() if user in assigned_users]
266
+ if len(user_assignments) > user_data["max_assignments"]:
267
+ errors["users"][user]["unmet_max_assignments"] = True
268
+ errors["users"][user]["effective_assignments"] = len(user_assignments)
269
+
270
+ for preferred_service in user_data["preferences"]:
271
+ if preferred_service not in user_assignments:
272
+ errors["users"][user]["unmet_preference"].append(preferred_service)
273
+
274
+ for cannot_assign_service in user_data["cannot_assign"]:
275
+ if cannot_assign_service in user_assignments:
276
+ errors["users"][user]["unmet_cannot_assign"].append(cannot_assign_service)
277
+
278
+ # Analyze service errors
279
+ for service, service_data in services.items():
280
+ errors["services"][service] = {"unmet_constraint": None, "extra_users": []}
281
+
282
+ assigned_users = solution[service]
283
+ num_assigned_users = len(assigned_users)
284
+
285
+ if num_assigned_users < service_data["min"]:
286
+ errors["services"][service]["unmet_constraint"] = "min"
287
+ elif num_assigned_users > service_data["rec"]:
288
+ errors["services"][service]["unmet_constraint"] = "rec"
289
+ elif num_assigned_users > service_data["max"]:
290
+ errors["services"][service]["unmet_constraint"] = "max"
291
+ extra_users = assigned_users[service_data["max"]:]
292
+ errors["services"][service]["extra_users"] = extra_users
293
+
294
+ return errors
295
+
296
+
297
+ def polish_errors(errors: dict) -> dict:
298
+ """
299
+ Remove users and services without unmet constraints from the errors object.
300
+
301
+ Args:
302
+ errors (dict): The errors object to polish.
303
+
304
+ Returns:
305
+ dict: A polished errors object without users and services with no unmet constraints.
306
+ """
307
+ polished_errors = {"users": {}, "services": {}}
308
+
309
+ for user, user_errors in errors["users"].items():
310
+ polished_user_errors = {}
311
+
312
+ if user_errors["unmet_max_assignments"]:
313
+ polished_user_errors["unmet_max_assignments"] = True
314
+
315
+ for key, value in user_errors.items():
316
+ if key not in ["unmet_max_assignments"] and value:
317
+ polished_user_errors[key] = value
318
+
319
+ if polished_user_errors:
320
+ polished_errors["users"][user] = polished_user_errors
321
+
322
+ for service, service_errors in errors["services"].items():
323
+ polished_service_errors = {}
324
+
325
+ for key, value in service_errors.items():
326
+ if value:
327
+ polished_service_errors[key] = value
328
+
329
+ if polished_service_errors:
330
+ polished_errors["services"][service] = polished_service_errors
331
+
332
+ return polished_errors
333
+
334
+
335
+ def genetic_algorithm(services: dict, users: dict, population_size: int = 100, num_generations: int = 100,
336
+ mutation_rate: float = 0.01, fitness_fn: Optional[Callable] = None) -> dict:
337
+ """
338
+ Run the genetic algorithm to find an optimal assignment solution based on user preferences and constraints.
339
+
340
+ Args:
341
+ services (dict): The input services dictionary.
342
+ users (dict): The input users dictionary.
343
+ population_size (int): The size of the population for each generation (default: 100).
344
+ num_generations (int): The number of generations for the genetic algorithm to run (default: 100).
345
+ mutation_rate (float): The probability of mutation for each individual in the population (default: 0.01).
346
+ fitness_fn (Callable, optional): An optional custom fitness function.
347
+
348
+ Returns:
349
+ dict: The best assignment solution found by the genetic algorithm.
350
+ """
351
+ # Initialize the population
352
+ population = initialize_population(services, users, population_size)
353
+
354
+ # If no custom fitness function is provided, use the default fitness function
355
+ if fitness_fn is None:
356
+ fitness_fn = default_fitness_function
357
+
358
+ # Calculate the initial fitness scores for the population
359
+ fitness_scores = calculate_fitness(population, services, users, fitness_fn)
360
+
361
+ best_solution = None
362
+ best_fitness = float('inf')
363
+
364
+ # Main loop of the genetic algorithm
365
+ for generation in range(num_generations):
366
+ # Select two parent solutions based on their fitness scores
367
+ parent1_index, parent2_index = selection(fitness_scores)
368
+
369
+ # Create a child solution by combining the parents using crossover
370
+ child_solution = crossover(population[parent1_index], population[parent2_index])
371
+
372
+ # Mutate the child solution
373
+ mutated_child_solution = mutation(child_solution, users, mutation_rate)
374
+
375
+ # Calculate the fitness of the child solution
376
+ child_fitness = fitness_fn(mutated_child_solution, services, users)
377
+
378
+ # Replace the least-fit solution in the population with the child solution
379
+ worst_fitness_index = fitness_scores.index(max(fitness_scores))
380
+ population[worst_fitness_index] = mutated_child_solution
381
+ fitness_scores[worst_fitness_index] = child_fitness
382
+
383
+ # Update the best solution found so far
384
+ if child_fitness < best_fitness:
385
+ best_solution = mutated_child_solution
386
+ best_fitness = child_fitness
387
+
388
+ # Print the progress of the algorithm
389
+ report_generation(generation, fitness_scores, best_solution, services, users)
390
+
391
+ return best_solution
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit
2
+ clipboard
ui.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Import required libraries
2
+ import streamlit as st
3
+ import json
4
+ import clipboard
5
+
6
+ from main import genetic_algorithm, polish_errors, calculate_errors
7
+
8
+ # Initialize session state
9
+ if 'services' not in st.session_state:
10
+ st.session_state.services = {}
11
+ if 'users' not in st.session_state:
12
+ st.session_state.users = {}
13
+
14
+ # App title
15
+ st.title('Services and Users JSON Builder')
16
+
17
+ # Add sliders for population_size, num_generations, and mutation_rate
18
+ st.subheader('Genetic Algorithm Parameters')
19
+ population_size = st.slider('Population Size', min_value=500, max_value=5000, value=2500, step=100)
20
+ num_generations = st.slider('Number of Generations', min_value=1000, max_value=10000, value=5000, step=500)
21
+ mutation_rate = st.slider('Mutation Rate', min_value=0.0, max_value=1.0, value=0.01, step=0.01)
22
+
23
+ # Button to run the genetic algorithm
24
+ if st.button('Run Genetic Algorithm'):
25
+ # Call the genetic_algorithm function and get the best_solution
26
+ best_solution = genetic_algorithm(st.session_state.services, st.session_state.users, population_size,
27
+ num_generations, mutation_rate)
28
+
29
+ # Convert the best_solution to JSON
30
+ best_solution_json = json.dumps(best_solution, indent=4)
31
+ best_solution_errors = calculate_errors(best_solution, st.session_state.services, st.session_state.users)
32
+ best_solution_errors = polish_errors(best_solution_errors)
33
+ best_solution_errors = json.dumps(best_solution_errors, indent=4)
34
+
35
+ # Display the output JSON in a read-only form
36
+ st.subheader('Best Solution JSON')
37
+ st.text_area('Best Solution', value=best_solution_json, height=400, max_chars=None, key=None, disabled=True)
38
+ st.text_area('Unmet constraints', value=best_solution_errors, height=200, max_chars=None, key=None, disabled=True)
39
+
40
+ if st.button('Copy solution to Clipboard'):
41
+ clipboard.copy(best_solution_json)
42
+ st.success('JSON copied to clipboard!')
43
+ if st.button('Copy unmet constraints to Clipboard'):
44
+ clipboard.copy(best_solution_errors)
45
+ st.success('JSON copied to clipboard!')
46
+
47
+ # Sidebar for uploading previously generated JSON
48
+ with st.sidebar.expander('Upload previously generated JSON'):
49
+ uploaded_json = st.text_area('Paste your JSON here')
50
+ merge_json = st.button('Merge with JSON')
51
+ reset_json = st.button('Reset JSON')
52
+
53
+ if reset_json:
54
+ st.session_state.services = {}
55
+ st.session_state.users = {}
56
+
57
+ if merge_json and uploaded_json:
58
+ try:
59
+ loaded_data = json.loads(uploaded_json)
60
+ st.session_state.services.update(loaded_data.get('services', {}))
61
+ st.session_state.users.update(loaded_data.get('users', {}))
62
+ st.success('JSON loaded successfully')
63
+ except json.JSONDecodeError:
64
+ st.error('Invalid JSON format')
65
+
66
+ # Update existing user or service object
67
+ with st.sidebar.expander('Update existing user or service'):
68
+ object_type = st.selectbox('Choose object type', ('Service', 'User'))
69
+
70
+ if object_type == 'Service':
71
+ service_key = st.selectbox('Select a service', list(st.session_state.services.keys()), key='update_service_key')
72
+ if service_key and st.button('Load Service'):
73
+ st.session_state.service_name = service_key
74
+ st.session_state.min_val = st.session_state.services[service_key]['min']
75
+ st.session_state.rec_val = st.session_state.services[service_key]['rec']
76
+ st.session_state.max_val = st.session_state.services[service_key]['max']
77
+ st.session_state.priority = st.session_state.services[service_key]['priority']
78
+
79
+ elif object_type == 'User':
80
+ user_key = st.selectbox('Select a user', list(st.session_state.users.keys()), key='update_user_key')
81
+ if user_key and st.button('Load User'):
82
+ st.session_state.user_name = user_key
83
+ st.session_state.max_assignments = st.session_state.users[user_key]['max_assignments']
84
+ st.session_state.preferences = st.session_state.users[user_key]['preferences']
85
+ st.session_state.cannot_assign = st.session_state.users[user_key]['cannot_assign']
86
+
87
+ # Add a service form
88
+ with st.form(key='service_form'):
89
+ st.subheader('Add a Service')
90
+ service_name = st.text_input('Service Name', value=st.session_state.get('service_name', ''))
91
+ min_val = st.number_input('Minimum Value', value=st.session_state.get('min_val', 0))
92
+ rec_val = st.number_input('Recommended Value', value=st.session_state.get('rec_val', 0))
93
+ max_val = st.number_input('Maximum Value', value=st.session_state.get('max_val', 0))
94
+ priority = st.number_input('Priority', value=st.session_state.get('priority', 0))
95
+ submit_service = st.form_submit_button('Save Service')
96
+
97
+ # Add a user form
98
+ with st.form(key='user_form'):
99
+ st.subheader('Add a User')
100
+ user_name = st.text_input('User Name', key='user_name', value=st.session_state.get('user_name', ''))
101
+ max_assignments = st.number_input('Max Assignments', value=st.session_state.get('max_assignments', 0),
102
+ key='max_assignments')
103
+ preferences = st.multiselect('Preferences', options=list(st.session_state.services.keys()),
104
+ default=st.session_state.get('preferences', []), key='preferences')
105
+ cannot_assign = st.multiselect('Cannot Assign', options=list(st.session_state.services.keys()),
106
+ default=st.session_state.get('cannot_assign', []), key='cannot_assign')
107
+ submit_user = st.form_submit_button('Save User')
108
+
109
+ # Add the submitted service to the services dictionary
110
+ if submit_service:
111
+ st.session_state.services[service_name] = {
112
+ 'min': min_val,
113
+ 'rec': rec_val,
114
+ 'max': max_val,
115
+ 'priority': priority
116
+ }
117
+
118
+ # Add the submitted user to the users dictionary
119
+ if submit_user:
120
+ st.session_state.users[user_name] = {
121
+ 'max_assignments': max_assignments,
122
+ 'preferences': preferences,
123
+ 'cannot_assign': cannot_assign
124
+ }
125
+
126
+ # Combine services and users dictionaries
127
+ combined_data = {
128
+ 'services': st.session_state.services,
129
+ 'users': st.session_state.users
130
+ }
131
+
132
+ # Convert combined_data to JSON
133
+ json_data = json.dumps(combined_data, indent=4)
134
+
135
+ # Display the generated JSON
136
+ st.subheader('Generated JSON')
137
+ st.code(json_data, language='json')
138
+
139
+ # Button to copy JSON to clipboard
140
+ if st.button('Copy JSON to Clipboard'):
141
+ clipboard.copy(json_data)
142
+ st.success('JSON copied to clipboard!')