jvcli 2.0.31__py3-none-any.whl → 2.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. jvcli/__init__.py +4 -2
  2. jvcli/cli.py +2 -6
  3. jvcli/commands/client.py +14 -38
  4. jvcli/commands/create.py +91 -86
  5. jvcli/commands/server.py +0 -6
  6. jvcli/commands/startproject.py +1 -1
  7. jvcli/templates/{2.0.0 → 2.1.2}/project/README.md +4 -40
  8. jvcli/templates/{2.0.0 → 2.1.2}/project/actions/README.md +1 -1
  9. jvcli/templates/{2.0.0 → 2.1.2}/project/daf/README.md +1 -1
  10. jvcli/templates/{2.0.0 → 2.1.2}/project/env.example +7 -4
  11. jvcli/templates/2.1.2/project/main.jac +2 -0
  12. jvcli/templates/2.1.2/sourcefiles/action_app.py +23 -0
  13. jvcli/templates/2.1.2/sourcefiles/action_archetype.jac +49 -0
  14. jvcli/templates/{2.0.0 → 2.1.2/sourcefiles}/action_info.yaml +1 -1
  15. jvcli/templates/2.1.2/sourcefiles/action_lib.jac +3 -0
  16. jvcli/templates/{2.0.0 → 2.1.2/sourcefiles}/agent_descriptor.yaml +4 -4
  17. jvcli/templates/2.1.2/sourcefiles/interact_action_archetype.jac +58 -0
  18. jvcli/utils.py +1 -1
  19. {jvcli-2.0.31.dist-info → jvcli-2.1.2.dist-info}/METADATA +8 -47
  20. jvcli-2.1.2.dist-info/RECORD +40 -0
  21. jvcli/client/__init__.py +0 -1
  22. jvcli/client/app.py +0 -188
  23. jvcli/client/lib/__init__.py +0 -1
  24. jvcli/client/lib/page.py +0 -68
  25. jvcli/client/lib/utils.py +0 -312
  26. jvcli/client/lib/widgets.py +0 -295
  27. jvcli/client/pages/__init__.py +0 -1
  28. jvcli/client/pages/action_dashboard_page.py +0 -120
  29. jvcli/client/pages/analytics_page.py +0 -245
  30. jvcli/client/pages/chat_page.py +0 -150
  31. jvcli/client/pages/graph_page.py +0 -20
  32. jvcli/commands/clean.py +0 -29
  33. jvcli/commands/studio.py +0 -258
  34. jvcli/studio/assets/index-DDV79SDu.js +0 -213
  35. jvcli/studio/assets/index-DdMMONxd.css +0 -1
  36. jvcli/studio/index.html +0 -15
  37. jvcli/studio/jac_logo.png +0 -0
  38. jvcli/studio/tauri.svg +0 -6
  39. jvcli/studio/vite.svg +0 -1
  40. jvcli/studio-auth/assets/index-Bh6lyeXA.js +0 -218
  41. jvcli/studio-auth/assets/index-DdMMONxd.css +0 -1
  42. jvcli/studio-auth/index.html +0 -15
  43. jvcli/studio-auth/jac_logo.png +0 -0
  44. jvcli/studio-auth/tauri.svg +0 -6
  45. jvcli/studio-auth/vite.svg +0 -1
  46. jvcli/templates/2.0.0/project/main.jac +0 -2
  47. jvcli-2.0.31.dist-info/RECORD +0 -61
  48. /jvcli/templates/{2.0.0 → 2.1.2}/project/gitignore.example +0 -0
  49. /jvcli/templates/{2.0.0 → 2.1.2}/project/globals.jac +0 -0
  50. /jvcli/templates/{2.0.0 → 2.1.2}/project/tests/README.md +0 -0
  51. /jvcli/templates/{CHANGELOG.md → 2.1.2/sourcefiles/CHANGELOG.md} +0 -0
  52. /jvcli/templates/{README.md → 2.1.2/sourcefiles/README.md} +0 -0
  53. /jvcli/templates/{2.0.0 → 2.1.2/sourcefiles}/agent_info.yaml +0 -0
  54. /jvcli/templates/{2.0.0 → 2.1.2/sourcefiles}/agent_knowledge.yaml +0 -0
  55. /jvcli/templates/{2.0.0 → 2.1.2/sourcefiles}/agent_memory.yaml +0 -0
  56. {jvcli-2.0.31.dist-info → jvcli-2.1.2.dist-info}/WHEEL +0 -0
  57. {jvcli-2.0.31.dist-info → jvcli-2.1.2.dist-info}/entry_points.txt +0 -0
  58. {jvcli-2.0.31.dist-info → jvcli-2.1.2.dist-info}/licenses/LICENSE +0 -0
  59. {jvcli-2.0.31.dist-info → jvcli-2.1.2.dist-info}/top_level.txt +0 -0
