reasoning-deployment-service 0.2.8__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.
Potentially problematic release.
This version of reasoning-deployment-service might be problematic. Click here for more details.
- examples/programmatic_usage.py +154 -0
- reasoning_deployment_service/__init__.py +25 -0
- reasoning_deployment_service/cli_editor/__init__.py +5 -0
- reasoning_deployment_service/cli_editor/api_client.py +666 -0
- reasoning_deployment_service/cli_editor/cli_runner.py +343 -0
- reasoning_deployment_service/cli_editor/config.py +82 -0
- reasoning_deployment_service/cli_editor/google_deps.py +29 -0
- reasoning_deployment_service/cli_editor/reasoning_engine_creator.py +448 -0
- reasoning_deployment_service/gui_editor/__init__.py +5 -0
- reasoning_deployment_service/gui_editor/main.py +280 -0
- reasoning_deployment_service/gui_editor/requirements_minimal.txt +54 -0
- reasoning_deployment_service/gui_editor/run_program.sh +55 -0
- reasoning_deployment_service/gui_editor/src/__init__.py +1 -0
- reasoning_deployment_service/gui_editor/src/core/__init__.py +1 -0
- reasoning_deployment_service/gui_editor/src/core/api_client.py +647 -0
- reasoning_deployment_service/gui_editor/src/core/config.py +43 -0
- reasoning_deployment_service/gui_editor/src/core/google_deps.py +22 -0
- reasoning_deployment_service/gui_editor/src/core/reasoning_engine_creator.py +448 -0
- reasoning_deployment_service/gui_editor/src/ui/__init__.py +1 -0
- reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py +312 -0
- reasoning_deployment_service/gui_editor/src/ui/authorization_view.py +280 -0
- reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py +354 -0
- reasoning_deployment_service/gui_editor/src/ui/reasoning_engines_view.py +204 -0
- reasoning_deployment_service/gui_editor/src/ui/ui_components.py +1221 -0
- reasoning_deployment_service/reasoning_deployment_service.py +687 -0
- reasoning_deployment_service-0.2.8.dist-info/METADATA +177 -0
- reasoning_deployment_service-0.2.8.dist-info/RECORD +29 -0
- reasoning_deployment_service-0.2.8.dist-info/WHEEL +5 -0
- reasoning_deployment_service-0.2.8.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
"""Common UI components and utilities."""
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import ttk, messagebox
|
|
4
|
+
from tkinter import filedialog
|
|
5
|
+
from tkinter.scrolledtext import ScrolledText
|
|
6
|
+
import threading
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from typing import Callable, Optional, Dict, Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def async_operation(func: Callable, *args, callback: Optional[Callable] = None, ui_widget=None):
|
|
13
|
+
"""Execute function asynchronously and call callback with result on UI thread."""
|
|
14
|
+
def run():
|
|
15
|
+
try:
|
|
16
|
+
result = func(*args)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
result = e
|
|
19
|
+
if callback and ui_widget:
|
|
20
|
+
try:
|
|
21
|
+
ui_widget.after(0, lambda: callback(result))
|
|
22
|
+
except RuntimeError:
|
|
23
|
+
# Handle case where widget is destroyed or not in main loop
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
threading.Thread(target=run, daemon=True).start()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LogConsole(ttk.Frame):
|
|
30
|
+
"""Scrollable text console for logging messages with automatic trimming."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, master, max_lines=1000):
|
|
33
|
+
super().__init__(master)
|
|
34
|
+
self.max_lines = max_lines
|
|
35
|
+
self.text = ScrolledText(self, wrap="word", height=10)
|
|
36
|
+
self.text.pack(fill="both", expand=True)
|
|
37
|
+
|
|
38
|
+
def log(self, msg: str):
|
|
39
|
+
"""Add a message to the console with automatic trimming to prevent memory bloat."""
|
|
40
|
+
self.text.insert("end", msg + "\n")
|
|
41
|
+
|
|
42
|
+
# Trim if we exceed max_lines to prevent performance degradation
|
|
43
|
+
current_lines = int(self.text.index("end-1c").split('.')[0])
|
|
44
|
+
if current_lines > self.max_lines:
|
|
45
|
+
# Delete from beginning until we're back to max_lines
|
|
46
|
+
lines_to_delete = current_lines - self.max_lines
|
|
47
|
+
self.text.delete("1.0", f"{lines_to_delete + 1}.0")
|
|
48
|
+
|
|
49
|
+
self.text.see("end")
|
|
50
|
+
|
|
51
|
+
def clear(self):
|
|
52
|
+
"""Clear the console."""
|
|
53
|
+
self.text.delete("1.0", "end")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CreateReasoningEngineDialog(tk.Toplevel):
|
|
57
|
+
"""Dialog for creating a new reasoning engine with metadata."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, parent, api_client):
|
|
60
|
+
super().__init__(parent)
|
|
61
|
+
self.title("Create Reasoning Engine")
|
|
62
|
+
self.resizable(False, False)
|
|
63
|
+
self.attributes('-topmost', True)
|
|
64
|
+
self.result = None
|
|
65
|
+
self.api = api_client
|
|
66
|
+
|
|
67
|
+
frm = ttk.Frame(self, padding=15)
|
|
68
|
+
frm.pack(fill="both", expand=True)
|
|
69
|
+
|
|
70
|
+
# Auto-populate from config/profile
|
|
71
|
+
try:
|
|
72
|
+
profile = getattr(self.api, '_profile', {})
|
|
73
|
+
config_display_name = profile.get("display_name", "Your Agent")
|
|
74
|
+
config_description = profile.get("description", "Live Agent")
|
|
75
|
+
config_requirements = profile.get("requirements", [])
|
|
76
|
+
config_extra_packages = profile.get("extra_packages", [])
|
|
77
|
+
config_tool_description = profile.get("tool_description", "Tooling")
|
|
78
|
+
except Exception:
|
|
79
|
+
# Fallback values if profile access fails
|
|
80
|
+
config_display_name = "Your Agent"
|
|
81
|
+
config_description = "Live Agent"
|
|
82
|
+
config_requirements = []
|
|
83
|
+
config_extra_packages = []
|
|
84
|
+
config_tool_description = "Tooling"
|
|
85
|
+
|
|
86
|
+
# Display Name
|
|
87
|
+
ttk.Label(frm, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
88
|
+
self.display_name_var = tk.StringVar(value=config_display_name)
|
|
89
|
+
ttk.Entry(frm, textvariable=self.display_name_var, width=50).grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
90
|
+
|
|
91
|
+
# Description
|
|
92
|
+
ttk.Label(frm, text="Description:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
|
|
93
|
+
self.description_var = tk.StringVar(value=config_description)
|
|
94
|
+
ttk.Entry(frm, textvariable=self.description_var, width=50).grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
95
|
+
|
|
96
|
+
# Tool Description
|
|
97
|
+
ttk.Label(frm, text="Tool Description:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
|
|
98
|
+
self.tool_description_var = tk.StringVar(value=config_tool_description)
|
|
99
|
+
ttk.Entry(frm, textvariable=self.tool_description_var, width=50).grid(row=2, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
100
|
+
|
|
101
|
+
# Requirements (one per line)
|
|
102
|
+
ttk.Label(frm, text="Requirements:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 5))
|
|
103
|
+
requirements_frame = ttk.Frame(frm)
|
|
104
|
+
requirements_frame.grid(row=3, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
105
|
+
|
|
106
|
+
self.requirements_text = tk.Text(requirements_frame, height=4, width=50, wrap="word")
|
|
107
|
+
self.requirements_text.pack(side="left", fill="both", expand=True)
|
|
108
|
+
|
|
109
|
+
req_scroll = ttk.Scrollbar(requirements_frame, orient="vertical", command=self.requirements_text.yview)
|
|
110
|
+
self.requirements_text.configure(yscrollcommand=req_scroll.set)
|
|
111
|
+
req_scroll.pack(side="right", fill="y")
|
|
112
|
+
|
|
113
|
+
# Pre-populate requirements
|
|
114
|
+
if config_requirements:
|
|
115
|
+
self.requirements_text.insert("1.0", "\n".join(config_requirements))
|
|
116
|
+
|
|
117
|
+
# Extra Packages (one per line)
|
|
118
|
+
ttk.Label(frm, text="Extra Packages:", font=("TkDefaultFont", 10, "bold")).grid(row=4, column=0, sticky="nw", pady=(0, 5))
|
|
119
|
+
packages_frame = ttk.Frame(frm)
|
|
120
|
+
packages_frame.grid(row=4, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
121
|
+
|
|
122
|
+
self.packages_text = tk.Text(packages_frame, height=4, width=50, wrap="word")
|
|
123
|
+
self.packages_text.pack(side="left", fill="both", expand=True)
|
|
124
|
+
|
|
125
|
+
pkg_scroll = ttk.Scrollbar(packages_frame, orient="vertical", command=self.packages_text.yview)
|
|
126
|
+
self.packages_text.configure(yscrollcommand=pkg_scroll.set)
|
|
127
|
+
pkg_scroll.pack(side="right", fill="y")
|
|
128
|
+
|
|
129
|
+
# Pre-populate extra packages
|
|
130
|
+
if config_extra_packages:
|
|
131
|
+
self.packages_text.insert("1.0", "\n".join(config_extra_packages))
|
|
132
|
+
|
|
133
|
+
# Agent Import (only for live mode)
|
|
134
|
+
agent_import_value = getattr(self.api, 'agent_import', '') or ''
|
|
135
|
+
if self.api.is_live:
|
|
136
|
+
ttk.Label(frm, text="Agent Import:", font=("TkDefaultFont", 10, "bold")).grid(row=5, column=0, sticky="nw", pady=(0, 5))
|
|
137
|
+
self.agent_import_var = tk.StringVar(value=agent_import_value)
|
|
138
|
+
ttk.Entry(frm, textvariable=self.agent_import_var, width=50).grid(row=5, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
139
|
+
|
|
140
|
+
# Help text for agent import
|
|
141
|
+
help_label = ttk.Label(frm, text="Format: module:attribute (e.g., my_agent:agent)",
|
|
142
|
+
font=("TkDefaultFont", 8), foreground="gray")
|
|
143
|
+
help_label.grid(row=6, column=1, sticky="w", padx=(10, 0), pady=(0, 10))
|
|
144
|
+
current_row = 7
|
|
145
|
+
else:
|
|
146
|
+
current_row = 5
|
|
147
|
+
|
|
148
|
+
# Buttons
|
|
149
|
+
btns = ttk.Frame(frm)
|
|
150
|
+
btns.grid(row=current_row, column=0, columnspan=2, pady=(15, 0), sticky="e")
|
|
151
|
+
ttk.Button(btns, text="Cancel", command=self._cancel).pack(side="right", padx=(4, 0))
|
|
152
|
+
ttk.Button(btns, text="Create Engine", command=self._ok).pack(side="right")
|
|
153
|
+
|
|
154
|
+
frm.grid_columnconfigure(1, weight=1)
|
|
155
|
+
|
|
156
|
+
# Keyboard shortcuts
|
|
157
|
+
self.bind("<Return>", lambda _: self._ok())
|
|
158
|
+
self.bind("<Escape>", lambda _: self._cancel())
|
|
159
|
+
|
|
160
|
+
# Focus the display name field
|
|
161
|
+
self.display_name_var.trace_add("write", lambda *_: None) # Ensure field is editable
|
|
162
|
+
self.after(100, lambda: frm.focus_set())
|
|
163
|
+
|
|
164
|
+
def _ok(self):
|
|
165
|
+
"""Validate and save the engine configuration."""
|
|
166
|
+
display_name = self.display_name_var.get().strip()
|
|
167
|
+
description = self.description_var.get().strip()
|
|
168
|
+
tool_description = self.tool_description_var.get().strip()
|
|
169
|
+
|
|
170
|
+
if not display_name:
|
|
171
|
+
messagebox.showerror("Missing Display Name", "Display name is required.", parent=self)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if not description:
|
|
175
|
+
messagebox.showerror("Missing Description", "Description is required.", parent=self)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Parse requirements and extra packages
|
|
179
|
+
requirements_text = self.requirements_text.get("1.0", "end-1c").strip()
|
|
180
|
+
requirements = [line.strip() for line in requirements_text.split("\n") if line.strip()]
|
|
181
|
+
|
|
182
|
+
packages_text = self.packages_text.get("1.0", "end-1c").strip()
|
|
183
|
+
extra_packages = [line.strip() for line in packages_text.split("\n") if line.strip()]
|
|
184
|
+
|
|
185
|
+
# Agent import (live mode only)
|
|
186
|
+
agent_import = ""
|
|
187
|
+
if self.api.is_live:
|
|
188
|
+
agent_import = self.agent_import_var.get().strip()
|
|
189
|
+
if not agent_import:
|
|
190
|
+
messagebox.showerror("Missing Agent Import",
|
|
191
|
+
"Agent import is required for live mode (format: module:attribute).",
|
|
192
|
+
parent=self)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
self.result = {
|
|
196
|
+
"display_name": display_name,
|
|
197
|
+
"description": description,
|
|
198
|
+
"tool_description": tool_description,
|
|
199
|
+
"requirements": requirements,
|
|
200
|
+
"extra_packages": extra_packages,
|
|
201
|
+
"agent_import": agent_import
|
|
202
|
+
}
|
|
203
|
+
self.destroy()
|
|
204
|
+
|
|
205
|
+
def _cancel(self):
|
|
206
|
+
self.result = None
|
|
207
|
+
self.destroy()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class CreateReasoningEngineAdvancedDialog(tk.Toplevel):
|
|
211
|
+
"""Advanced dialog for creating reasoning engines with flexible project support."""
|
|
212
|
+
|
|
213
|
+
def __init__(self, parent, api_client):
|
|
214
|
+
super().__init__(parent)
|
|
215
|
+
self.title("Create Reasoning Engine - Advanced")
|
|
216
|
+
self.resizable(True, True)
|
|
217
|
+
self.geometry("750x725") # Increased from 700x800 for better visibility
|
|
218
|
+
self.attributes('-topmost', True)
|
|
219
|
+
self.result = None
|
|
220
|
+
self.api = api_client
|
|
221
|
+
|
|
222
|
+
# Create main scrollable frame
|
|
223
|
+
main_frame = ttk.Frame(self)
|
|
224
|
+
main_frame.pack(fill="both", expand=True, padx=15, pady=15)
|
|
225
|
+
|
|
226
|
+
# Create canvas and scrollbar for scrolling
|
|
227
|
+
canvas = tk.Canvas(main_frame)
|
|
228
|
+
scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview)
|
|
229
|
+
scrollable_frame = ttk.Frame(canvas)
|
|
230
|
+
|
|
231
|
+
scrollable_frame.bind(
|
|
232
|
+
"<Configure>",
|
|
233
|
+
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
|
237
|
+
canvas.configure(yscrollcommand=scrollbar.set)
|
|
238
|
+
|
|
239
|
+
# Pack canvas and scrollbar
|
|
240
|
+
canvas.pack(side="left", fill="both", expand=True)
|
|
241
|
+
scrollbar.pack(side="right", fill="y")
|
|
242
|
+
|
|
243
|
+
# Bind mousewheel to canvas (bind to canvas only, not globally)
|
|
244
|
+
def _on_mousewheel(event):
|
|
245
|
+
try:
|
|
246
|
+
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
|
|
247
|
+
except tk.TclError:
|
|
248
|
+
# Canvas destroyed, ignore
|
|
249
|
+
pass
|
|
250
|
+
canvas.bind("<MouseWheel>", _on_mousewheel)
|
|
251
|
+
self._canvas = canvas # Store reference for cleanup
|
|
252
|
+
|
|
253
|
+
frm = scrollable_frame
|
|
254
|
+
current_row = 0
|
|
255
|
+
|
|
256
|
+
# === ENGINE METADATA ===
|
|
257
|
+
meta_frame = ttk.LabelFrame(frm, text="Engine Configuration", padding=15)
|
|
258
|
+
meta_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
|
|
259
|
+
meta_frame.grid_columnconfigure(1, weight=1)
|
|
260
|
+
current_row += 1
|
|
261
|
+
|
|
262
|
+
# Auto-populate staging bucket from config
|
|
263
|
+
try:
|
|
264
|
+
config_bucket = getattr(self.api, 'staging_bucket', 'gs://my-staging-bucket')
|
|
265
|
+
except Exception:
|
|
266
|
+
config_bucket = 'gs://my-staging-bucket'
|
|
267
|
+
|
|
268
|
+
# Staging Bucket
|
|
269
|
+
ttk.Label(meta_frame, text="Staging Bucket:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
270
|
+
self.bucket_var = tk.StringVar(value=config_bucket)
|
|
271
|
+
bucket_frame = ttk.Frame(meta_frame)
|
|
272
|
+
bucket_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
273
|
+
ttk.Entry(bucket_frame, textvariable=self.bucket_var, width=35).pack(side="left", fill="x", expand=True)
|
|
274
|
+
ttk.Label(bucket_frame, text="(gs:// prefix optional)", font=("TkDefaultFont", 8), foreground="gray").pack(side="right", padx=(5, 0))
|
|
275
|
+
|
|
276
|
+
# Display Name
|
|
277
|
+
ttk.Label(meta_frame, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
|
|
278
|
+
self.display_name_var = tk.StringVar(value="Meeting Follow-Up Assistant")
|
|
279
|
+
ttk.Entry(meta_frame, textvariable=self.display_name_var, width=40).grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
280
|
+
|
|
281
|
+
# Description
|
|
282
|
+
ttk.Label(meta_frame, text="Description:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
|
|
283
|
+
self.description_var = tk.StringVar(value="AI-powered assistant with Gmail + Calendar")
|
|
284
|
+
ttk.Entry(meta_frame, textvariable=self.description_var, width=40).grid(row=2, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
285
|
+
|
|
286
|
+
# === AGENT SOURCE CONFIGURATION ===
|
|
287
|
+
source_frame = ttk.LabelFrame(frm, text="Agent Source Configuration", padding=15)
|
|
288
|
+
source_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
|
|
289
|
+
source_frame.grid_columnconfigure(1, weight=1)
|
|
290
|
+
current_row += 1
|
|
291
|
+
|
|
292
|
+
# Agent File - simplified to just file picker
|
|
293
|
+
ttk.Label(source_frame, text="Agent File:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
294
|
+
file_frame = ttk.Frame(source_frame)
|
|
295
|
+
file_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
296
|
+
self.agent_file_var = tk.StringVar(value="/Users/me/projects/my_agent_project/agent.py")
|
|
297
|
+
ttk.Entry(file_frame, textvariable=self.agent_file_var, width=40).pack(side="left", fill="x", expand=True)
|
|
298
|
+
ttk.Button(file_frame, text="Browse", command=self._browse_agent_file).pack(side="right", padx=(5, 0))
|
|
299
|
+
|
|
300
|
+
# Help text
|
|
301
|
+
ttk.Label(source_frame, text="Select the Python file containing your 'root_agent' definition",
|
|
302
|
+
font=("TkDefaultFont", 8), foreground="gray").grid(row=1, column=1, sticky="w", padx=(10, 0))
|
|
303
|
+
|
|
304
|
+
# === REQUIREMENTS CONFIGURATION ===
|
|
305
|
+
req_frame = ttk.LabelFrame(frm, text="Requirements Configuration", padding=15)
|
|
306
|
+
req_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
|
|
307
|
+
req_frame.grid_columnconfigure(1, weight=1)
|
|
308
|
+
current_row += 1
|
|
309
|
+
|
|
310
|
+
# Requirements source (Radio buttons)
|
|
311
|
+
ttk.Label(req_frame, text="Requirements Source:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
312
|
+
|
|
313
|
+
req_source_frame = ttk.Frame(req_frame)
|
|
314
|
+
req_source_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
315
|
+
|
|
316
|
+
self.req_source_var = tk.StringVar(value="file")
|
|
317
|
+
|
|
318
|
+
# Requirements file option
|
|
319
|
+
req_file_frame = ttk.Frame(req_source_frame)
|
|
320
|
+
req_file_frame.pack(fill="x", pady=(0, 5))
|
|
321
|
+
ttk.Radiobutton(req_file_frame, text="requirements.txt file:", variable=self.req_source_var,
|
|
322
|
+
value="file", command=self._toggle_req_source).pack(side="left")
|
|
323
|
+
self.req_file_var = tk.StringVar(value="requirements.txt")
|
|
324
|
+
self.req_file_entry = ttk.Entry(req_file_frame, textvariable=self.req_file_var, width=25)
|
|
325
|
+
self.req_file_entry.pack(side="left", padx=(10, 0), fill="x", expand=True)
|
|
326
|
+
self.req_file_button = ttk.Button(req_file_frame, text="Browse", command=self._browse_req_file)
|
|
327
|
+
self.req_file_button.pack(side="right", padx=(5, 0))
|
|
328
|
+
|
|
329
|
+
# Paste requirements option
|
|
330
|
+
req_text_frame = ttk.Frame(req_source_frame)
|
|
331
|
+
req_text_frame.pack(fill="x", pady=(0, 5))
|
|
332
|
+
ttk.Radiobutton(req_text_frame, text="Paste here:", variable=self.req_source_var,
|
|
333
|
+
value="text", command=self._toggle_req_source).pack(side="left")
|
|
334
|
+
|
|
335
|
+
# Requirements text area
|
|
336
|
+
self.req_text_frame = ttk.Frame(req_frame)
|
|
337
|
+
self.req_text_frame.grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(5, 0))
|
|
338
|
+
|
|
339
|
+
self.req_text = tk.Text(self.req_text_frame, height=6, width=50, wrap="word", state="disabled")
|
|
340
|
+
self.req_text.pack(side="left", fill="both", expand=True)
|
|
341
|
+
|
|
342
|
+
req_text_scroll = ttk.Scrollbar(self.req_text_frame, orient="vertical", command=self.req_text.yview)
|
|
343
|
+
self.req_text.configure(yscrollcommand=req_text_scroll.set)
|
|
344
|
+
req_text_scroll.pack(side="right", fill="y")
|
|
345
|
+
|
|
346
|
+
# Default requirements text
|
|
347
|
+
default_reqs = """pydantic>=2.0.0
|
|
348
|
+
google-auth>=2.23.0
|
|
349
|
+
google-adk>=1.0.0
|
|
350
|
+
google-cloud-aiplatform[adk,agent-engines]>=1.93.0
|
|
351
|
+
google-auth-oauthlib>=1.0.0
|
|
352
|
+
google-auth-httplib2>=0.1.0
|
|
353
|
+
google-api-python-client>=2.86.0"""
|
|
354
|
+
self.req_text.config(state="normal")
|
|
355
|
+
self.req_text.insert("1.0", default_reqs)
|
|
356
|
+
self.req_text.config(state="disabled")
|
|
357
|
+
|
|
358
|
+
# === OPTIONS ===
|
|
359
|
+
options_frame = ttk.LabelFrame(frm, text="Deployment Options", padding=15)
|
|
360
|
+
options_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
|
|
361
|
+
current_row += 1
|
|
362
|
+
|
|
363
|
+
# Exclude dev files checkbox
|
|
364
|
+
self.exclude_dev_var = tk.BooleanVar(value=True)
|
|
365
|
+
ttk.Checkbutton(options_frame, text="Exclude dev files (.env, __pycache__, .git, tests, etc.)",
|
|
366
|
+
variable=self.exclude_dev_var).pack(anchor="w")
|
|
367
|
+
|
|
368
|
+
# Enable tracing checkbox
|
|
369
|
+
self.enable_tracing_var = tk.BooleanVar(value=True)
|
|
370
|
+
ttk.Checkbutton(options_frame, text="Enable tracing for debugging",
|
|
371
|
+
variable=self.enable_tracing_var).pack(anchor="w", pady=(5, 0))
|
|
372
|
+
|
|
373
|
+
# === BUTTONS ===
|
|
374
|
+
button_frame = ttk.Frame(frm)
|
|
375
|
+
button_frame.grid(row=current_row, column=0, sticky="ew", pady=(20, 0))
|
|
376
|
+
|
|
377
|
+
# Left side buttons (utility functions)
|
|
378
|
+
left_buttons = ttk.Frame(button_frame)
|
|
379
|
+
left_buttons.pack(side="left")
|
|
380
|
+
ttk.Button(left_buttons, text="Populate with JSON", command=self._populate_from_json).pack(side="left", padx=(0, 4))
|
|
381
|
+
ttk.Button(left_buttons, text="Export to JSON", command=self._export_to_json).pack(side="left")
|
|
382
|
+
ttk.Button(left_buttons, text="Cancel", command=self._cancel).pack(side="right", padx=(4, 0))
|
|
383
|
+
|
|
384
|
+
# Right side buttons (main actions)
|
|
385
|
+
right_buttons = ttk.Frame(button_frame)
|
|
386
|
+
right_buttons.pack(side="right")
|
|
387
|
+
ttk.Button(right_buttons, text="Finalize Reasoning Engine", command=self._ok).pack(side="right")
|
|
388
|
+
|
|
389
|
+
# Configure column weights
|
|
390
|
+
frm.grid_columnconfigure(0, weight=1)
|
|
391
|
+
|
|
392
|
+
# Initialize UI state
|
|
393
|
+
self._toggle_req_source()
|
|
394
|
+
|
|
395
|
+
# Keyboard shortcuts
|
|
396
|
+
self.bind("<Return>", lambda _: self._ok())
|
|
397
|
+
self.bind("<Escape>", lambda _: self._cancel())
|
|
398
|
+
|
|
399
|
+
# Ensure proper cleanup on window close
|
|
400
|
+
self.protocol("WM_DELETE_WINDOW", self._cancel)
|
|
401
|
+
|
|
402
|
+
# Focus
|
|
403
|
+
self.focus_set()
|
|
404
|
+
|
|
405
|
+
def _cleanup(self):
|
|
406
|
+
"""Clean up resources to prevent memory leaks."""
|
|
407
|
+
# Cleanup is handled automatically since we bind to canvas, not globally
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
def _browse_directory(self):
|
|
411
|
+
"""Browse for agent directory."""
|
|
412
|
+
directory = filedialog.askdirectory(title="Select Agent Project Directory")
|
|
413
|
+
if directory:
|
|
414
|
+
self.agent_dir_var.set(directory)
|
|
415
|
+
|
|
416
|
+
def _browse_agent_file(self):
|
|
417
|
+
"""Browse for agent Python file and automatically set directory."""
|
|
418
|
+
file_path = filedialog.askopenfilename(
|
|
419
|
+
title="Select Agent Python File (containing root_agent)",
|
|
420
|
+
filetypes=[("Python files", "*.py"), ("All files", "*.*")]
|
|
421
|
+
)
|
|
422
|
+
if file_path:
|
|
423
|
+
self.agent_file_var.set(file_path)
|
|
424
|
+
# Automatically infer and set the directory from the file path
|
|
425
|
+
agent_directory = os.path.dirname(file_path)
|
|
426
|
+
print(f"🔍 Auto-detected agent directory: {agent_directory}")
|
|
427
|
+
|
|
428
|
+
def _browse_req_file(self):
|
|
429
|
+
"""Browse for requirements file."""
|
|
430
|
+
file_path = filedialog.askopenfilename(
|
|
431
|
+
title="Select Requirements File",
|
|
432
|
+
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
|
|
433
|
+
)
|
|
434
|
+
if file_path:
|
|
435
|
+
self.req_file_var.set(file_path)
|
|
436
|
+
|
|
437
|
+
def _toggle_agent_source(self):
|
|
438
|
+
"""No longer needed - simplified to just file picker."""
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
def _toggle_req_source(self):
|
|
442
|
+
"""Toggle requirements source input fields."""
|
|
443
|
+
if self.req_source_var.get() == "file":
|
|
444
|
+
self.req_file_entry.config(state="normal")
|
|
445
|
+
self.req_file_button.config(state="normal")
|
|
446
|
+
self.req_text.config(state="disabled")
|
|
447
|
+
else:
|
|
448
|
+
self.req_file_entry.config(state="disabled")
|
|
449
|
+
self.req_file_button.config(state="disabled")
|
|
450
|
+
self.req_text.config(state="normal")
|
|
451
|
+
|
|
452
|
+
def _validate(self):
|
|
453
|
+
"""Validate the current configuration."""
|
|
454
|
+
errors = []
|
|
455
|
+
|
|
456
|
+
# Validate staging bucket
|
|
457
|
+
if not self.bucket_var.get().strip():
|
|
458
|
+
errors.append("Staging bucket is required")
|
|
459
|
+
|
|
460
|
+
# Validate agent file
|
|
461
|
+
agent_file_path = self.agent_file_var.get().strip()
|
|
462
|
+
if not agent_file_path:
|
|
463
|
+
errors.append("Agent file path is required")
|
|
464
|
+
elif not os.path.exists(agent_file_path):
|
|
465
|
+
errors.append(f"Agent file does not exist: {agent_file_path}")
|
|
466
|
+
|
|
467
|
+
# Validate requirements source
|
|
468
|
+
if self.req_source_var.get() == "file":
|
|
469
|
+
req_file = self.req_file_var.get().strip()
|
|
470
|
+
if req_file and not os.path.exists(req_file):
|
|
471
|
+
errors.append(f"Requirements file does not exist: {req_file}")
|
|
472
|
+
else:
|
|
473
|
+
req_text = self.req_text.get("1.0", "end-1c").strip()
|
|
474
|
+
if not req_text:
|
|
475
|
+
errors.append("Requirements text is empty")
|
|
476
|
+
|
|
477
|
+
# Validate metadata
|
|
478
|
+
if not self.display_name_var.get().strip():
|
|
479
|
+
errors.append("Display name is required")
|
|
480
|
+
|
|
481
|
+
# Show results
|
|
482
|
+
if errors:
|
|
483
|
+
messagebox.showerror("Validation Errors", "\n".join(f"• {err}" for err in errors), parent=self)
|
|
484
|
+
else:
|
|
485
|
+
messagebox.showinfo("Validation Success", "✅ Configuration is valid!", parent=self)
|
|
486
|
+
|
|
487
|
+
def _populate_from_json(self):
|
|
488
|
+
"""Populate dialog fields from a JSON file."""
|
|
489
|
+
file_path = filedialog.askopenfilename(
|
|
490
|
+
title="Select Configuration JSON File",
|
|
491
|
+
filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
|
|
492
|
+
parent=self
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if not file_path:
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
with open(file_path, 'r') as f:
|
|
500
|
+
config = json.load(f)
|
|
501
|
+
|
|
502
|
+
# Populate basic fields
|
|
503
|
+
if "staging_bucket" in config:
|
|
504
|
+
self.bucket_var.set(config["staging_bucket"])
|
|
505
|
+
if "display_name" in config:
|
|
506
|
+
self.display_name_var.set(config["display_name"])
|
|
507
|
+
if "description" in config:
|
|
508
|
+
self.description_var.set(config["description"])
|
|
509
|
+
|
|
510
|
+
# Populate agent file path
|
|
511
|
+
if "agent_file_path" in config:
|
|
512
|
+
self.agent_file_var.set(config["agent_file_path"])
|
|
513
|
+
|
|
514
|
+
# Populate requirements
|
|
515
|
+
if "requirements_source_type" in config:
|
|
516
|
+
self.req_source_var.set(config["requirements_source_type"])
|
|
517
|
+
self._toggle_req_source() # Update UI state
|
|
518
|
+
|
|
519
|
+
if config["requirements_source_type"] == "file" and "requirements_file" in config:
|
|
520
|
+
self.req_file_var.set(config["requirements_file"])
|
|
521
|
+
elif config["requirements_source_type"] == "text" and "requirements_text" in config:
|
|
522
|
+
self.req_text.config(state="normal")
|
|
523
|
+
self.req_text.delete("1.0", "end")
|
|
524
|
+
self.req_text.insert("1.0", config["requirements_text"])
|
|
525
|
+
# Note: req_text state is managed by _toggle_req_source
|
|
526
|
+
|
|
527
|
+
# Populate options
|
|
528
|
+
if "exclude_dev_files" in config:
|
|
529
|
+
self.exclude_dev_var.set(bool(config["exclude_dev_files"]))
|
|
530
|
+
if "enable_tracing" in config:
|
|
531
|
+
self.enable_tracing_var.set(bool(config["enable_tracing"]))
|
|
532
|
+
|
|
533
|
+
messagebox.showinfo("Configuration Loaded",
|
|
534
|
+
f"✅ Configuration successfully loaded from:\n{os.path.basename(file_path)}",
|
|
535
|
+
parent=self)
|
|
536
|
+
|
|
537
|
+
except json.JSONDecodeError as e:
|
|
538
|
+
messagebox.showerror("JSON Error",
|
|
539
|
+
f"Invalid JSON file:\n{str(e)}",
|
|
540
|
+
parent=self)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
messagebox.showerror("Load Error",
|
|
543
|
+
f"Failed to load configuration:\n{str(e)}",
|
|
544
|
+
parent=self)
|
|
545
|
+
|
|
546
|
+
def _export_to_json(self):
|
|
547
|
+
"""Export current dialog configuration to a JSON file."""
|
|
548
|
+
# Get project info from API client
|
|
549
|
+
try:
|
|
550
|
+
project_id = getattr(self.api, 'project_id', 'unknown-project')
|
|
551
|
+
location = getattr(self.api, 'location', 'us-central1')
|
|
552
|
+
except Exception:
|
|
553
|
+
project_id = 'unknown-project'
|
|
554
|
+
location = 'us-central1'
|
|
555
|
+
|
|
556
|
+
# Collect current configuration
|
|
557
|
+
config = {
|
|
558
|
+
"project_id": project_id,
|
|
559
|
+
"location": location,
|
|
560
|
+
"staging_bucket": self.bucket_var.get().strip(),
|
|
561
|
+
"agent_file_path": self.agent_file_var.get().strip(),
|
|
562
|
+
"requirements_source_type": self.req_source_var.get(),
|
|
563
|
+
"requirements_file": self.req_file_var.get().strip() if self.req_source_var.get() == "file" else None,
|
|
564
|
+
"requirements_text": self.req_text.get("1.0", "end-1c").strip() if self.req_source_var.get() == "text" else None,
|
|
565
|
+
"display_name": self.display_name_var.get().strip(),
|
|
566
|
+
"description": self.description_var.get().strip(),
|
|
567
|
+
"exclude_dev_files": self.exclude_dev_var.get(),
|
|
568
|
+
"enable_tracing": self.enable_tracing_var.get(),
|
|
569
|
+
"_metadata": {
|
|
570
|
+
"exported_from": "AgentSpaceDeploymentService",
|
|
571
|
+
"dialog_version": "1.0",
|
|
572
|
+
"export_timestamp": str(__import__('datetime').datetime.now())
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
# Remove None values for cleaner JSON
|
|
577
|
+
config = {k: v for k, v in config.items() if v is not None}
|
|
578
|
+
|
|
579
|
+
# Default filename based on display name
|
|
580
|
+
default_name = self.display_name_var.get().strip()
|
|
581
|
+
if default_name:
|
|
582
|
+
default_filename = f"{default_name.lower().replace(' ', '_')}_config.json"
|
|
583
|
+
else:
|
|
584
|
+
default_filename = "reasoning_engine_config.json"
|
|
585
|
+
|
|
586
|
+
file_path = filedialog.asksaveasfilename(
|
|
587
|
+
title="Save Configuration as JSON",
|
|
588
|
+
defaultextension=".json",
|
|
589
|
+
initialfile=default_filename,
|
|
590
|
+
filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
|
|
591
|
+
parent=self
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
if not file_path:
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
with open(file_path, 'w') as f:
|
|
599
|
+
json.dump(config, f, indent=2)
|
|
600
|
+
|
|
601
|
+
messagebox.showinfo("Configuration Exported",
|
|
602
|
+
f"✅ Configuration successfully exported to:\n{os.path.basename(file_path)}",
|
|
603
|
+
parent=self)
|
|
604
|
+
|
|
605
|
+
except Exception as e:
|
|
606
|
+
messagebox.showerror("Export Error",
|
|
607
|
+
f"Failed to export configuration:\n{str(e)}",
|
|
608
|
+
parent=self)
|
|
609
|
+
|
|
610
|
+
def _ok(self):
|
|
611
|
+
import os
|
|
612
|
+
"""Validate and save the configuration."""
|
|
613
|
+
# Run validation first
|
|
614
|
+
errors = []
|
|
615
|
+
|
|
616
|
+
# Validate staging bucket
|
|
617
|
+
if not self.bucket_var.get().strip():
|
|
618
|
+
errors.append("Staging bucket is required")
|
|
619
|
+
|
|
620
|
+
# Validate agent file
|
|
621
|
+
agent_file = self.agent_file_var.get().strip()
|
|
622
|
+
if not agent_file:
|
|
623
|
+
errors.append("Agent file is required")
|
|
624
|
+
elif not os.path.exists(agent_file):
|
|
625
|
+
errors.append(f"Agent file does not exist: {agent_file}")
|
|
626
|
+
|
|
627
|
+
# Validate requirements source
|
|
628
|
+
if self.req_source_var.get() == "file":
|
|
629
|
+
req_file = self.req_file_var.get().strip()
|
|
630
|
+
if req_file and not os.path.exists(req_file):
|
|
631
|
+
errors.append(f"Requirements file does not exist: {req_file}")
|
|
632
|
+
else:
|
|
633
|
+
req_text = self.req_text.get("1.0", "end-1c").strip()
|
|
634
|
+
if not req_text:
|
|
635
|
+
errors.append("Requirements text is empty")
|
|
636
|
+
|
|
637
|
+
# Validate metadata
|
|
638
|
+
if not self.display_name_var.get().strip():
|
|
639
|
+
errors.append("Display name is required")
|
|
640
|
+
|
|
641
|
+
# Show validation errors if any
|
|
642
|
+
if errors:
|
|
643
|
+
messagebox.showerror("Validation Errors", "\n".join(f"• {err}" for err in errors), parent=self)
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
# Get project info from API client
|
|
647
|
+
try:
|
|
648
|
+
project_id = getattr(self.api, 'project_id', 'unknown-project')
|
|
649
|
+
location = getattr(self.api, 'location', 'us-central1')
|
|
650
|
+
except Exception:
|
|
651
|
+
project_id = 'unknown-project'
|
|
652
|
+
location = 'us-central1'
|
|
653
|
+
|
|
654
|
+
# Collect all configuration
|
|
655
|
+
import os
|
|
656
|
+
agent_file_path = self.agent_file_var.get().strip()
|
|
657
|
+
agent_directory = os.path.dirname(agent_file_path) if agent_file_path else ""
|
|
658
|
+
|
|
659
|
+
config = {
|
|
660
|
+
"project_id": project_id,
|
|
661
|
+
"location": location,
|
|
662
|
+
"staging_bucket": self.bucket_var.get().strip(),
|
|
663
|
+
"agent_directory": agent_directory, # Auto-inferred from file path
|
|
664
|
+
"agent_file_path": agent_file_path,
|
|
665
|
+
"requirements_source_type": self.req_source_var.get(),
|
|
666
|
+
"requirements_file": self.req_file_var.get().strip() if self.req_source_var.get() == "file" else None,
|
|
667
|
+
"requirements_text": self.req_text.get("1.0", "end-1c").strip() if self.req_source_var.get() == "text" else None,
|
|
668
|
+
"display_name": self.display_name_var.get().strip(),
|
|
669
|
+
"description": self.description_var.get().strip(),
|
|
670
|
+
"exclude_dev_files": self.exclude_dev_var.get(),
|
|
671
|
+
"enable_tracing": self.enable_tracing_var.get()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
self.result = config
|
|
675
|
+
self._cleanup()
|
|
676
|
+
self.destroy()
|
|
677
|
+
|
|
678
|
+
def _cancel(self):
|
|
679
|
+
"""Cancel dialog."""
|
|
680
|
+
self.result = None
|
|
681
|
+
self._cleanup()
|
|
682
|
+
self.destroy()
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class DeployToAgentSpaceDialog(tk.Toplevel):
|
|
686
|
+
"""Dialog for deploying reasoning engine to Agent Space with full configuration."""
|
|
687
|
+
|
|
688
|
+
def __init__(self, parent, api_client, engine_resource_name: str = ""):
|
|
689
|
+
super().__init__(parent)
|
|
690
|
+
self.title("Deploy to Agent Space")
|
|
691
|
+
self.resizable(False, False)
|
|
692
|
+
self.attributes('-topmost', True)
|
|
693
|
+
self.result = None
|
|
694
|
+
self.api = api_client
|
|
695
|
+
self.engine_resource_name = engine_resource_name
|
|
696
|
+
|
|
697
|
+
frm = ttk.Frame(self, padding=15)
|
|
698
|
+
frm.pack(fill="both", expand=True)
|
|
699
|
+
|
|
700
|
+
# Auto-populate from config/profile
|
|
701
|
+
try:
|
|
702
|
+
profile = getattr(self.api, '_profile', {})
|
|
703
|
+
config_display_name = profile.get("display_name", "Your Agent")
|
|
704
|
+
config_description = profile.get("description", "Live Agent")
|
|
705
|
+
config_tool_description = profile.get("tool_description", "Tooling")
|
|
706
|
+
except Exception:
|
|
707
|
+
# Fallback values if profile access fails
|
|
708
|
+
config_display_name = "Your Agent"
|
|
709
|
+
config_description = "Live Agent"
|
|
710
|
+
config_tool_description = "Tooling"
|
|
711
|
+
|
|
712
|
+
# Engine Info (read-only)
|
|
713
|
+
ttk.Label(frm, text="Reasoning Engine:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
714
|
+
engine_display = engine_resource_name if engine_resource_name else "Current Engine"
|
|
715
|
+
ttk.Label(frm, text=engine_display, wraplength=400, foreground="gray").grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
716
|
+
|
|
717
|
+
# Agent Display Name
|
|
718
|
+
ttk.Label(frm, text="Agent Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
|
|
719
|
+
self.agent_display_name_var = tk.StringVar(value=config_display_name)
|
|
720
|
+
ttk.Entry(frm, textvariable=self.agent_display_name_var, width=50).grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
721
|
+
|
|
722
|
+
# Agent Description
|
|
723
|
+
ttk.Label(frm, text="Agent Description:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
|
|
724
|
+
self.agent_description_var = tk.StringVar(value=config_description)
|
|
725
|
+
ttk.Entry(frm, textvariable=self.agent_description_var, width=50).grid(row=2, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
726
|
+
|
|
727
|
+
# Tool Description
|
|
728
|
+
ttk.Label(frm, text="Tool Description:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 5))
|
|
729
|
+
self.tool_description_var = tk.StringVar(value=config_tool_description)
|
|
730
|
+
ttk.Entry(frm, textvariable=self.tool_description_var, width=50).grid(row=3, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
731
|
+
|
|
732
|
+
# Authorization section
|
|
733
|
+
auth_frame = ttk.LabelFrame(frm, text="Authorization Settings", padding=10)
|
|
734
|
+
auth_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(15, 0))
|
|
735
|
+
auth_frame.grid_columnconfigure(1, weight=1)
|
|
736
|
+
|
|
737
|
+
# Enable Authorization checkbox
|
|
738
|
+
self.with_auth_var = tk.BooleanVar(value=True)
|
|
739
|
+
ttk.Checkbutton(auth_frame, text="Configure OAuth Authorization",
|
|
740
|
+
variable=self.with_auth_var, command=self._toggle_auth).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 5))
|
|
741
|
+
|
|
742
|
+
# Authorization Name
|
|
743
|
+
ttk.Label(auth_frame, text="Authorization Name:").grid(row=1, column=0, sticky="nw", pady=(0, 5))
|
|
744
|
+
self.auth_name_var = tk.StringVar(value="default-auth")
|
|
745
|
+
self.auth_entry = ttk.Entry(auth_frame, textvariable=self.auth_name_var, width=40)
|
|
746
|
+
self.auth_entry.grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
|
|
747
|
+
|
|
748
|
+
# Help text for authorization
|
|
749
|
+
self.auth_help = ttk.Label(auth_frame, text="Authorization will be created/attached for OAuth access",
|
|
750
|
+
font=("TkDefaultFont", 8), foreground="gray")
|
|
751
|
+
self.auth_help.grid(row=2, column=0, columnspan=2, sticky="w", pady=(0, 5))
|
|
752
|
+
|
|
753
|
+
# Advanced section (collapsible)
|
|
754
|
+
self.show_advanced_var = tk.BooleanVar(value=False)
|
|
755
|
+
advanced_check = ttk.Checkbutton(frm, text="Show Advanced Settings",
|
|
756
|
+
variable=self.show_advanced_var, command=self._toggle_advanced)
|
|
757
|
+
advanced_check.grid(row=5, column=0, columnspan=2, sticky="w", pady=(15, 5))
|
|
758
|
+
|
|
759
|
+
# Advanced frame (initially hidden)
|
|
760
|
+
self.advanced_frame = ttk.LabelFrame(frm, text="Advanced Settings", padding=10)
|
|
761
|
+
|
|
762
|
+
# Project details (read-only, for reference)
|
|
763
|
+
ttk.Label(self.advanced_frame, text="Project ID:", font=("TkDefaultFont", 9)).grid(row=0, column=0, sticky="nw", pady=(0, 3))
|
|
764
|
+
ttk.Label(self.advanced_frame, text=getattr(self.api, 'project_id', 'N/A'),
|
|
765
|
+
font=("TkDefaultFont", 9), foreground="gray").grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
|
|
766
|
+
|
|
767
|
+
ttk.Label(self.advanced_frame, text="Project Number:", font=("TkDefaultFont", 9)).grid(row=1, column=0, sticky="nw", pady=(0, 3))
|
|
768
|
+
ttk.Label(self.advanced_frame, text=getattr(self.api, 'project_number', 'N/A'),
|
|
769
|
+
font=("TkDefaultFont", 9), foreground="gray").grid(row=1, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
|
|
770
|
+
|
|
771
|
+
ttk.Label(self.advanced_frame, text="Location:", font=("TkDefaultFont", 9)).grid(row=2, column=0, sticky="nw", pady=(0, 3))
|
|
772
|
+
ttk.Label(self.advanced_frame, text=getattr(self.api, 'location', 'N/A'),
|
|
773
|
+
font=("TkDefaultFont", 9), foreground="gray").grid(row=2, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
|
|
774
|
+
|
|
775
|
+
ttk.Label(self.advanced_frame, text="Engine Name:", font=("TkDefaultFont", 9)).grid(row=3, column=0, sticky="nw", pady=(0, 3))
|
|
776
|
+
ttk.Label(self.advanced_frame, text=getattr(self.api, 'engine_name', 'N/A'),
|
|
777
|
+
font=("TkDefaultFont", 9), foreground="gray").grid(row=3, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
|
|
778
|
+
|
|
779
|
+
self.advanced_frame.grid_columnconfigure(1, weight=1)
|
|
780
|
+
|
|
781
|
+
# Buttons
|
|
782
|
+
btns = ttk.Frame(frm)
|
|
783
|
+
btns.grid(row=7, column=0, columnspan=2, pady=(20, 0), sticky="e")
|
|
784
|
+
ttk.Button(btns, text="Cancel", command=self._cancel).pack(side="right", padx=(4, 0))
|
|
785
|
+
ttk.Button(btns, text="Deploy to Agent Space", command=self._ok).pack(side="right")
|
|
786
|
+
|
|
787
|
+
frm.grid_columnconfigure(1, weight=1)
|
|
788
|
+
|
|
789
|
+
# Keyboard shortcuts
|
|
790
|
+
self.bind("<Return>", lambda _: self._ok())
|
|
791
|
+
self.bind("<Escape>", lambda _: self._cancel())
|
|
792
|
+
|
|
793
|
+
# Initialize UI state
|
|
794
|
+
self._toggle_auth()
|
|
795
|
+
self._toggle_advanced()
|
|
796
|
+
|
|
797
|
+
# Focus the agent display name field
|
|
798
|
+
self.after(100, lambda: self.agent_display_name_var.get() and None)
|
|
799
|
+
|
|
800
|
+
def _toggle_auth(self):
|
|
801
|
+
"""Toggle authorization fields based on checkbox."""
|
|
802
|
+
enabled = self.with_auth_var.get()
|
|
803
|
+
state = "normal" if enabled else "disabled"
|
|
804
|
+
self.auth_entry.config(state=state)
|
|
805
|
+
self.auth_help.config(foreground="black" if enabled else "gray")
|
|
806
|
+
|
|
807
|
+
def _toggle_advanced(self):
|
|
808
|
+
"""Toggle advanced settings visibility."""
|
|
809
|
+
if self.show_advanced_var.get():
|
|
810
|
+
self.advanced_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
|
811
|
+
else:
|
|
812
|
+
self.advanced_frame.grid_remove()
|
|
813
|
+
|
|
814
|
+
def _ok(self):
|
|
815
|
+
"""Validate and save the deployment configuration."""
|
|
816
|
+
agent_display_name = self.agent_display_name_var.get().strip()
|
|
817
|
+
agent_description = self.agent_description_var.get().strip()
|
|
818
|
+
tool_description = self.tool_description_var.get().strip()
|
|
819
|
+
with_authorization = self.with_auth_var.get()
|
|
820
|
+
auth_name = self.auth_name_var.get().strip()
|
|
821
|
+
|
|
822
|
+
if not agent_display_name:
|
|
823
|
+
messagebox.showerror("Missing Agent Display Name", "Agent display name is required.", parent=self)
|
|
824
|
+
return
|
|
825
|
+
|
|
826
|
+
if not agent_description:
|
|
827
|
+
messagebox.showerror("Missing Agent Description", "Agent description is required.", parent=self)
|
|
828
|
+
return
|
|
829
|
+
|
|
830
|
+
if with_authorization and not auth_name:
|
|
831
|
+
messagebox.showerror("Missing Authorization Name",
|
|
832
|
+
"Authorization name is required when OAuth is enabled.", parent=self)
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
self.result = {
|
|
836
|
+
"agent_display_name": agent_display_name,
|
|
837
|
+
"agent_description": agent_description,
|
|
838
|
+
"tool_description": tool_description,
|
|
839
|
+
"with_authorization": with_authorization,
|
|
840
|
+
"authorization_name": auth_name
|
|
841
|
+
}
|
|
842
|
+
self.destroy()
|
|
843
|
+
|
|
844
|
+
def _cancel(self):
|
|
845
|
+
self.result = None
|
|
846
|
+
self.destroy()
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class DeployDialog(tk.Toplevel):
|
|
850
|
+
"""Dialog for configuring deployment to Agent Space."""
|
|
851
|
+
|
|
852
|
+
def __init__(self, parent, default_auth_name: str = "", default_with_auth: bool = True):
|
|
853
|
+
super().__init__(parent)
|
|
854
|
+
self.title("Deploy to Agent Space")
|
|
855
|
+
self.resizable(False, False)
|
|
856
|
+
self.attributes('-topmost', True)
|
|
857
|
+
self.result = None
|
|
858
|
+
|
|
859
|
+
frm = ttk.Frame(self, padding=10)
|
|
860
|
+
frm.pack(fill="both", expand=True)
|
|
861
|
+
|
|
862
|
+
ttk.Label(frm, text="Authorization name").grid(row=0, column=0, sticky="w")
|
|
863
|
+
self.auth_var = tk.StringVar(value=default_auth_name)
|
|
864
|
+
ttk.Entry(frm, textvariable=self.auth_var, width=40).grid(row=0, column=1, padx=(8, 0), sticky="we")
|
|
865
|
+
|
|
866
|
+
self.chk_var = tk.BooleanVar(value=default_with_auth)
|
|
867
|
+
ttk.Checkbutton(frm, text="Configure OAuth (create/attach authorization)", variable=self.chk_var)\
|
|
868
|
+
.grid(row=1, column=0, columnspan=2, pady=(8, 0), sticky="w")
|
|
869
|
+
|
|
870
|
+
btns = ttk.Frame(frm)
|
|
871
|
+
btns.grid(row=2, column=0, columnspan=2, pady=(12, 0), sticky="e")
|
|
872
|
+
ttk.Button(btns, text="Cancel", command=self._cancel).pack(side="right", padx=4)
|
|
873
|
+
ttk.Button(btns, text="Deploy", command=self._ok).pack(side="right")
|
|
874
|
+
|
|
875
|
+
frm.grid_columnconfigure(1, weight=1)
|
|
876
|
+
self.bind("<Return>", lambda _: self._ok())
|
|
877
|
+
self.bind("<Escape>", lambda _: self._cancel())
|
|
878
|
+
|
|
879
|
+
def _ok(self):
|
|
880
|
+
name = self.auth_var.get().strip()
|
|
881
|
+
with_auth = self.chk_var.get()
|
|
882
|
+
if with_auth and not name:
|
|
883
|
+
messagebox.showerror("Missing authorization name", "Provide an authorization name or uncheck OAuth.")
|
|
884
|
+
return
|
|
885
|
+
self.result = {"authorization_name": name, "with_authorization": with_auth}
|
|
886
|
+
self.destroy()
|
|
887
|
+
|
|
888
|
+
def _cancel(self):
|
|
889
|
+
self.result = None
|
|
890
|
+
self.destroy()
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
class AgentDetailsDialog(tk.Toplevel):
|
|
894
|
+
"""Dialog for displaying detailed agent information."""
|
|
895
|
+
|
|
896
|
+
def __init__(self, parent, agent_data: dict):
|
|
897
|
+
super().__init__(parent)
|
|
898
|
+
self.title(f"Agent Details - {agent_data.get('display_name', 'Unknown')}")
|
|
899
|
+
self.resizable(True, True)
|
|
900
|
+
self.geometry("700x450")
|
|
901
|
+
|
|
902
|
+
# Improved dialog setup for reliability
|
|
903
|
+
self.transient(parent)
|
|
904
|
+
|
|
905
|
+
# Defer grab_set to avoid blocking issues
|
|
906
|
+
self.after(1, self._setup_modal)
|
|
907
|
+
|
|
908
|
+
# Store variables as instance attributes to avoid garbage collection
|
|
909
|
+
self.resource_var = tk.StringVar(value=agent_data.get("full_name", "N/A"))
|
|
910
|
+
auth_full = agent_data.get("authorization_full", "")
|
|
911
|
+
engine_full = agent_data.get("engine_full", "")
|
|
912
|
+
|
|
913
|
+
if auth_full and auth_full != "N/A":
|
|
914
|
+
self.auth_var = tk.StringVar(value=auth_full)
|
|
915
|
+
else:
|
|
916
|
+
self.auth_var = None
|
|
917
|
+
|
|
918
|
+
if engine_full and engine_full != "N/A":
|
|
919
|
+
self.engine_var = tk.StringVar(value=engine_full)
|
|
920
|
+
else:
|
|
921
|
+
self.engine_var = None
|
|
922
|
+
|
|
923
|
+
frm = ttk.Frame(self, padding=15)
|
|
924
|
+
frm.pack(fill="both", expand=True)
|
|
925
|
+
|
|
926
|
+
# Agent details
|
|
927
|
+
ttk.Label(frm, text="Agent ID:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
928
|
+
ttk.Label(frm, text=agent_data.get("id", "N/A"), wraplength=400).grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
929
|
+
|
|
930
|
+
ttk.Label(frm, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
|
|
931
|
+
ttk.Label(frm, text=agent_data.get("display_name", "N/A"), wraplength=400).grid(row=1, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
932
|
+
|
|
933
|
+
# Authorization ID (short name)
|
|
934
|
+
ttk.Label(frm, text="Authorization ID:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
|
|
935
|
+
auth_id = agent_data.get("authorization_id", "N/A")
|
|
936
|
+
ttk.Label(frm, text=auth_id, wraplength=400).grid(row=2, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
937
|
+
|
|
938
|
+
# Engine ID (short name)
|
|
939
|
+
ttk.Label(frm, text="Engine ID:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 5))
|
|
940
|
+
engine_id = agent_data.get("engine_id", "N/A")
|
|
941
|
+
ttk.Label(frm, text=engine_id, wraplength=400).grid(row=3, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
942
|
+
|
|
943
|
+
# Full Resource Name with copy button
|
|
944
|
+
ttk.Label(frm, text="Full Resource Name:", font=("TkDefaultFont", 10, "bold")).grid(row=4, column=0, sticky="nw", pady=(0, 10))
|
|
945
|
+
|
|
946
|
+
resource_frame = ttk.Frame(frm)
|
|
947
|
+
resource_frame.grid(row=4, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
|
|
948
|
+
|
|
949
|
+
resource_entry = ttk.Entry(resource_frame, textvariable=self.resource_var, state="readonly", width=50)
|
|
950
|
+
resource_entry.pack(side="left", fill="x", expand=True)
|
|
951
|
+
|
|
952
|
+
copy_resource_btn = ttk.Button(resource_frame, text="Copy", command=self._copy_resource_name)
|
|
953
|
+
copy_resource_btn.pack(side="right", padx=(5, 0))
|
|
954
|
+
|
|
955
|
+
current_row = 5
|
|
956
|
+
|
|
957
|
+
# Full Authorization Path with copy button (if available)
|
|
958
|
+
if self.auth_var:
|
|
959
|
+
ttk.Label(frm, text="Full Authorization Path:", font=("TkDefaultFont", 10, "bold")).grid(row=current_row, column=0, sticky="nw", pady=(0, 10))
|
|
960
|
+
|
|
961
|
+
auth_frame = ttk.Frame(frm)
|
|
962
|
+
auth_frame.grid(row=current_row, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
|
|
963
|
+
|
|
964
|
+
auth_entry = ttk.Entry(auth_frame, textvariable=self.auth_var, state="readonly", width=50)
|
|
965
|
+
auth_entry.pack(side="left", fill="x", expand=True)
|
|
966
|
+
|
|
967
|
+
copy_auth_btn = ttk.Button(auth_frame, text="Copy", command=self._copy_auth_path)
|
|
968
|
+
copy_auth_btn.pack(side="right", padx=(5, 0))
|
|
969
|
+
current_row += 1
|
|
970
|
+
|
|
971
|
+
# Full Engine Path with copy button (if available)
|
|
972
|
+
if self.engine_var:
|
|
973
|
+
ttk.Label(frm, text="Full Engine Path:", font=("TkDefaultFont", 10, "bold")).grid(row=current_row, column=0, sticky="nw", pady=(0, 10))
|
|
974
|
+
|
|
975
|
+
engine_frame = ttk.Frame(frm)
|
|
976
|
+
engine_frame.grid(row=current_row, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
|
|
977
|
+
|
|
978
|
+
engine_entry = ttk.Entry(engine_frame, textvariable=self.engine_var, state="readonly", width=50)
|
|
979
|
+
engine_entry.pack(side="left", fill="x", expand=True)
|
|
980
|
+
|
|
981
|
+
copy_engine_btn = ttk.Button(engine_frame, text="Copy", command=self._copy_engine_path)
|
|
982
|
+
copy_engine_btn.pack(side="right", padx=(5, 0))
|
|
983
|
+
current_row += 1
|
|
984
|
+
|
|
985
|
+
# Close button
|
|
986
|
+
close_btn = ttk.Button(frm, text="Close", command=self._close_dialog)
|
|
987
|
+
close_btn.grid(row=current_row, column=0, columnspan=2, pady=(20, 0))
|
|
988
|
+
|
|
989
|
+
frm.grid_columnconfigure(1, weight=1)
|
|
990
|
+
self.bind("<Escape>", lambda _: self._close_dialog())
|
|
991
|
+
|
|
992
|
+
# Ensure dialog is visible and focused
|
|
993
|
+
self.lift()
|
|
994
|
+
self.focus_set()
|
|
995
|
+
|
|
996
|
+
def _setup_modal(self):
|
|
997
|
+
"""Set up modal behavior after dialog is fully created."""
|
|
998
|
+
try:
|
|
999
|
+
self.attributes('-topmost', True)
|
|
1000
|
+
except tk.TclError:
|
|
1001
|
+
# If attributes fails, dialog is still functional
|
|
1002
|
+
pass
|
|
1003
|
+
|
|
1004
|
+
def _close_dialog(self):
|
|
1005
|
+
"""Properly close the dialog."""
|
|
1006
|
+
self.destroy()
|
|
1007
|
+
|
|
1008
|
+
def _copy_resource_name(self):
|
|
1009
|
+
"""Copy resource name to clipboard."""
|
|
1010
|
+
try:
|
|
1011
|
+
self.clipboard_clear()
|
|
1012
|
+
self.clipboard_append(self.resource_var.get())
|
|
1013
|
+
messagebox.showinfo("Copied", "Resource name copied to clipboard!", parent=self)
|
|
1014
|
+
except Exception:
|
|
1015
|
+
messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
|
|
1016
|
+
|
|
1017
|
+
def _copy_auth_path(self):
|
|
1018
|
+
"""Copy authorization path to clipboard."""
|
|
1019
|
+
if not self.auth_var:
|
|
1020
|
+
return
|
|
1021
|
+
try:
|
|
1022
|
+
self.clipboard_clear()
|
|
1023
|
+
self.clipboard_append(self.auth_var.get())
|
|
1024
|
+
messagebox.showinfo("Copied", "Authorization path copied to clipboard!", parent=self)
|
|
1025
|
+
except Exception:
|
|
1026
|
+
messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
|
|
1027
|
+
|
|
1028
|
+
def _copy_engine_path(self):
|
|
1029
|
+
"""Copy engine path to clipboard."""
|
|
1030
|
+
if not self.engine_var:
|
|
1031
|
+
return
|
|
1032
|
+
try:
|
|
1033
|
+
self.clipboard_clear()
|
|
1034
|
+
self.clipboard_append(self.engine_var.get())
|
|
1035
|
+
messagebox.showinfo("Copied", "Engine path copied to clipboard!", parent=self)
|
|
1036
|
+
except Exception:
|
|
1037
|
+
messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
class EngineDetailsDialog(tk.Toplevel):
|
|
1041
|
+
"""Dialog for displaying detailed engine information."""
|
|
1042
|
+
|
|
1043
|
+
def __init__(self, parent, engine_data: dict):
|
|
1044
|
+
super().__init__(parent)
|
|
1045
|
+
self.title(f"Engine Details - {engine_data.get('display_name', 'Unknown')}")
|
|
1046
|
+
self.resizable(True, True)
|
|
1047
|
+
self.geometry("600x400")
|
|
1048
|
+
|
|
1049
|
+
# Center the dialog
|
|
1050
|
+
self.transient(parent)
|
|
1051
|
+
self.attributes('-topmost', True)
|
|
1052
|
+
|
|
1053
|
+
# Store variable as instance attribute to avoid garbage collection
|
|
1054
|
+
self.resource_var = tk.StringVar(value=engine_data.get("resource_name", "N/A"))
|
|
1055
|
+
|
|
1056
|
+
frm = ttk.Frame(self, padding=15)
|
|
1057
|
+
frm.pack(fill="both", expand=True)
|
|
1058
|
+
|
|
1059
|
+
# Engine details
|
|
1060
|
+
ttk.Label(frm, text="Engine ID:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
|
|
1061
|
+
ttk.Label(frm, text=engine_data.get("id", "N/A"), wraplength=400).grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
1062
|
+
|
|
1063
|
+
ttk.Label(frm, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
|
|
1064
|
+
ttk.Label(frm, text=engine_data.get("display_name", "N/A"), wraplength=400).grid(row=1, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
1065
|
+
|
|
1066
|
+
ttk.Label(frm, text="Created:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
|
|
1067
|
+
ttk.Label(frm, text=engine_data.get("create_time", "N/A"), wraplength=400).grid(row=2, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
|
|
1068
|
+
|
|
1069
|
+
ttk.Label(frm, text="Resource Name:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 10))
|
|
1070
|
+
|
|
1071
|
+
# Resource name with copy button
|
|
1072
|
+
resource_frame = ttk.Frame(frm)
|
|
1073
|
+
resource_frame.grid(row=3, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
|
|
1074
|
+
|
|
1075
|
+
resource_entry = ttk.Entry(resource_frame, textvariable=self.resource_var, state="readonly", width=50)
|
|
1076
|
+
resource_entry.pack(side="left", fill="x", expand=True)
|
|
1077
|
+
|
|
1078
|
+
copy_btn = ttk.Button(resource_frame, text="Copy", command=self._copy_resource_name)
|
|
1079
|
+
copy_btn.pack(side="right", padx=(5, 0))
|
|
1080
|
+
|
|
1081
|
+
# Close button
|
|
1082
|
+
close_btn = ttk.Button(frm, text="Close", command=self._close_dialog)
|
|
1083
|
+
close_btn.grid(row=4, column=0, columnspan=2, pady=(20, 0))
|
|
1084
|
+
|
|
1085
|
+
frm.grid_columnconfigure(1, weight=1)
|
|
1086
|
+
self.bind("<Escape>", lambda _: self._close_dialog())
|
|
1087
|
+
|
|
1088
|
+
# Focus the dialog
|
|
1089
|
+
self.focus_set()
|
|
1090
|
+
|
|
1091
|
+
def _close_dialog(self):
|
|
1092
|
+
"""Properly close the dialog."""
|
|
1093
|
+
self.destroy()
|
|
1094
|
+
|
|
1095
|
+
def _copy_resource_name(self):
|
|
1096
|
+
"""Copy resource name to clipboard."""
|
|
1097
|
+
try:
|
|
1098
|
+
self.clipboard_clear()
|
|
1099
|
+
self.clipboard_append(self.resource_var.get())
|
|
1100
|
+
messagebox.showinfo("Copied", "Resource name copied to clipboard!", parent=self)
|
|
1101
|
+
except Exception:
|
|
1102
|
+
messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class LoadingDialog(tk.Toplevel):
|
|
1106
|
+
"""Loading dialog with spinner animation."""
|
|
1107
|
+
|
|
1108
|
+
def __init__(self, parent, message="Loading..."):
|
|
1109
|
+
super().__init__(parent)
|
|
1110
|
+
|
|
1111
|
+
# Initialize animation control first
|
|
1112
|
+
self._animation_active = True
|
|
1113
|
+
|
|
1114
|
+
self.title("Loading")
|
|
1115
|
+
self.resizable(False, False)
|
|
1116
|
+
self.geometry("300x120")
|
|
1117
|
+
|
|
1118
|
+
# Center the dialog
|
|
1119
|
+
self.transient(parent)
|
|
1120
|
+
self.attributes('-topmost', True)
|
|
1121
|
+
|
|
1122
|
+
# Try to remove window decorations, but handle errors gracefully
|
|
1123
|
+
try:
|
|
1124
|
+
self.overrideredirect(True)
|
|
1125
|
+
except tk.TclError:
|
|
1126
|
+
# Fall back to normal window if overrideredirect fails
|
|
1127
|
+
pass
|
|
1128
|
+
|
|
1129
|
+
# Center on parent (deferred to avoid blocking)
|
|
1130
|
+
self.after(0, self._center_on_parent, parent)
|
|
1131
|
+
|
|
1132
|
+
# Create frame with border
|
|
1133
|
+
main_frame = ttk.Frame(self, relief="solid", borderwidth=2, padding=20)
|
|
1134
|
+
main_frame.pack(fill="both", expand=True)
|
|
1135
|
+
|
|
1136
|
+
# Loading message
|
|
1137
|
+
ttk.Label(main_frame, text=message, font=("TkDefaultFont", 11)).pack(pady=(0, 10))
|
|
1138
|
+
|
|
1139
|
+
# Simple text-based spinner instead of progress bar (macOS compatibility)
|
|
1140
|
+
self.spinner_label = ttk.Label(main_frame, text="●●●", font=("TkDefaultFont", 12))
|
|
1141
|
+
self.spinner_label.pack(pady=(0, 10))
|
|
1142
|
+
|
|
1143
|
+
# Start text animation
|
|
1144
|
+
self.spinner_states = ["● ", " ● ", " ●", " ● "]
|
|
1145
|
+
self.spinner_index = 0
|
|
1146
|
+
self._animate_spinner()
|
|
1147
|
+
|
|
1148
|
+
# Cancel button (optional)
|
|
1149
|
+
ttk.Button(main_frame, text="Cancel", command=self.destroy).pack()
|
|
1150
|
+
|
|
1151
|
+
# Make sure it's on top
|
|
1152
|
+
self.focus_set()
|
|
1153
|
+
self.lift()
|
|
1154
|
+
|
|
1155
|
+
def _center_on_parent(self, parent):
|
|
1156
|
+
"""Center dialog on parent without blocking the UI."""
|
|
1157
|
+
try:
|
|
1158
|
+
x = parent.winfo_x() + (parent.winfo_width() // 2) - 150
|
|
1159
|
+
y = parent.winfo_y() + (parent.winfo_height() // 2) - 60
|
|
1160
|
+
self.geometry(f"300x120+{x}+{y}")
|
|
1161
|
+
except tk.TclError:
|
|
1162
|
+
# Fall back to default position if centering fails
|
|
1163
|
+
pass
|
|
1164
|
+
|
|
1165
|
+
def _animate_spinner(self):
|
|
1166
|
+
"""Animate the text spinner."""
|
|
1167
|
+
if not self._animation_active:
|
|
1168
|
+
return
|
|
1169
|
+
|
|
1170
|
+
try:
|
|
1171
|
+
self.spinner_label.config(text=self.spinner_states[self.spinner_index])
|
|
1172
|
+
self.spinner_index = (self.spinner_index + 1) % len(self.spinner_states)
|
|
1173
|
+
self.after(300, self._animate_spinner) # Update every 300ms
|
|
1174
|
+
except tk.TclError:
|
|
1175
|
+
# Widget destroyed, stop animation
|
|
1176
|
+
self._animation_active = False
|
|
1177
|
+
|
|
1178
|
+
def close(self):
|
|
1179
|
+
"""Properly close the loading dialog."""
|
|
1180
|
+
self._animation_active = False
|
|
1181
|
+
self.destroy()
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
class StatusButton(ttk.Button):
|
|
1185
|
+
"""Button that can be enabled/disabled based on conditions with status text."""
|
|
1186
|
+
|
|
1187
|
+
def __init__(self, master, text: str, command: Callable, **kwargs):
|
|
1188
|
+
super().__init__(master, text=text, command=self._wrapped_command, **kwargs)
|
|
1189
|
+
self._original_text = text
|
|
1190
|
+
self._original_command = command
|
|
1191
|
+
self._is_enabled = True
|
|
1192
|
+
|
|
1193
|
+
def _wrapped_command(self):
|
|
1194
|
+
"""Wrapped command that provides immediate visual feedback."""
|
|
1195
|
+
if self._is_enabled and self._original_command:
|
|
1196
|
+
# Provide immediate visual feedback without blocking UI
|
|
1197
|
+
self.config(state="active")
|
|
1198
|
+
# Remove blocking update_idletasks() call
|
|
1199
|
+
|
|
1200
|
+
# Execute the actual command
|
|
1201
|
+
try:
|
|
1202
|
+
self._original_command()
|
|
1203
|
+
finally:
|
|
1204
|
+
# Restore normal state with ultra-minimal delay (5ms for immediate feel)
|
|
1205
|
+
self.after(5, lambda: self.config(state="normal" if self._is_enabled else "disabled"))
|
|
1206
|
+
|
|
1207
|
+
def set_enabled(self, enabled: bool, reason: str = ""):
|
|
1208
|
+
"""Enable/disable button with optional reason tooltip."""
|
|
1209
|
+
self._is_enabled = enabled
|
|
1210
|
+
if enabled:
|
|
1211
|
+
self.config(state="normal")
|
|
1212
|
+
self.config(text=self._original_text)
|
|
1213
|
+
else:
|
|
1214
|
+
self.config(state="disabled")
|
|
1215
|
+
if reason:
|
|
1216
|
+
# You could add tooltip here if needed
|
|
1217
|
+
pass
|
|
1218
|
+
|
|
1219
|
+
@property
|
|
1220
|
+
def is_enabled(self) -> bool:
|
|
1221
|
+
return self._is_enabled
|