Add ProcessingLogDialog for displaying processing logs and enhance SettingsDialog with package status checks
This commit is contained in:
@@ -16,6 +16,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .utils import Logger
|
from .utils import Logger
|
||||||
from .settings_dialog import SettingsDialog
|
from .settings_dialog import SettingsDialog
|
||||||
|
from .processing_log_dialog import ProcessingLogDialog
|
||||||
|
|
||||||
|
|
||||||
class DryRunVar:
|
class DryRunVar:
|
||||||
@@ -47,6 +48,7 @@ class MainGUI:
|
|||||||
self.edit_mode = False
|
self.edit_mode = False
|
||||||
self.workflow_items = {}
|
self.workflow_items = {}
|
||||||
self.current_workflow_items = []
|
self.current_workflow_items = []
|
||||||
|
self.active_workflow_item = None # Currently selected item from All Items list
|
||||||
|
|
||||||
# Repository data
|
# Repository data
|
||||||
self.target_repos = []
|
self.target_repos = []
|
||||||
@@ -77,6 +79,7 @@ class MainGUI:
|
|||||||
self.target_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
|
self.target_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
self.forked_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
|
self.forked_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
self.workflow_item_dropdown_ref = ft.Ref[ft.Dropdown]()
|
self.workflow_item_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
|
self.active_item_display_ref = ft.Ref[ft.Container]()
|
||||||
self.item_counter_ref = ft.Ref[ft.Text]()
|
self.item_counter_ref = ft.Ref[ft.Text]()
|
||||||
|
|
||||||
# DataTable ref for all items
|
# DataTable ref for all items
|
||||||
@@ -84,6 +87,9 @@ class MainGUI:
|
|||||||
|
|
||||||
# All items display
|
# All items display
|
||||||
self.all_items_container_ref = ft.Ref[ft.Column]()
|
self.all_items_container_ref = ft.Ref[ft.Column]()
|
||||||
|
self.all_items_search_ref = ft.Ref[ft.TextField]()
|
||||||
|
self.all_items_type_filter_ref = ft.Ref[ft.RadioGroup]()
|
||||||
|
self.all_items_repo_filter_ref = ft.Ref[ft.RadioGroup]()
|
||||||
self.item_detail_dialog_ref = ft.Ref[ft.AlertDialog]()
|
self.item_detail_dialog_ref = ft.Ref[ft.AlertDialog]()
|
||||||
|
|
||||||
# Sidebar state
|
# Sidebar state
|
||||||
@@ -121,9 +127,9 @@ class MainGUI:
|
|||||||
),
|
),
|
||||||
ft.Container(expand=True),
|
ft.Container(expand=True),
|
||||||
ft.IconButton(
|
ft.IconButton(
|
||||||
icon=ft.icons.PSYCHOLOGY,
|
icon=ft.icons.LIST_ALT,
|
||||||
tooltip="Check AI Modules",
|
tooltip="Processing Log",
|
||||||
on_click=self._check_ai_modules_manual,
|
on_click=self._open_processing_log,
|
||||||
),
|
),
|
||||||
ft.IconButton(
|
ft.IconButton(
|
||||||
icon=ft.icons.SETTINGS,
|
icon=ft.icons.SETTINGS,
|
||||||
@@ -172,12 +178,22 @@ class MainGUI:
|
|||||||
vertical_alignment=ft.CrossAxisAlignment.STRETCH,
|
vertical_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Overall layout: Top nav + bottom section
|
# Create hidden log text field for the processing log dialog
|
||||||
|
hidden_log_text = ft.TextField(
|
||||||
|
ref=self.log_text_ref,
|
||||||
|
multiline=True,
|
||||||
|
read_only=True,
|
||||||
|
text_style=ft.TextStyle(font_family="Courier New"),
|
||||||
|
visible=False, # Hidden from main UI
|
||||||
|
)
|
||||||
|
|
||||||
|
# Overall layout: Top nav + bottom section + hidden log field
|
||||||
app_layout = ft.Column(
|
app_layout = ft.Column(
|
||||||
[
|
[
|
||||||
top_nav,
|
top_nav,
|
||||||
ft.Divider(height=1),
|
ft.Divider(height=1),
|
||||||
bottom_section,
|
bottom_section,
|
||||||
|
hidden_log_text, # Hidden but accessible for dialog
|
||||||
],
|
],
|
||||||
spacing=0,
|
spacing=0,
|
||||||
expand=True,
|
expand=True,
|
||||||
@@ -221,11 +237,6 @@ class MainGUI:
|
|||||||
content=ft.Row(
|
content=ft.Row(
|
||||||
[
|
[
|
||||||
ft.Container(expand=True),
|
ft.Container(expand=True),
|
||||||
ft.IconButton(
|
|
||||||
icon=ft.icons.PSYCHOLOGY,
|
|
||||||
tooltip="Check AI Modules",
|
|
||||||
on_click=self._check_ai_modules_manual,
|
|
||||||
),
|
|
||||||
ft.IconButton(
|
ft.IconButton(
|
||||||
icon=ft.icons.SETTINGS,
|
icon=ft.icons.SETTINGS,
|
||||||
tooltip="Settings",
|
tooltip="Settings",
|
||||||
@@ -303,43 +314,62 @@ class MainGUI:
|
|||||||
# Action controls (for action mode)
|
# Action controls (for action mode)
|
||||||
action_controls = ft.Column(
|
action_controls = ft.Column(
|
||||||
[
|
[
|
||||||
ft.Text("View", weight=ft.FontWeight.BOLD),
|
ft.Text("Active Item", weight=ft.FontWeight.BOLD, size=14),
|
||||||
ft.RadioGroup(
|
ft.Row([
|
||||||
ref=self.repo_source_ref,
|
ft.Container(
|
||||||
content=ft.Row([
|
ref=self.active_item_display_ref,
|
||||||
ft.Radio(value="target", label="Target"),
|
content=ft.Text(
|
||||||
ft.Radio(value="fork", label="Fork"),
|
"No item selected",
|
||||||
]),
|
color=ft.colors.GREY_500,
|
||||||
value="target",
|
italic=True,
|
||||||
on_change=lambda e: self._filter_workflow_items(),
|
text_align=ft.TextAlign.CENTER,
|
||||||
),
|
),
|
||||||
ft.Text("Item Type", weight=ft.FontWeight.BOLD),
|
padding=10,
|
||||||
ft.RadioGroup(
|
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||||
ref=self.item_type_ref,
|
border_radius=8,
|
||||||
content=ft.Row([
|
bgcolor=ft.colors.GREY_900,
|
||||||
ft.Radio(value="pull_request", label="PRs"),
|
expand=True,
|
||||||
ft.Radio(value="issue", label="Issues"),
|
|
||||||
]),
|
|
||||||
value="pull_request",
|
|
||||||
on_change=lambda e: self._filter_workflow_items(),
|
|
||||||
),
|
),
|
||||||
|
], spacing=5),
|
||||||
|
ft.Divider(height=10),
|
||||||
|
ft.Text("All Items", weight=ft.FontWeight.BOLD, size=14),
|
||||||
ft.Row([
|
ft.Row([
|
||||||
ft.ElevatedButton(
|
ft.ElevatedButton(
|
||||||
"📥 Load Items",
|
"📥 Pull PRs/Issues",
|
||||||
on_click=lambda e: self.page.run_task(self._load_workflow_items_async),
|
on_click=lambda e: self.page.run_task(self._load_workflow_items_async),
|
||||||
),
|
),
|
||||||
ft.Text(ref=self.item_counter_ref, value="No items loaded"),
|
ft.Text(ref=self.item_counter_ref, value="No items loaded"),
|
||||||
]),
|
]),
|
||||||
ft.Dropdown(
|
ft.TextField(
|
||||||
ref=self.workflow_item_dropdown_ref,
|
ref=self.all_items_search_ref,
|
||||||
label="Select Workflow Item",
|
hint_text="Search items...",
|
||||||
hint_text="Select an item",
|
prefix_icon=ft.icons.SEARCH,
|
||||||
options=[],
|
dense=True,
|
||||||
expand=True,
|
on_change=self._on_all_items_search_changed,
|
||||||
on_change=self._on_workflow_item_selected,
|
border_radius=8,
|
||||||
|
),
|
||||||
|
ft.Text("Source Repo", weight=ft.FontWeight.BOLD),
|
||||||
|
ft.RadioGroup(
|
||||||
|
ref=self.all_items_type_filter_ref,
|
||||||
|
content=ft.Row([
|
||||||
|
ft.Radio(value="both", label="Both"),
|
||||||
|
ft.Radio(value="prs", label="PRs"),
|
||||||
|
ft.Radio(value="issues", label="Issues"),
|
||||||
|
], spacing=5),
|
||||||
|
value="both",
|
||||||
|
on_change=self._on_all_items_filter_changed,
|
||||||
|
),
|
||||||
|
ft.Text("Item Type", weight=ft.FontWeight.BOLD),
|
||||||
|
ft.RadioGroup(
|
||||||
|
ref=self.all_items_repo_filter_ref,
|
||||||
|
content=ft.Row([
|
||||||
|
ft.Radio(value="both", label="Both"),
|
||||||
|
ft.Radio(value="target", label="Target"),
|
||||||
|
ft.Radio(value="fork", label="Fork"),
|
||||||
|
], spacing=5),
|
||||||
|
value="both",
|
||||||
|
on_change=self._on_all_items_filter_changed,
|
||||||
),
|
),
|
||||||
ft.Divider(height=10),
|
|
||||||
ft.Text("All Items", weight=ft.FontWeight.BOLD, size=14),
|
|
||||||
ft.Container(
|
ft.Container(
|
||||||
content=ft.Column(
|
content=ft.Column(
|
||||||
ref=self.all_items_container_ref,
|
ref=self.all_items_container_ref,
|
||||||
@@ -427,16 +457,6 @@ class MainGUI:
|
|||||||
icon=ft.icons.DIFFERENCE,
|
icon=ft.icons.DIFFERENCE,
|
||||||
content=self._create_diff_tab()
|
content=self._create_diff_tab()
|
||||||
),
|
),
|
||||||
ft.Tab(
|
|
||||||
text="Processing Log",
|
|
||||||
icon=ft.icons.LIST_ALT,
|
|
||||||
content=self._create_log_tab()
|
|
||||||
),
|
|
||||||
ft.Tab(
|
|
||||||
text="All Items",
|
|
||||||
icon=ft.icons.VIEW_LIST,
|
|
||||||
content=self._create_all_items_tab()
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
expand=True,
|
expand=True,
|
||||||
)
|
)
|
||||||
@@ -607,22 +627,6 @@ class MainGUI:
|
|||||||
expand=True,
|
expand=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_log_tab(self) -> ft.Container:
|
|
||||||
"""Create the processing log tab"""
|
|
||||||
log_text = ft.TextField(
|
|
||||||
ref=self.log_text_ref,
|
|
||||||
multiline=True,
|
|
||||||
read_only=True,
|
|
||||||
expand=True,
|
|
||||||
text_style=ft.TextStyle(font_family="Courier New"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return ft.Container(
|
|
||||||
content=log_text,
|
|
||||||
padding=20,
|
|
||||||
expand=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_all_items_tab(self) -> ft.Container:
|
def _create_all_items_tab(self) -> ft.Container:
|
||||||
"""Create the all items tab"""
|
"""Create the all items tab"""
|
||||||
# DataTable for items
|
# DataTable for items
|
||||||
@@ -732,77 +736,65 @@ class MainGUI:
|
|||||||
self._display_workflow_item(item)
|
self._display_workflow_item(item)
|
||||||
break
|
break
|
||||||
|
|
||||||
def _filter_workflow_items(self):
|
def _on_all_items_search_changed(self, e):
|
||||||
"""Filter workflow items based on current selections"""
|
"""Handle search field change in All Items list"""
|
||||||
print("=" * 60)
|
if not self.all_items_search_ref.current:
|
||||||
print("FILTER METHOD CALLED")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
if not self.repo_source_ref.current or not self.item_type_ref.current:
|
|
||||||
print("ERROR: repo_source or item_type ref not available")
|
|
||||||
if self.logger:
|
|
||||||
self.logger.log("Cannot filter: repo source or item type not selected")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
source = self.repo_source_ref.current.value
|
search_query = self.all_items_search_ref.current.value or ""
|
||||||
item_type = self.item_type_ref.current.value
|
type_filter = self.all_items_type_filter_ref.current.value if self.all_items_type_filter_ref.current else "both"
|
||||||
print(f"DEBUG: source='{source}', item_type='{item_type}'")
|
repo_filter = self.all_items_repo_filter_ref.current.value if self.all_items_repo_filter_ref.current else "both"
|
||||||
|
self._populate_all_items(search_query, type_filter, repo_filter)
|
||||||
|
|
||||||
# Map item_type to the correct key suffix
|
def _on_all_items_filter_changed(self, e):
|
||||||
# "pull_request" → "prs", "issue" → "issues"
|
"""Handle filter change in All Items list (type or repo source)"""
|
||||||
if item_type == "pull_request":
|
search_query = self.all_items_search_ref.current.value if self.all_items_search_ref.current else ""
|
||||||
type_suffix = "prs"
|
type_filter = self.all_items_type_filter_ref.current.value if self.all_items_type_filter_ref.current else "both"
|
||||||
elif item_type == "issue":
|
repo_filter = self.all_items_repo_filter_ref.current.value if self.all_items_repo_filter_ref.current else "both"
|
||||||
type_suffix = "issues"
|
self._populate_all_items(search_query, type_filter, repo_filter)
|
||||||
else:
|
|
||||||
type_suffix = f"{item_type}s"
|
|
||||||
|
|
||||||
key = f"{source}_{type_suffix}"
|
def _filter_workflow_items(self):
|
||||||
print(f"DEBUG: Mapped item_type '{item_type}' to suffix '{type_suffix}'")
|
"""Collect all workflow items (no filtering since toggles were removed)"""
|
||||||
print(f"DEBUG: Looking for key '{key}'")
|
print("=" * 60)
|
||||||
|
print("COLLECTING WORKFLOW ITEMS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Collect all items from all categories since filter toggles are removed
|
||||||
|
all_items = []
|
||||||
|
for key, items in self.workflow_items.items():
|
||||||
|
all_items.extend(items)
|
||||||
|
|
||||||
|
self.current_workflow_items = all_items
|
||||||
|
print(f"DEBUG: Collected {len(all_items)} total items")
|
||||||
print(f"DEBUG: Available keys in workflow_items: {list(self.workflow_items.keys())}")
|
print(f"DEBUG: Available keys in workflow_items: {list(self.workflow_items.keys())}")
|
||||||
|
|
||||||
self.current_workflow_items = self.workflow_items.get(key, [])
|
|
||||||
print(f"DEBUG: Found {len(self.current_workflow_items)} items for key '{key}'")
|
|
||||||
|
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.log(f"Filtering workflow items: source={source}, type={item_type}, key={key}")
|
self.logger.log(f"Collected {len(all_items)} workflow items from all categories")
|
||||||
self.logger.log(f"Available workflow item keys: {list(self.workflow_items.keys())}")
|
self.logger.log(f"Available workflow item keys: {list(self.workflow_items.keys())}")
|
||||||
self.logger.log(f"Found {len(self.current_workflow_items)} items for key '{key}'")
|
|
||||||
|
|
||||||
# Update dropdown
|
|
||||||
if self.workflow_item_dropdown_ref.current:
|
|
||||||
options = []
|
|
||||||
for item in self.current_workflow_items:
|
|
||||||
if hasattr(item, 'title'):
|
|
||||||
options.append(ft.dropdown.Option(item.title))
|
|
||||||
print(f" - Added item: {item.title}")
|
|
||||||
else:
|
|
||||||
print(f" - WARNING: Item has no title attribute: {item}")
|
|
||||||
|
|
||||||
print(f"DEBUG: Created {len(options)} dropdown options")
|
|
||||||
self.workflow_item_dropdown_ref.current.options = options
|
|
||||||
|
|
||||||
|
# Update item counter if it exists
|
||||||
if self.item_counter_ref.current:
|
if self.item_counter_ref.current:
|
||||||
count_text = f"{len(options)} item(s) loaded"
|
count_text = f"{len(all_items)} item(s) loaded"
|
||||||
if len(options) == 0:
|
|
||||||
count_text = f"No {item_type}s found in {source} repo"
|
|
||||||
self.item_counter_ref.current.value = count_text
|
self.item_counter_ref.current.value = count_text
|
||||||
print(f"DEBUG: Counter text set to: {count_text}")
|
print(f"DEBUG: Counter text set to: {count_text}")
|
||||||
|
|
||||||
print("DEBUG: Calling page.update()...")
|
print("DEBUG: Calling page.update()...")
|
||||||
self.page.update()
|
self.page.update()
|
||||||
print("DEBUG: page.update() completed")
|
print("DEBUG: page.update() completed")
|
||||||
else:
|
|
||||||
print("ERROR: workflow_item_dropdown_ref.current is None!")
|
|
||||||
|
|
||||||
def _display_workflow_item(self, item):
|
def _display_workflow_item(self, item):
|
||||||
"""Display a workflow item"""
|
"""Display a workflow item"""
|
||||||
# Implementation would populate fields with workflow item data
|
# Implementation would populate fields with workflow item data
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _populate_all_items(self):
|
def _populate_all_items(self, search_query: str = "", type_filter: str = "both", repo_filter: str = "both"):
|
||||||
"""Populate the all items list with all loaded PRs and Issues"""
|
"""Populate the all items list with all loaded PRs and Issues
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_query: Optional search string to filter items
|
||||||
|
type_filter: Filter by item type - "both", "prs", or "issues"
|
||||||
|
repo_filter: Filter by repo source - "both", "target", or "fork"
|
||||||
|
"""
|
||||||
if not self.all_items_container_ref.current:
|
if not self.all_items_container_ref.current:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -811,7 +803,53 @@ class MainGUI:
|
|||||||
for key, items in self.workflow_items.items():
|
for key, items in self.workflow_items.items():
|
||||||
all_items.extend(items)
|
all_items.extend(items)
|
||||||
|
|
||||||
|
# Apply repo source filter
|
||||||
|
if repo_filter == "target":
|
||||||
|
all_items = [item for item in all_items if item.repo_source == "target"]
|
||||||
|
elif repo_filter == "fork":
|
||||||
|
all_items = [item for item in all_items if item.repo_source == "fork"]
|
||||||
|
# "both" shows everything, no filtering needed
|
||||||
|
|
||||||
|
# Apply type filter
|
||||||
|
if type_filter == "prs":
|
||||||
|
all_items = [item for item in all_items if item.item_type == "pull_request"]
|
||||||
|
elif type_filter == "issues":
|
||||||
|
all_items = [item for item in all_items if item.item_type == "issue"]
|
||||||
|
# "both" shows everything, no filtering needed
|
||||||
|
|
||||||
|
# Apply search filter if provided
|
||||||
|
if search_query:
|
||||||
|
search_lower = search_query.lower()
|
||||||
|
filtered_items = []
|
||||||
|
for item in all_items:
|
||||||
|
# Search in title, number, state, author, and labels
|
||||||
|
if (search_lower in item.title.lower() or
|
||||||
|
search_lower in str(item.number) or
|
||||||
|
search_lower in item.state.lower() or
|
||||||
|
(item.author and search_lower in item.author.lower()) or
|
||||||
|
any(search_lower in label.lower() for label in (item.labels or []))):
|
||||||
|
filtered_items.append(item)
|
||||||
|
all_items = filtered_items
|
||||||
|
|
||||||
if not all_items:
|
if not all_items:
|
||||||
|
if search_query or type_filter != "both" or repo_filter != "both":
|
||||||
|
filter_desc = []
|
||||||
|
if search_query:
|
||||||
|
filter_desc.append(f"matching '{search_query}'")
|
||||||
|
if type_filter == "prs":
|
||||||
|
filter_desc.append("PRs only")
|
||||||
|
elif type_filter == "issues":
|
||||||
|
filter_desc.append("Issues only")
|
||||||
|
if repo_filter == "target":
|
||||||
|
filter_desc.append("Target repo only")
|
||||||
|
elif repo_filter == "fork":
|
||||||
|
filter_desc.append("Fork repo only")
|
||||||
|
|
||||||
|
msg = "No items " + " and ".join(filter_desc) if filter_desc else "No items loaded"
|
||||||
|
self.all_items_container_ref.current.controls = [
|
||||||
|
ft.Text(msg, color=ft.colors.GREY_500, italic=True)
|
||||||
|
]
|
||||||
|
else:
|
||||||
self.all_items_container_ref.current.controls = [
|
self.all_items_container_ref.current.controls = [
|
||||||
ft.Text("No items loaded", color=ft.colors.GREY_500, italic=True)
|
ft.Text("No items loaded", color=ft.colors.GREY_500, italic=True)
|
||||||
]
|
]
|
||||||
@@ -942,25 +980,55 @@ class MainGUI:
|
|||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def _select_item_as_current(self, item):
|
def _select_item_as_current(self, item):
|
||||||
"""Select an item as the current workflow item in the dropdown"""
|
"""Select an item as the current active workflow item"""
|
||||||
if not self.workflow_item_dropdown_ref.current:
|
if not self.active_item_display_ref.current:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update filters to match the selected item
|
# Store the active item
|
||||||
# Set repo source (target/fork)
|
self.active_workflow_item = item
|
||||||
if self.repo_source_ref.current:
|
|
||||||
self.repo_source_ref.current.value = item.repo_source
|
|
||||||
|
|
||||||
# Set item type (pull_request/issue)
|
# Determine display labels
|
||||||
if self.item_type_ref.current:
|
repo_label = "Target" if item.repo_source == "target" else "Fork"
|
||||||
self.item_type_ref.current.value = item.item_type
|
repo_color = ft.colors.BLUE if item.repo_source == "target" else ft.colors.PURPLE
|
||||||
|
type_label = "PR" if item.item_type == "pull_request" else "Issue"
|
||||||
|
type_color = ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE
|
||||||
|
|
||||||
# Re-filter the workflow items with the new settings
|
# Update the active item display with a nice card
|
||||||
|
self.active_item_display_ref.current.content = ft.Column([
|
||||||
|
ft.Row([
|
||||||
|
# Repo badge
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Text(repo_label, size=10, weight=ft.FontWeight.BOLD, color=ft.colors.WHITE),
|
||||||
|
bgcolor=repo_color,
|
||||||
|
padding=ft.padding.symmetric(horizontal=6, vertical=2),
|
||||||
|
border_radius=4,
|
||||||
|
),
|
||||||
|
# Type badge
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Text(type_label, size=10, weight=ft.FontWeight.BOLD, color=ft.colors.WHITE),
|
||||||
|
bgcolor=type_color,
|
||||||
|
padding=ft.padding.symmetric(horizontal=6, vertical=2),
|
||||||
|
border_radius=4,
|
||||||
|
),
|
||||||
|
ft.Container(expand=True),
|
||||||
|
# Clear button
|
||||||
|
ft.IconButton(
|
||||||
|
icon=ft.icons.CLOSE,
|
||||||
|
icon_size=16,
|
||||||
|
tooltip="Clear selection",
|
||||||
|
on_click=self._clear_active_item,
|
||||||
|
),
|
||||||
|
], spacing=5),
|
||||||
|
ft.Text(
|
||||||
|
f"#{item.number}: {item.title}",
|
||||||
|
size=12,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
),
|
||||||
|
], spacing=5)
|
||||||
|
|
||||||
|
# Collect workflow items (filter toggles were removed, so this just collects all items)
|
||||||
self._filter_workflow_items()
|
self._filter_workflow_items()
|
||||||
|
|
||||||
# Set the dropdown value to this item's title
|
|
||||||
self.workflow_item_dropdown_ref.current.value = item.title
|
|
||||||
|
|
||||||
# Display the item
|
# Display the item
|
||||||
self._display_workflow_item(item)
|
self._display_workflow_item(item)
|
||||||
|
|
||||||
@@ -972,6 +1040,28 @@ class MainGUI:
|
|||||||
repo_label = "Target" if item.repo_source == "target" else "Fork"
|
repo_label = "Target" if item.repo_source == "target" else "Fork"
|
||||||
self._show_snackbar(f"Selected {item_type_label} from {repo_label}: {item.title}", error=False)
|
self._show_snackbar(f"Selected {item_type_label} from {repo_label}: {item.title}", error=False)
|
||||||
|
|
||||||
|
def _clear_active_item(self, e=None):
|
||||||
|
"""Clear the active item selection"""
|
||||||
|
if not self.active_item_display_ref.current:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear the stored active item
|
||||||
|
self.active_workflow_item = None
|
||||||
|
|
||||||
|
# Reset the display to default "No item selected"
|
||||||
|
self.active_item_display_ref.current.content = ft.Text(
|
||||||
|
"No item selected",
|
||||||
|
color=ft.colors.GREY_500,
|
||||||
|
italic=True,
|
||||||
|
text_align=ft.TextAlign.CENTER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the page
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
# Show confirmation
|
||||||
|
self._show_snackbar("Active item cleared", error=False)
|
||||||
|
|
||||||
def _show_item_detail(self, item):
|
def _show_item_detail(self, item):
|
||||||
"""Show detail dialog for a workflow item"""
|
"""Show detail dialog for a workflow item"""
|
||||||
# Get repo string for fetching comments
|
# Get repo string for fetching comments
|
||||||
@@ -1314,9 +1404,6 @@ class MainGUI:
|
|||||||
# Populate all items list in sidebar
|
# Populate all items list in sidebar
|
||||||
self._populate_all_items()
|
self._populate_all_items()
|
||||||
|
|
||||||
# Populate all items table in the All Items tab
|
|
||||||
self._populate_all_items_table()
|
|
||||||
|
|
||||||
print("✅ Auto-load completed successfully")
|
print("✅ Auto-load completed successfully")
|
||||||
else:
|
else:
|
||||||
print("No cached items found, waiting for manual load")
|
print("No cached items found, waiting for manual load")
|
||||||
@@ -1394,9 +1481,6 @@ class MainGUI:
|
|||||||
# Populate all items list in sidebar
|
# Populate all items list in sidebar
|
||||||
self._populate_all_items()
|
self._populate_all_items()
|
||||||
|
|
||||||
# Populate all items table in the All Items tab
|
|
||||||
self._populate_all_items_table()
|
|
||||||
|
|
||||||
print("✅ Cached items loaded for selected repositories")
|
print("✅ Cached items loaded for selected repositories")
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.log("✅ Cached items loaded for selected repositories")
|
self.logger.log("✅ Cached items loaded for selected repositories")
|
||||||
@@ -1868,9 +1952,6 @@ class MainGUI:
|
|||||||
# Populate all items list in sidebar
|
# Populate all items list in sidebar
|
||||||
self._populate_all_items()
|
self._populate_all_items()
|
||||||
|
|
||||||
# Populate all items table in the All Items tab
|
|
||||||
self._populate_all_items_table()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.log(f"Error loading workflow items: {e}")
|
self.logger.log(f"Error loading workflow items: {e}")
|
||||||
@@ -1971,6 +2052,26 @@ class MainGUI:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self._show_snackbar(f"Error opening settings: {ex}", error=True)
|
self._show_snackbar(f"Error opening settings: {ex}", error=True)
|
||||||
|
|
||||||
|
def _open_processing_log(self, e):
|
||||||
|
"""Open processing log dialog"""
|
||||||
|
try:
|
||||||
|
print("Processing Log button clicked!")
|
||||||
|
|
||||||
|
processing_log_dialog = ProcessingLogDialog(
|
||||||
|
self.page,
|
||||||
|
self.log_text_ref
|
||||||
|
)
|
||||||
|
print("ProcessingLogDialog created")
|
||||||
|
|
||||||
|
processing_log_dialog.show()
|
||||||
|
print("ProcessingLogDialog.show() completed")
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Error in _open_processing_log: {ex}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
self._show_snackbar(f"Error opening processing log: {ex}", error=True)
|
||||||
|
|
||||||
def _show_real_settings(self):
|
def _show_real_settings(self):
|
||||||
"""Show the real settings dialog"""
|
"""Show the real settings dialog"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Processing Log Dialog
|
||||||
|
Displays the processing log in a separate dialog window
|
||||||
|
"""
|
||||||
|
|
||||||
|
import flet as ft
|
||||||
|
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
|
||||||
|
ft.icons = ft.Icons
|
||||||
|
ft.colors = ft.Colors
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessingLogDialog:
|
||||||
|
"""Processing log display dialog"""
|
||||||
|
|
||||||
|
def __init__(self, page: ft.Page, log_text_ref: ft.Ref):
|
||||||
|
self.page = page
|
||||||
|
self.log_text_ref = log_text_ref
|
||||||
|
self.dialog_ref = ft.Ref[ft.AlertDialog]()
|
||||||
|
self.log_display_ref = ft.Ref[ft.TextField]()
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Show the processing log dialog"""
|
||||||
|
try:
|
||||||
|
print("ProcessingLogDialog.show() called")
|
||||||
|
|
||||||
|
# Create the dialog
|
||||||
|
dialog = self._create_dialog()
|
||||||
|
self.dialog_ref.current = dialog
|
||||||
|
|
||||||
|
# Sync the log content before showing
|
||||||
|
self._sync_log_content()
|
||||||
|
|
||||||
|
# Open the dialog
|
||||||
|
self.page.open(dialog)
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Error in ProcessingLogDialog.show(): {ex}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def _sync_log_content(self):
|
||||||
|
"""Sync log content from main log to dialog display"""
|
||||||
|
if self.log_text_ref.current and self.log_display_ref.current:
|
||||||
|
self.log_display_ref.current.value = self.log_text_ref.current.value
|
||||||
|
if self.page:
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def _create_dialog(self) -> ft.AlertDialog:
|
||||||
|
"""Create the processing log dialog"""
|
||||||
|
# Create a display field that will show a copy of the log
|
||||||
|
# This is synced from the main log field
|
||||||
|
log_display = ft.TextField(
|
||||||
|
ref=self.log_display_ref,
|
||||||
|
value=self.log_text_ref.current.value if self.log_text_ref.current else "",
|
||||||
|
multiline=True,
|
||||||
|
read_only=True,
|
||||||
|
expand=True,
|
||||||
|
text_style=ft.TextStyle(font_family="Courier New"),
|
||||||
|
min_lines=20,
|
||||||
|
max_lines=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Refresh button
|
||||||
|
refresh_button = ft.TextButton(
|
||||||
|
"Refresh",
|
||||||
|
icon=ft.icons.REFRESH,
|
||||||
|
on_click=self._refresh_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear button
|
||||||
|
clear_button = ft.TextButton(
|
||||||
|
"Clear Log",
|
||||||
|
icon=ft.icons.DELETE_OUTLINE,
|
||||||
|
on_click=self._clear_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Close button
|
||||||
|
close_button = ft.TextButton(
|
||||||
|
"Close",
|
||||||
|
on_click=self._close_clicked,
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog = ft.AlertDialog(
|
||||||
|
ref=self.dialog_ref,
|
||||||
|
modal=True,
|
||||||
|
title=ft.Row(
|
||||||
|
[
|
||||||
|
ft.Icon(ft.icons.LIST_ALT, color="blue"),
|
||||||
|
ft.Text("Processing Log", size=20, weight=ft.FontWeight.BOLD),
|
||||||
|
],
|
||||||
|
alignment=ft.MainAxisAlignment.START,
|
||||||
|
),
|
||||||
|
content=ft.Container(
|
||||||
|
content=log_display,
|
||||||
|
width=800,
|
||||||
|
height=500,
|
||||||
|
),
|
||||||
|
actions=[
|
||||||
|
refresh_button,
|
||||||
|
clear_button,
|
||||||
|
close_button,
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
|
||||||
|
def _refresh_log(self, e):
|
||||||
|
"""Refresh the log content from the main log"""
|
||||||
|
self._sync_log_content()
|
||||||
|
|
||||||
|
def _clear_log(self, e):
|
||||||
|
"""Clear the log"""
|
||||||
|
# Clear both the main log and the display
|
||||||
|
if self.log_text_ref.current:
|
||||||
|
self.log_text_ref.current.value = ""
|
||||||
|
if self.log_display_ref.current:
|
||||||
|
self.log_display_ref.current.value = ""
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def _close_clicked(self, e):
|
||||||
|
"""Handle close button click"""
|
||||||
|
if self.dialog_ref.current:
|
||||||
|
self.page.close(self.dialog_ref.current)
|
||||||
@@ -7,9 +7,11 @@ import flet as ft
|
|||||||
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
|
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
|
||||||
ft.icons = ft.Icons
|
ft.icons = ft.Icons
|
||||||
ft.colors = ft.Colors
|
ft.colors = ft.Colors
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog:
|
class SettingsDialog:
|
||||||
@@ -28,6 +30,9 @@ class SettingsDialog:
|
|||||||
self.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]()
|
self.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]()
|
self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
|
|
||||||
|
# Package checker refs
|
||||||
|
self.package_status_ref = ft.Ref[ft.Container]()
|
||||||
|
|
||||||
def show(self, on_result=None):
|
def show(self, on_result=None):
|
||||||
"""Show the settings dialog"""
|
"""Show the settings dialog"""
|
||||||
try:
|
try:
|
||||||
@@ -62,6 +67,8 @@ class SettingsDialog:
|
|||||||
"""Initialize async operations"""
|
"""Initialize async operations"""
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
await self._scan_repos_async()
|
await self._scan_repos_async()
|
||||||
|
# Check packages for current AI provider
|
||||||
|
await self._check_packages_for_current_provider()
|
||||||
|
|
||||||
def _create_dialog(self) -> ft.AlertDialog:
|
def _create_dialog(self) -> ft.AlertDialog:
|
||||||
"""Create the settings dialog"""
|
"""Create the settings dialog"""
|
||||||
@@ -232,6 +239,31 @@ class SettingsDialog:
|
|||||||
"""Create AI settings tab"""
|
"""Create AI settings tab"""
|
||||||
controls = []
|
controls = []
|
||||||
|
|
||||||
|
# Package Status Section (at the top)
|
||||||
|
controls.append(ft.Container(
|
||||||
|
content=ft.Column([
|
||||||
|
ft.Row([
|
||||||
|
ft.Text("Package Status", size=16, weight=ft.FontWeight.BOLD),
|
||||||
|
ft.IconButton(
|
||||||
|
icon=ft.icons.REFRESH,
|
||||||
|
tooltip="Refresh package status",
|
||||||
|
on_click=lambda e: self.page.run_task(self._check_packages_for_current_provider),
|
||||||
|
),
|
||||||
|
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
|
||||||
|
ft.Container(
|
||||||
|
ref=self.package_status_ref,
|
||||||
|
content=ft.Row([
|
||||||
|
ft.ProgressRing(width=20, height=20),
|
||||||
|
ft.Text("Checking packages...", color=ft.colors.BLUE),
|
||||||
|
]),
|
||||||
|
padding=10,
|
||||||
|
bgcolor=ft.colors.BLUE_100,
|
||||||
|
border_radius=5,
|
||||||
|
),
|
||||||
|
], spacing=10),
|
||||||
|
padding=ft.padding.only(bottom=10),
|
||||||
|
))
|
||||||
|
|
||||||
# AI Provider Section
|
# AI Provider Section
|
||||||
controls.append(self._create_section_header("🤖 AI Provider Configuration"))
|
controls.append(self._create_section_header("🤖 AI Provider Configuration"))
|
||||||
|
|
||||||
@@ -247,6 +279,7 @@ class SettingsDialog:
|
|||||||
ft.dropdown.Option("ollama", "Ollama"),
|
ft.dropdown.Option("ollama", "Ollama"),
|
||||||
],
|
],
|
||||||
expand=True,
|
expand=True,
|
||||||
|
on_change=lambda e: self.page.run_task(self._check_packages_for_current_provider),
|
||||||
)
|
)
|
||||||
self.entries['AI_PROVIDER'] = ai_provider
|
self.entries['AI_PROVIDER'] = ai_provider
|
||||||
controls.append(ai_provider)
|
controls.append(ai_provider)
|
||||||
@@ -375,6 +408,304 @@ class SettingsDialog:
|
|||||||
padding=ft.padding.only(top=20, bottom=10),
|
padding=ft.padding.only(top=20, bottom=10),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _check_ai_packages(self, provider_name: str) -> Tuple[bool, List[str]]:
|
||||||
|
"""Check if required packages for AI provider are installed"""
|
||||||
|
try:
|
||||||
|
from .ai_manager import AIManager
|
||||||
|
ai_manager = AIManager()
|
||||||
|
available, missing = ai_manager.check_ai_module_availability(provider_name)
|
||||||
|
return available, missing
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking AI packages: {e}")
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
def _detect_environment(self) -> Tuple[bool, str]:
|
||||||
|
"""Detect if running in virtual environment"""
|
||||||
|
in_venv = (hasattr(sys, 'real_prefix') or
|
||||||
|
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or
|
||||||
|
os.environ.get('VIRTUAL_ENV') is not None)
|
||||||
|
|
||||||
|
if in_venv:
|
||||||
|
venv_path = os.environ.get('VIRTUAL_ENV', sys.prefix)
|
||||||
|
venv_name = os.path.basename(venv_path)
|
||||||
|
return True, venv_name
|
||||||
|
else:
|
||||||
|
return False, "system-wide"
|
||||||
|
|
||||||
|
async def _check_packages_for_current_provider(self):
|
||||||
|
"""Check packages for the currently selected AI provider"""
|
||||||
|
if not self.package_status_ref.current:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current provider selection
|
||||||
|
ai_provider_dropdown = self.entries.get('AI_PROVIDER')
|
||||||
|
if not ai_provider_dropdown:
|
||||||
|
return
|
||||||
|
|
||||||
|
provider = ai_provider_dropdown.value
|
||||||
|
if not provider or provider == 'none':
|
||||||
|
self.package_status_ref.current.content = ft.Container(
|
||||||
|
content=ft.Row([
|
||||||
|
ft.Icon(ft.icons.INFO, color=ft.colors.BLUE),
|
||||||
|
ft.Text("No AI provider selected", color=ft.colors.BLUE),
|
||||||
|
]),
|
||||||
|
padding=10,
|
||||||
|
bgcolor=ft.colors.BLUE_100,
|
||||||
|
border_radius=5,
|
||||||
|
)
|
||||||
|
self.page.update()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check packages in background thread
|
||||||
|
def check_packages():
|
||||||
|
return self._check_ai_packages(provider)
|
||||||
|
|
||||||
|
available, missing = await asyncio.to_thread(check_packages)
|
||||||
|
|
||||||
|
# Update UI with results
|
||||||
|
if available:
|
||||||
|
self.package_status_ref.current.content = ft.Container(
|
||||||
|
content=ft.Row([
|
||||||
|
ft.Icon(ft.icons.CHECK_CIRCLE, color=ft.colors.GREEN),
|
||||||
|
ft.Text(f"All required packages for {provider} are installed", color=ft.colors.GREEN),
|
||||||
|
]),
|
||||||
|
padding=10,
|
||||||
|
bgcolor=ft.colors.GREEN_100,
|
||||||
|
border_radius=5,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
in_venv, env_name = self._detect_environment()
|
||||||
|
env_text = f"Virtual environment: {env_name}" if in_venv else "System-wide installation"
|
||||||
|
|
||||||
|
self.package_status_ref.current.content = ft.Container(
|
||||||
|
content=ft.Column([
|
||||||
|
ft.Row([
|
||||||
|
ft.Icon(ft.icons.WARNING, color=ft.colors.ORANGE),
|
||||||
|
ft.Text(f"Missing packages for {provider}", color=ft.colors.ORANGE, weight=ft.FontWeight.BOLD),
|
||||||
|
]),
|
||||||
|
ft.Text(f"Required: {', '.join(missing)}", size=12),
|
||||||
|
ft.Text(f"Environment: {env_text}", size=12, italic=True),
|
||||||
|
ft.ElevatedButton(
|
||||||
|
"Install Packages",
|
||||||
|
icon=ft.icons.DOWNLOAD,
|
||||||
|
on_click=lambda e: self._install_packages(missing, provider),
|
||||||
|
),
|
||||||
|
], spacing=5),
|
||||||
|
padding=10,
|
||||||
|
bgcolor=ft.colors.ORANGE_100,
|
||||||
|
border_radius=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def _install_packages(self, packages: List[str], provider: str):
|
||||||
|
"""Install missing packages"""
|
||||||
|
in_venv, env_name = self._detect_environment()
|
||||||
|
env_text = f"virtual environment '{env_name}'" if in_venv else "system-wide (may require administrator rights)"
|
||||||
|
|
||||||
|
# Create confirmation dialog
|
||||||
|
package_list = ', '.join(packages)
|
||||||
|
message = (f"Install the following packages for {provider}?\n\n"
|
||||||
|
f"Packages: {package_list}\n\n"
|
||||||
|
f"Installation location: {env_text}\n\n"
|
||||||
|
f"Command: pip install {' '.join(packages)}")
|
||||||
|
|
||||||
|
def handle_install(e):
|
||||||
|
self.page.close(install_dialog)
|
||||||
|
# Run installation in background
|
||||||
|
self.page.run_task(lambda: self._do_install_packages(packages, provider))
|
||||||
|
|
||||||
|
def handle_cancel(e):
|
||||||
|
self.page.close(install_dialog)
|
||||||
|
|
||||||
|
install_dialog = ft.AlertDialog(
|
||||||
|
modal=True,
|
||||||
|
title=ft.Text("Install AI Packages"),
|
||||||
|
content=ft.Text(message),
|
||||||
|
actions=[
|
||||||
|
ft.TextButton("Cancel", on_click=handle_cancel),
|
||||||
|
ft.FilledButton("Install", on_click=handle_install),
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.page.open(install_dialog)
|
||||||
|
|
||||||
|
async def _do_install_packages(self, packages: List[str], provider: str):
|
||||||
|
"""Actually install the packages"""
|
||||||
|
in_venv, env_name = self._detect_environment()
|
||||||
|
|
||||||
|
# Update status to show installation in progress
|
||||||
|
if self.package_status_ref.current:
|
||||||
|
self.package_status_ref.current.content = ft.Container(
|
||||||
|
content=ft.Row([
|
||||||
|
ft.ProgressRing(width=20, height=20),
|
||||||
|
ft.Text(f"Installing packages for {provider}...", color=ft.colors.BLUE),
|
||||||
|
]),
|
||||||
|
padding=10,
|
||||||
|
bgcolor=ft.colors.BLUE_100,
|
||||||
|
border_radius=5,
|
||||||
|
)
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
# Install packages in background thread
|
||||||
|
def install():
|
||||||
|
try:
|
||||||
|
for package in packages:
|
||||||
|
print(f"Installing {package}...")
|
||||||
|
pip_cmd = [sys.executable, '-m', 'pip', 'install', package]
|
||||||
|
result = subprocess.run(pip_cmd, capture_output=True, text=True, timeout=300)
|
||||||
|
|
||||||
|
# If direct install fails and we're not in venv, try with --user flag
|
||||||
|
if result.returncode != 0 and not in_venv:
|
||||||
|
print(f" Direct installation failed, trying with --user flag...")
|
||||||
|
pip_cmd_user = [sys.executable, '-m', 'pip', 'install', '--user', package]
|
||||||
|
result = subprocess.run(pip_cmd_user, capture_output=True, text=True, timeout=300)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Failed to install {package}: {result.stderr}"
|
||||||
|
|
||||||
|
return True, "All packages installed successfully"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Installation timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Error installing packages: {str(e)}"
|
||||||
|
|
||||||
|
success, message = await asyncio.to_thread(install)
|
||||||
|
|
||||||
|
# Show result and offer to restart
|
||||||
|
if success:
|
||||||
|
def handle_restart(e):
|
||||||
|
self.page.close(result_dialog)
|
||||||
|
self._restart_application()
|
||||||
|
|
||||||
|
def handle_later(e):
|
||||||
|
self.page.close(result_dialog)
|
||||||
|
# Re-check packages after installation
|
||||||
|
self.page.run_task(self._check_packages_for_current_provider)
|
||||||
|
|
||||||
|
result_dialog = ft.AlertDialog(
|
||||||
|
modal=True,
|
||||||
|
title=ft.Text("Installation Complete"),
|
||||||
|
content=ft.Text(f"{message}\n\nThe application needs to restart to use the newly installed packages."),
|
||||||
|
actions=[
|
||||||
|
ft.TextButton("Restart Later", on_click=handle_later),
|
||||||
|
ft.FilledButton("Restart Now", on_click=handle_restart),
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.page.open(result_dialog)
|
||||||
|
else:
|
||||||
|
self._show_alert("Installation Failed", message)
|
||||||
|
# Re-check packages to update status
|
||||||
|
await self._check_packages_for_current_provider()
|
||||||
|
|
||||||
|
def _restart_application(self):
|
||||||
|
"""Restart the application"""
|
||||||
|
try:
|
||||||
|
# Close the dialog first
|
||||||
|
if self.dialog_ref.current:
|
||||||
|
self.page.close(self.dialog_ref.current)
|
||||||
|
|
||||||
|
# Show restart message
|
||||||
|
restart_msg = ft.SnackBar(
|
||||||
|
content=ft.Text("Restarting application..."),
|
||||||
|
bgcolor=ft.colors.BLUE,
|
||||||
|
)
|
||||||
|
self.page.open(restart_msg)
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
# Restart the application
|
||||||
|
python = sys.executable
|
||||||
|
os.execl(python, python, *sys.argv)
|
||||||
|
except Exception as e:
|
||||||
|
self._show_alert("Restart Failed", f"Could not restart application: {str(e)}\n\nPlease restart manually.")
|
||||||
|
|
||||||
|
async def _install_and_save(self, packages: List[str], provider: str, config_values: Dict[str, Any]):
|
||||||
|
"""Install packages and then save configuration"""
|
||||||
|
# Install packages
|
||||||
|
in_venv, _ = self._detect_environment()
|
||||||
|
|
||||||
|
def install():
|
||||||
|
try:
|
||||||
|
for package in packages:
|
||||||
|
print(f"Installing {package}...")
|
||||||
|
pip_cmd = [sys.executable, '-m', 'pip', 'install', package]
|
||||||
|
result = subprocess.run(pip_cmd, capture_output=True, text=True, timeout=300)
|
||||||
|
|
||||||
|
# If direct install fails and we're not in venv, try with --user flag
|
||||||
|
if result.returncode != 0 and not in_venv:
|
||||||
|
print(f" Direct installation failed, trying with --user flag...")
|
||||||
|
pip_cmd_user = [sys.executable, '-m', 'pip', 'install', '--user', package]
|
||||||
|
result = subprocess.run(pip_cmd_user, capture_output=True, text=True, timeout=300)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Failed to install {package}: {result.stderr}"
|
||||||
|
|
||||||
|
return True, "All packages installed successfully"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Installation timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Error installing packages: {str(e)}"
|
||||||
|
|
||||||
|
success, message = await asyncio.to_thread(install)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Save configuration after successful installation
|
||||||
|
self._do_save(config_values)
|
||||||
|
|
||||||
|
# Offer to restart
|
||||||
|
def handle_restart(e):
|
||||||
|
self.page.close(restart_dialog)
|
||||||
|
self._restart_application()
|
||||||
|
|
||||||
|
def handle_later(e):
|
||||||
|
self.page.close(restart_dialog)
|
||||||
|
|
||||||
|
restart_dialog = ft.AlertDialog(
|
||||||
|
modal=True,
|
||||||
|
title=ft.Text("Installation Complete"),
|
||||||
|
content=ft.Text(
|
||||||
|
f"Packages installed successfully!\n"
|
||||||
|
f"Settings have been saved.\n\n"
|
||||||
|
f"The application needs to restart to use the newly installed packages."
|
||||||
|
),
|
||||||
|
actions=[
|
||||||
|
ft.TextButton("Restart Later", on_click=handle_later),
|
||||||
|
ft.FilledButton("Restart Now", on_click=handle_restart),
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.page.open(restart_dialog)
|
||||||
|
else:
|
||||||
|
self._show_alert("Installation Failed", f"{message}\n\nSettings were not saved.")
|
||||||
|
|
||||||
|
def _do_save(self, config_values: Dict[str, Any]):
|
||||||
|
"""Actually save the configuration"""
|
||||||
|
try:
|
||||||
|
# Save configuration
|
||||||
|
if self.config_manager:
|
||||||
|
success = self.config_manager.save_configuration(config_values)
|
||||||
|
else:
|
||||||
|
success = self._save_to_env_file(config_values)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.result = config_values
|
||||||
|
self._show_alert(
|
||||||
|
"Settings Saved",
|
||||||
|
"Settings saved successfully!\n\nChanges applied immediately - no restart needed! ✨"
|
||||||
|
)
|
||||||
|
self._close_dialog()
|
||||||
|
else:
|
||||||
|
self._show_alert("Save Error", "Failed to save settings to .env file.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._show_alert("Save Error", f"Error saving settings:\n{str(e)}")
|
||||||
|
|
||||||
async def _scan_repos_async(self):
|
async def _scan_repos_async(self):
|
||||||
"""Scan for git repositories in the local repo path"""
|
"""Scan for git repositories in the local repo path"""
|
||||||
try:
|
try:
|
||||||
@@ -590,36 +921,48 @@ class SettingsDialog:
|
|||||||
# Check AI provider setup
|
# Check AI provider setup
|
||||||
ai_provider = config_values.get('AI_PROVIDER', '').strip().lower()
|
ai_provider = config_values.get('AI_PROVIDER', '').strip().lower()
|
||||||
if ai_provider and ai_provider not in ['none', '']:
|
if ai_provider and ai_provider not in ['none', '']:
|
||||||
if ai_provider in ['chatgpt', 'claude', 'anthropic', 'github-copilot', 'copilot', 'github_copilot']:
|
if ai_provider in ['chatgpt', 'claude', 'anthropic', 'github-copilot', 'copilot', 'github_copilot', 'ollama']:
|
||||||
try:
|
available, missing = self._check_ai_packages(ai_provider)
|
||||||
from .ai_manager import AIManager
|
if not available and missing:
|
||||||
ai_manager = AIManager()
|
# Offer to install missing packages
|
||||||
available, missing = ai_manager.check_ai_module_availability(ai_provider)
|
in_venv, env_name = self._detect_environment()
|
||||||
if not available:
|
env_text = f"virtual environment '{env_name}'" if in_venv else "system-wide"
|
||||||
# Show warning but continue
|
|
||||||
self._show_alert(
|
|
||||||
"AI Modules Not Installed",
|
|
||||||
f"Settings saved, but AI provider '{ai_provider}' requires additional packages: {', '.join(missing)}\n\n"
|
|
||||||
f"You can install them later with:\npip install {' '.join(missing)}"
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Save configuration
|
def handle_install_and_save(e):
|
||||||
if self.config_manager:
|
self.page.close(package_warning_dialog)
|
||||||
success = self.config_manager.save_configuration(config_values)
|
# Install packages and then save
|
||||||
else:
|
self.page.run_task(lambda: self._install_and_save(missing, ai_provider, config_values))
|
||||||
success = self._save_to_env_file(config_values)
|
|
||||||
|
|
||||||
if success:
|
def handle_save_anyway(e):
|
||||||
self.result = config_values
|
self.page.close(package_warning_dialog)
|
||||||
self._show_alert(
|
# Continue with save
|
||||||
"Settings Saved",
|
self._do_save(config_values)
|
||||||
"Settings saved successfully!\n\nChanges applied immediately - no restart needed! ✨"
|
|
||||||
|
def handle_cancel_save(e):
|
||||||
|
self.page.close(package_warning_dialog)
|
||||||
|
|
||||||
|
package_warning_dialog = ft.AlertDialog(
|
||||||
|
modal=True,
|
||||||
|
title=ft.Text("Missing AI Packages"),
|
||||||
|
content=ft.Text(
|
||||||
|
f"AI provider '{ai_provider}' requires additional packages:\n\n"
|
||||||
|
f"{', '.join(missing)}\n\n"
|
||||||
|
f"Installation location: {env_text}\n\n"
|
||||||
|
f"Would you like to install them now?"
|
||||||
|
),
|
||||||
|
actions=[
|
||||||
|
ft.TextButton("Cancel", on_click=handle_cancel_save),
|
||||||
|
ft.TextButton("Save Without Installing", on_click=handle_save_anyway),
|
||||||
|
ft.FilledButton("Install & Save", on_click=handle_install_and_save),
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
)
|
)
|
||||||
self._close_dialog()
|
|
||||||
else:
|
self.page.open(package_warning_dialog)
|
||||||
self._show_alert("Save Error", "Failed to save settings to .env file.")
|
return # Don't save yet, wait for user choice
|
||||||
|
|
||||||
|
# Save configuration (packages are already installed or not needed)
|
||||||
|
self._do_save(config_values)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._show_alert("Save Error", f"Error saving settings:\n{str(e)}")
|
self._show_alert("Save Error", f"Error saving settings:\n{str(e)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user