appkit-assistant 0.7.1__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.
@@ -0,0 +1,344 @@
1
+ """Dialog components for MCP server management."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ import reflex as rx
7
+ from reflex.vars import var_operation, var_operation_return
8
+ from reflex.vars.base import RETURN, CustomVarOperationReturn
9
+
10
+ import appkit_mantine as mn
11
+ from appkit_assistant.backend.models import MCPServer
12
+ from appkit_assistant.state.mcp_server_state import MCPServerState
13
+ from appkit_ui.components.dialogs import (
14
+ delete_dialog,
15
+ dialog_buttons,
16
+ dialog_header,
17
+ )
18
+ from appkit_ui.components.form_inputs import form_field
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ValidationState(rx.State):
24
+ url: str = ""
25
+ name: str = ""
26
+ desciption: str = ""
27
+ prompt: str = ""
28
+
29
+ url_error: str = ""
30
+ name_error: str = ""
31
+ description_error: str = ""
32
+ prompt_error: str = ""
33
+
34
+ @rx.event
35
+ def initialize(self, server: MCPServer | None = None) -> None:
36
+ """Reset validation state."""
37
+ logger.debug("Initializing ValidationState")
38
+ if server is None:
39
+ self.url = ""
40
+ self.name = ""
41
+ self.desciption = ""
42
+ self.prompt = ""
43
+ else:
44
+ self.url = server.url
45
+ self.name = server.name
46
+ self.desciption = server.description
47
+ self.prompt = server.prompt or ""
48
+
49
+ self.url_error = ""
50
+ self.name_error = ""
51
+ self.description_error = ""
52
+ self.prompt_error = ""
53
+
54
+ @rx.event
55
+ def validate_url(self) -> None:
56
+ """Validate the URL field."""
57
+ if not self.url or self.url.strip() == "":
58
+ self.url_error = "Die URL darf nicht leer sein."
59
+ elif not self.url.startswith("http://") and not self.url.startswith("https://"):
60
+ self.url_error = "Die URL muss mit http:// oder https:// beginnen."
61
+ else:
62
+ self.url_error = ""
63
+
64
+ @rx.event
65
+ def validate_name(self) -> None:
66
+ """Validate the name field."""
67
+ if not self.name or self.name.strip() == "":
68
+ self.name_error = "Der Name darf nicht leer sein."
69
+ elif len(self.name) < 3: # noqa: PLR2004
70
+ self.name_error = "Der Name muss mindestens 3 Zeichen lang sein."
71
+ else:
72
+ self.name_error = ""
73
+
74
+ @rx.event
75
+ def validate_description(self) -> None:
76
+ """Validate the description field."""
77
+ if self.desciption and len(self.desciption) > 200: # noqa: PLR2004
78
+ self.description_error = (
79
+ "Die Beschreibung darf maximal 200 Zeichen lang sein."
80
+ )
81
+ elif not self.desciption or self.desciption.strip() == "":
82
+ self.description_error = "Die Beschreibung darf nicht leer sein."
83
+ else:
84
+ self.description_error = ""
85
+
86
+ @rx.event
87
+ def validate_prompt(self) -> None:
88
+ """Validate the prompt field."""
89
+ if self.prompt and len(self.prompt) > 2000: # noqa: PLR2004
90
+ self.prompt_error = "Die Anweisung darf maximal 2000 Zeichen lang sein."
91
+ else:
92
+ self.prompt_error = ""
93
+
94
+ @rx.var
95
+ def has_errors(self) -> bool:
96
+ """Check if the form can be submitted."""
97
+ errors = bool(
98
+ self.url_error
99
+ or self.name_error
100
+ or self.description_error
101
+ or self.prompt_error
102
+ )
103
+
104
+ logger.debug("Has validation errors: %s", errors)
105
+ return errors
106
+
107
+ @rx.var
108
+ def prompt_remaining(self) -> int:
109
+ """Calculate remaining characters for prompt field."""
110
+ return 2000 - len(self.prompt or "")
111
+
112
+ def set_url(self, url: str) -> None:
113
+ """Set the URL and validate it."""
114
+ self.url = url
115
+ self.validate_url()
116
+
117
+ def set_name(self, name: str) -> None:
118
+ """Set the name and validate it."""
119
+ self.name = name
120
+ self.validate_name()
121
+
122
+ def set_description(self, description: str) -> None:
123
+ """Set the description and validate it."""
124
+ self.desciption = description
125
+ self.validate_description()
126
+
127
+ def set_prompt(self, prompt: str) -> None:
128
+ """Set the prompt and validate it."""
129
+ self.prompt = prompt
130
+ self.validate_prompt()
131
+
132
+
133
+ @var_operation
134
+ def json(obj: rx.Var, indent: int = 4) -> CustomVarOperationReturn[RETURN]:
135
+ return var_operation_return(
136
+ js_expression=f"JSON.stringify(JSON.parse({obj}), null, {indent})",
137
+ var_type=Any,
138
+ )
139
+
140
+
141
+ def mcp_server_form_fields(server: MCPServer | None = None) -> rx.Component:
142
+ """Reusable form fields for MCP server add/update dialogs."""
143
+ is_edit_mode = server is not None
144
+
145
+ fields = [
146
+ form_field(
147
+ name="name",
148
+ icon="server",
149
+ label="Name",
150
+ hint="Eindeutiger Name des MCP-Servers",
151
+ type="text",
152
+ placeholder="MCP-Server Name",
153
+ default_value=server.name if is_edit_mode else "",
154
+ required=True,
155
+ max_length=64,
156
+ on_change=ValidationState.set_name,
157
+ on_blur=ValidationState.validate_name,
158
+ validation_error=ValidationState.name_error,
159
+ ),
160
+ form_field(
161
+ name="description",
162
+ icon="text",
163
+ label="Beschreibung",
164
+ hint=(
165
+ "Kurze Beschreibung zur besseren Identifikation und Auswahl "
166
+ "durch den Nutzer"
167
+ ),
168
+ type="text",
169
+ placeholder="Beschreibung...",
170
+ max_length=200,
171
+ default_value=server.description if is_edit_mode else "",
172
+ required=True,
173
+ on_change=ValidationState.set_description,
174
+ on_blur=ValidationState.validate_description,
175
+ validation_error=ValidationState.description_error,
176
+ ),
177
+ form_field(
178
+ name="url",
179
+ icon="link",
180
+ label="URL",
181
+ hint="Vollständige URL des MCP-Servers (z. B. https://example.com/mcp/v1/sse)",
182
+ type="text",
183
+ placeholder="https://example.com/mcp/v1/sse",
184
+ default_value=server.url if is_edit_mode else "",
185
+ required=True,
186
+ on_change=ValidationState.set_url,
187
+ on_blur=ValidationState.validate_url,
188
+ validation_error=ValidationState.url_error,
189
+ ),
190
+ rx.flex(
191
+ mn.textarea(
192
+ name="prompt",
193
+ label="Prompt",
194
+ description=(
195
+ "Beschreiben Sie, wie das MCP-Tool verwendet werden soll. "
196
+ "Dies wird als Ergänzung des Systemprompts im Chat genutzt."
197
+ ),
198
+ placeholder=("Anweidungen an das Modell..."),
199
+ default_value=server.prompt if is_edit_mode else "",
200
+ on_change=ValidationState.set_prompt,
201
+ on_blur=ValidationState.validate_prompt,
202
+ validation_error=ValidationState.prompt_error,
203
+ autosize=True,
204
+ min_rows=3,
205
+ max_rows=8,
206
+ width="100%",
207
+ ),
208
+ rx.flex(
209
+ rx.cond(
210
+ ValidationState.prompt_remaining >= 0,
211
+ rx.text(
212
+ f"{ValidationState.prompt_remaining}/2000",
213
+ size="1",
214
+ color="gray",
215
+ ),
216
+ rx.text(
217
+ f"{ValidationState.prompt_remaining}/2000",
218
+ size="1",
219
+ color="red",
220
+ weight="bold",
221
+ ),
222
+ ),
223
+ justify="end",
224
+ width="100%",
225
+ margin_top="4px",
226
+ ),
227
+ direction="column",
228
+ spacing="0",
229
+ width="100%",
230
+ ),
231
+ mn.form.json(
232
+ name="headers_json",
233
+ label="HTTP Headers",
234
+ description=(
235
+ "Geben Sie die HTTP-Header im JSON-Format ein. "
236
+ 'Beispiel: {"Content-Type": "application/json", '
237
+ '"Authorization": "Bearer token"}'
238
+ ),
239
+ placeholder="{}",
240
+ validation_error="Ungültiges JSON",
241
+ default_value=json(server.headers) if is_edit_mode else "{}",
242
+ format_on_blur=True,
243
+ autosize=True,
244
+ min_rows=4,
245
+ max_rows=6,
246
+ width="100%",
247
+ ),
248
+ ]
249
+
250
+ return rx.flex(
251
+ *fields,
252
+ direction="column",
253
+ spacing="1",
254
+ )
255
+
256
+
257
+ def add_mcp_server_button() -> rx.Component:
258
+ """Button and dialog for adding a new MCP server."""
259
+ ValidationState.initialize()
260
+ return rx.dialog.root(
261
+ rx.dialog.trigger(
262
+ rx.button(
263
+ rx.icon("plus"),
264
+ rx.text("Neuen MCP Server anlegen", display=["none", "none", "block"]),
265
+ size="3",
266
+ variant="solid",
267
+ on_click=[ValidationState.initialize(server=None)],
268
+ ),
269
+ ),
270
+ rx.dialog.content(
271
+ dialog_header(
272
+ icon="server",
273
+ title="Neuen MCP Server anlegen",
274
+ description="Geben Sie die Details des neuen MCP Servers ein",
275
+ ),
276
+ rx.flex(
277
+ rx.form.root(
278
+ mcp_server_form_fields(),
279
+ dialog_buttons(
280
+ "MCP Server anlegen",
281
+ has_errors=ValidationState.has_errors,
282
+ ),
283
+ on_submit=MCPServerState.add_server,
284
+ reset_on_submit=False,
285
+ ),
286
+ width="100%",
287
+ direction="column",
288
+ spacing="4",
289
+ ),
290
+ class_name="dialog",
291
+ ),
292
+ )
293
+
294
+
295
+ def delete_mcp_server_dialog(server: MCPServer) -> rx.Component:
296
+ """Use the generic delete dialog component for MCP servers."""
297
+ return delete_dialog(
298
+ title="MCP Server löschen",
299
+ content=server.name,
300
+ on_click=lambda: MCPServerState.delete_server(server.id),
301
+ icon_button=True,
302
+ size="2",
303
+ variant="ghost",
304
+ color_scheme="crimson",
305
+ )
306
+
307
+
308
+ def update_mcp_server_dialog(server: MCPServer) -> rx.Component:
309
+ """Dialog for updating an existing MCP server."""
310
+ return rx.dialog.root(
311
+ rx.dialog.trigger(
312
+ rx.icon_button(
313
+ rx.icon("square-pen", size=20),
314
+ size="2",
315
+ variant="ghost",
316
+ on_click=[
317
+ lambda: MCPServerState.get_server(server.id),
318
+ ValidationState.initialize(server),
319
+ ],
320
+ ),
321
+ ),
322
+ rx.dialog.content(
323
+ dialog_header(
324
+ icon="server",
325
+ title="MCP Server aktualisieren",
326
+ description="Aktualisieren Sie die Details des MCP Servers",
327
+ ),
328
+ rx.flex(
329
+ rx.form.root(
330
+ mcp_server_form_fields(server),
331
+ dialog_buttons(
332
+ "MCP Server aktualisieren",
333
+ has_errors=ValidationState.has_errors,
334
+ ),
335
+ on_submit=MCPServerState.modify_server,
336
+ reset_on_submit=False,
337
+ ),
338
+ width="100%",
339
+ direction="column",
340
+ spacing="4",
341
+ ),
342
+ class_name="dialog",
343
+ ),
344
+ )
@@ -0,0 +1,76 @@
1
+ """Table component for displaying MCP servers."""
2
+
3
+ import reflex as rx
4
+ from reflex.components.radix.themes.components.table import TableRow
5
+
6
+ from appkit_assistant.backend.models import MCPServer
7
+ from appkit_assistant.components.mcp_server_dialogs import (
8
+ add_mcp_server_button,
9
+ delete_mcp_server_dialog,
10
+ update_mcp_server_dialog,
11
+ )
12
+ from appkit_assistant.state.mcp_server_state import MCPServerState
13
+
14
+
15
+ def mcp_server_table_row(server: MCPServer) -> TableRow:
16
+ """Show an MCP server in a table row."""
17
+ return rx.table.row(
18
+ rx.table.cell(
19
+ server.name,
20
+ white_space="nowrap",
21
+ ),
22
+ rx.table.cell(
23
+ rx.text(
24
+ server.description,
25
+ title=server.description,
26
+ style={
27
+ "display": "block",
28
+ "overflow": "hidden",
29
+ "text_overflow": "ellipsis",
30
+ "white_space": "nowrap",
31
+ },
32
+ ),
33
+ white_space="nowrap",
34
+ style={
35
+ "max_width": "0",
36
+ "width": "100%",
37
+ },
38
+ ),
39
+ rx.table.cell(
40
+ rx.hstack(
41
+ update_mcp_server_dialog(server),
42
+ delete_mcp_server_dialog(server),
43
+ spacing="2",
44
+ align_items="center",
45
+ ),
46
+ white_space="nowrap",
47
+ ),
48
+ justify="center",
49
+ vertical_align="middle",
50
+ style={"_hover": {"bg": rx.color("gray", 2)}},
51
+ )
52
+
53
+
54
+ def mcp_servers_table() -> rx.Fragment:
55
+ return rx.fragment(
56
+ rx.flex(
57
+ add_mcp_server_button(),
58
+ rx.spacer(),
59
+ ),
60
+ rx.table.root(
61
+ rx.table.header(
62
+ rx.table.row(
63
+ rx.table.column_header_cell("Name", width="20%"),
64
+ rx.table.column_header_cell(
65
+ "Beschreibung", width="calc(80% - 140px)"
66
+ ),
67
+ rx.table.column_header_cell("", width="140px"),
68
+ ),
69
+ ),
70
+ rx.table.body(rx.foreach(MCPServerState.servers, mcp_server_table_row)),
71
+ size="3",
72
+ width="100%",
73
+ table_layout="fixed",
74
+ on_mount=MCPServerState.load_servers_with_toast,
75
+ ),
76
+ )