jvcli/client/lib/utils.py DELETED
@@ -1,312 +0,0 @@
1
- """This module contains utility functions for the JVCLI client."""
2
-
3
- import base64
4
- import json
5
- import os
6
- from importlib.util import module_from_spec, spec_from_file_location
7
- from io import BytesIO
8
- from typing import Any, Callable, Dict, List, Optional
9
-
10
- import requests
11
- import streamlit as st
12
- import yaml
13
- from PIL import Image
14
-
15
- JIVAS_BASE_URL = os.environ.get("JIVAS_BASE_URL", "http://localhost:8000")
16
-
17
-
18
- def load_function(file_path: str, function_name: str, **kwargs: Any) -> Callable:
19
- """Dynamically loads and returns a function from a Python file, with optional keyword arguments."""
20
-
21
- if not os.path.exists(file_path):
22
- raise FileNotFoundError(f"No file found at {file_path}")
23
-
24
- # Get the module name from the file name
25
- module_name = os.path.splitext(os.path.basename(file_path))[0]
26
-
27
- # Load the module specification
28
- spec = spec_from_file_location(module_name, file_path)
29
- if spec is None:
30
- raise ImportError(f"Could not load specification for module {module_name}")
31
-
32
- # Create the module
33
- module = module_from_spec(spec)
34
- if spec.loader is None:
35
- raise ImportError(f"Could not load module {module_name}")
36
-
37
- # Execute the module
38
- spec.loader.exec_module(module)
39
-
40
- # Get the function
41
- if not hasattr(module, function_name):
42
- raise AttributeError(f"Function '{function_name}' not found in {file_path}")
43
-
44
- func = getattr(module, function_name)
45
-
46
- # Ensure the returned callable can accept any kwargs passed to it
47
- def wrapped_func(*args: Any, **func_kwargs: Any) -> Any:
48
- return func(*args, **{**kwargs, **func_kwargs})
49
-
50
- return wrapped_func
51
-
52
-
53
- def call_api(
54
- endpoint: str,
55
- method: str = "POST",
56
- headers: Optional[Dict] = None,
57
- json_data: Optional[Dict] = None,
58
- files: Optional[List] = None,
59
- data: Optional[Dict] = None,
60
- timeout: int = 10,
61
- ) -> Optional[requests.Response]:
62
- """Generic function to call an API endpoint."""
63
-
64
- if not endpoint.startswith("http"):
65
- endpoint = f"{JIVAS_BASE_URL}/{endpoint}"
66
-
67
- ctx = get_user_info() # Assumes a function that fetches user info
68
-
69
- if ctx.get("token"):
70
- try:
71
- headers = headers or {}
72
- headers["Authorization"] = f"Bearer {ctx['token']}"
73
-
74
- response = requests.request(
75
- method=method,
76
- url=endpoint,
77
- headers=headers,
78
- json=json_data,
79
- files=files,
80
- data=data,
81
- timeout=timeout,
82
- )
83
-
84
- if response.status_code == 401:
85
- st.session_state.EXPIRATION = ""
86
- return None
87
-
88
- return response
89
-
90
- except Exception as e:
91
- st.session_state.EXPIRATION = ""
92
- st.write(e)
93
-
94
- return None
95
-
96
-
97
- def call_action_walker_exec(
98
- agent_id: str,
99
- module_root: str,
100
- walker: str,
101
- args: Optional[Dict] = None,
102
- files: Optional[List] = None,
103
- headers: Optional[Dict] = None,
104
- ) -> list:
105
- """Call the API to execute a walker action for a given agent."""
106
-
107
- endpoint = f"{JIVAS_BASE_URL}/action/walker"
108
-
109
- # Create form data
110
- data = {"agent_id": agent_id, "module_root": module_root, "walker": walker}
111
-
112
- if args:
113
- data["args"] = json.dumps(args)
114
-
115
- file_list = []
116
-
117
- if files:
118
- for file in files:
119
- file_list.append(("attachments", (file[0], file[1], file[2])))
120
-
121
- response = call_api(endpoint, headers=headers, data=data, files=file_list)
122
-
123
- if response is not None and response.status_code == 200:
124
- result = response.json()
125
- return result if result else []
126
-
127
- return []
128
-
129
-
130
- def call_healthcheck(agent_id: str, headers: Optional[Dict] = None) -> Optional[dict]:
131
- """Call the API to check the health of an agent."""
132
- endpoint = "walker/healthcheck"
133
- json_data = {"agent_id": agent_id}
134
- response = call_api(endpoint, headers=headers, json_data=json_data)
135
-
136
- if response is not None and response.status_code in [200, 501, 503]:
137
- result = response.json()
138
- reports = result.get("reports", [])
139
- return reports[0] if reports else {}
140
-
141
- return {}
142
-
143
-
144
- def call_list_agents(headers: Optional[Dict] = None) -> list:
145
- """Call the API to list agents."""
146
- endpoint = "walker/list_agents"
147
- json_data = {"reporting": True}
148
- response = call_api(endpoint, headers=headers, json_data=json_data)
149
-
150
- if response is not None and response.status_code == 200:
151
- result = response.json()
152
- reports = result.get("reports", [])
153
- return [
154
- {"id": agent.get("id", ""), "label": agent.get("name", "")}
155
- for agent in reports
156
- ]
157
-
158
- return []
159
-
160
-
161
- def call_get_agent(agent_id: str, headers: Optional[Dict] = None) -> dict:
162
- """Call the API to get details of a specific agent."""
163
- endpoint = "walker/get_agent"
164
- json_data = {"agent_id": agent_id}
165
- response = call_api(endpoint, headers=headers, json_data=json_data)
166
-
167
- if response is not None and response.status_code == 200:
168
- result = response.json()
169
- reports = result.get("reports", [])
170
- return reports[0] if reports else {}
171
-
172
- return {}
173
-
174
-
175
- def call_list_actions(agent_id: str, headers: Optional[Dict] = None) -> list:
176
- """Call the API to list actions for a given agent."""
177
- endpoint = "walker/list_actions"
178
- json_data = {"agent_id": agent_id}
179
- response = call_api(endpoint, headers=headers, json_data=json_data)
180
-
181
- if response is not None and response.status_code == 200:
182
- result = response.json()
183
- reports = result.get("reports", [])
184
- return reports[0] if reports else []
185
-
186
- return []
187
-
188
-
189
- def call_get_action(
190
- agent_id: str, action_id: str, headers: Optional[Dict] = None
191
- ) -> dict:
192
- """Call the API to get a specific action for a given agent."""
193
- endpoint = "walker/get_action"
194
- json_data = {"agent_id": agent_id, "action_id": action_id}
195
- response = call_api(endpoint, headers=headers, json_data=json_data)
196
-
197
- if response is not None and response.status_code == 200:
198
- result = response.json()
199
- reports = result.get("reports", [])
200
- return reports[0] if reports else {}
201
-
202
- return {}
203
-
204
-
205
- def call_update_action(
206
- agent_id: str, action_id: str, action_data: dict, headers: Optional[Dict] = None
207
- ) -> dict:
208
- """Call the API to update a specific action for a given agent."""
209
- endpoint = "walker/update_action"
210
- json_data = {
211
- "agent_id": agent_id,
212
- "action_id": action_id,
213
- "action_data": action_data,
214
- }
215
- response = call_api(endpoint, headers=headers, json_data=json_data)
216
-
217
- if response is not None and response.status_code == 200:
218
- result = response.json()
219
- reports = result.get("reports", [])
220
- return reports[0] if reports else {}
221
-
222
- return {}
223
-
224
-
225
- def call_update_agent(
226
- agent_id: str, agent_data: dict, headers: Optional[Dict] = None
227
- ) -> dict:
228
- """Call the API to update a specific agent."""
229
- endpoint = "walker/update_agent"
230
- json_data = {"agent_id": agent_id, "agent_data": agent_data}
231
- response = call_api(endpoint, headers=headers, json_data=json_data)
232
-
233
- if response is not None and response.status_code == 200:
234
- result = response.json()
235
- reports = result.get("reports", [])
236
- return reports[0] if reports else {}
237
-
238
- return {}
239
-
240
-
241
- def call_import_agent(descriptor: str, headers: Optional[Dict] = None) -> list:
242
- """Call the API to import an agent."""
243
- endpoint = "walker/import_agent"
244
- json_data = {"descriptor": descriptor}
245
- response = call_api(endpoint, headers=headers, json_data=json_data)
246
-
247
- if response is not None and response.status_code == 200:
248
- result = response.json()
249
- reports = result.get("reports", [])
250
- return reports[0] if reports else []
251
-
252
- return []
253
-
254
-
255
- def get_user_info() -> dict:
256
- """Get user information from the session state."""
257
- return {
258
- "root_id": st.session_state.get("ROOT_ID", ""),
259
- "token": st.session_state.get("TOKEN", ""),
260
- "expiration": st.session_state.get("EXPIRATION", ""),
261
- }
262
-
263
-
264
- def decode_base64_image(base64_string: str) -> Image:
265
- """Decode a base64 string into an image."""
266
- # Decode the base64 string
267
- image_data = base64.b64decode(base64_string)
268
-
269
- # Create a bytes buffer from the decoded bytes
270
- image_buffer = BytesIO(image_data)
271
-
272
- # Open the image using PIL
273
- return Image.open(image_buffer)
274
-
275
-
276
- class LongStringDumper(yaml.SafeDumper):
277
- """Custom YAML dumper to handle long strings."""
278
-
279
- def represent_scalar(
280
- self, tag: str, value: str, style: Optional[str] = None
281
- ) -> yaml.ScalarNode:
282
- """Represent scalar values, using block style for long strings."""
283
- # Replace any escape sequences to format the output as desired
284
- if (
285
- len(value) > 150 or "\n" in value
286
- ): # Adjust the threshold for long strings as needed
287
- style = "|"
288
- # converts all newline escapes to actual representations
289
- value = "\n".join([line.rstrip() for line in value.split("\n")])
290
- else:
291
- # converts all newline escapes to actual representations
292
- value = "\n".join([line.rstrip() for line in value.split("\n")]).rstrip()
293
-
294
- return super().represent_scalar(tag, value, style)
295
-
296
-
297
- def jac_yaml_dumper(
298
- data: Any,
299
- indent: int = 2,
300
- default_flow_style: bool = False,
301
- allow_unicode: bool = True,
302
- sort_keys: bool = False,
303
- ) -> str:
304
- """Dumps YAML data using LongStringDumper with customizable options."""
305
- return yaml.dump(
306
- data,
307
- Dumper=LongStringDumper,
308
- indent=indent,
309
- default_flow_style=default_flow_style,
310
- allow_unicode=allow_unicode,
311
- sort_keys=sort_keys,
312
- )
@@ -1,295 +0,0 @@
1
- """Streamlit widgets for JVCLI client app."""
2
-
3
- from typing import Any, Optional
4
-
5
- import streamlit as st
6
- import yaml
7
-
8
- from jvcli.client.lib.utils import call_get_action, call_update_action
9
-
10
-
11
- def app_header(agent_id: str, action_id: str, info: dict) -> tuple:
12
- """Render the app header and return model key and module root."""
13
-
14
- # Create a dynamic key for the session state using the action_id
15
- model_key = f"model_{agent_id}_{action_id}"
16
- module_root = info.get("config", {}).get("module_root")
17
-
18
- # Initialize session state if not already
19
- if model_key not in st.session_state:
20
- # Copy original data to prevent modification of original_data
21
- st.session_state[model_key] = call_get_action(
22
- agent_id=agent_id, action_id=action_id
23
- )
24
-
25
- # add standard action app header
26
- st.header(
27
- st.session_state[model_key]
28
- .get("_package", {})
29
- .get("meta", {})
30
- .get("title", "Action"),
31
- divider=True,
32
- )
33
-
34
- # Display the description from the model
35
- if description := st.session_state[model_key].get("description", False):
36
- st.text(description)
37
-
38
- def update_action() -> None:
39
- st.session_state[model_key]
40
- call_update_action(
41
- agent_id=agent_id,
42
- action_id=action_id,
43
- action_data=st.session_state[model_key],
44
- )
45
-
46
- current_state = st.session_state[model_key]["enabled"]
47
- new_state = st.checkbox(
48
- "Enabled",
49
- key="enabled",
50
- value=current_state,
51
- )
52
-
53
- if new_state != current_state:
54
- st.session_state[model_key]["enabled"] = new_state
55
- update_action()
56
- st.rerun()
57
-
58
- return model_key, module_root
59
-
60
-
61
- def snake_to_title(snake_str: str) -> str:
62
- """Convert a snake_case string to Title Case."""
63
- return snake_str.replace("_", " ").title()
64
-
65
-
66
- def app_controls(
67
- agent_id: str,
68
- action_id: str,
69
- hidden: Optional[list] = None,
70
- masked: Optional[list] = None,
71
- ) -> None:
72
- """Render the app controls for a given agent and action."""
73
- if hidden is None:
74
- hidden = []
75
- if masked is None:
76
- masked = []
77
-
78
- # Generate a dynamic key for the session state using the action_id
79
- model_key = f"model_{agent_id}_{action_id}"
80
-
81
- # Combine default masked keys with additional keys specified in 'masked'
82
- default_masked_keys = [
83
- "password",
84
- "token",
85
- "api_key",
86
- "key",
87
- "secret",
88
- "secret_key",
89
- ]
90
- all_masked_keys = set(default_masked_keys + masked)
91
-
92
- # Recursive function to handle nested dictionaries
93
- def render_fields(item_key: str, value: Any, parent_key: str = "") -> None:
94
- """Render fields based on their type."""
95
-
96
- # Skip rendering if the field is in the hidden list
97
- if item_key in hidden:
98
- return
99
-
100
- field_type = type(value)
101
- label = snake_to_title(item_key) # Convert item_key to Title Case
102
-
103
- # Special case for masked fields to render as a password field
104
- if item_key.lower() in all_masked_keys:
105
- st.session_state[model_key][item_key] = st.text_input(
106
- label, value=value, type="password", key=item_key
107
- )
108
-
109
- elif field_type == int:
110
- st.session_state[model_key][item_key] = st.number_input(
111
- label, value=value, step=1, key=item_key
112
- )
113
-
114
- elif field_type == float:
115
- st.session_state[model_key][item_key] = st.number_input(
116
- label, value=value, step=0.01, key=item_key
117
- )
118
-
119
- elif field_type == bool:
120
- st.session_state[model_key][item_key] = st.checkbox(
121
- label, value=value, key=item_key
122
- )
123
-
124
- elif field_type == list:
125
- yaml_str = st.text_area(
126
- label + " (YAML format)",
127
- value=yaml.dump(value, sort_keys=False),
128
- key=item_key,
129
- )
130
- try:
131
- # Update the list with the user-defined YAML
132
- loaded_value = yaml.safe_load(yaml_str)
133
- if not isinstance(loaded_value, list):
134
- raise ValueError("The provided YAML does not produce a list.")
135
- st.session_state[model_key][item_key] = loaded_value
136
- except (yaml.YAMLError, ValueError) as e:
137
- st.error(f"Error parsing YAML for {item_key}: {e}")
138
-
139
- elif field_type == str:
140
- if len(value) > 100:
141
- st.session_state[model_key][item_key] = st.text_area(
142
- label, value=value, key=item_key
143
- )
144
- else:
145
- st.session_state[model_key][item_key] = st.text_input(
146
- label, value=value, key=item_key
147
- )
148
-
149
- elif field_type == dict:
150
- yaml_str = st.text_area(
151
- label + " (YAML format)",
152
- value=yaml.dump(value, sort_keys=False),
153
- key=item_key,
154
- )
155
- try:
156
- # Update the dictionary with the user-defined YAML
157
- st.session_state[model_key][item_key] = yaml.safe_load(yaml_str) or {}
158
- except yaml.YAMLError as e:
159
- st.error(f"Error parsing YAML for {item_key}: {e}")
160
-
161
- else:
162
- st.write(f"Unsupported type for {item_key}: {field_type}")
163
-
164
- # Iterate over keys of context except specific keys
165
- keys_to_iterate = [
166
- key
167
- for key in (st.session_state[model_key]).keys()
168
- if key not in ["id", "version", "label", "description", "enabled", "_package"]
169
- ]
170
-
171
- for item_key in keys_to_iterate:
172
- render_fields(item_key, st.session_state[model_key][item_key])
173
-
174
-
175
- def app_update_action(agent_id: str, action_id: str) -> None:
176
- """Add a standard update button to apply changes."""
177
-
178
- model_key = f"model_{agent_id}_{action_id}"
179
-
180
- st.divider()
181
-
182
- if st.button("Update"):
183
- result = call_update_action(
184
- agent_id=agent_id,
185
- action_id=action_id,
186
- action_data=st.session_state[model_key],
187
- )
188
- if result and result.get("id", "") == action_id:
189
- st.success("Changes saved")
190
- else:
191
- st.error("Unable to save changes")
192
-
193
-
194
- def dynamic_form(
195
- field_definitions: list,
196
- initial_data: Optional[list] = None,
197
- session_key: str = "dynamic_form",
198
- ) -> list:
199
- """
200
- Create a dynamic form widget with add/remove functionality.
201
-
202
- Parameters:
203
- - field_definitions: A list of dictionaries where each dictionary defines a field
204
- with 'name', 'type', and any specific 'options' if needed.
205
- - initial_data: A list of dictionaries to initialize the form with predefined values.
206
- - session_key: A unique key to store and manage session state of the form.
207
-
208
- Returns:
209
- - list: The current value of the dynamic form.
210
- """
211
- if session_key not in st.session_state:
212
- if initial_data is not None:
213
- st.session_state[session_key] = []
214
- for idx, row_data in enumerate(initial_data):
215
- fields = {
216
- field["name"]: row_data.get(field["name"], "")
217
- for field in field_definitions
218
- }
219
- st.session_state[session_key].append({"id": idx, "fields": fields})
220
- else:
221
- st.session_state[session_key] = [
222
- {"id": 0, "fields": {field["name"]: "" for field in field_definitions}}
223
- ]
224
-
225
- def add_row() -> None: # pragma: no cover, don't know how to test this yet 😅
226
- """Add a new row to the dynamic form."""
227
- new_id = (
228
- max((item["id"] for item in st.session_state[session_key]), default=-1) + 1
229
- )
230
- new_row = {
231
- "id": new_id,
232
- "fields": {field["name"]: "" for field in field_definitions},
233
- }
234
- st.session_state[session_key].append(new_row)
235
-
236
- def remove_row(
237
- id_to_remove: int,
238
- ) -> None: # pragma: no cover, don't know how to test this yet 😅
239
- """Remove a row from the dynamic form."""
240
- st.session_state[session_key] = [
241
- item for item in st.session_state[session_key] if item["id"] != id_to_remove
242
- ]
243
-
244
- for item in st.session_state[session_key]:
245
- # Display fields in a row
246
- row_cols = st.columns(len(field_definitions))
247
- for i, field in enumerate(field_definitions):
248
- field_name = field["name"]
249
- field_type = field.get("type", "text")
250
- options = field.get("options", [])
251
-
252
- if field_type == "text":
253
- item["fields"][field_name] = row_cols[i].text_input(
254
- field_name,
255
- value=item["fields"][field_name],
256
- key=f"{session_key}_{item['id']}_{field_name}",
257
- )
258
- elif field_type == "number":
259
- field_value = item["fields"][field_name]
260
- if field_value == "":
261
- field_value = 0
262
- item["fields"][field_name] = row_cols[i].number_input(
263
- field_name,
264
- value=int(field_value),
265
- key=f"{session_key}_{item['id']}_{field_name}",
266
- )
267
- elif field_type == "select":
268
- item["fields"][field_name] = row_cols[i].selectbox(
269
- field_name,
270
- options,
271
- index=(
272
- options.index(item["fields"][field_name])
273
- if item["fields"][field_name] in options
274
- else 0
275
- ),
276
- key=f"{session_key}_{item['id']}_{field_name}",
277
- )
278
-
279
- # Add a remove button in a new row beneath the fields, aligned to the left
280
- with st.container():
281
- if st.button(
282
- "Remove",
283
- key=f"remove_{item['id']}",
284
- on_click=lambda id=item["id"]: remove_row(id),
285
- ):
286
- pass
287
-
288
- # Add a divider above the "Add Row" button
289
- st.divider()
290
-
291
- # Button to add a new row
292
- st.button("Add Row", on_click=add_row)
293
-
294
- # Return the current value of the dynamic form
295
- return [item["fields"] for item in st.session_state[session_key]]
@@ -1 +0,0 @@
1
- """Streamlit pages for the JVCLI client module."""