canns 0.12.7__py3-none-any.whl → 0.13.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- canns/analyzer/data/__init__.py +3 -11
- canns/analyzer/data/asa/__init__.py +84 -0
- canns/analyzer/data/asa/cohospace.py +905 -0
- canns/analyzer/data/asa/config.py +246 -0
- canns/analyzer/data/asa/decode.py +445 -0
- canns/analyzer/data/asa/embedding.py +269 -0
- canns/analyzer/data/asa/filters.py +208 -0
- canns/analyzer/data/{cann1d.py → asa/fly_roi.py} +98 -45
- canns/analyzer/data/asa/fr.py +431 -0
- canns/analyzer/data/asa/path.py +389 -0
- canns/analyzer/data/asa/plotting.py +1287 -0
- canns/analyzer/data/asa/tda.py +901 -0
- canns/analyzer/visualization/core/backend.py +1 -1
- canns/analyzer/visualization/core/config.py +77 -0
- canns/analyzer/visualization/core/rendering.py +10 -6
- canns/analyzer/visualization/energy_plots.py +22 -8
- canns/analyzer/visualization/spatial_plots.py +31 -11
- canns/analyzer/visualization/theta_sweep_plots.py +15 -6
- canns/pipeline/__init__.py +4 -8
- canns/pipeline/asa/__init__.py +21 -0
- canns/pipeline/asa/__main__.py +11 -0
- canns/pipeline/asa/app.py +1000 -0
- canns/pipeline/asa/runner.py +1095 -0
- canns/pipeline/asa/screens.py +215 -0
- canns/pipeline/asa/state.py +248 -0
- canns/pipeline/asa/styles.tcss +221 -0
- canns/pipeline/asa/widgets.py +233 -0
- canns/pipeline/gallery/__init__.py +7 -0
- canns/task/open_loop_navigation.py +3 -1
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/METADATA +6 -3
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/RECORD +34 -17
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/entry_points.txt +1 -0
- canns/analyzer/data/cann2d.py +0 -2565
- canns/pipeline/theta_sweep.py +0 -573
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/WHEEL +0 -0
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Modal screens for ASA TUI.
|
|
2
|
+
|
|
3
|
+
This module provides modal overlays for directory selection, help, and error display.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Container
|
|
10
|
+
from textual.screen import ModalScreen
|
|
11
|
+
from textual.widgets import Button, DirectoryTree, Label, Static
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkdirScreen(ModalScreen[Path]):
|
|
15
|
+
"""Modal screen for selecting working directory."""
|
|
16
|
+
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
WorkdirScreen {
|
|
19
|
+
align: center middle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
WorkdirScreen > Container {
|
|
23
|
+
width: 80;
|
|
24
|
+
height: 30;
|
|
25
|
+
border: thick $accent;
|
|
26
|
+
background: $surface;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#workdir-tree {
|
|
30
|
+
height: 1fr;
|
|
31
|
+
}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def compose(self) -> ComposeResult:
|
|
35
|
+
with Container():
|
|
36
|
+
yield Label("Select Working Directory")
|
|
37
|
+
yield DirectoryTree(Path.home(), id="workdir-tree")
|
|
38
|
+
with Container(id="button-container"):
|
|
39
|
+
yield Button("Select", variant="primary", id="select-btn")
|
|
40
|
+
yield Button("Cancel", id="cancel-btn")
|
|
41
|
+
|
|
42
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
43
|
+
if event.button.id == "select-btn":
|
|
44
|
+
tree = self.query_one("#workdir-tree", DirectoryTree)
|
|
45
|
+
if tree.cursor_node:
|
|
46
|
+
selected_path = tree.cursor_node.data.path
|
|
47
|
+
self.dismiss(selected_path)
|
|
48
|
+
elif event.button.id == "cancel-btn":
|
|
49
|
+
self.dismiss(None)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HelpScreen(ModalScreen):
|
|
53
|
+
"""Modal screen showing help and key bindings."""
|
|
54
|
+
|
|
55
|
+
DEFAULT_CSS = """
|
|
56
|
+
HelpScreen {
|
|
57
|
+
align: center middle;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
HelpScreen > Container {
|
|
61
|
+
width: 70;
|
|
62
|
+
height: 25;
|
|
63
|
+
border: thick $primary;
|
|
64
|
+
background: $surface;
|
|
65
|
+
padding: 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#help-content {
|
|
69
|
+
overflow-y: scroll;
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
HELP_TEXT = """
|
|
74
|
+
ASA TUI - Terminal User Interface for ASA Analysis
|
|
75
|
+
|
|
76
|
+
KEY BINDINGS:
|
|
77
|
+
Ctrl-W Change working directory
|
|
78
|
+
Ctrl-R Run analysis
|
|
79
|
+
F5 Refresh previews
|
|
80
|
+
? Show this help
|
|
81
|
+
Esc Quit application
|
|
82
|
+
Tab Navigate between panels
|
|
83
|
+
|
|
84
|
+
ANALYSIS MODULES:
|
|
85
|
+
TDA Topological Data Analysis
|
|
86
|
+
CohoMap Cohomology Map (requires TDA)
|
|
87
|
+
PathCompare Trajectory Comparison (requires CohoMap)
|
|
88
|
+
CohoSpace Cohomology Space Visualization (requires CohoMap)
|
|
89
|
+
FR Firing Rate Heatmap
|
|
90
|
+
FRM Single Neuron Firing Rate Map
|
|
91
|
+
GridScore Grid Cell Analysis
|
|
92
|
+
|
|
93
|
+
WORKFLOW:
|
|
94
|
+
1. Select working directory (Ctrl-W)
|
|
95
|
+
2. Choose input mode (ASA or Neuron+Traj)
|
|
96
|
+
3. Load files
|
|
97
|
+
4. Configure preprocessing
|
|
98
|
+
5. Select analysis mode
|
|
99
|
+
6. Set parameters
|
|
100
|
+
7. Run analysis (Ctrl-R)
|
|
101
|
+
8. View results
|
|
102
|
+
|
|
103
|
+
TERMINAL REQUIREMENTS:
|
|
104
|
+
Minimum size: 100 cols × 30 rows
|
|
105
|
+
Recommended: 120 cols × 40 rows
|
|
106
|
+
Tip: Use smaller font size for better display
|
|
107
|
+
|
|
108
|
+
If display is incomplete, try:
|
|
109
|
+
- Reduce terminal font size
|
|
110
|
+
- Maximize terminal window
|
|
111
|
+
- Use fullscreen mode
|
|
112
|
+
|
|
113
|
+
Press any key to close...
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def compose(self) -> ComposeResult:
|
|
117
|
+
with Container():
|
|
118
|
+
yield Label("Help", id="help-title")
|
|
119
|
+
yield Static(self.HELP_TEXT, id="help-content")
|
|
120
|
+
yield Button("Close", variant="primary", id="close-btn")
|
|
121
|
+
|
|
122
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
123
|
+
if event.button.id == "close-btn":
|
|
124
|
+
self.dismiss()
|
|
125
|
+
|
|
126
|
+
def on_key(self, event) -> None:
|
|
127
|
+
self.dismiss()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ErrorScreen(ModalScreen):
|
|
131
|
+
"""Modal screen for displaying errors."""
|
|
132
|
+
|
|
133
|
+
DEFAULT_CSS = """
|
|
134
|
+
ErrorScreen {
|
|
135
|
+
align: center middle;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ErrorScreen > Container {
|
|
139
|
+
width: 60;
|
|
140
|
+
height: 20;
|
|
141
|
+
border: thick $error;
|
|
142
|
+
background: $surface;
|
|
143
|
+
padding: 2;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#error-message {
|
|
147
|
+
color: $error;
|
|
148
|
+
overflow-y: scroll;
|
|
149
|
+
}
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, title: str, message: str, **kwargs):
|
|
153
|
+
super().__init__(**kwargs)
|
|
154
|
+
self.error_title = title
|
|
155
|
+
self.error_message = message
|
|
156
|
+
|
|
157
|
+
def compose(self) -> ComposeResult:
|
|
158
|
+
with Container():
|
|
159
|
+
yield Label(self.error_title, id="error-title")
|
|
160
|
+
yield Static(self.error_message, id="error-message")
|
|
161
|
+
yield Button("Close", variant="error", id="close-btn")
|
|
162
|
+
|
|
163
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
164
|
+
if event.button.id == "close-btn":
|
|
165
|
+
self.dismiss()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TerminalSizeWarning(ModalScreen):
|
|
169
|
+
"""Warning screen for insufficient terminal size."""
|
|
170
|
+
|
|
171
|
+
DEFAULT_CSS = """
|
|
172
|
+
TerminalSizeWarning {
|
|
173
|
+
align: center middle;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
TerminalSizeWarning > Container {
|
|
177
|
+
width: 50;
|
|
178
|
+
height: 15;
|
|
179
|
+
border: thick $warning;
|
|
180
|
+
background: $surface;
|
|
181
|
+
padding: 2;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#warning-message {
|
|
185
|
+
color: $warning;
|
|
186
|
+
text-align: center;
|
|
187
|
+
}
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, current_width: int, current_height: int, **kwargs):
|
|
191
|
+
super().__init__(**kwargs)
|
|
192
|
+
self.current_width = current_width
|
|
193
|
+
self.current_height = current_height
|
|
194
|
+
|
|
195
|
+
def compose(self) -> ComposeResult:
|
|
196
|
+
with Container():
|
|
197
|
+
yield Label("⚠ Terminal Size Warning", id="warning-title")
|
|
198
|
+
yield Static(
|
|
199
|
+
f"Current terminal size: {self.current_width} cols × {self.current_height} rows\n\n"
|
|
200
|
+
f"Recommended minimum:\n"
|
|
201
|
+
f"• Width: 100 columns (recommended 120+)\n"
|
|
202
|
+
f"• Height: 30 rows (recommended 40+)\n\n"
|
|
203
|
+
f"Please resize terminal or reduce font size.\n"
|
|
204
|
+
f"Press Esc to continue (may not display properly)",
|
|
205
|
+
id="warning-message",
|
|
206
|
+
)
|
|
207
|
+
yield Button("Continue", variant="warning", id="continue-btn")
|
|
208
|
+
|
|
209
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
210
|
+
if event.button.id == "continue-btn":
|
|
211
|
+
self.dismiss()
|
|
212
|
+
|
|
213
|
+
def on_key(self, event) -> None:
|
|
214
|
+
if event.key == "escape":
|
|
215
|
+
self.dismiss()
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""State management for ASA TUI.
|
|
2
|
+
|
|
3
|
+
This module provides centralized workflow state management with workdir-centric design.
|
|
4
|
+
All file paths are stored relative to the working directory for portability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class WorkflowState:
|
|
14
|
+
"""Centralized state for ASA analysis workflow.
|
|
15
|
+
|
|
16
|
+
All file paths are relative to workdir for portability.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Core paths
|
|
20
|
+
workdir: Path = field(default_factory=lambda: Path("."))
|
|
21
|
+
|
|
22
|
+
# Input configuration
|
|
23
|
+
input_mode: str = "asa" # "asa" | "neuron_traj" | "batch"
|
|
24
|
+
preset: str = "grid" # "grid" | "hd" | "none"
|
|
25
|
+
|
|
26
|
+
# File paths (relative to workdir)
|
|
27
|
+
asa_file: Path | None = None
|
|
28
|
+
neuron_file: Path | None = None
|
|
29
|
+
traj_file: Path | None = None
|
|
30
|
+
|
|
31
|
+
# Preprocessing
|
|
32
|
+
preprocess_method: str = "none" # "none" | "embed_spike_trains"
|
|
33
|
+
preprocess_params: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
# Analysis configuration
|
|
36
|
+
analysis_mode: str = (
|
|
37
|
+
"tda" # "tda" | "cohomap" | "pathcompare" | "cohospace" | "fr" | "frm" | "gridscore"
|
|
38
|
+
)
|
|
39
|
+
analysis_params: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
# Results
|
|
42
|
+
artifacts: dict[str, Path] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
# Runtime state
|
|
45
|
+
is_running: bool = False
|
|
46
|
+
current_stage: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def relative_path(state: WorkflowState, path: Path) -> Path:
|
|
50
|
+
"""Convert absolute path to workdir-relative path.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
state: Current workflow state
|
|
54
|
+
path: Absolute path to convert
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Path relative to workdir
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
return path.relative_to(state.workdir)
|
|
61
|
+
except ValueError:
|
|
62
|
+
# Path is not relative to workdir, return as-is
|
|
63
|
+
return path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_path(state: WorkflowState, path: Path | None) -> Path | None:
|
|
67
|
+
"""Convert relative path to absolute path.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
state: Current workflow state
|
|
71
|
+
path: Relative path to convert
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Absolute path or None if path is None
|
|
75
|
+
"""
|
|
76
|
+
if path is None:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
if path.is_absolute():
|
|
80
|
+
return path
|
|
81
|
+
|
|
82
|
+
return state.workdir / path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_files(state: WorkflowState) -> tuple[bool, str]:
|
|
86
|
+
"""Check if required files exist.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
state: Current workflow state
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of (is_valid, error_message)
|
|
93
|
+
"""
|
|
94
|
+
if state.input_mode == "asa":
|
|
95
|
+
if state.asa_file is None:
|
|
96
|
+
return False, "ASA file not selected"
|
|
97
|
+
|
|
98
|
+
asa_path = resolve_path(state, state.asa_file)
|
|
99
|
+
if not asa_path.exists():
|
|
100
|
+
return False, f"ASA file not found: {asa_path}"
|
|
101
|
+
|
|
102
|
+
# Validate .npz structure
|
|
103
|
+
try:
|
|
104
|
+
import numpy as np
|
|
105
|
+
|
|
106
|
+
data = np.load(asa_path, allow_pickle=True)
|
|
107
|
+
required_keys = ["spike", "t"]
|
|
108
|
+
missing = [k for k in required_keys if k not in data.files]
|
|
109
|
+
if missing:
|
|
110
|
+
return False, f"ASA file missing required keys: {missing}"
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return False, f"Failed to load ASA file: {e}"
|
|
113
|
+
|
|
114
|
+
elif state.input_mode == "neuron_traj":
|
|
115
|
+
if state.neuron_file is None:
|
|
116
|
+
return False, "Neuron file not selected"
|
|
117
|
+
if state.traj_file is None:
|
|
118
|
+
return False, "Trajectory file not selected"
|
|
119
|
+
|
|
120
|
+
neuron_path = resolve_path(state, state.neuron_file)
|
|
121
|
+
traj_path = resolve_path(state, state.traj_file)
|
|
122
|
+
|
|
123
|
+
if not neuron_path.exists():
|
|
124
|
+
return False, f"Neuron file not found: {neuron_path}"
|
|
125
|
+
if not traj_path.exists():
|
|
126
|
+
return False, f"Trajectory file not found: {traj_path}"
|
|
127
|
+
|
|
128
|
+
return True, ""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_preset_params(preset: str) -> dict[str, Any]:
|
|
132
|
+
"""Load preset configurations for analysis.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
preset: Preset name ("grid", "hd", or "none")
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary of preset parameters
|
|
139
|
+
"""
|
|
140
|
+
if preset == "grid":
|
|
141
|
+
return {
|
|
142
|
+
"preprocess": {
|
|
143
|
+
"method": "embed_spike_trains",
|
|
144
|
+
"dt": 0.02,
|
|
145
|
+
"sigma": 0.1,
|
|
146
|
+
"speed_filter": False,
|
|
147
|
+
"min_speed": 2.5,
|
|
148
|
+
},
|
|
149
|
+
"tda": {
|
|
150
|
+
"dim": 6,
|
|
151
|
+
"num_times": 5,
|
|
152
|
+
"active_times": 15000,
|
|
153
|
+
"k": 1000,
|
|
154
|
+
"n_points": 1200,
|
|
155
|
+
"metric": "cosine",
|
|
156
|
+
"nbs": 800,
|
|
157
|
+
"maxdim": 1,
|
|
158
|
+
"coeff": 47,
|
|
159
|
+
"do_shuffle": False,
|
|
160
|
+
"num_shuffles": 1000,
|
|
161
|
+
},
|
|
162
|
+
"gridscore": {
|
|
163
|
+
"annulus_inner": 0.3,
|
|
164
|
+
"annulus_outer": 0.7,
|
|
165
|
+
"bin_size": 2.5,
|
|
166
|
+
"smooth_sigma": 2.0,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
elif preset == "hd":
|
|
170
|
+
return {
|
|
171
|
+
"preprocess": {
|
|
172
|
+
"method": "embed_spike_trains",
|
|
173
|
+
"dt": 0.02,
|
|
174
|
+
"sigma": 0.1,
|
|
175
|
+
"speed_filter": False,
|
|
176
|
+
"min_speed": 2.5,
|
|
177
|
+
},
|
|
178
|
+
"tda": {
|
|
179
|
+
"dim": 4,
|
|
180
|
+
"num_times": 5,
|
|
181
|
+
"active_times": 15000,
|
|
182
|
+
"k": 800,
|
|
183
|
+
"n_points": 1000,
|
|
184
|
+
"metric": "cosine",
|
|
185
|
+
"nbs": 600,
|
|
186
|
+
"maxdim": 1,
|
|
187
|
+
"coeff": 47,
|
|
188
|
+
"do_shuffle": False,
|
|
189
|
+
"num_shuffles": 1000,
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
else: # "none"
|
|
193
|
+
return {}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def check_cached_artifacts(state: WorkflowState, stage: str) -> bool:
|
|
197
|
+
"""Check if stage artifacts exist and are valid.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
state: Current workflow state
|
|
201
|
+
stage: Analysis stage name
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if cached artifacts exist and are valid
|
|
205
|
+
"""
|
|
206
|
+
stage_dir = state.workdir / stage
|
|
207
|
+
if not stage_dir.exists():
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
# Define required files for each stage
|
|
211
|
+
stage_artifacts = {
|
|
212
|
+
"TDA": ["barcode.png", "persistence.npz"],
|
|
213
|
+
"CohoMap": ["decoding.npz", "cohomap.png"],
|
|
214
|
+
"PathCompare": ["path_compare.png"],
|
|
215
|
+
"CohoSpace": ["cohospace_trajectory.png"],
|
|
216
|
+
"FR": ["fr_heatmap.png"],
|
|
217
|
+
"FRM": [], # Dynamic based on neuron_id
|
|
218
|
+
"GridScore": ["gridscore_distribution.png", "gridscore.npz"],
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
required_files = stage_artifacts.get(stage, [])
|
|
222
|
+
return all((stage_dir / f).exists() for f in required_files)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def load_cached_result(state: WorkflowState, stage: str) -> dict[str, Any]:
|
|
226
|
+
"""Load cached results from previous run.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
state: Current workflow state
|
|
230
|
+
stage: Analysis stage name
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary of cached data
|
|
234
|
+
"""
|
|
235
|
+
import numpy as np
|
|
236
|
+
|
|
237
|
+
stage_dir = state.workdir / stage
|
|
238
|
+
result = {}
|
|
239
|
+
|
|
240
|
+
# Load .npz files
|
|
241
|
+
for npz_file in stage_dir.glob("*.npz"):
|
|
242
|
+
try:
|
|
243
|
+
data = np.load(npz_file, allow_pickle=True)
|
|
244
|
+
result[npz_file.stem] = dict(data)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
return result
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* ASA TUI Styles */
|
|
2
|
+
|
|
3
|
+
/* Hidden class */
|
|
4
|
+
.hidden {
|
|
5
|
+
display: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Main layout - three columns */
|
|
9
|
+
#main-container {
|
|
10
|
+
layout: horizontal;
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Left panel - narrow action panel */
|
|
15
|
+
#left-panel {
|
|
16
|
+
width: 22;
|
|
17
|
+
min-width: 20;
|
|
18
|
+
max-width: 26;
|
|
19
|
+
height: 100%; /* CRITICAL: Fill parent height */
|
|
20
|
+
border-right: solid $primary;
|
|
21
|
+
padding: 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Scrollable controls area - fill remaining height to enable scrolling */
|
|
25
|
+
#controls-scroll {
|
|
26
|
+
height: 1fr;
|
|
27
|
+
overflow-y: auto;
|
|
28
|
+
scrollbar-size-vertical: 1;
|
|
29
|
+
min-width: 100%;
|
|
30
|
+
padding-bottom: 2;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Child containers expand naturally to allow scrolling */
|
|
34
|
+
#preprocess-controls,
|
|
35
|
+
#analysis-controls {
|
|
36
|
+
height: auto !important;
|
|
37
|
+
width: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Middle panel - parameters + file browser */
|
|
41
|
+
#middle-panel {
|
|
42
|
+
layout: horizontal;
|
|
43
|
+
width: 0.5fr;
|
|
44
|
+
min-width: 30;
|
|
45
|
+
border-right: solid $primary;
|
|
46
|
+
padding: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#params-panel {
|
|
50
|
+
width: 0.7fr;
|
|
51
|
+
padding: 1;
|
|
52
|
+
border-right: solid $secondary;
|
|
53
|
+
height: 100%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#params-header {
|
|
57
|
+
background: $boost;
|
|
58
|
+
padding: 1;
|
|
59
|
+
margin-bottom: 1;
|
|
60
|
+
text-style: bold;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#file-tree-panel {
|
|
64
|
+
width: 0.7fr;
|
|
65
|
+
padding: 1;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#files-header {
|
|
70
|
+
background: $boost;
|
|
71
|
+
padding: 1;
|
|
72
|
+
margin-bottom: 1;
|
|
73
|
+
text-style: bold;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#right-panel {
|
|
77
|
+
width: 1.5fr;
|
|
78
|
+
padding: 1;
|
|
79
|
+
height: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Results tabs take 75% of right panel height */
|
|
83
|
+
#results-tabs {
|
|
84
|
+
height: 5fr;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Log viewer at bottom - 25% of right panel height */
|
|
88
|
+
#log-viewer {
|
|
89
|
+
height: 1fr;
|
|
90
|
+
border: solid $primary;
|
|
91
|
+
padding: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Workdir label */
|
|
95
|
+
#workdir-label {
|
|
96
|
+
background: $boost;
|
|
97
|
+
padding: 1;
|
|
98
|
+
margin-bottom: 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Parameter groups */
|
|
102
|
+
ParamGroup {
|
|
103
|
+
border: round $secondary;
|
|
104
|
+
padding: 1;
|
|
105
|
+
padding-bottom: 1;
|
|
106
|
+
margin: 1 0;
|
|
107
|
+
height: auto;
|
|
108
|
+
width: 100%;
|
|
109
|
+
min-height: 10;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.param-group-title {
|
|
113
|
+
text-style: bold;
|
|
114
|
+
color: $accent;
|
|
115
|
+
dock: top;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Embedding parameters container */
|
|
119
|
+
#emb-params {
|
|
120
|
+
margin-top: 1;
|
|
121
|
+
margin-bottom: 1;
|
|
122
|
+
height: auto !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#emb-params Label {
|
|
126
|
+
margin-top: 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#emb-params Input {
|
|
130
|
+
margin-bottom: 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#emb-params Checkbox {
|
|
134
|
+
margin: 1 0; /* Changed from 0.5 to 1 */
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Buttons */
|
|
138
|
+
Button {
|
|
139
|
+
margin: 1 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#actions-bar {
|
|
143
|
+
width: 100%;
|
|
144
|
+
margin: 0 0 1 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#actions-bar Button {
|
|
148
|
+
width: 100%;
|
|
149
|
+
margin: 0 0 1 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#run-btn {
|
|
153
|
+
width: 100%;
|
|
154
|
+
margin-top: 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Progress bar */
|
|
158
|
+
#progress-bar {
|
|
159
|
+
margin: 1 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#run-status {
|
|
163
|
+
margin-top: 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Image preview - allow more space */
|
|
167
|
+
ImagePreview {
|
|
168
|
+
height: 1fr;
|
|
169
|
+
min-height: 20;
|
|
170
|
+
border: solid $accent;
|
|
171
|
+
padding: 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#preview-path {
|
|
175
|
+
color: $text-muted;
|
|
176
|
+
margin-bottom: 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#preview-path-input {
|
|
180
|
+
margin-bottom: 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* File tree - compact for small terminals, now full height */
|
|
184
|
+
#file-tree {
|
|
185
|
+
height: 1fr; /* Fill available space */
|
|
186
|
+
border: solid $secondary;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Status indicators */
|
|
190
|
+
.running {
|
|
191
|
+
background: $warning;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.success {
|
|
195
|
+
background: $success;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.error {
|
|
199
|
+
background: $error;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Tab content */
|
|
203
|
+
TabbedContent {
|
|
204
|
+
height: 1fr;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
TabPane {
|
|
208
|
+
padding: 1;
|
|
209
|
+
height: 100%;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
TabbedContent > ContentSwitcher {
|
|
213
|
+
height: 1fr;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Result status */
|
|
217
|
+
#result-status {
|
|
218
|
+
margin-top: 2;
|
|
219
|
+
padding: 1;
|
|
220
|
+
border: solid $primary;
|
|
221
|
+
}
|