Skip to content

Commit 3c55533

Browse files
committed
Merge branch 'main' of github.com:emagedoc/CodeClash
2 parents 5a4d888 + c645f85 commit 3c55533

6 files changed

Lines changed: 741 additions & 49 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,15 +357,48 @@ Both the viewer and picker pages include a help modal accessible via the `?` but
357357
- **Copy foldernames**: Copies only folder names (not full paths) as space-separated string to clipboard
358358
- **Move to subfolder**: Prompts user for subfolder name and moves all selected folders into it (creates subfolder if needed)
359359

360+
### Model Multi-Select Filter
361+
362+
The game picker includes a multi-select dropdown filter for models in the Models column header:
363+
364+
#### Functionality
365+
* **Multi-Select Dropdown**: Click the "All Models" dropdown to open a list of all available models
366+
* **Add/Remove Selection**: Click on a model name to add it to the selection; click again to remove it
367+
* **Visual Indication**: Selected models are highlighted with accent color and bold text
368+
* **Display Text**:
369+
- Shows "All Models" when no models are selected (displays all games)
370+
- Shows single model name when only one is selected
371+
- Shows "N models selected" when multiple models are selected
372+
* **Clear Selection Button**: Integrated button in the dropdown that says "Clear selection" to reset the filter
373+
* **Click Model Tags**: Clicking on model tags in the Models column also toggles that model in the filter selection
374+
* **Intersection Filtering**: When multiple models are selected, only shows games that have ALL selected models (intersection, not union)
375+
* **Empty Selection**: If no model is selected, it behaves the same as if all models were selected (shows all games)
376+
377+
#### Implementation Details
378+
* **Dropdown Position**: Uses fixed positioning to avoid being cut off by table boundaries
379+
* **Dynamic Positioning**: JavaScript calculates dropdown position based on button location
380+
* **Close Behavior**: Dropdown closes when clicking outside or pressing Escape key
381+
* **Keyboard Support**: Escape key closes the dropdown; navigation disabled while dropdown is open
382+
* **CSS Styling**: Consistent with other filter controls, using accent colors and hover effects
383+
360384
### Layout
361385

362-
* Full-width table structure with six columns: Select, Game Session, Note, Rounds, Models, Action
386+
* Full-width table structure with seven columns: Select, Name, Date, Game, Rounds, Models, Action
387+
* **Name column**: Sortable column showing the folder name with hierarchy indentation
388+
* **Date column**: Sortable column showing creation date in YYYY-MM-DD HH:MM format (from `metadata["created_timestamp"]`)
363389
* **Action column includes**: Open, Open in new tab (↗), and Move/rename (📁) buttons for each game folder
364-
* Responsive design that adapts to mobile screens (hides some columns on mobile)
390+
* Responsive design that adapts to mobile screens (hides some columns on mobile, including Date)
365391
* Hover effects and visual feedback
366392
* Base directory path display at the top
367393
* Control bar with selection controls and action dropdown
368394

395+
### Sorting
396+
397+
* **Name column**: Click to sort alphabetically by folder path (ascending/descending toggle)
398+
* **Date column**: Click to sort chronologically by creation timestamp (ascending/descending toggle)
399+
* Sorting preserves intermediate folder hierarchy
400+
* Sort indicators appear in column headers with hover effects
401+
369402
## Trajectory Viewer Design (`/`)
370403

371404
### Header

codeclash/viewer/app.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def scan_directory(directory: Path, relative_path: str = ""):
217217
round_info = metadata.round_count_info
218218
models = metadata.models
219219
game_name = metadata.game_name
220+
created_timestamp = metadata.get_path("created_timestamp")
220221
game_folders.add(current_relative)
221222
all_folders.append(
222223
{
@@ -225,6 +226,7 @@ def scan_directory(directory: Path, relative_path: str = ""):
225226
"round_info": round_info, # Now stores (completed, total) tuple or None
226227
"models": models,
227228
"game_name": game_name,
229+
"created_timestamp": created_timestamp,
228230
"is_game": True,
229231
"depth": depth,
230232
"parent": relative_path if relative_path else None,
@@ -239,6 +241,7 @@ def scan_directory(directory: Path, relative_path: str = ""):
239241
"round_info": None,
240242
"models": [],
241243
"game_name": "",
244+
"created_timestamp": None,
242245
"is_game": False,
243246
"depth": depth,
244247
"parent": relative_path if relative_path else None,
@@ -745,6 +748,29 @@ def get_parent_folder(path):
745748
return parent.name
746749

747750

751+
def format_timestamp(timestamp):
752+
"""Format Unix timestamp as YYYY-MM-DD HH:MM"""
753+
if timestamp is None:
754+
return ""
755+
from datetime import datetime
756+
757+
try:
758+
dt = datetime.fromtimestamp(timestamp)
759+
return dt.strftime("%Y-%m-%d %H:%M")
760+
except (ValueError, OSError):
761+
return ""
762+
763+
764+
def strip_model_prefix(model_name):
765+
"""Strip provider prefix from model name (e.g., 'openai/gpt-5' -> 'gpt-5')"""
766+
if not model_name:
767+
return ""
768+
# Strip everything up to and including the last slash
769+
if "/" in model_name:
770+
return model_name.split("/")[-1]
771+
return model_name
772+
773+
748774
def get_navigation_info(selected_folder: str) -> dict[str, str | None]:
749775
"""Get previous and next game folders for navigation"""
750776
# Get all game folders
@@ -810,6 +836,8 @@ def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
810836
app.jinja_env.filters["unescape_content"] = unescape_content
811837
app.jinja_env.filters["get_folder_name"] = get_folder_name
812838
app.jinja_env.filters["get_parent_folder"] = get_parent_folder
839+
app.jinja_env.filters["format_timestamp"] = format_timestamp
840+
app.jinja_env.filters["strip_model_prefix"] = strip_model_prefix
813841

814842

815843
@app.route("/")

0 commit comments

Comments
 (0)