Spaces:
Running
Running
Empty PR for testing
#9
by gneubig - opened
- .gitattributes +0 -1
- OpenHands-Design/DESIGN.md +0 -597
- OpenHands-Design/NORMALIZATION_LOG.md +0 -36
- OpenHands-Design/README.md +0 -222
- OpenHands-Design/index.html +0 -396
- OpenHands-Design/src/components/ui/button.tsx +0 -50
- OpenHands-Design/src/components/ui/input.tsx +0 -22
- OpenHands-Design/src/components/ui/native-select.tsx +0 -26
- OpenHands-Design/src/components/ui/search-input.tsx +0 -74
- OpenHands-Design/src/globals.css +0 -135
- OpenHands-Design/tailwind.config.js +0 -99
- alternative_agents_page.py +0 -103
- app.py +4 -31
- assets/harnesses/README.md +0 -59
- assets/harnesses/claude-code.svg +0 -1
- assets/harnesses/codex-cli.svg +0 -1
- assets/harnesses/gemini-cli.svg +0 -1
- assets/harnesses/openhands.svg +0 -1
- assets/openhands-logotype-design.svg +0 -1
- assets/openhands-logotype-on-dark.svg +0 -1
- assets/openhands-logotype-on-light.svg +0 -1
- content.py +0 -5
- docs/screenshots/alternative-agents.png +0 -3
- leaderboard_transformer.py +88 -249
- main_page.py +41 -81
- setup_data.py +5 -17
- simple_data_loader.py +59 -156
- tests/test_runtime_sorting.py +0 -40
- ui_components.py +56 -97
.gitattributes
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
|
| 2 |
-
docs/screenshots/alternative-agents.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
|
|
|
OpenHands-Design/DESIGN.md
DELETED
|
@@ -1,597 +0,0 @@
|
|
| 1 |
-
# OpenHands UI Design System
|
| 2 |
-
|
| 3 |
-
## 1. Visual Theme & Atmosphere
|
| 4 |
-
|
| 5 |
-
OpenHands is a dark-first AI agent platform built on a near-black monochrome canvas. The entire experience lives on a `0 0% 5%` HSL background — effectively `#0d0d0d` — with `0 0% 98%` foreground text that reads as warm off-white. Every surface is a shade of neutral grey scaled in 2–5% lightness increments, creating depth through tonal variation rather than color. The only chromatic moments are semantic: green for success, red-orange for danger, amber for warnings, and blue for informational states.
|
| 6 |
-
|
| 7 |
-
Typography is carried by **Inter** (sans-serif) for all UI text and **JetBrains Mono** for code, terminals, and technical labels. The type system is weight-restrained — `font-medium` (500) is the workhorse, `font-semibold` (600) for headings and emphasis, and `font-normal` (400) for body. Bold (700) is rare and reserved for maximum emphasis.
|
| 8 |
-
|
| 9 |
-
The UI framework is **React + Tailwind CSS + Radix primitives** (shadcn/ui pattern). All colors flow through CSS custom properties declared in `:root` and consumed via `hsl(var(--token))` in the Tailwind config. This means every color in the system is overridable by changing a single HSL triplet.
|
| 10 |
-
|
| 11 |
-
**Key characteristics:**
|
| 12 |
-
- Near-black monochrome canvas (`#0d0d0d` background, `#fafafa` foreground)
|
| 13 |
-
- Neutral grey surface scale in 2–5% lightness increments (5% → 7% → 8% → 12% → 14% → 18%)
|
| 14 |
-
- Inter + JetBrains Mono dual-font system
|
| 15 |
-
- HSL-based CSS custom property architecture for full theme overridability
|
| 16 |
-
- Tailwind utility-first styling with Radix UI headless primitives
|
| 17 |
-
- `transition-colors` as the dominant transition (958 uses) — UI feels responsive but not animated
|
| 18 |
-
- Dark-only primary mode; light and sepia modes exist as secondary via class-map theming
|
| 19 |
-
|
| 20 |
-
---
|
| 21 |
-
|
| 22 |
-
## 2. Color Palette & Roles
|
| 23 |
-
|
| 24 |
-
All colors are declared as HSL triplets (without the `hsl()` wrapper) in CSS custom properties. Tailwind maps them as `hsl(var(--token))`.
|
| 25 |
-
|
| 26 |
-
### Core Surfaces
|
| 27 |
-
|
| 28 |
-
| Token | HSL | Hex | Role |
|
| 29 |
-
|-------|-----|-----|------|
|
| 30 |
-
| `--background` | `0 0% 5%` | `#0d0d0d` | Page background, app shell |
|
| 31 |
-
| `--card` | `0 0% 7%` | `#121212` | Card surfaces, elevated containers |
|
| 32 |
-
| `--secondary` | `0 0% 8%` | `#141414` | Secondary surfaces, sidebar accent |
|
| 33 |
-
| `--popover` | `0 0% 7%` | `#121212` | Dropdown menus, popovers |
|
| 34 |
-
| `--muted` | `0 0% 12%` | `#1f1f1f` | Muted backgrounds, hover fills, badges, **tooltip surfaces** |
|
| 35 |
-
| `--border` / `--input` | `0 0% 14%` | `#242424` | Borders, input borders, dividers |
|
| 36 |
-
| `--muted-hover` | `0 0% 18%` | `#2e2e2e` | Hover state for muted surfaces |
|
| 37 |
-
| `--modal-background` | Inherits `--background` | `#0d0d0d` | Dialogs, sheets, modals (can diverge) |
|
| 38 |
-
|
| 39 |
-
### Core Text
|
| 40 |
-
|
| 41 |
-
| Token | HSL | Hex | Role |
|
| 42 |
-
|-------|-----|-----|------|
|
| 43 |
-
| `--foreground` | `0 0% 98%` | `#fafafa` | Primary text, headings |
|
| 44 |
-
| `--muted-foreground` | `0 0% 55%` | `#8c8c8c` | Secondary text, labels, placeholders, icons |
|
| 45 |
-
| `--primary` | `0 0% 100%` | `#ffffff` | Maximum emphasis text, primary buttons |
|
| 46 |
-
| `--primary-foreground` | `0 0% 0%` | `#000000` | Text on primary (white) surfaces |
|
| 47 |
-
| `--accent` | `0 0% 100%` | `#ffffff` | Accent elements (matches primary in dark) |
|
| 48 |
-
|
| 49 |
-
### Sidebar (inherits core but isolated for overridability)
|
| 50 |
-
|
| 51 |
-
| Token | HSL | Role |
|
| 52 |
-
|-------|-----|------|
|
| 53 |
-
| `--sidebar-background` | `0 0% 5%` | Sidebar background |
|
| 54 |
-
| `--sidebar-foreground` | `0 0% 98%` | Sidebar text |
|
| 55 |
-
| `--sidebar-accent` | `0 0% 8%` | Sidebar hover/active background |
|
| 56 |
-
| `--sidebar-border` | `0 0% 14%` | Sidebar dividers |
|
| 57 |
-
| `--sidebar-ring` | `0 0% 50%` | Sidebar focus ring |
|
| 58 |
-
|
| 59 |
-
### Semantic / Status
|
| 60 |
-
|
| 61 |
-
| Token | HSL | Hex | Role |
|
| 62 |
-
|-------|-----|-----|------|
|
| 63 |
-
| `--success` | `142 71% 45%` | `#22c55e` | Success states, running indicators |
|
| 64 |
-
| `--success-foreground` | `142 71% 76%` | `#86efac` | Success text on dark surfaces |
|
| 65 |
-
| `--warning` | `38 92% 50%` | `#f59e0b` | Warning states, caution badges |
|
| 66 |
-
| `--info` | `217 91% 60%` | `#3b82f6` | Informational states, links |
|
| 67 |
-
| `--destructive` | `0 72% 51%` | `#dc2626` | Error states, danger actions, delete |
|
| 68 |
-
| `--destructive-foreground` | `0 0% 98%` | `#fafafa` | Text on destructive surfaces |
|
| 69 |
-
| `--ring` | `0 0% 80%` | `#cccccc` | Focus rings (1px, keyboard-only via `focus-visible:`) |
|
| 70 |
-
|
| 71 |
-
### Gradients & Decorative
|
| 72 |
-
|
| 73 |
-
| Token | Value | Role |
|
| 74 |
-
|-------|-------|------|
|
| 75 |
-
| `--gradient-card-hover` | `linear-gradient(180deg, hsl(0 0% 9%) 0%, hsl(0 0% 7%) 100%)` | Subtle card hover gradient |
|
| 76 |
-
| `--shadow-card` | `0 1px 2px 0 hsl(0 0% 0% / 0.3)` | Default card shadow |
|
| 77 |
-
|
| 78 |
-
### Hover Backgrounds
|
| 79 |
-
|
| 80 |
-
| Surface | Hover Token | Use |
|
| 81 |
-
|---------|-------------|-----|
|
| 82 |
-
| Dark surfaces (cards, nav items, menus, rows) | `hover:bg-muted/60` | **Standard hover** — the single canonical dark-surface hover |
|
| 83 |
-
| White/primary buttons | `hover:bg-primary/85` | Light grey hover on white buttons (85% opacity white) |
|
| 84 |
-
|
| 85 |
-
**Canonical dark-surface hover: `hover:bg-muted/60`** — used consistently across the codebase. Do **not** mix `/40`, `/50`, `/70` variants.
|
| 86 |
-
**Canonical primary-button hover: `hover:bg-primary/85`** — never use `hover:bg-muted/60` on a `bg-primary`/`bg-white` button (causes dark flash).
|
| 87 |
-
|
| 88 |
-
---
|
| 89 |
-
|
| 90 |
-
## 3. Typography Rules
|
| 91 |
-
|
| 92 |
-
### Font Families
|
| 93 |
-
|
| 94 |
-
| Role | Family | CSS Variable | Tailwind Class | Fallbacks |
|
| 95 |
-
|------|--------|-------------|----------------|-----------|
|
| 96 |
-
| UI / Body | Inter | `--font-sans` | `font-sans` | `system-ui, sans-serif` |
|
| 97 |
-
| Code / Technical | JetBrains Mono | `--font-mono` | `font-mono` | `monospace` |
|
| 98 |
-
|
| 99 |
-
Fonts are loaded via Google Fonts `@import` in `index.css`.
|
| 100 |
-
|
| 101 |
-
### Type Scale
|
| 102 |
-
|
| 103 |
-
The app uses Tailwind's default type scale. These are the **canonical sizes** ordered by frequency of use:
|
| 104 |
-
|
| 105 |
-
| Tailwind Class | Size | Uses | Role |
|
| 106 |
-
|----------------|------|------|------|
|
| 107 |
-
| `text-sm` | 14px / 0.875rem | 711 | **Primary body text**, labels, button text, descriptions |
|
| 108 |
-
| `text-xs` | 12px / 0.75rem | 427 | **Secondary text**, metadata, badges, menu items, captions |
|
| 109 |
-
| `text-base` | 16px / 1rem | 52 | Larger body text, input text, chat messages |
|
| 110 |
-
| `text-lg` | 18px / 1.125rem | 67 | Section sub-headings, dialog titles |
|
| 111 |
-
| `text-xl` | 20px / 1.25rem | 28 | Page sub-headings |
|
| 112 |
-
| `text-2xl` | 24px / 1.5rem | 31 | Page headings, modal titles |
|
| 113 |
-
| `text-3xl` | 30px / 1.875rem | 12 | Hero headings, landing sections |
|
| 114 |
-
|
| 115 |
-
### Arbitrary Font Sizes (to normalize)
|
| 116 |
-
|
| 117 |
-
These arbitrary sizes appear frequently and should be migrated to the standard scale or formalized as tokens:
|
| 118 |
-
|
| 119 |
-
| Arbitrary | Count | Recommended Replacement |
|
| 120 |
-
|-----------|-------|------------------------|
|
| 121 |
-
| `text-[11px]` | 46 | `text-xs` (12px) — or formalize as `--text-2xs` if 11px is intentional |
|
| 122 |
-
| `text-[10px]` | 20 | `text-xs` (12px) — or formalize as `--text-2xs` |
|
| 123 |
-
| `text-[12px]` | 8 | `text-xs` (already 12px — use the utility) |
|
| 124 |
-
| `text-[40px]` | 5 | `text-4xl` (36px) or formalize as hero display size |
|
| 125 |
-
| `text-[28px]` | 3 | `text-3xl` (30px) or formalize |
|
| 126 |
-
| `text-[32px]` | 1 | `text-3xl` (30px) or `text-4xl` (36px) |
|
| 127 |
-
| `text-[8px]` | 1 | Likely a micro label — evaluate if needed |
|
| 128 |
-
|
| 129 |
-
### Font Weight Scale
|
| 130 |
-
|
| 131 |
-
| Tailwind Class | Weight | Uses | Role |
|
| 132 |
-
|----------------|--------|------|------|
|
| 133 |
-
| `font-medium` | 500 | 304 | Labels, nav items, badges (note: buttons use `font-normal`) |
|
| 134 |
-
| `font-semibold` | 600 | 229 | **Headings**, section titles, strong emphasis |
|
| 135 |
-
| `font-normal` | 400 | 106 | **Body text**, descriptions, long-form content |
|
| 136 |
-
| `font-bold` | 700 | 29 | Maximum emphasis (use sparingly) |
|
| 137 |
-
| `font-light` | 300 | 13 | De-emphasized text (use sparingly) |
|
| 138 |
-
|
| 139 |
-
### Line Height
|
| 140 |
-
|
| 141 |
-
| Tailwind Class | Uses | Role |
|
| 142 |
-
|----------------|------|------|
|
| 143 |
-
| `leading-4` | 38 | Tight — compact UI, badges |
|
| 144 |
-
| `leading-6` | 34 | Standard — body text |
|
| 145 |
-
| `leading-relaxed` | 33 | Comfortable — long-form, descriptions |
|
| 146 |
-
| `leading-5` | 28 | Medium — labels, short text |
|
| 147 |
-
| `leading-tight` | 27 | Condensed — headings |
|
| 148 |
-
| `leading-snug` | 17 | Slightly condensed |
|
| 149 |
-
| `leading-none` | 16 | No leading — single-line elements |
|
| 150 |
-
|
| 151 |
-
### Letter Spacing
|
| 152 |
-
|
| 153 |
-
| Tailwind Class | Uses | Role |
|
| 154 |
-
|----------------|------|------|
|
| 155 |
-
| `tracking-wide` | 28 | Uppercase labels, section headers |
|
| 156 |
-
| `tracking-wider` | 20 | Small-caps metadata |
|
| 157 |
-
| `tracking-tight` | 19 | Display headings |
|
| 158 |
-
|
| 159 |
-
### Canonical Patterns
|
| 160 |
-
|
| 161 |
-
**Body text:** `text-sm font-normal text-foreground`
|
| 162 |
-
**Label:** `text-sm font-medium text-foreground`
|
| 163 |
-
**Secondary text:** `text-sm text-muted-foreground`
|
| 164 |
-
**Metadata/caption:** `text-xs text-muted-foreground`
|
| 165 |
-
**Uppercase category:** `text-[11px] font-medium uppercase tracking-wide text-muted-foreground`
|
| 166 |
-
**Heading (page):** `text-2xl font-semibold text-foreground`
|
| 167 |
-
**Heading (section):** `text-lg font-semibold text-foreground`
|
| 168 |
-
**Code/mono:** `text-sm font-mono`
|
| 169 |
-
|
| 170 |
-
---
|
| 171 |
-
|
| 172 |
-
## 4. Component Stylings
|
| 173 |
-
|
| 174 |
-
### Buttons (`Button` component — `src/components/ui/button.tsx`)
|
| 175 |
-
|
| 176 |
-
**Base classes (all variants):**
|
| 177 |
-
`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-normal ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`
|
| 178 |
-
|
| 179 |
-
| Variant | Background | Text | Border | Hover | Use |
|
| 180 |
-
|---------|-----------|------|--------|-------|-----|
|
| 181 |
-
| `default` | `bg-primary` | `text-primary-foreground` | — | `hover:bg-primary/85` | Primary CTA (white button, black text) |
|
| 182 |
-
| `destructive` | `bg-destructive` | `text-destructive-foreground` | — | `hover:bg-destructive/85` | Delete, danger actions |
|
| 183 |
-
| `outline` | `bg-background` | — | `border border-input` | `hover:bg-muted hover:text-foreground` | Secondary actions (most used — 53 instances) |
|
| 184 |
-
| `light` | `bg-primary` | `text-primary-foreground` | `border border-input` | `hover:bg-primary/85` | High-contrast primary on dark bg (token-based, no raw `bg-white`) |
|
| 185 |
-
| `secondary` | `bg-secondary` | `text-secondary-foreground` | — | `hover:bg-muted-hover` | Tertiary actions |
|
| 186 |
-
| `muted` | `bg-muted` | `text-muted-foreground` | — | `hover:bg-muted-hover hover:text-foreground` | Subdued actions |
|
| 187 |
-
| `ghost` | transparent | — | — | `hover:bg-muted hover:text-foreground` | Minimal chrome actions |
|
| 188 |
-
| `link` | transparent | `text-primary underline-offset-4` | — | `hover:underline` | Inline links |
|
| 189 |
-
|
| 190 |
-
**Primary button convention:** All white/primary buttons use `bg-primary text-primary-foreground hover:bg-primary/85`. Never use `bg-white text-black hover:bg-muted/60` inline — the dark hover on a white button is incorrect. Use the `Button` component or match its tokens.
|
| 191 |
-
|
| 192 |
-
| Size | Height | Padding | Font |
|
| 193 |
-
|------|--------|---------|------|
|
| 194 |
-
| `default` | `h-10` | `px-4 py-2` | `text-sm` |
|
| 195 |
-
| `sm` | `h-10` | `px-3` | `text-sm` |
|
| 196 |
-
| `xs` | `h-10` | `px-3` | `text-xs` |
|
| 197 |
-
| `lg` | `h-11` | `px-8` | `text-sm` |
|
| 198 |
-
| `icon` | `h-10 w-10` | — | — |
|
| 199 |
-
|
| 200 |
-
### Cards & Containers
|
| 201 |
-
|
| 202 |
-
There is no dedicated `Card` primitive — cards are composed with utilities.
|
| 203 |
-
|
| 204 |
-
**Standard card recipe:**
|
| 205 |
-
```
|
| 206 |
-
bg-card border border-border rounded-lg p-4
|
| 207 |
-
```
|
| 208 |
-
|
| 209 |
-
**Elevated card:**
|
| 210 |
-
```
|
| 211 |
-
bg-card border border-border rounded-xl p-6 shadow-lg
|
| 212 |
-
```
|
| 213 |
-
|
| 214 |
-
**Interactive card:**
|
| 215 |
-
```
|
| 216 |
-
bg-card border border-border rounded-lg p-4 transition-colors hover:border-white/30
|
| 217 |
-
```
|
| 218 |
-
|
| 219 |
-
**Glass / backdrop card:**
|
| 220 |
-
```
|
| 221 |
-
bg-card/70 border border-border/60 rounded-lg p-6 shadow-lg backdrop-blur-xl supports-[backdrop-filter]:bg-card/50
|
| 222 |
-
```
|
| 223 |
-
|
| 224 |
-
### Inputs (`Input` component — `src/components/ui/input.tsx`)
|
| 225 |
-
|
| 226 |
-
**Standard input:**
|
| 227 |
-
```
|
| 228 |
-
h-10 w-full rounded-md border border-border bg-muted/40 px-3 py-2 text-base md:text-sm
|
| 229 |
-
ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
| 230 |
-
focus-visible:ring-offset-2 focus-visible:bg-muted/60 hover:bg-muted/60
|
| 231 |
-
placeholder:text-muted-foreground
|
| 232 |
-
disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted/30
|
| 233 |
-
```
|
| 234 |
-
|
| 235 |
-
**Canonical focus style (all inputs, textareas, selects must match):**
|
| 236 |
-
```
|
| 237 |
-
ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:bg-muted/60
|
| 238 |
-
```
|
| 239 |
-
|
| 240 |
-
Key rules:
|
| 241 |
-
- Always use `focus-visible:` (keyboard-only), never `focus:` (fires on click too)
|
| 242 |
-
- Always include `ring-offset-background` and `focus-visible:ring-offset-2`
|
| 243 |
-
- Always include `focus-visible:bg-muted/60` for the subtle fill on focus
|
| 244 |
-
- Search inputs (`type="search"`) have `appearance: none` in global CSS to strip browser default focus chrome
|
| 245 |
-
|
| 246 |
-
**Size variants (via SearchInput wrapper):**
|
| 247 |
-
- `sm`: `h-9` + `pl-8 pr-8` (icon padding)
|
| 248 |
-
- `default`: `h-10` + `pl-9 pr-9`
|
| 249 |
-
- `lg`: `h-11` + `pl-10 pr-10`
|
| 250 |
-
|
| 251 |
-
### Dropdown Menus (`DropdownMenu` — Radix-based)
|
| 252 |
-
|
| 253 |
-
**Menu content:**
|
| 254 |
-
```
|
| 255 |
-
z-[100] min-w-[8rem] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md
|
| 256 |
-
```
|
| 257 |
-
|
| 258 |
-
**Menu item:**
|
| 259 |
-
```
|
| 260 |
-
group relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm
|
| 261 |
-
transition-colors focus:bg-muted/60 data-[highlighted]:bg-muted/60
|
| 262 |
-
```
|
| 263 |
-
|
| 264 |
-
**Icon treatment in menu items:**
|
| 265 |
-
- Default: `[&_svg]:text-muted-foreground` (grey)
|
| 266 |
-
- Hover/highlight: `group-hover:[&_svg]:!text-foreground` (white)
|
| 267 |
-
|
| 268 |
-
### Popover
|
| 269 |
-
|
| 270 |
-
**Content:**
|
| 271 |
-
```
|
| 272 |
-
z-50 max-h-[min(24rem,calc(100dvh-2rem))] shadow-md rounded-[12px] border border-border
|
| 273 |
-
bg-sidebar p-6 text-sidebar-foreground overflow-y-auto
|
| 274 |
-
```
|
| 275 |
-
|
| 276 |
-
### Navigation (LeftNav sidebar)
|
| 277 |
-
|
| 278 |
-
- Collapsed: 56px wide icon rail
|
| 279 |
-
- Expanded: 240px+ with text labels
|
| 280 |
-
- Items: `flex items-center gap-2 rounded-md px-3 py-1.5 text-xs transition-colors`
|
| 281 |
-
- Icons: `w-4 h-4 text-muted-foreground group-hover:text-white`
|
| 282 |
-
- Active: `bg-muted/60 text-foreground`
|
| 283 |
-
- Hover: `hover:bg-muted/60 hover:text-white`
|
| 284 |
-
|
| 285 |
-
### Scrollbar Variants
|
| 286 |
-
|
| 287 |
-
| Class | Width | Behavior | Use |
|
| 288 |
-
|-------|-------|----------|-----|
|
| 289 |
-
| `.dropdown-scroll` | 6px thin | Always visible | Menus, popovers |
|
| 290 |
-
| `.custom-scrollbar` | 8px thin | Always visible | Chat, main content |
|
| 291 |
-
| `.scrollbar-on-hover` | 8px thin | Visible on hover only | Chat threads |
|
| 292 |
-
| `.hide-scrollbar` | hidden | Hidden | Horizontal scroll areas |
|
| 293 |
-
|
| 294 |
-
All scrollbar thumbs: `hsl(var(--muted-foreground) / 0.5)` with hover at `0.7`.
|
| 295 |
-
|
| 296 |
-
### Tooltips
|
| 297 |
-
|
| 298 |
-
All tooltips use `bg-muted` for a lighter surface that visually separates from the dark page background.
|
| 299 |
-
|
| 300 |
-
**Standard tooltip (rounded-md):**
|
| 301 |
-
```
|
| 302 |
-
whitespace-nowrap rounded-md bg-muted px-2 py-1 text-xs text-foreground shadow-md
|
| 303 |
-
```
|
| 304 |
-
|
| 305 |
-
**Pill tooltip (rounded-full):**
|
| 306 |
-
```
|
| 307 |
-
bg-muted text-foreground text-xs rounded-full shadow-lg px-3 py-1
|
| 308 |
-
```
|
| 309 |
-
|
| 310 |
-
### Dialog Close Button
|
| 311 |
-
|
| 312 |
-
The dialog close "×" button has no focus ring (focus ring removed to avoid visual noise on click):
|
| 313 |
-
```
|
| 314 |
-
absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md opacity-70
|
| 315 |
-
ring-offset-background transition-colors hover:opacity-100 hover:bg-muted/60 focus:outline-none
|
| 316 |
-
```
|
| 317 |
-
|
| 318 |
-
---
|
| 319 |
-
|
| 320 |
-
## 5. Layout Principles
|
| 321 |
-
|
| 322 |
-
### Spacing System
|
| 323 |
-
|
| 324 |
-
The app uses Tailwind's default 4px-based spacing scale. These are the most common values by usage:
|
| 325 |
-
|
| 326 |
-
**Gaps (flex/grid):**
|
| 327 |
-
|
| 328 |
-
| Class | Px | Uses | Context |
|
| 329 |
-
|-------|-----|------|---------|
|
| 330 |
-
| `gap-2` | 8px | 414 | **Standard gap** — between items in rows, icon + label |
|
| 331 |
-
| `gap-3` | 12px | 144 | Comfortable gap — form groups, card content |
|
| 332 |
-
| `gap-4` | 16px | 110 | Generous gap — section spacing, grid layouts |
|
| 333 |
-
| `gap-1` | 4px | 86 | Tight gap — inline badges, compact lists |
|
| 334 |
-
| `gap-1.5` | 6px | 59 | Between tight and standard |
|
| 335 |
-
| `gap-6` | 24px | 50 | Large gap — major sections |
|
| 336 |
-
|
| 337 |
-
**Padding:**
|
| 338 |
-
|
| 339 |
-
| Class | Px | Uses | Context |
|
| 340 |
-
|-------|-----|------|---------|
|
| 341 |
-
| `px-4` | 16px | 216 | **Standard horizontal padding** — buttons, cards |
|
| 342 |
-
| `px-3` | 12px | 212 | Compact horizontal padding — menu items, inputs |
|
| 343 |
-
| `py-2` | 8px | 200 | **Standard vertical padding** — buttons, rows |
|
| 344 |
-
| `px-2` | 8px | 198 | Tight horizontal padding — badges, pills |
|
| 345 |
-
| `py-1` | 4px | 138 | Compact vertical padding |
|
| 346 |
-
| `py-1.5` | 6px | 76 | Slightly more than compact |
|
| 347 |
-
| `p-4` | 16px | 89 | Uniform card/container padding |
|
| 348 |
-
| `p-6` | 24px | 26 | Generous container/dialog padding |
|
| 349 |
-
|
| 350 |
-
### Grid & Container
|
| 351 |
-
|
| 352 |
-
- Max container width: `1400px` (via Tailwind `container` config with `2rem` padding)
|
| 353 |
-
- Primary layout: sidebar (56–240px) + main content area
|
| 354 |
-
- Settings layout: custom CSS vars for independent nav/main vertical inset
|
| 355 |
-
- `--settings-nav-padding-top/bottom`: `2rem`
|
| 356 |
-
- `--settings-main-padding-top/bottom`: `2rem`
|
| 357 |
-
|
| 358 |
-
### Whitespace Philosophy
|
| 359 |
-
|
| 360 |
-
- **Dense but breathable**: The app uses `text-sm` (14px) as the default with `gap-2` (8px) standard spacing — dense enough for a productivity tool, but never cramped.
|
| 361 |
-
- **Consistent rhythm**: Sections are separated by `border-t border-border` dividers with `my-3` (12px) vertical margin. No heavy horizontal rules.
|
| 362 |
-
- **Surface differentiation over spacing**: Rather than using large whitespace to separate areas, the app uses background color shifts (`bg-background` → `bg-card` → `bg-muted`) to create visual sections.
|
| 363 |
-
|
| 364 |
-
### Border Radius Scale
|
| 365 |
-
|
| 366 |
-
Defined via CSS custom properties and Tailwind mapping:
|
| 367 |
-
|
| 368 |
-
| Token | Value | Tailwind | Uses | Role |
|
| 369 |
-
|-------|-------|----------|------|------|
|
| 370 |
-
| `--radius` | `0.375rem` (6px) | `rounded-lg` | 112 | **Standard container radius** — cards, panels |
|
| 371 |
-
| `calc(--radius - 2px)` | `0.25rem` (4px) | `rounded-md` | 501 | **Default element radius** — buttons, inputs, menu items |
|
| 372 |
-
| `calc(--radius - 4px)` | `0.125rem` (2px) | `rounded-sm` | 12 | Subtle radius — small inline elements |
|
| 373 |
-
| `--radius-modal` | `0.75rem` (12px) | `rounded-modal` | — | Modal/dialog/popover radius |
|
| 374 |
-
| — | — | `rounded-xl` | 90 | Larger cards, featured containers |
|
| 375 |
-
| — | — | `rounded-2xl` | 23 | Hero elements, large cards |
|
| 376 |
-
| — | — | `rounded-full` | 185 | Avatars, pills, circular buttons, badges |
|
| 377 |
-
|
| 378 |
-
**Arbitrary radii to normalize:**
|
| 379 |
-
|
| 380 |
-
| Arbitrary | Count | Recommended |
|
| 381 |
-
|-----------|-------|-------------|
|
| 382 |
-
| `rounded-[6px]` | 20 | `rounded-lg` (already 6px via `--radius`) |
|
| 383 |
-
| `rounded-[100px]` | 15 | `rounded-full` (same visual effect) |
|
| 384 |
-
| `rounded-[12px]` | 8 | `rounded-modal` or `rounded-xl` (12px) |
|
| 385 |
-
| `rounded-[4px]` | 4 | `rounded-md` (already 4px) |
|
| 386 |
-
|
| 387 |
-
---
|
| 388 |
-
|
| 389 |
-
## 6. Depth & Elevation
|
| 390 |
-
|
| 391 |
-
### Shadow Scale
|
| 392 |
-
|
| 393 |
-
| Tailwind | Uses | Role |
|
| 394 |
-
|----------|------|------|
|
| 395 |
-
| `shadow-sm` | 21 | Subtle elevation — small cards, badges |
|
| 396 |
-
| `shadow` | 22 | Default — standalone cards |
|
| 397 |
-
| `shadow-md` | 31 | Medium — dropdown menus, popovers |
|
| 398 |
-
| `shadow-lg` | 49 | **Most used** — modals, dialogs, elevated panels |
|
| 399 |
-
| `shadow-xl` | 14 | High emphasis — floating panels |
|
| 400 |
-
| `shadow-2xl` | 5 | Maximum — overlay dialogs |
|
| 401 |
-
| `shadow-inner` | 8 | Inset — pressed buttons, input focus |
|
| 402 |
-
| `shadow-none` | 19 | Reset — flat elements |
|
| 403 |
-
|
| 404 |
-
### Custom Shadows
|
| 405 |
-
|
| 406 |
-
| Token | Value | Role |
|
| 407 |
-
|-------|-------|------|
|
| 408 |
-
| `--shadow-card` | `0 1px 2px 0 hsl(0 0% 0% / 0.3)` | Card resting shadow |
|
| 409 |
-
|
| 410 |
-
### Elevation Levels
|
| 411 |
-
|
| 412 |
-
| Level | Treatment | Use |
|
| 413 |
-
|-------|-----------|-----|
|
| 414 |
-
| 0 — Flat | No shadow, `bg-background` | Page background |
|
| 415 |
-
| 1 — Surface | `bg-card` + `border border-border` | Cards, content panels |
|
| 416 |
-
| 2 — Raised | `shadow-md` + `border` | Dropdown menus, popovers |
|
| 417 |
-
| 3 — Floating | `shadow-lg` + `border` | Modals, dialogs, sheets |
|
| 418 |
-
| 4 — Overlay | `shadow-xl` or `shadow-2xl` | Full-screen overlays, drawers |
|
| 419 |
-
|
| 420 |
-
### Border System
|
| 421 |
-
|
| 422 |
-
- **Standard border:** `border border-border` (1px solid `hsl(0 0% 14%)`)
|
| 423 |
-
- **Subtle border:** `border border-border/60` (reduced opacity)
|
| 424 |
-
- **Interactive hover:** `hover:border-white/30` or `hover:border-muted-foreground/30`
|
| 425 |
-
- **Section divider:** `border-t border-border` (horizontal rule) or `border-t border-sidebar-border` (in sidebar)
|
| 426 |
-
- **Focus ring:** `ring-1 ring-ring ring-offset-2 ring-offset-background` (1px, `focus-visible:` only)
|
| 427 |
-
|
| 428 |
-
---
|
| 429 |
-
|
| 430 |
-
## 7. Do's and Don'ts
|
| 431 |
-
|
| 432 |
-
### Colors
|
| 433 |
-
|
| 434 |
-
| Do | Don't |
|
| 435 |
-
|----|-------|
|
| 436 |
-
| Use `text-foreground` for primary text | Use `text-white` for primary text (278 instances to migrate) |
|
| 437 |
-
| Use `text-muted-foreground` for secondary text | Use `text-stone-400` or `text-gray-400` (raw palette) |
|
| 438 |
-
| Use `bg-background` for page surfaces | Use `bg-black` or hardcoded `bg-[#0d0d0d]` |
|
| 439 |
-
| Use `bg-card` for elevated surfaces | Use `bg-stone-800` or `bg-neutral-900` |
|
| 440 |
-
| Use `bg-muted` for subtle backgrounds | Use `bg-stone-700` or `bg-gray-800` |
|
| 441 |
-
| Use `border-border` for all borders | Use `border-stone-700` or `border-gray-700` |
|
| 442 |
-
| Use `text-success-foreground` for success text | Use `text-emerald-400` or `text-green-400` |
|
| 443 |
-
| Use `text-destructive` for error text | Use `text-red-500` or `text-rose-500` |
|
| 444 |
-
| Use `hover:text-foreground` for hover text brightening | Use `hover:text-white` except in sidebar context |
|
| 445 |
-
|
| 446 |
-
**Semantic status colors:** Use `text-success` / `bg-success`, `text-warning` / `bg-warning`, `text-info` / `bg-info`, `text-destructive` / `bg-destructive` — never raw chromatic palette classes like `text-green-500`, `bg-amber-400`, `text-blue-500`, etc.
|
| 447 |
-
|
| 448 |
-
**Current debt:** `themeAppClassMap.ts` and `NewUserExperienceFlowchart.tsx` still use raw `stone-*` / `rgb()` values (theme definition maps — intentionally deferred; requires per-theme CSS variable architecture). `ChatThread.tsx` `messageTypeColors` has 4 remaining raw palette colors (`orange-500`, `indigo-500`, `purple-500`, `pink-500`) for categorical distinctness — no semantic tokens defined for these yet.
|
| 449 |
-
|
| 450 |
-
### Typography
|
| 451 |
-
|
| 452 |
-
| Do | Don't |
|
| 453 |
-
|----|-------|
|
| 454 |
-
| Use `text-sm` (14px) as default body size | Use `text-[14px]` or arbitrary pixel values |
|
| 455 |
-
| Use `text-xs` (12px) for small/meta text | Use arbitrary pixel sizes for general text |
|
| 456 |
-
| Use Tailwind scale (`text-lg`, `text-xl`, `text-2xl`) | Use arbitrary sizes like `text-[28px]`, `text-[40px]` |
|
| 457 |
-
| Use `font-medium` as default weight | Use `font-bold` for general emphasis |
|
| 458 |
-
| Keep heading hierarchy: `2xl` → `xl` → `lg` → `base` | Skip levels or invert the scale |
|
| 459 |
-
|
| 460 |
-
### Border Radius
|
| 461 |
-
|
| 462 |
-
| Do | Don't |
|
| 463 |
-
|----|-------|
|
| 464 |
-
| Use `rounded-md` (4px) for buttons, inputs, menu items | Use `rounded-[4px]` (same value, less maintainable) |
|
| 465 |
-
| Use `rounded-lg` (6px) for cards, containers | Use `rounded-[6px]` (use the token) |
|
| 466 |
-
| Use `rounded-xl` or `rounded-modal` for dialogs | Use `rounded-[12px]` (use the token) |
|
| 467 |
-
| Use `rounded-full` for pills and avatars | Use `rounded-[100px]` (use `rounded-full`) |
|
| 468 |
-
|
| 469 |
-
### Spacing
|
| 470 |
-
|
| 471 |
-
| Do | Don't |
|
| 472 |
-
|----|-------|
|
| 473 |
-
| Use `gap-2` (8px) as standard item gap | Use arbitrary gap values |
|
| 474 |
-
| Use `px-3`/`px-4` for horizontal padding | Mix `px-2.5` and `px-3.5` without reason |
|
| 475 |
-
| Use `p-4` for card padding, `p-6` for dialogs | Use `p-[24px]` (same as `p-6`) |
|
| 476 |
-
| Use `my-3` for section divider spacing | Use inconsistent vertical margins around dividers |
|
| 477 |
-
|
| 478 |
-
### Hover & Interaction
|
| 479 |
-
|
| 480 |
-
| Do | Don't |
|
| 481 |
-
|----|-------|
|
| 482 |
-
| Use `hover:bg-muted/60` as standard hover bg on dark surfaces | Mix `/40`, `/50`, `/60`, `/70` without hierarchy |
|
| 483 |
-
| Use `hover:bg-primary/85` for white/primary buttons | Use `hover:bg-muted/60` on white buttons (creates dark hover) |
|
| 484 |
-
| Use `transition-colors` for color-only changes | Use `transition-all` when only color changes |
|
| 485 |
-
| Use `duration-200` as standard transition speed | Mix `duration-150`, `duration-200`, `duration-300` randomly |
|
| 486 |
-
| Use `group` + `group-hover:` for parent-child hover | Apply hover to each child independently |
|
| 487 |
-
| Use `active:scale-[0.97]` for button press feedback | Use `active:scale-95` (inconsistent with Button component) |
|
| 488 |
-
|
| 489 |
-
### Icons
|
| 490 |
-
|
| 491 |
-
| Do | Don't |
|
| 492 |
-
|----|-------|
|
| 493 |
-
| Use `w-4 h-4` as standard icon size in menus/buttons | Use `w-3 h-3` or `w-5 h-5` without size hierarchy reason |
|
| 494 |
-
| Set icon color to `text-muted-foreground` by default | Leave icons inheriting parent text color (appears too bright) |
|
| 495 |
-
| Brighten on hover: `group-hover:text-foreground` or `group-hover:text-white` | Omit icon hover transitions |
|
| 496 |
-
| Use `shrink-0` on icons in flex layouts | Let icons squish when text wraps |
|
| 497 |
-
|
| 498 |
-
---
|
| 499 |
-
|
| 500 |
-
## 8. Responsive Behavior
|
| 501 |
-
|
| 502 |
-
### Breakpoints (Tailwind defaults)
|
| 503 |
-
|
| 504 |
-
| Prefix | Min Width | Key Changes |
|
| 505 |
-
|--------|-----------|-------------|
|
| 506 |
-
| (none) | 0px | Mobile-first base styles |
|
| 507 |
-
| `sm` | 640px | Wider cards, more padding |
|
| 508 |
-
| `md` | 768px | Multi-column layouts begin, `md:text-sm` on inputs |
|
| 509 |
-
| `lg` | 1024px | Full sidebar visible, expanded grid |
|
| 510 |
-
| `xl` | 1280px | Maximum content width, full feature layout |
|
| 511 |
-
| `2xl` | 1400px | Container max-width ceiling |
|
| 512 |
-
|
| 513 |
-
### Touch Targets
|
| 514 |
-
- Minimum interactive height: `h-10` (40px) for buttons and inputs
|
| 515 |
-
- Small variant: `h-9` (36px) for compact contexts
|
| 516 |
-
- Icon buttons: `h-10 w-10` (40×40px)
|
| 517 |
-
- Menu items: `py-1.5` (6px) vertical padding at `text-sm` yields ~32px touch target
|
| 518 |
-
|
| 519 |
-
### Collapsing Strategy
|
| 520 |
-
- Sidebar: collapses from expanded (labels) to icon-only rail on narrow viewports
|
| 521 |
-
- Navigation menus: horizontal → hamburger on mobile
|
| 522 |
-
- Grid layouts: multi-column → single-column stacked
|
| 523 |
-
- Container padding: reduces from `p-6` → `p-4` → `p-3` at smaller breakpoints
|
| 524 |
-
|
| 525 |
-
---
|
| 526 |
-
|
| 527 |
-
## 9. Interaction & Motion
|
| 528 |
-
|
| 529 |
-
### Transitions
|
| 530 |
-
|
| 531 |
-
| Pattern | Uses | When |
|
| 532 |
-
|---------|------|------|
|
| 533 |
-
| `transition-colors` | 958 | **Default** — use for any color/bg/border change |
|
| 534 |
-
| `transition-opacity` | 96 | Fade in/out |
|
| 535 |
-
| `transition-all` | 96 | Multiple properties changing simultaneously |
|
| 536 |
-
| `transition-transform` | 59 | Scale/translate animations |
|
| 537 |
-
|
| 538 |
-
### Duration
|
| 539 |
-
|
| 540 |
-
| Duration | Uses | When |
|
| 541 |
-
|----------|------|------|
|
| 542 |
-
| `duration-200` | ~48 | **Standard** — local, small feedback: toggles, chevron rotation, sidebar width, card/row hovers, opacity on hover, dialogs |
|
| 543 |
-
| `duration-300` | ~34 | **Layout motion** — panel/drawer resize, sheet exit, canvas split, login/marketing card hover, grid row animations |
|
| 544 |
-
|
| 545 |
-
### Easing
|
| 546 |
-
|
| 547 |
-
| Easing | Uses | When |
|
| 548 |
-
|--------|------|------|
|
| 549 |
-
| `ease-in-out` | 111 | **Default** — smooth symmetrical transitions |
|
| 550 |
-
| `ease-out` | 52 | Enter animations — elements arriving |
|
| 551 |
-
|
| 552 |
-
### Framer Motion Patterns (23 files)
|
| 553 |
-
- `AnimatePresence` for mount/unmount transitions
|
| 554 |
-
- Standard enter: `initial={{ opacity: 0 }}` → `animate={{ opacity: 1 }}`
|
| 555 |
-
- Standard exit: `exit={{ opacity: 0 }}`
|
| 556 |
-
- Duration: typically `0.2s`–`0.3s`
|
| 557 |
-
- Used for: panel reveals, notification toasts, drawer slides, loading states
|
| 558 |
-
|
| 559 |
-
### Interactive Feedback
|
| 560 |
-
- **Button press:** `active:scale-[0.97]` (slight shrink on click)
|
| 561 |
-
- **Card hover:** `hover:scale-[1.02]` (subtle grow, 12 uses)
|
| 562 |
-
- **Focus:** `focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2` (1px ring, keyboard-only)
|
| 563 |
-
|
| 564 |
-
---
|
| 565 |
-
|
| 566 |
-
## 10. Agent Prompt Guide
|
| 567 |
-
|
| 568 |
-
### Quick Color Reference
|
| 569 |
-
- Page background: `bg-background` → `hsl(0 0% 5%)` → `#0d0d0d`
|
| 570 |
-
- Primary text: `text-foreground` → `hsl(0 0% 98%)` → `#fafafa`
|
| 571 |
-
- Secondary text: `text-muted-foreground` → `hsl(0 0% 55%)` → `#8c8c8c`
|
| 572 |
-
- Card surface: `bg-card` → `hsl(0 0% 7%)` → `#121212`
|
| 573 |
-
- Border: `border-border` → `hsl(0 0% 14%)` → `#242424`
|
| 574 |
-
- Hover background: `bg-muted/60` → `hsl(0 0% 12% / 0.6)`
|
| 575 |
-
- Success: `text-success-foreground` → `hsl(142 71% 76%)` → `#86efac`
|
| 576 |
-
- Error: `text-destructive` → `hsl(0 72% 51%)` → `#dc2626`
|
| 577 |
-
|
| 578 |
-
### Example Component Prompts
|
| 579 |
-
|
| 580 |
-
- **"Create a settings card"**: `bg-card border border-border rounded-lg p-4`. Title at `text-lg font-semibold text-foreground`. Description at `text-sm text-muted-foreground`. Action button: `<Button variant="outline">`.
|
| 581 |
-
- **"Create a sidebar menu item"**: `group flex items-center gap-2 rounded-md px-3 py-1.5 text-xs text-sidebar-foreground hover:text-white hover:bg-muted/60 transition-colors`. Icon: `w-4 h-4 shrink-0 text-muted-foreground transition-colors group-hover:text-white`.
|
| 582 |
-
- **"Create a dropdown menu"**: Use `DropdownMenu` + `DropdownMenuTrigger` + `DropdownMenuContent` + `DropdownMenuItem` from `src/components/ui/dropdown-menu.tsx`. Icons auto-styled grey → white on hover via the component's built-in `[&_svg]` selectors.
|
| 583 |
-
- **"Create a form field"**: Label at `text-sm font-medium text-foreground mb-1.5`. Use `<Input>` component (never inline raw `<input>` with custom focus styles). Help text at `text-xs text-muted-foreground mt-1`.
|
| 584 |
-
- **"Create a tooltip"**: `bg-muted text-foreground text-xs rounded-md px-2 py-1 shadow-md`. For pill-style: use `rounded-full` instead of `rounded-md`.
|
| 585 |
-
- **"Create a status badge"**: `inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium`. Success: `bg-success/10 text-success-foreground`. Error: `bg-destructive/10 text-destructive`.
|
| 586 |
-
|
| 587 |
-
### Iteration Guide
|
| 588 |
-
|
| 589 |
-
1. **Always use semantic color tokens** — never raw palette colors (`stone-*`, `gray-*`, `slate-*`). Every color should trace back to a `--css-variable`.
|
| 590 |
-
2. **`text-sm` is the default** — don't reach for `text-base` unless the context genuinely needs larger text (e.g., chat messages, hero content).
|
| 591 |
-
3. **`rounded-md` for elements, `rounded-lg` for containers** — this is the consistent radius hierarchy. Dialogs get `rounded-xl` or `rounded-modal`.
|
| 592 |
-
4. **`gap-2` is the standard** — 8px between items in any flex/grid layout. Use `gap-4` for major sections.
|
| 593 |
-
5. **Icons are always `text-muted-foreground`** by default and brighten to `text-foreground` or `text-white` on hover via `group` + `group-hover:`.
|
| 594 |
-
6. **`transition-colors duration-200`** is the standard animation. Don't add `transition-all` unless multiple property types are actually changing.
|
| 595 |
-
7. **`hover:bg-muted/60`** is the canonical hover background. Use it consistently across menus, nav items, and interactive rows.
|
| 596 |
-
8. **The `Button` component handles its own variants** — don't rebuild button styles from scratch. Use `variant="outline"` for most secondary actions.
|
| 597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/NORMALIZATION_LOG.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
# Token Normalization Log
|
| 2 |
-
|
| 3 |
-
Tracking the migration from raw Tailwind palette classes and arbitrary values to semantic design tokens.
|
| 4 |
-
|
| 5 |
-
## Completed
|
| 6 |
-
|
| 7 |
-
- [x] `.dark` CSS block now declares all variables from `:root` (`--modal-background`, `--radius-*`, `--success`, `--warning`, `--info`, `--gradient-*`, `--shadow-*`, `--font-*`, `--settings-*`)
|
| 8 |
-
- [x] Migrated ~160 raw `stone-*` utility classes across 17 component files to semantic tokens (excluding `themeAppClassMap.ts`, `index.ts`, and scrollbar utilities)
|
| 9 |
-
- [x] Migrated ~44 raw `gray-*` utility classes across 10 files to semantic tokens
|
| 10 |
-
- [x] Migrated ~83 raw `neutral-*` classes across 3 files to semantic tokens (preserving VS Code diff mock borders)
|
| 11 |
-
- [x] Replaced 47 arbitrary border radius values (`rounded-[6px]` → `rounded-lg`, `rounded-[100px]` → `rounded-full`, `rounded-[12px]` → `rounded-xl`, `rounded-[4px]` → `rounded-md`, `rounded-r-[100px]` → `rounded-r-full`)
|
| 12 |
-
- [x] Replaced 74 arbitrary font sizes (`text-[11px]` → `text-xs`, `text-[10px]` → `text-xs`, `text-[12px]` → `text-xs`)
|
| 13 |
-
- [x] Standardized 57 `hover:bg-muted` opacity variants (`/40`, `/50`, `/70`, `/80`) to canonical `/60`
|
| 14 |
-
- [x] Replaced 6 unsafe `text-white` usages with semantic tokens (`text-foreground`, `text-card-foreground`)
|
| 15 |
-
- [x] Replaced `bg-[#141414]` → `bg-secondary` (2 instances)
|
| 16 |
-
- [x] Migrated ~100+ chromatic palette classes to semantic tokens: `amber/yellow` → `warning`, `blue/sky` → `info`, `green/emerald` → `success`, `red` → `destructive`
|
| 17 |
-
- [x] Unified tooltip backgrounds from `bg-popover`/`bg-card` to `bg-muted` across all 6 tooltip instances
|
| 18 |
-
- [x] Fixed 33 inline white buttons (`bg-white text-black hover:bg-muted/60` → `bg-primary text-primary-foreground hover:bg-primary/85`)
|
| 19 |
-
- [x] Fixed Button `light` variant from `bg-white text-black hover:bg-zinc-200` to `bg-primary text-primary-foreground hover:bg-primary/85`
|
| 20 |
-
- [x] Removed Dialog `--ring` inline override (`0 0% 95%`) that caused inconsistent focus ring color in modals
|
| 21 |
-
- [x] Updated global `--ring` from `0 0% 50%` to `0 0% 80%` for better visibility
|
| 22 |
-
- [x] Normalized ~50 inline input/textarea/select focus styles to canonical `focus-visible:` pattern (from mixed `focus:`/`focus-visible:` with missing offsets)
|
| 23 |
-
- [x] Changed all focus rings from `ring-2` to `ring-1` site-wide (~97 instances across 39 files)
|
| 24 |
-
- [x] Added `appearance: none` on `input[type="search"]` to strip browser default focus chrome
|
| 25 |
-
- [x] Removed focus ring from dialog close button
|
| 26 |
-
- [x] Consolidated `active:scale-95` (3 uses) → `active:scale-[0.97]` to match Button standard
|
| 27 |
-
- [x] Migrated `ChatThread.tsx` `messageTypeColors`: `yellow-500` → `warning`, `blue-500` → `info` (3 categories). Remaining categorical colors (`orange-500`, `indigo-500`, `purple-500`, `pink-500`) kept as raw palette — no semantic equivalent for multi-category distinctness
|
| 28 |
-
- [x] Migrated 14 remaining `bg-white` usages in non-button contexts to semantic tokens: toggles → `bg-primary`, resize grips → `bg-foreground`, badge → `bg-primary`, attachment previews → `bg-foreground/5`, CTA buttons → `bg-primary text-primary-foreground hover:bg-primary/85`, ghost hover buttons → `hover:bg-primary hover:text-primary-foreground`
|
| 29 |
-
- [x] Documented `duration-200` vs `duration-300` convention (200ms = local feedback, 300ms = layout/panel motion)
|
| 30 |
-
- [x] ~~Migrate legacy `index.ts`~~ — invalid; `screens/index.ts` and `components/workflow/index.ts` are barrel files with zero raw color classes
|
| 31 |
-
|
| 32 |
-
## Deferred by Design
|
| 33 |
-
|
| 34 |
-
- [ ] `themeAppClassMap.ts` and `NewUserExperienceFlowchart.tsx` use raw `stone-*` / `rgb()` values — these are **theme definition maps** that intentionally encode per-theme palettes (dark/light/sepia). Migrating requires defining CSS variables for each theme mode, which is an architectural change
|
| 35 |
-
- [ ] `sepia` theme in `themeAppClassMap.ts` uses hardcoded `rgb()` — requires defining a `.theme-sepia` CSS variable block before semantic classes can replace arbitrary values
|
| 36 |
-
- [ ] `ChatThread.tsx` categorical palette (`orange-500`, `indigo-500`, `purple-500`, `pink-500`) for `bug`, `docs`, `dependency`, `git` message types — no semantic tokens exist for multi-category distinctness; would need new `--chart-*` or `--category-*` CSS variables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/README.md
DELETED
|
@@ -1,222 +0,0 @@
|
|
| 1 |
-
# OpenHands Design System
|
| 2 |
-
|
| 3 |
-
A portable design system extracted from the OpenHands UI. Drop it into any React + Tailwind project to get a consistent dark-first interface with semantic color tokens, pre-built components, and a comprehensive style guide.
|
| 4 |
-
|
| 5 |
-
## What's Included
|
| 6 |
-
|
| 7 |
-
```
|
| 8 |
-
OpenHands-Design/
|
| 9 |
-
DESIGN.md # Full design system specification
|
| 10 |
-
README.md # This file
|
| 11 |
-
tailwind.config.js # Tailwind theme (colors, radii, fonts, animations)
|
| 12 |
-
src/
|
| 13 |
-
globals.css # CSS custom properties (design tokens) + base resets
|
| 14 |
-
lib/
|
| 15 |
-
utils.ts # cn() helper (clsx + tailwind-merge)
|
| 16 |
-
components/ui/
|
| 17 |
-
button.tsx # Button with 8 variants (default, destructive, outline, light, secondary, muted, ghost, link)
|
| 18 |
-
input.tsx # Text input with unified focus style
|
| 19 |
-
search-input.tsx # Search input with icon, clear button, and 3 sizes
|
| 20 |
-
native-select.tsx # Native <select> with consistent styling
|
| 21 |
-
```
|
| 22 |
-
|
| 23 |
-
## Quick Start
|
| 24 |
-
|
| 25 |
-
### Install with npx
|
| 26 |
-
|
| 27 |
-
From your project root:
|
| 28 |
-
|
| 29 |
-
```bash
|
| 30 |
-
npx openhands-design
|
| 31 |
-
```
|
| 32 |
-
|
| 33 |
-
This adds `./OpenHands-Design/` (including `DESIGN.md`, tokens, and UI components). Then ask your AI assistant to use **`DESIGN.md`** for UI work. If the folder already exists, run `npx openhands-design --force` to replace it.
|
| 34 |
-
|
| 35 |
-
### 1. Install dependencies
|
| 36 |
-
|
| 37 |
-
```bash
|
| 38 |
-
npm install clsx tailwind-merge class-variance-authority @radix-ui/react-slot lucide-react tailwindcss-animate
|
| 39 |
-
```
|
| 40 |
-
|
| 41 |
-
### 2. Copy files into your project
|
| 42 |
-
|
| 43 |
-
```bash
|
| 44 |
-
# Copy the design tokens and global CSS
|
| 45 |
-
cp OpenHands-Design/src/globals.css your-project/src/globals.css
|
| 46 |
-
|
| 47 |
-
# Copy the Tailwind config (or merge into your existing one)
|
| 48 |
-
cp OpenHands-Design/tailwind.config.js your-project/tailwind.config.js
|
| 49 |
-
|
| 50 |
-
# Copy the utility helper
|
| 51 |
-
cp OpenHands-Design/src/lib/utils.ts your-project/src/lib/utils.ts
|
| 52 |
-
|
| 53 |
-
# Copy the UI components
|
| 54 |
-
cp -r OpenHands-Design/src/components/ui/ your-project/src/components/ui/
|
| 55 |
-
```
|
| 56 |
-
|
| 57 |
-
### 3. Import globals.css
|
| 58 |
-
|
| 59 |
-
In your app entry point (e.g., `main.tsx` or `App.tsx`):
|
| 60 |
-
|
| 61 |
-
```tsx
|
| 62 |
-
import './globals.css';
|
| 63 |
-
```
|
| 64 |
-
|
| 65 |
-
### 4. Add the dark class
|
| 66 |
-
|
| 67 |
-
The system is dark-first. Add the `dark` class to your `<html>` tag:
|
| 68 |
-
|
| 69 |
-
```html
|
| 70 |
-
<html lang="en" class="dark">
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
### 5. Start using components
|
| 74 |
-
|
| 75 |
-
```tsx
|
| 76 |
-
import { Button } from './components/ui/button';
|
| 77 |
-
import { Input } from './components/ui/input';
|
| 78 |
-
import { SearchInput } from './components/ui/search-input';
|
| 79 |
-
import { NativeSelect } from './components/ui/native-select';
|
| 80 |
-
|
| 81 |
-
function Example() {
|
| 82 |
-
return (
|
| 83 |
-
<div className="flex flex-col gap-4 bg-background p-6 text-foreground">
|
| 84 |
-
<h1 className="text-2xl font-semibold">Settings</h1>
|
| 85 |
-
<p className="text-sm text-muted-foreground">Manage your account.</p>
|
| 86 |
-
|
| 87 |
-
<Input placeholder="Your name" />
|
| 88 |
-
|
| 89 |
-
<NativeSelect>
|
| 90 |
-
<option>Option A</option>
|
| 91 |
-
<option>Option B</option>
|
| 92 |
-
</NativeSelect>
|
| 93 |
-
|
| 94 |
-
<div className="flex gap-2">
|
| 95 |
-
<Button>Save</Button>
|
| 96 |
-
<Button variant="outline">Cancel</Button>
|
| 97 |
-
<Button variant="destructive">Delete</Button>
|
| 98 |
-
</div>
|
| 99 |
-
</div>
|
| 100 |
-
);
|
| 101 |
-
}
|
| 102 |
-
```
|
| 103 |
-
|
| 104 |
-
## Using with AI Agents (Cursor, Copilot, etc.)
|
| 105 |
-
|
| 106 |
-
The `DESIGN.md` file is structured as an AI-readable specification. Two ways to use it:
|
| 107 |
-
|
| 108 |
-
### Option A: Cursor Rule (recommended)
|
| 109 |
-
|
| 110 |
-
Create `.cursor/rules/design-system.md` in your project:
|
| 111 |
-
|
| 112 |
-
```markdown
|
| 113 |
-
When building UI components, follow the design system in /DESIGN.md.
|
| 114 |
-
|
| 115 |
-
Key rules:
|
| 116 |
-
- Use semantic color tokens (bg-card, text-foreground, border-border) — never raw palette classes
|
| 117 |
-
- Use the Button, Input, SearchInput, and NativeSelect components — never raw HTML with inline styles
|
| 118 |
-
- Hover on dark surfaces: hover:bg-muted/60
|
| 119 |
-
- Hover on white/primary buttons: hover:bg-primary/85
|
| 120 |
-
- Focus rings: focus-visible:ring-1 (keyboard-only, 1px)
|
| 121 |
-
- Default text: text-sm font-normal text-foreground
|
| 122 |
-
- Secondary text: text-sm text-muted-foreground
|
| 123 |
-
- Standard gap: gap-2 (8px)
|
| 124 |
-
- Standard card: bg-card border border-border rounded-lg p-4
|
| 125 |
-
```
|
| 126 |
-
|
| 127 |
-
Every Cursor conversation will now follow your design system automatically.
|
| 128 |
-
|
| 129 |
-
### Option B: Direct prompt
|
| 130 |
-
|
| 131 |
-
Paste this at the start of a conversation:
|
| 132 |
-
|
| 133 |
-
> Build this feature following the design system in DESIGN.md. Use semantic tokens for all colors, the Button component for actions, and the Input component for form fields.
|
| 134 |
-
|
| 135 |
-
## Token Architecture
|
| 136 |
-
|
| 137 |
-
All colors are HSL triplets stored as CSS custom properties. Tailwind maps them via `hsl(var(--token))`.
|
| 138 |
-
|
| 139 |
-
```
|
| 140 |
-
Background scale (darkest → lightest):
|
| 141 |
-
--background 5% #0d0d0d Page background
|
| 142 |
-
--card 7% #121212 Cards, elevated surfaces
|
| 143 |
-
--secondary 8% #141414 Secondary surfaces
|
| 144 |
-
--muted 12% #1f1f1f Hover fills, badges, tooltips
|
| 145 |
-
--border 14% #242424 Borders, dividers
|
| 146 |
-
--muted-hover 18% #2e2e2e Hover on muted surfaces
|
| 147 |
-
|
| 148 |
-
Text scale:
|
| 149 |
-
--foreground 98% #fafafa Primary text
|
| 150 |
-
--muted-foreground 55% #8c8c8c Secondary text, placeholders
|
| 151 |
-
--primary 100% #ffffff Maximum emphasis, button bg
|
| 152 |
-
--primary-foreground 0% #000000 Text on white buttons
|
| 153 |
-
|
| 154 |
-
Semantic colors:
|
| 155 |
-
--success hsl(142 71% 45%) Green — success states
|
| 156 |
-
--warning hsl(38 92% 50%) Amber — warnings, in-progress
|
| 157 |
-
--info hsl(217 91% 60%) Blue — links, informational
|
| 158 |
-
--destructive hsl(0 72% 51%) Red — errors, danger
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
## Button Variants
|
| 162 |
-
|
| 163 |
-
| Variant | Look | Use |
|
| 164 |
-
|---------|------|-----|
|
| 165 |
-
| `default` | White bg, black text | Primary CTA |
|
| 166 |
-
| `destructive` | Red bg, white text | Delete, danger |
|
| 167 |
-
| `outline` | Transparent, border | Secondary actions |
|
| 168 |
-
| `light` | White bg, border | High-contrast primary |
|
| 169 |
-
| `secondary` | Dark bg | Tertiary actions |
|
| 170 |
-
| `muted` | Muted bg, grey text | Subdued actions |
|
| 171 |
-
| `ghost` | Transparent, no border | Minimal chrome |
|
| 172 |
-
| `link` | Underline on hover | Inline links |
|
| 173 |
-
|
| 174 |
-
## Customization
|
| 175 |
-
|
| 176 |
-
### Changing the color scheme
|
| 177 |
-
|
| 178 |
-
Edit the HSL values in `globals.css`. Every UI element updates automatically:
|
| 179 |
-
|
| 180 |
-
```css
|
| 181 |
-
:root {
|
| 182 |
-
--background: 220 20% 5%; /* Add a blue tint */
|
| 183 |
-
--card: 220 15% 8%;
|
| 184 |
-
--border: 220 10% 16%;
|
| 185 |
-
}
|
| 186 |
-
```
|
| 187 |
-
|
| 188 |
-
### Adding a light theme
|
| 189 |
-
|
| 190 |
-
Create a new class block in `globals.css` with inverted values:
|
| 191 |
-
|
| 192 |
-
```css
|
| 193 |
-
.light {
|
| 194 |
-
--background: 0 0% 100%;
|
| 195 |
-
--foreground: 0 0% 5%;
|
| 196 |
-
--card: 0 0% 97%;
|
| 197 |
-
--border: 0 0% 88%;
|
| 198 |
-
/* ... */
|
| 199 |
-
}
|
| 200 |
-
```
|
| 201 |
-
|
| 202 |
-
Then toggle `class="light"` on the `<html>` element.
|
| 203 |
-
|
| 204 |
-
## Reference
|
| 205 |
-
|
| 206 |
-
See [DESIGN.md](./DESIGN.md) for the complete specification including:
|
| 207 |
-
|
| 208 |
-
1. Visual theme and atmosphere
|
| 209 |
-
2. Full color palette with hex values
|
| 210 |
-
3. Typography rules and type scale
|
| 211 |
-
4. Component styling recipes
|
| 212 |
-
5. Layout principles and spacing system
|
| 213 |
-
6. Depth and elevation system
|
| 214 |
-
7. Do's and Don'ts
|
| 215 |
-
8. Responsive behavior
|
| 216 |
-
9. Interaction and motion patterns
|
| 217 |
-
10. AI agent prompt guide
|
| 218 |
-
11. Normalization backlog
|
| 219 |
-
|
| 220 |
-
## License
|
| 221 |
-
|
| 222 |
-
MIT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/index.html
DELETED
|
@@ -1,396 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>OpenHands Design System</title>
|
| 7 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 9 |
-
<style>
|
| 10 |
-
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 11 |
-
|
| 12 |
-
:root{
|
| 13 |
-
--bg:hsl(0 0% 5%);
|
| 14 |
-
--card:hsl(0 0% 7%);
|
| 15 |
-
--secondary:hsl(0 0% 8%);
|
| 16 |
-
--muted:hsl(0 0% 12%);
|
| 17 |
-
--border:hsl(0 0% 14%);
|
| 18 |
-
/* DESIGN.md: interactive border emphasis (e.g. card/button hover) */
|
| 19 |
-
--border-hover:hsl(0 0% 24%);
|
| 20 |
-
--muted-hover:hsl(0 0% 18%);
|
| 21 |
-
--fg:hsl(0 0% 98%);
|
| 22 |
-
--fg-muted:hsl(0 0% 55%);
|
| 23 |
-
--primary:hsl(0 0% 100%);
|
| 24 |
-
--primary-fg:hsl(0 0% 0%);
|
| 25 |
-
--success:#22c55e;
|
| 26 |
-
--success-fg:#86efac;
|
| 27 |
-
--warning:#f59e0b;
|
| 28 |
-
--info:#3b82f6;
|
| 29 |
-
--destructive:#dc2626;
|
| 30 |
-
--destructive-fg:#fafafa;
|
| 31 |
-
--ring:#cccccc;
|
| 32 |
-
--shadow-card:0 1px 2px 0 hsl(0 0% 0% / 0.3);
|
| 33 |
-
--font-sans:'Inter',system-ui,sans-serif;
|
| 34 |
-
--font-mono:'JetBrains Mono',monospace;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
html{scroll-behavior:smooth}
|
| 38 |
-
body{font-family:var(--font-sans);background:var(--bg);color:var(--fg);line-height:1.6;-webkit-font-smoothing:antialiased}
|
| 39 |
-
|
| 40 |
-
/* Nav */
|
| 41 |
-
.nav{position:sticky;top:0;z-index:50;display:flex;align-items:center;flex-wrap:nowrap;gap:16px;padding:0 24px 0 16px;height:56px;background:hsl(0 0% 5% / 0.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);overflow:hidden}
|
| 42 |
-
.nav-start{display:flex;align-items:center;flex-wrap:nowrap;gap:12px;flex-shrink:0;min-width:0}
|
| 43 |
-
.nav-github{display:inline-flex;align-items:center;gap:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--fg-muted);text-decoration:none;padding:4px 10px;border-radius:6px;border:1px solid var(--border);transition:all 0.2s;flex-shrink:0}
|
| 44 |
-
.nav-github:hover{background:var(--muted);color:var(--fg);border-color:var(--border-hover)}
|
| 45 |
-
.nav-links{display:flex;flex:1;justify-content:flex-end;gap:4px;list-style:none;margin:0;padding:0;min-width:0;flex-wrap:nowrap}
|
| 46 |
-
.nav-links a{font-size:13px;color:var(--fg-muted);text-decoration:none;padding:6px 12px;border-radius:6px;transition:all 0.2s}
|
| 47 |
-
.nav-links a:hover{color:var(--fg);background:var(--muted)}
|
| 48 |
-
.nav-logo{display:flex;align-items:center;flex-shrink:0;opacity:0.85;transition:opacity 0.2s}
|
| 49 |
-
.nav-logo:hover{opacity:1}
|
| 50 |
-
.nav-logo svg{height:22px;width:auto;display:block}
|
| 51 |
-
/* Hero */
|
| 52 |
-
.hero{text-align:center;padding:80px 24px 64px;max-width:720px;margin:0 auto}
|
| 53 |
-
.hero h1{font-size:48px;font-weight:600;line-height:1.1;letter-spacing:-1.5px;margin-bottom:16px}
|
| 54 |
-
.hero p{font-size:16px;color:var(--fg-muted);line-height:1.6;max-width:520px;margin:0 auto 32px}
|
| 55 |
-
.hero-buttons{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
| 56 |
-
|
| 57 |
-
/* Buttons */
|
| 58 |
-
.btn-primary{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;background:var(--secondary);color:var(--fg);border:1px solid var(--border)}
|
| 59 |
-
.btn-primary:hover{background:var(--muted-hover);border-color:var(--border-hover)}
|
| 60 |
-
.btn-dark{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;background:var(--primary);color:var(--primary-fg)}
|
| 61 |
-
.btn-dark:hover{opacity:0.85}
|
| 62 |
-
.btn-ghost{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;color:var(--fg-muted);background:transparent}
|
| 63 |
-
.btn-ghost:hover{color:var(--fg);background:var(--muted)}
|
| 64 |
-
.btn-destructive{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;background:var(--destructive);color:var(--destructive-fg)}
|
| 65 |
-
.btn-destructive:hover{opacity:0.85}
|
| 66 |
-
.btn-outline{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;color:var(--fg);background:var(--bg);border:1px solid var(--border)}
|
| 67 |
-
.btn-outline:hover{background:var(--muted);color:var(--fg)}
|
| 68 |
-
.btn-muted{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;color:var(--fg-muted);background:var(--muted)}
|
| 69 |
-
.btn-muted:hover{background:var(--muted-hover);color:var(--fg)}
|
| 70 |
-
.btn-pill{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 12px;font-size:12px;font-weight:500;border-radius:9999px;text-decoration:none;transition:all 0.2s;background:var(--muted);color:var(--fg-muted)}
|
| 71 |
-
.btn-pill:hover{color:var(--fg);background:var(--muted-hover)}
|
| 72 |
-
|
| 73 |
-
/* Sections */
|
| 74 |
-
.section{max-width:1100px;margin:0 auto;padding:64px 24px}
|
| 75 |
-
.section-label{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:1.5px;color:var(--fg-muted);margin-bottom:8px}
|
| 76 |
-
.section-title{font-size:28px;font-weight:600;letter-spacing:-0.5px;margin-bottom:40px}
|
| 77 |
-
.section-divider{border:none;border-top:1px solid var(--border);margin:0}
|
| 78 |
-
|
| 79 |
-
/* Color swatches */
|
| 80 |
-
.color-group-label{font-size:13px;font-weight:500;color:var(--fg-muted);margin-bottom:12px;margin-top:32px}
|
| 81 |
-
.color-group-label:first-of-type{margin-top:0}
|
| 82 |
-
.color-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-bottom:24px}
|
| 83 |
-
.color-swatch{border-radius:10px;overflow:hidden;border:1px solid var(--border);background:var(--card);transition:transform 0.2s}
|
| 84 |
-
.color-swatch:hover{transform:translateY(-2px)}
|
| 85 |
-
.color-swatch-block{height:80px}
|
| 86 |
-
.color-swatch-info{padding:10px 12px}
|
| 87 |
-
.color-swatch-name{font-size:13px;font-weight:500;margin-bottom:2px}
|
| 88 |
-
.color-swatch-hex{font-family:var(--font-mono);font-size:11px;color:var(--fg-muted)}
|
| 89 |
-
.color-swatch-role{font-size:11px;color:var(--fg-muted);margin-top:4px}
|
| 90 |
-
|
| 91 |
-
/* Typography */
|
| 92 |
-
.type-sample{padding:20px 0;border-bottom:1px solid var(--border)}
|
| 93 |
-
.type-sample:last-child{border-bottom:none}
|
| 94 |
-
.type-meta{font-size:12px;color:var(--fg-muted);margin-top:8px;font-family:var(--font-mono)}
|
| 95 |
-
|
| 96 |
-
/* Buttons section */
|
| 97 |
-
.button-row{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start}
|
| 98 |
-
.button-item{display:flex;flex-direction:column;align-items:center;gap:8px}
|
| 99 |
-
.button-label{font-size:11px;color:var(--fg-muted);text-align:center}
|
| 100 |
-
|
| 101 |
-
/* Cards */
|
| 102 |
-
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}
|
| 103 |
-
.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:24px;transition:all 0.2s}
|
| 104 |
-
.card:hover{border-color:var(--border-hover)}
|
| 105 |
-
.card h3{font-size:16px;font-weight:600;margin-bottom:8px}
|
| 106 |
-
.card p{font-size:14px;color:var(--fg-muted);line-height:1.6}
|
| 107 |
-
.card-badge{display:inline-block;font-size:11px;font-weight:500;padding:2px 8px;border-radius:9999px;margin-bottom:12px}
|
| 108 |
-
|
| 109 |
-
/* Forms */
|
| 110 |
-
.form-group{max-width:480px;margin-bottom:24px}
|
| 111 |
-
.form-label{display:block;font-size:14px;font-weight:500;margin-bottom:6px}
|
| 112 |
-
.form-input,.form-textarea{width:100%;height:40px;padding:0 12px;font-size:14px;font-family:var(--font-sans);color:var(--fg);background:hsl(0 0% 12% / 0.4);border:1px solid var(--border);border-radius:6px;outline:none;transition:all 0.2s}
|
| 113 |
-
.form-input::placeholder,.form-textarea::placeholder{color:var(--fg-muted)}
|
| 114 |
-
.form-input:hover,.form-textarea:hover{background:hsl(0 0% 12% / 0.6)}
|
| 115 |
-
.form-input:focus,.form-textarea:focus{background:hsl(0 0% 12% / 0.6);box-shadow:0 0 0 1px var(--ring),0 0 0 3px var(--bg)}
|
| 116 |
-
.form-input--focus{background:hsl(0 0% 12% / 0.6) !important;box-shadow:0 0 0 1px var(--ring),0 0 0 3px var(--bg) !important}
|
| 117 |
-
.form-input--error{border-color:var(--destructive) !important;box-shadow:0 0 0 1px var(--destructive),0 0 0 3px var(--bg) !important}
|
| 118 |
-
.form-textarea{height:auto;min-height:80px;padding:10px 12px;resize:vertical}
|
| 119 |
-
.form-state-label{font-size:11px;color:var(--fg-muted);margin-top:6px}
|
| 120 |
-
|
| 121 |
-
/* Spacing */
|
| 122 |
-
.spacing-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end}
|
| 123 |
-
.spacing-item{display:flex;flex-direction:column;align-items:center;gap:8px}
|
| 124 |
-
.spacing-block{height:40px;background:var(--primary);border-radius:3px;min-width:2px}
|
| 125 |
-
.spacing-value{font-family:var(--font-mono);font-size:11px;color:var(--fg-muted)}
|
| 126 |
-
|
| 127 |
-
/* Radius */
|
| 128 |
-
.radius-row{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start}
|
| 129 |
-
.radius-item{display:flex;flex-direction:column;align-items:center;gap:8px}
|
| 130 |
-
.radius-box{width:64px;height:64px;border:2px solid var(--fg-muted);background:var(--card)}
|
| 131 |
-
.radius-label{font-family:var(--font-mono);font-size:12px;color:var(--fg)}
|
| 132 |
-
.radius-context{font-size:11px;color:var(--fg-muted)}
|
| 133 |
-
|
| 134 |
-
/* Elevation */
|
| 135 |
-
.elevation-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}
|
| 136 |
-
.elevation-card{background:var(--card);border-radius:10px;padding:24px;min-height:100px;display:flex;flex-direction:column;justify-content:flex-end}
|
| 137 |
-
.elevation-label{font-size:13px;font-weight:500;margin-bottom:4px}
|
| 138 |
-
.elevation-desc{font-size:12px;color:var(--fg-muted)}
|
| 139 |
-
|
| 140 |
-
/* Footer */
|
| 141 |
-
.footer{text-align:center;padding:48px 24px;font-size:13px;color:var(--fg-muted);border-top:1px solid var(--border)}
|
| 142 |
-
.footer a{color:var(--fg-muted);text-decoration:none;transition:color 0.2s}
|
| 143 |
-
.footer a:hover{color:var(--fg)}
|
| 144 |
-
|
| 145 |
-
/* Responsive */
|
| 146 |
-
@media(max-width:768px){
|
| 147 |
-
.nav-links{display:none}
|
| 148 |
-
.hero h1{font-size:32px}
|
| 149 |
-
.color-grid{grid-template-columns:repeat(2,1fr)}
|
| 150 |
-
.card-grid{grid-template-columns:1fr}
|
| 151 |
-
}
|
| 152 |
-
</style>
|
| 153 |
-
</head>
|
| 154 |
-
<body>
|
| 155 |
-
|
| 156 |
-
<nav class="nav">
|
| 157 |
-
<div class="nav-start">
|
| 158 |
-
<a class="nav-logo" href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank" rel="noopener noreferrer" aria-label="OpenHands">
|
| 159 |
-
<svg viewBox="0 0 599.09 99.17" fill="#fff" aria-hidden="true"><path d="M159.74,53.71c0-18.39,11.81-31.21,28.17-31.21s28.17,12.82,28.17,31.21-11.81,31.21-28.17,31.21-28.17-12.82-28.17-31.21ZM205.29,53.71c0-13.16-7.17-21.68-17.38-21.68s-17.38,8.52-17.38,21.68,7.17,21.68,17.38,21.68,17.38-8.52,17.38-21.68Z"/><path d="M246.07,84.92c-5.74,0-9.95-2.28-12.74-5.57v19.82h-10.12v-59.47h10.12v4.72c2.78-3.29,7-5.57,12.74-5.57,12.4,0,19.48,10.46,19.48,23.03s-7.09,23.03-19.48,23.03ZM233.08,60.63v2.61c0,8.18,4.72,12.82,10.97,12.82,7.34,0,11.3-5.74,11.3-14.17s-3.96-14.17-11.3-14.17c-6.24,0-10.97,4.56-10.97,12.91Z"/><path d="M291.26,84.92c-12.65,0-21.51-9.36-21.51-23.03s8.77-23.03,21.09-23.03,19.65,9.7,19.65,21.85v3.37h-31.04c.76,7.59,5.32,12.23,11.81,12.23,4.98,0,8.94-2.53,10.29-7.09l8.69,3.29c-3.12,7.76-10.12,12.4-18.98,12.4ZM290.76,47.38c-5.23,0-9.28,3.12-10.8,9.11h20.33c-.08-4.89-3.12-9.11-9.53-9.11Z"/><path d="M317.25,83.99v-44.29h10.12v4.72c2.53-2.95,6.49-5.57,12.23-5.57,9.28,0,14.85,6.41,14.85,15.94v29.19h-10.12v-26.23c0-5.48-2.19-9.45-7.76-9.45-4.55,0-9.19,3.37-9.19,9.7v25.98h-10.12Z"/><path d="M404.17,23.43h10.8v60.57h-10.8v-26.49h-29.02v26.49h-10.8V23.43h10.8v24.63h29.02v-24.63Z"/><path d="M436.93,84.75c-8.43,0-14.93-5.15-14.93-13.07,0-8.44,6.33-12.15,14.85-13.92l12.23-2.53v-.76c0-4.22-2.19-6.83-7.59-6.83-4.81,0-7.34,2.19-8.52,6.5l-9.53-2.19c2.19-7.34,8.69-13.07,18.47-13.07,10.63,0,17.04,5.06,17.04,15.27v19.06c0,2.53,1.1,3.29,3.88,2.95v7.84c-7.34.84-11.22-.59-12.74-4.22-2.78,3.12-7.42,4.98-13.16,4.98ZM449.08,68.39v-5.4l-9.53,2.02c-4.3.93-7.51,2.28-7.51,6.24,0,3.46,2.53,5.4,6.41,5.4,5.4,0,10.63-2.87,10.63-8.27Z"/><path d="M469.17,83.99v-44.29h10.12v4.72c2.53-2.95,6.49-5.57,12.23-5.57,9.28,0,14.85,6.41,14.85,15.94v29.19h-10.12v-26.23c0-5.48-2.19-9.45-7.76-9.45-4.56,0-9.2,3.37-9.2,9.7v25.98h-10.12Z"/><path d="M532.56,84.92c-12.4,0-19.48-10.46-19.48-23.03s7.08-23.03,19.48-23.03c5.74,0,9.95,2.28,12.74,5.57v-21h10.12v60.57h-10.12v-4.64c-2.78,3.29-7,5.57-12.74,5.57ZM545.55,60.63c0-8.35-4.72-12.91-10.96-12.91-7.34,0-11.3,5.74-11.3,14.17s3.96,14.17,11.3,14.17c6.24,0,10.96-4.64,10.96-12.82v-2.61Z"/><path d="M560.54,75.56l7.59-6.07c2.62,4.3,7.68,7.17,12.82,7.17,4.3,0,8.27-1.52,8.27-5.48s-3.71-4.22-10.71-5.65c-7-1.43-15.01-3.21-15.01-12.65,0-8.1,7.09-14,17.29-14,7.76,0,14.68,3.46,17.88,8.35l-6.83,6.16c-2.53-3.96-6.75-6.24-11.64-6.24-4.13,0-6.83,1.86-6.83,4.81,0,3.21,3.2,3.8,8.77,4.98,7.51,1.6,16.95,3.21,16.95,13.33,0,8.94-8.18,14.68-18.22,14.68-8.18,0-16.36-3.29-20.33-9.36Z"/><path d="M64.97,14.8V1.93c0-1.07.86-1.93,1.93-1.93s1.93.86,1.93,1.93v12.87c0,1.07-.86,1.93-1.93,1.93s-1.93-.86-1.93-1.93Z"/><path d="M74.95,16.72l6.43-11.15c.53-.92,1.71-1.24,2.64-.71.92.53,1.24,1.71.71,2.64l-6.43,11.15c-.53.92-1.71,1.24-2.64.71-.92-.53-1.24-1.71-.71-2.64Z"/><path d="M58.85,16.72l-6.43-11.15c-.53-.92-1.71-1.24-2.64-.71-.92.53-1.24,1.71-.71,2.64l6.43,11.15c.53.92,1.71,1.24,2.64.71.92-.53,1.24-1.71.71-2.64Z"/><path d="M128.77,56.65c0-3.35.9-13.3,1.19-16.58.19-2.22-.07-3.44-.43-4.06-.26-.46-.67-.78-1.66-.84-.71-.05-1.49.16-2.07.68-.54.49-1.15,1.48-1.15,3.47v.11s-.89,15.12-.89,15.12c-.03.54-.29,1.05-.72,1.39-.42.34-.97.49-1.51.4l-9.29-1.47-10.02-1.33c-.93-.12-1.63-.89-1.67-1.82l-.55-11.95v-.1c-.25-4.76-.49-9.1-.49-10.44,0-3.75-.63-5.33-1.19-5.99-.44-.53-1.08-.76-2.44-.76-.49,0-.83.1-1.09.25-.25.15-.54.41-.82.94-.59,1.12-1.02,3.22-.86,6.88.21,4.76.53,8.31.85,11.51.32,3.2.63,6.1.81,9.47.27,5.28.25,8.92.03,11.39-.11,1.23-.27,2.23-.48,3.02-.2.75-.51,1.51-1.04,2.07-.64.69-1.56,1.02-2.52.79-.76-.18-1.29-.66-1.58-.97-.61-.64-1.04-1.46-1.21-1.89-.98-2.47-4.01-8.22-8.12-11.46-1.2-.95-2.07-1.22-2.62-1.26-.52-.04-.89.11-1.19.35-.33.26-.57.63-.69.99-.04.13-.06.22-.07.27,1.11,1.88,5.53,8.77,7.61,15.76,1.55,5.21,5.29,10.52,8.09,12.8,2.71,2.2,7.57,3.57,13.05,3.84,5.42.27,11.01-.57,14.95-2.33,7.6-3.41,9.14-10.91,9.84-14.16.54-2.52.55-5.22.4-7.72-.07-1.25-.18-2.41-.27-3.49-.09-1.04-.17-2.05-.17-2.88ZM110.59,24.28c0-1.17-.31-2.21-.83-2.91-.47-.63-1.16-1.07-2.26-1.07-.91,0-1.52.11-1.94.29-.39.16-.71.42-1,.9-.68,1.1-1.18,3.3-1.18,7.69l.48,10.39c.18,3.47.37,7.22.49,10.35l6.25.83v-26.47ZM114.45,51.31l5.58.88.76-12.93v-9.97c0-1.37-.56-2.21-1.22-2.74-.74-.6-1.6-.81-2-.81-.74,0-1.5.11-2.05.5-.42.3-1.07,1.01-1.07,3.05v22.01ZM124.65,32c1.15-.58,2.39-.76,3.48-.69,1.97.13,3.71.96,4.75,2.77.95,1.65,1.15,3.83.93,6.31-.3,3.43-1.18,13.11-1.18,16.25,0,.63.06,1.47.16,2.54.09,1.05.21,2.28.28,3.6.15,2.63.16,5.72-.48,8.75-.67,3.15-2.49,12.6-12.03,16.88-4.64,2.08-10.87,2.95-16.72,2.66-5.79-.28-11.64-1.73-15.29-4.7-3.44-2.8-7.59-8.79-9.35-14.69-1.99-6.67-6.29-13.24-7.36-15.11-.63-1.1-.43-2.4-.14-3.27.33-.98.98-2,1.94-2.77,1-.79,2.32-1.29,3.88-1.18,1.53.12,3.11.81,4.72,2.08,4.14,3.27,7.18,8.43,8.67,11.59.02-.15.03-.3.05-.46.19-2.21.23-5.65-.04-10.86-.17-3.26-.47-6.05-.79-9.29-.32-3.24-.65-6.87-.87-11.72-.17-3.88.23-6.82,1.31-8.86.56-1.06,1.32-1.9,2.28-2.46.96-.56,2.01-.78,3.04-.78,1.53,0,3.43.22,4.95,1.66.13-.29.28-.56.44-.81.7-1.13,1.63-1.93,2.77-2.42,1.1-.47,2.29-.6,3.46-.6,2.36,0,4.19,1.04,5.36,2.63.76,1.03,1.22,2.23,1.44,3.46,1.25-.57,2.51-.64,3.28-.64,1.31,0,3.02.53,4.43,1.68,1.49,1.21,2.65,3.11,2.65,5.74v2.71Z"/><path d="M5.12,56.65c0-3.35-.9-13.3-1.19-16.58-.19-2.22.07-3.44.43-4.06.26-.46.67-.78,1.66-.84.71-.05,1.49.16,2.07.68.54.49,1.15,1.48,1.15,3.47v.11s.89,15.12.89,15.12c.03.54.29,1.05.72,1.39.42.34.97.49,1.51.4l9.29-1.47,10.02-1.33c.93-.12,1.63-.89,1.67-1.82l.55-11.95v-.1c.25-4.76.48-9.1.48-10.44,0-3.75.63-5.33,1.19-5.99.44-.53,1.08-.76,2.44-.76.49,0,.83.1,1.09.25.25.15.54.41.82.94.59,1.12,1.02,3.22.86,6.88-.21,4.76-.53,8.31-.85,11.51-.32,3.2-.63,6.1-.81,9.47-.27,5.28-.25,8.92-.03,11.39.11,1.23.27,2.23.48,3.02.2.75.51,1.51,1.04,2.07.65.69,1.56,1.02,2.52.79.76-.18,1.29-.66,1.58-.97.61-.64,1.04-1.46,1.21-1.89.98-2.47,4.01-8.22,8.12-11.46,1.2-.95,2.07-1.22,2.62-1.26.52-.04.89.11,1.19.35.33.26.57.63.69.99.04.13.06.22.07.27-1.11,1.88-5.53,8.77-7.61,15.76-1.55,5.21-5.29,10.52-8.09,12.8-2.71,2.2-7.57,3.57-13.05,3.84-5.43.27-11.01-.57-14.95-2.33-7.6-3.41-9.15-10.91-9.84-14.16-.54-2.52-.55-5.22-.4-7.72.07-1.25.18-2.41.27-3.49.09-1.04.17-2.05.17-2.88ZM23.29,24.28c0-1.17.31-2.21.83-2.91.47-.63,1.16-1.07,2.26-1.07.91,0,1.52.11,1.95.29.39.16.71.42,1,.9.68,1.1,1.18,3.3,1.18,7.69l-.48,10.39c-.18,3.47-.37,7.22-.49,10.35l-6.25.83v-26.47ZM19.43,51.31l-5.58.88-.76-12.93v-9.97c0-1.37.56-2.21,1.22-2.74.74-.6,1.59-.81,2-.81.74,0,1.5.11,2.05.5.42.3,1.07,1.01,1.07,3.05v22.01ZM9.24,32c-1.15-.58-2.39-.76-3.48-.69-1.97.13-3.7.96-4.75,2.77-.95,1.65-1.15,3.83-.93,6.31.3,3.43,1.18,13.11,1.18,16.25,0,.63-.07,1.47-.16,2.54-.09,1.05-.21,2.28-.28,3.6-.15,2.63-.16,5.72.48,8.75.67,3.15,2.49,12.6,12.04,16.88,4.64,2.08,10.87,2.95,16.72,2.66,5.79-.28,11.65-1.73,15.29-4.7,3.44-2.8,7.59-8.79,9.35-14.69,1.99-6.67,6.29-13.24,7.36-15.11.63-1.1.43-2.4.14-3.27-.33-.98-.98-2-1.94-2.77-1-.79-2.32-1.29-3.88-1.18-1.53.12-3.11.81-4.72,2.08-4.14,3.27-7.18,8.43-8.67,11.59-.02-.15-.03-.3-.05-.46-.19-2.21-.23-5.65.04-10.86.17-3.26.47-6.05.79-9.29.32-3.24.65-6.87.87-11.72.17-3.88-.23-6.82-1.31-8.86-.56-1.06-1.32-1.9-2.28-2.46-.96-.56-2.01-.78-3.04-.78-1.53,0-3.43.22-4.95,1.66-.13-.29-.28-.56-.44-.81-.7-1.13-1.63-1.93-2.77-2.42-1.1-.47-2.28-.6-3.46-.6-2.36,0-4.19,1.04-5.36,2.63-.76,1.03-1.22,2.23-1.44,3.46-1.25-.57-2.51-.64-3.27-.64-1.31,0-3.02.53-4.43,1.68-1.49,1.21-2.64,3.11-2.64,5.74v2.71Z"/></svg>
|
| 160 |
-
</a>
|
| 161 |
-
<a class="nav-github" href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank" rel="noopener noreferrer" aria-label="OpenHands-Design.md on GitHub">
|
| 162 |
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2 .37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
| 163 |
-
OpenHands-Design.md
|
| 164 |
-
</a>
|
| 165 |
-
</div>
|
| 166 |
-
<ul class="nav-links">
|
| 167 |
-
<li><a href="#colors">Colors</a></li>
|
| 168 |
-
<li><a href="#typography">Typography</a></li>
|
| 169 |
-
<li><a href="#buttons">Buttons</a></li>
|
| 170 |
-
<li><a href="#cards">Cards</a></li>
|
| 171 |
-
<li><a href="#forms">Forms</a></li>
|
| 172 |
-
<li><a href="#spacing">Spacing</a></li>
|
| 173 |
-
<li><a href="#radius">Radius</a></li>
|
| 174 |
-
<li><a href="#elevation">Elevation</a></li>
|
| 175 |
-
</ul>
|
| 176 |
-
</nav>
|
| 177 |
-
|
| 178 |
-
<section class="hero">
|
| 179 |
-
<h1>OpenHands<br>Design System</h1>
|
| 180 |
-
<p>A design token catalog generated from DESIGN.md. Every color, font, component, and spacing value, visualized on the near-black monochrome canvas.</p>
|
| 181 |
-
<div class="hero-buttons">
|
| 182 |
-
<a class="btn-dark" href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank">View Repository</a>
|
| 183 |
-
<a class="btn-primary" href="#colors">Explore Tokens</a>
|
| 184 |
-
</div>
|
| 185 |
-
</section>
|
| 186 |
-
|
| 187 |
-
<hr class="section-divider">
|
| 188 |
-
|
| 189 |
-
<!-- ==================== COLORS ==================== -->
|
| 190 |
-
<section class="section" id="colors">
|
| 191 |
-
<div class="section-label">01 / Colors</div>
|
| 192 |
-
<h2 class="section-title">Color Palette</h2>
|
| 193 |
-
|
| 194 |
-
<div class="color-group-label">Core Surfaces</div>
|
| 195 |
-
<div class="color-grid">
|
| 196 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#0d0d0d"></div><div class="color-swatch-info"><div class="color-swatch-name">background</div><div class="color-swatch-hex">#0d0d0d</div><div class="color-swatch-role">Page background, app shell</div></div></div>
|
| 197 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#121212"></div><div class="color-swatch-info"><div class="color-swatch-name">card</div><div class="color-swatch-hex">#121212</div><div class="color-swatch-role">Card surfaces, elevated containers</div></div></div>
|
| 198 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#141414"></div><div class="color-swatch-info"><div class="color-swatch-name">secondary</div><div class="color-swatch-hex">#141414</div><div class="color-swatch-role">Secondary surfaces, sidebar accent</div></div></div>
|
| 199 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#1f1f1f"></div><div class="color-swatch-info"><div class="color-swatch-name">muted</div><div class="color-swatch-hex">#1f1f1f</div><div class="color-swatch-role">Hover fills, badges, tooltips</div></div></div>
|
| 200 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#242424"></div><div class="color-swatch-info"><div class="color-swatch-name">border</div><div class="color-swatch-hex">#242424</div><div class="color-swatch-role">Borders, input borders, dividers</div></div></div>
|
| 201 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#2e2e2e"></div><div class="color-swatch-info"><div class="color-swatch-name">muted-hover</div><div class="color-swatch-hex">#2e2e2e</div><div class="color-swatch-role">Hover on muted surfaces</div></div></div>
|
| 202 |
-
</div>
|
| 203 |
-
|
| 204 |
-
<div class="color-group-label">Core Text</div>
|
| 205 |
-
<div class="color-grid">
|
| 206 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#fafafa"></div><div class="color-swatch-info"><div class="color-swatch-name">foreground</div><div class="color-swatch-hex">#fafafa</div><div class="color-swatch-role">Primary text, headings</div></div></div>
|
| 207 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#8c8c8c"></div><div class="color-swatch-info"><div class="color-swatch-name">muted-foreground</div><div class="color-swatch-hex">#8c8c8c</div><div class="color-swatch-role">Secondary text, labels, placeholders</div></div></div>
|
| 208 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#ffffff;border-bottom:1px solid hsl(0 0% 14%)"></div><div class="color-swatch-info"><div class="color-swatch-name">primary</div><div class="color-swatch-hex">#ffffff</div><div class="color-swatch-role">Maximum emphasis, button bg</div></div></div>
|
| 209 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#000000"></div><div class="color-swatch-info"><div class="color-swatch-name">primary-foreground</div><div class="color-swatch-hex">#000000</div><div class="color-swatch-role">Text on white buttons</div></div></div>
|
| 210 |
-
</div>
|
| 211 |
-
|
| 212 |
-
<div class="color-group-label">Semantic / Status</div>
|
| 213 |
-
<div class="color-grid">
|
| 214 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#22c55e"></div><div class="color-swatch-info"><div class="color-swatch-name">success</div><div class="color-swatch-hex">#22c55e</div><div class="color-swatch-role">Success states, running</div></div></div>
|
| 215 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#86efac"></div><div class="color-swatch-info"><div class="color-swatch-name">success-foreground</div><div class="color-swatch-hex">#86efac</div><div class="color-swatch-role">Success text on dark</div></div></div>
|
| 216 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#f59e0b"></div><div class="color-swatch-info"><div class="color-swatch-name">warning</div><div class="color-swatch-hex">#f59e0b</div><div class="color-swatch-role">Warning, caution badges</div></div></div>
|
| 217 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#3b82f6"></div><div class="color-swatch-info"><div class="color-swatch-name">info</div><div class="color-swatch-hex">#3b82f6</div><div class="color-swatch-role">Informational, links</div></div></div>
|
| 218 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#dc2626"></div><div class="color-swatch-info"><div class="color-swatch-name">destructive</div><div class="color-swatch-hex">#dc2626</div><div class="color-swatch-role">Error, danger, delete</div></div></div>
|
| 219 |
-
<div class="color-swatch"><div class="color-swatch-block" style="background:#cccccc"></div><div class="color-swatch-info"><div class="color-swatch-name">ring</div><div class="color-swatch-hex">#cccccc</div><div class="color-swatch-role">Focus rings (1px, keyboard-only)</div></div></div>
|
| 220 |
-
</div>
|
| 221 |
-
|
| 222 |
-
<div class="color-group-label">Surface Scale (5% → 18% lightness)</div>
|
| 223 |
-
<div style="display:flex;border-radius:10px;overflow:hidden;height:64px;border:1px solid var(--border);margin-bottom:24px">
|
| 224 |
-
<div style="flex:1;background:#0d0d0d" title="5% — background"></div>
|
| 225 |
-
<div style="flex:1;background:#121212" title="7% — card"></div>
|
| 226 |
-
<div style="flex:1;background:#141414" title="8% — secondary"></div>
|
| 227 |
-
<div style="flex:1;background:#1f1f1f" title="12% — muted"></div>
|
| 228 |
-
<div style="flex:1;background:#242424" title="14% — border"></div>
|
| 229 |
-
<div style="flex:1;background:#2e2e2e" title="18% — muted-hover"></div>
|
| 230 |
-
</div>
|
| 231 |
-
<div style="display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:11px;color:var(--fg-muted);margin-bottom:8px">
|
| 232 |
-
<span>5%</span><span>7%</span><span>8%</span><span>12%</span><span>14%</span><span>18%</span>
|
| 233 |
-
</div>
|
| 234 |
-
</section>
|
| 235 |
-
|
| 236 |
-
<hr class="section-divider">
|
| 237 |
-
|
| 238 |
-
<!-- ==================== TYPOGRAPHY ==================== -->
|
| 239 |
-
<section class="section" id="typography">
|
| 240 |
-
<div class="section-label">02 / Typography</div>
|
| 241 |
-
<h2 class="section-title">Typography Scale</h2>
|
| 242 |
-
|
| 243 |
-
<div class="type-sample"><div style="font-size:30px;font-weight:600;line-height:1.2;letter-spacing:-0.5px">Hero Heading (text-3xl)</div><div class="type-meta">30px / 600 / 1.2 / -0.5px / Inter</div></div>
|
| 244 |
-
<div class="type-sample"><div style="font-size:24px;font-weight:600;line-height:1.25;letter-spacing:-0.3px">Page Heading (text-2xl)</div><div class="type-meta">24px / 600 / 1.25 / -0.3px / Inter</div></div>
|
| 245 |
-
<div class="type-sample"><div style="font-size:20px;font-weight:600;line-height:1.3">Sub-heading (text-xl)</div><div class="type-meta">20px / 600 / 1.3 / Inter</div></div>
|
| 246 |
-
<div class="type-sample"><div style="font-size:18px;font-weight:600;line-height:1.4">Section Title (text-lg)</div><div class="type-meta">18px / 600 / 1.4 / Inter</div></div>
|
| 247 |
-
<div class="type-sample"><div style="font-size:16px;font-weight:400;line-height:1.5">Body Large — Larger body text for chat messages and hero content. (text-base)</div><div class="type-meta">16px / 400 / 1.5 / Inter</div></div>
|
| 248 |
-
<div class="type-sample"><div style="font-size:14px;font-weight:400;line-height:1.5">Body — Standard UI text for labels, descriptions, and interface elements. (text-sm)</div><div class="type-meta">14px / 400 / 1.5 / Inter — primary body size (711 uses)</div></div>
|
| 249 |
-
<div class="type-sample"><div style="font-size:14px;font-weight:500;line-height:1.5">Label — Form labels, nav items, and badges. (text-sm font-medium)</div><div class="type-meta">14px / 500 / 1.5 / Inter — label weight (304 uses)</div></div>
|
| 250 |
-
<div class="type-sample"><div style="font-size:12px;font-weight:400;line-height:1.5;color:var(--fg-muted)">Secondary — Metadata, captions, and small labels. (text-xs)</div><div class="type-meta">12px / 400 / 1.5 / Inter — secondary size (427 uses)</div></div>
|
| 251 |
-
<div class="type-sample"><div style="font-family:var(--font-mono);font-size:14px;font-weight:400;line-height:1.6">const design = await openHands.init({ tokens: true });</div><div class="type-meta">14px / 400 / 1.6 / JetBrains Mono — code / technical text</div></div>
|
| 252 |
-
<div class="type-sample"><div style="font-size:11px;font-weight:500;line-height:1.27;text-transform:uppercase;letter-spacing:1.5px;color:var(--fg-muted)">SYSTEM CATEGORY</div><div class="type-meta">11px / 500 / uppercase / tracking-wide — section labels</div></div>
|
| 253 |
-
</section>
|
| 254 |
-
|
| 255 |
-
<hr class="section-divider">
|
| 256 |
-
|
| 257 |
-
<!-- ==================== BUTTONS ==================== -->
|
| 258 |
-
<section class="section" id="buttons">
|
| 259 |
-
<div class="section-label">03 / Buttons</div>
|
| 260 |
-
<h2 class="section-title">Button Variants</h2>
|
| 261 |
-
<div class="button-row">
|
| 262 |
-
<div class="button-item"><a class="btn-dark" href="#">Save Changes</a><div class="button-label">default</div></div>
|
| 263 |
-
<div class="button-item"><a class="btn-destructive" href="#">Delete</a><div class="button-label">destructive</div></div>
|
| 264 |
-
<div class="button-item"><a class="btn-outline" href="#">Cancel</a><div class="button-label">outline</div></div>
|
| 265 |
-
<div class="button-item"><a class="btn-primary" href="#">Settings</a><div class="button-label">secondary</div></div>
|
| 266 |
-
<div class="button-item"><a class="btn-muted" href="#">Archive</a><div class="button-label">muted</div></div>
|
| 267 |
-
<div class="button-item"><a class="btn-ghost" href="#">Learn More</a><div class="button-label">ghost</div></div>
|
| 268 |
-
<div class="button-item"><a href="#" style="font-size:14px;color:var(--primary);text-decoration:none;text-underline-offset:4px" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">View docs</a><div class="button-label">link</div></div>
|
| 269 |
-
</div>
|
| 270 |
-
|
| 271 |
-
<div style="margin-top:40px">
|
| 272 |
-
<div class="color-group-label">Status Pills</div>
|
| 273 |
-
<div class="button-row">
|
| 274 |
-
<div class="button-item"><span style="display:inline-block;background:hsl(142 71% 45% / 0.1);color:var(--success-fg);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Success</span><div class="button-label">success</div></div>
|
| 275 |
-
<div class="button-item"><span style="display:inline-block;background:hsl(38 92% 50% / 0.15);color:var(--warning);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Warning</span><div class="button-label">warning</div></div>
|
| 276 |
-
<div class="button-item"><span style="display:inline-block;background:hsl(217 91% 60% / 0.15);color:var(--info);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Info</span><div class="button-label">info</div></div>
|
| 277 |
-
<div class="button-item"><span style="display:inline-block;background:hsl(0 72% 51% / 0.1);color:var(--destructive);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Error</span><div class="button-label">destructive</div></div>
|
| 278 |
-
</div>
|
| 279 |
-
</div>
|
| 280 |
-
</section>
|
| 281 |
-
|
| 282 |
-
<hr class="section-divider">
|
| 283 |
-
|
| 284 |
-
<!-- ==================== CARDS ==================== -->
|
| 285 |
-
<section class="section" id="cards">
|
| 286 |
-
<div class="section-label">04 / Cards</div>
|
| 287 |
-
<h2 class="section-title">Card Examples</h2>
|
| 288 |
-
<div class="card-grid">
|
| 289 |
-
<div class="card">
|
| 290 |
-
<div class="card-badge" style="background:hsl(142 71% 45% / 0.1);color:var(--success-fg)">Standard</div>
|
| 291 |
-
<h3>Standard Card</h3>
|
| 292 |
-
<p>bg-card border border-border rounded-lg p-4. The workhorse container for settings panels, content sections, and list items.</p>
|
| 293 |
-
</div>
|
| 294 |
-
<div class="card" style="box-shadow:var(--shadow-card);border-radius:12px;padding:24px">
|
| 295 |
-
<div class="card-badge" style="background:hsl(217 91% 60% / 0.15);color:var(--info)">Elevated</div>
|
| 296 |
-
<h3>Elevated Card</h3>
|
| 297 |
-
<p>bg-card border border-border rounded-xl p-6 shadow-lg. For modals, dialogs, and featured content that needs to float above the surface.</p>
|
| 298 |
-
</div>
|
| 299 |
-
<div class="card" style="border-color:var(--border-hover)">
|
| 300 |
-
<div class="card-badge" style="background:hsl(38 92% 50% / 0.15);color:var(--warning)">Interactive</div>
|
| 301 |
-
<h3>Interactive Card</h3>
|
| 302 |
-
<p>hover:border-white/30. Cards that respond to hover with a subtle border brightening to indicate they are clickable.</p>
|
| 303 |
-
</div>
|
| 304 |
-
</div>
|
| 305 |
-
</section>
|
| 306 |
-
|
| 307 |
-
<hr class="section-divider">
|
| 308 |
-
|
| 309 |
-
<!-- ==================== FORMS ==================== -->
|
| 310 |
-
<section class="section" id="forms">
|
| 311 |
-
<div class="section-label">05 / Forms</div>
|
| 312 |
-
<h2 class="section-title">Form Elements</h2>
|
| 313 |
-
<div class="form-group"><label class="form-label">Project Name</label><input class="form-input" type="text" placeholder="my-openhands-project"><div class="form-state-label">Default (border-border, bg-muted/40)</div></div>
|
| 314 |
-
<div class="form-group"><label class="form-label">Repository</label><input class="form-input form-input--focus" type="text" value="openhands/agent"><div class="form-state-label">Focus (ring-1, ring-ring, bg-muted/60)</div></div>
|
| 315 |
-
<div class="form-group"><label class="form-label">API Key</label><input class="form-input form-input--error" type="text" value="invalid-key-123"><div class="form-state-label">Error (border-destructive)</div></div>
|
| 316 |
-
<div class="form-group"><label class="form-label">Instructions</label><textarea class="form-textarea" placeholder="Describe the task for the agent..."></textarea></div>
|
| 317 |
-
<div class="form-group">
|
| 318 |
-
<label class="form-label">Framework</label>
|
| 319 |
-
<div style="position:relative">
|
| 320 |
-
<select class="form-input" style="appearance:none;padding-right:36px;cursor:pointer">
|
| 321 |
-
<option>React + Tailwind</option>
|
| 322 |
-
<option>Next.js</option>
|
| 323 |
-
<option>Vue</option>
|
| 324 |
-
</select>
|
| 325 |
-
<svg style="position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
|
| 326 |
-
</div>
|
| 327 |
-
<div class="form-state-label">NativeSelect (appearance: none, chevron overlay)</div>
|
| 328 |
-
</div>
|
| 329 |
-
</section>
|
| 330 |
-
|
| 331 |
-
<hr class="section-divider">
|
| 332 |
-
|
| 333 |
-
<!-- ==================== SPACING ==================== -->
|
| 334 |
-
<section class="section" id="spacing">
|
| 335 |
-
<div class="section-label">06 / Spacing</div>
|
| 336 |
-
<h2 class="section-title">Spacing Scale</h2>
|
| 337 |
-
<div class="spacing-row">
|
| 338 |
-
<div class="spacing-item"><div class="spacing-block" style="width:4px"></div><div class="spacing-value">4 (gap-1)</div></div>
|
| 339 |
-
<div class="spacing-item"><div class="spacing-block" style="width:6px"></div><div class="spacing-value">6 (gap-1.5)</div></div>
|
| 340 |
-
<div class="spacing-item"><div class="spacing-block" style="width:8px"></div><div class="spacing-value">8 (gap-2)</div></div>
|
| 341 |
-
<div class="spacing-item"><div class="spacing-block" style="width:12px"></div><div class="spacing-value">12 (gap-3)</div></div>
|
| 342 |
-
<div class="spacing-item"><div class="spacing-block" style="width:16px"></div><div class="spacing-value">16 (gap-4)</div></div>
|
| 343 |
-
<div class="spacing-item"><div class="spacing-block" style="width:24px"></div><div class="spacing-value">24 (gap-6)</div></div>
|
| 344 |
-
<div class="spacing-item"><div class="spacing-block" style="width:32px"></div><div class="spacing-value">32 (gap-8)</div></div>
|
| 345 |
-
<div class="spacing-item"><div class="spacing-block" style="width:48px"></div><div class="spacing-value">48 (gap-12)</div></div>
|
| 346 |
-
</div>
|
| 347 |
-
|
| 348 |
-
<div style="margin-top:40px">
|
| 349 |
-
<div class="color-group-label">Padding Patterns</div>
|
| 350 |
-
<div style="display:flex;flex-wrap:wrap;gap:16px;margin-top:12px">
|
| 351 |
-
<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:12px;color:var(--fg-muted)">px-3 py-2 <span style="color:var(--fg)">compact</span></div>
|
| 352 |
-
<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 16px;font-size:12px;color:var(--fg-muted)">px-4 py-2 <span style="color:var(--fg)">standard</span></div>
|
| 353 |
-
<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:16px;font-size:12px;color:var(--fg-muted)">p-4 <span style="color:var(--fg)">card</span></div>
|
| 354 |
-
<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:24px;font-size:12px;color:var(--fg-muted)">p-6 <span style="color:var(--fg)">dialog</span></div>
|
| 355 |
-
</div>
|
| 356 |
-
</div>
|
| 357 |
-
</section>
|
| 358 |
-
|
| 359 |
-
<hr class="section-divider">
|
| 360 |
-
|
| 361 |
-
<!-- ==================== RADIUS ==================== -->
|
| 362 |
-
<section class="section" id="radius">
|
| 363 |
-
<div class="section-label">07 / Radius</div>
|
| 364 |
-
<h2 class="section-title">Border Radius Scale</h2>
|
| 365 |
-
<div class="radius-row">
|
| 366 |
-
<div class="radius-item"><div class="radius-box" style="border-radius:2px"></div><div class="radius-label">2px</div><div class="radius-context">rounded-sm</div></div>
|
| 367 |
-
<div class="radius-item"><div class="radius-box" style="border-radius:4px"></div><div class="radius-label">4px</div><div class="radius-context">rounded-md</div></div>
|
| 368 |
-
<div class="radius-item"><div class="radius-box" style="border-radius:6px"></div><div class="radius-label">6px</div><div class="radius-context">rounded-lg</div></div>
|
| 369 |
-
<div class="radius-item"><div class="radius-box" style="border-radius:12px"></div><div class="radius-label">12px</div><div class="radius-context">rounded-modal</div></div>
|
| 370 |
-
<div class="radius-item"><div class="radius-box" style="border-radius:16px"></div><div class="radius-label">16px</div><div class="radius-context">rounded-2xl</div></div>
|
| 371 |
-
<div class="radius-item"><div class="radius-box" style="border-radius:9999px"></div><div class="radius-label">9999px</div><div class="radius-context">rounded-full</div></div>
|
| 372 |
-
</div>
|
| 373 |
-
</section>
|
| 374 |
-
|
| 375 |
-
<hr class="section-divider">
|
| 376 |
-
|
| 377 |
-
<!-- ==================== ELEVATION ==================== -->
|
| 378 |
-
<section class="section" id="elevation">
|
| 379 |
-
<div class="section-label">08 / Elevation</div>
|
| 380 |
-
<h2 class="section-title">Elevation & Depth</h2>
|
| 381 |
-
<div class="elevation-grid">
|
| 382 |
-
<div class="elevation-card" style="border:1px solid var(--border)"><div class="elevation-label">Level 0: Flat</div><div class="elevation-desc">No shadow, bg-background</div></div>
|
| 383 |
-
<div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 1px 2px 0 hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 1: Surface</div><div class="elevation-desc">shadow-card + border</div></div>
|
| 384 |
-
<div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 4px 6px -1px hsl(0 0% 0% / 0.3),0 2px 4px -2px hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 2: Raised</div><div class="elevation-desc">shadow-md — dropdowns, popovers</div></div>
|
| 385 |
-
<div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 10px 15px -3px hsl(0 0% 0% / 0.3),0 4px 6px -4px hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 3: Floating</div><div class="elevation-desc">shadow-lg — modals, dialogs</div></div>
|
| 386 |
-
<div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 20px 25px -5px hsl(0 0% 0% / 0.3),0 8px 10px -6px hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 4: Overlay</div><div class="elevation-desc">shadow-xl — full-screen overlays</div></div>
|
| 387 |
-
<div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 0 0 1px var(--ring),0 0 0 3px var(--bg)"><div class="elevation-label">Focus Ring</div><div class="elevation-desc">ring-1 ring-ring ring-offset-2</div></div>
|
| 388 |
-
</div>
|
| 389 |
-
</section>
|
| 390 |
-
|
| 391 |
-
<footer class="footer">
|
| 392 |
-
<a href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank" rel="noopener noreferrer">OpenHands-Design.md</a> — Design tokens for the OpenHands UI
|
| 393 |
-
</footer>
|
| 394 |
-
|
| 395 |
-
</body>
|
| 396 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/src/components/ui/button.tsx
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
import * as React from 'react';
|
| 2 |
-
import { Slot } from '@radix-ui/react-slot';
|
| 3 |
-
import { cva, type VariantProps } from 'class-variance-authority';
|
| 4 |
-
|
| 5 |
-
import { cn } from '../../lib/utils';
|
| 6 |
-
|
| 7 |
-
const buttonVariants = cva(
|
| 8 |
-
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-normal ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
| 9 |
-
{
|
| 10 |
-
variants: {
|
| 11 |
-
variant: {
|
| 12 |
-
default: 'bg-primary text-primary-foreground hover:bg-primary/85',
|
| 13 |
-
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85',
|
| 14 |
-
outline: 'border border-input bg-background hover:bg-muted hover:text-foreground',
|
| 15 |
-
light: 'border border-input bg-primary text-primary-foreground hover:bg-primary/85',
|
| 16 |
-
secondary: 'bg-secondary text-secondary-foreground hover:bg-muted-hover',
|
| 17 |
-
muted: 'bg-muted text-muted-foreground hover:bg-muted-hover hover:text-foreground',
|
| 18 |
-
ghost: 'hover:bg-muted hover:text-foreground',
|
| 19 |
-
link: 'text-primary underline-offset-4 hover:underline',
|
| 20 |
-
},
|
| 21 |
-
size: {
|
| 22 |
-
default: 'h-10 px-4 py-2',
|
| 23 |
-
sm: 'h-10 rounded-md px-3',
|
| 24 |
-
xs: 'h-10 rounded-md px-3 text-xs',
|
| 25 |
-
lg: 'h-10 rounded-md px-8',
|
| 26 |
-
icon: 'h-10 w-10',
|
| 27 |
-
},
|
| 28 |
-
},
|
| 29 |
-
defaultVariants: {
|
| 30 |
-
variant: 'default',
|
| 31 |
-
size: 'default',
|
| 32 |
-
},
|
| 33 |
-
}
|
| 34 |
-
);
|
| 35 |
-
|
| 36 |
-
export interface ButtonProps
|
| 37 |
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
| 38 |
-
VariantProps<typeof buttonVariants> {
|
| 39 |
-
asChild?: boolean;
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 43 |
-
({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 44 |
-
const Comp = asChild ? Slot : 'button';
|
| 45 |
-
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
| 46 |
-
}
|
| 47 |
-
);
|
| 48 |
-
Button.displayName = 'Button';
|
| 49 |
-
|
| 50 |
-
export { Button, buttonVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/src/components/ui/input.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
import * as React from 'react';
|
| 2 |
-
|
| 3 |
-
import { cn } from '../../lib/utils';
|
| 4 |
-
|
| 5 |
-
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
| 6 |
-
({ className, type, ...props }, ref) => {
|
| 7 |
-
return (
|
| 8 |
-
<input
|
| 9 |
-
type={type}
|
| 10 |
-
className={cn(
|
| 11 |
-
'flex h-10 w-full rounded-md border border-border bg-muted/40 px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:bg-muted/60 hover:bg-muted/60 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted/30 md:text-sm',
|
| 12 |
-
className
|
| 13 |
-
)}
|
| 14 |
-
ref={ref}
|
| 15 |
-
{...props}
|
| 16 |
-
/>
|
| 17 |
-
);
|
| 18 |
-
}
|
| 19 |
-
);
|
| 20 |
-
Input.displayName = 'Input';
|
| 21 |
-
|
| 22 |
-
export { Input };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/src/components/ui/native-select.tsx
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
import * as React from 'react';
|
| 2 |
-
import { ChevronDown } from 'lucide-react';
|
| 3 |
-
|
| 4 |
-
import { cn } from '../../lib/utils';
|
| 5 |
-
|
| 6 |
-
const nativeSelectClassName =
|
| 7 |
-
'h-10 w-full appearance-none rounded-md border border-border bg-muted/40 py-2 pl-3 pr-10 text-sm text-foreground ring-offset-background hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:bg-muted/30 disabled:opacity-50';
|
| 8 |
-
|
| 9 |
-
export type NativeSelectProps = React.ComponentPropsWithoutRef<'select'> & {
|
| 10 |
-
wrapperClassName?: string;
|
| 11 |
-
};
|
| 12 |
-
|
| 13 |
-
export const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
|
| 14 |
-
({ className, wrapperClassName, children, ...props }, ref) => (
|
| 15 |
-
<div className={cn('relative w-full', wrapperClassName)}>
|
| 16 |
-
<select ref={ref} className={cn(nativeSelectClassName, className)} {...props}>
|
| 17 |
-
{children}
|
| 18 |
-
</select>
|
| 19 |
-
<ChevronDown
|
| 20 |
-
className="pointer-events-none absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
|
| 21 |
-
aria-hidden
|
| 22 |
-
/>
|
| 23 |
-
</div>
|
| 24 |
-
)
|
| 25 |
-
);
|
| 26 |
-
NativeSelect.displayName = 'NativeSelect';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/src/components/ui/search-input.tsx
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
import * as React from 'react';
|
| 2 |
-
import { Search, XCircle } from 'lucide-react';
|
| 3 |
-
import { Input } from './input';
|
| 4 |
-
import { cn } from '../../lib/utils';
|
| 5 |
-
|
| 6 |
-
type InputProps = React.ComponentProps<typeof Input>;
|
| 7 |
-
export type SearchInputProps = Omit<InputProps, 'type' | 'size'> & {
|
| 8 |
-
value: string;
|
| 9 |
-
onValueChange: (value: string) => void;
|
| 10 |
-
/** Size: sm (h-9), default (h-10), lg (h-11) */
|
| 11 |
-
size?: 'sm' | 'default' | 'lg';
|
| 12 |
-
};
|
| 13 |
-
|
| 14 |
-
const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
| 15 |
-
(
|
| 16 |
-
{
|
| 17 |
-
value,
|
| 18 |
-
onValueChange,
|
| 19 |
-
placeholder,
|
| 20 |
-
'aria-label': ariaLabel,
|
| 21 |
-
className,
|
| 22 |
-
size = 'default',
|
| 23 |
-
...props
|
| 24 |
-
},
|
| 25 |
-
ref
|
| 26 |
-
) => {
|
| 27 |
-
const sizeClasses = {
|
| 28 |
-
sm: 'h-9 pl-9 pr-9',
|
| 29 |
-
default: 'h-10 pl-10 pr-10',
|
| 30 |
-
lg: 'h-11 pl-11 pr-11 text-base',
|
| 31 |
-
};
|
| 32 |
-
const iconSizes = {
|
| 33 |
-
sm: 'h-4 w-4',
|
| 34 |
-
default: 'h-4 w-4',
|
| 35 |
-
lg: 'h-5 w-5',
|
| 36 |
-
};
|
| 37 |
-
const hasValue = value.length > 0;
|
| 38 |
-
|
| 39 |
-
return (
|
| 40 |
-
<div className={cn('relative w-full', className)}>
|
| 41 |
-
<Search
|
| 42 |
-
className={cn(
|
| 43 |
-
'absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none',
|
| 44 |
-
iconSizes[size]
|
| 45 |
-
)}
|
| 46 |
-
aria-hidden
|
| 47 |
-
/>
|
| 48 |
-
<Input
|
| 49 |
-
ref={ref}
|
| 50 |
-
type="search"
|
| 51 |
-
value={value}
|
| 52 |
-
onChange={(e) => onValueChange(e.target.value)}
|
| 53 |
-
placeholder={placeholder}
|
| 54 |
-
aria-label={ariaLabel}
|
| 55 |
-
className={cn(sizeClasses[size])}
|
| 56 |
-
{...props}
|
| 57 |
-
/>
|
| 58 |
-
{hasValue && (
|
| 59 |
-
<button
|
| 60 |
-
type="button"
|
| 61 |
-
onClick={() => onValueChange('')}
|
| 62 |
-
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-1 text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2"
|
| 63 |
-
aria-label="Clear search"
|
| 64 |
-
>
|
| 65 |
-
<XCircle className={cn(iconSizes[size])} strokeWidth={2} />
|
| 66 |
-
</button>
|
| 67 |
-
)}
|
| 68 |
-
</div>
|
| 69 |
-
);
|
| 70 |
-
}
|
| 71 |
-
);
|
| 72 |
-
SearchInput.displayName = 'SearchInput';
|
| 73 |
-
|
| 74 |
-
export { SearchInput };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/src/globals.css
DELETED
|
@@ -1,135 +0,0 @@
|
|
| 1 |
-
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
|
| 2 |
-
@tailwind base;
|
| 3 |
-
@tailwind components;
|
| 4 |
-
@tailwind utilities;
|
| 5 |
-
|
| 6 |
-
@layer base {
|
| 7 |
-
:root {
|
| 8 |
-
--background: 0 0% 5%;
|
| 9 |
-
--modal-background: var(--background);
|
| 10 |
-
--foreground: 0 0% 98%;
|
| 11 |
-
|
| 12 |
-
--card: 0 0% 7%;
|
| 13 |
-
--card-foreground: 0 0% 98%;
|
| 14 |
-
|
| 15 |
-
--popover: 0 0% 7%;
|
| 16 |
-
--popover-foreground: 0 0% 98%;
|
| 17 |
-
|
| 18 |
-
--primary: 0 0% 100%;
|
| 19 |
-
--primary-foreground: 0 0% 0%;
|
| 20 |
-
|
| 21 |
-
--secondary: 0 0% 8%;
|
| 22 |
-
--secondary-foreground: 0 0% 98%;
|
| 23 |
-
|
| 24 |
-
--muted: 0 0% 12%;
|
| 25 |
-
--muted-foreground: 0 0% 55%;
|
| 26 |
-
--muted-hover: 0 0% 18%;
|
| 27 |
-
|
| 28 |
-
--accent: 0 0% 100%;
|
| 29 |
-
--accent-foreground: 0 0% 0%;
|
| 30 |
-
|
| 31 |
-
--destructive: 0 72% 51%;
|
| 32 |
-
--destructive-foreground: 0 0% 98%;
|
| 33 |
-
|
| 34 |
-
--border: 0 0% 14%;
|
| 35 |
-
--input: 0 0% 14%;
|
| 36 |
-
--ring: 0 0% 80%;
|
| 37 |
-
|
| 38 |
-
--radius: 0.375rem;
|
| 39 |
-
--radius-sm: 0.25rem;
|
| 40 |
-
--radius-modal: 0.75rem;
|
| 41 |
-
|
| 42 |
-
--success: 142 71% 45%;
|
| 43 |
-
--success-foreground: 142 71% 76%;
|
| 44 |
-
--warning: 38 92% 50%;
|
| 45 |
-
--info: 217 91% 60%;
|
| 46 |
-
--gradient-card-hover: linear-gradient(180deg, hsl(0 0% 9%) 0%, hsl(0 0% 7%) 100%);
|
| 47 |
-
|
| 48 |
-
--shadow-card: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
|
| 49 |
-
|
| 50 |
-
--font-sans: 'Inter', system-ui, sans-serif;
|
| 51 |
-
--font-mono: 'JetBrains Mono', monospace;
|
| 52 |
-
|
| 53 |
-
--sidebar-background: 0 0% 5%;
|
| 54 |
-
--sidebar-foreground: 0 0% 98%;
|
| 55 |
-
--sidebar-primary: 0 0% 100%;
|
| 56 |
-
--sidebar-primary-foreground: 0 0% 0%;
|
| 57 |
-
--sidebar-accent: 0 0% 8%;
|
| 58 |
-
--sidebar-accent-foreground: 0 0% 98%;
|
| 59 |
-
--sidebar-border: 0 0% 14%;
|
| 60 |
-
--sidebar-ring: 0 0% 50%;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
.dark {
|
| 64 |
-
--background: 0 0% 5%;
|
| 65 |
-
--modal-background: var(--background);
|
| 66 |
-
--foreground: 0 0% 98%;
|
| 67 |
-
|
| 68 |
-
--card: 0 0% 7%;
|
| 69 |
-
--card-foreground: 0 0% 98%;
|
| 70 |
-
|
| 71 |
-
--popover: 0 0% 7%;
|
| 72 |
-
--popover-foreground: 0 0% 98%;
|
| 73 |
-
|
| 74 |
-
--primary: 0 0% 100%;
|
| 75 |
-
--primary-foreground: 0 0% 0%;
|
| 76 |
-
|
| 77 |
-
--secondary: 0 0% 8%;
|
| 78 |
-
--secondary-foreground: 0 0% 98%;
|
| 79 |
-
|
| 80 |
-
--muted: 0 0% 12%;
|
| 81 |
-
--muted-foreground: 0 0% 55%;
|
| 82 |
-
--muted-hover: 0 0% 18%;
|
| 83 |
-
|
| 84 |
-
--accent: 0 0% 100%;
|
| 85 |
-
--accent-foreground: 0 0% 0%;
|
| 86 |
-
|
| 87 |
-
--destructive: 0 72% 51%;
|
| 88 |
-
--destructive-foreground: 0 0% 98%;
|
| 89 |
-
|
| 90 |
-
--border: 0 0% 14%;
|
| 91 |
-
--input: 0 0% 14%;
|
| 92 |
-
--ring: 0 0% 80%;
|
| 93 |
-
|
| 94 |
-
--radius: 0.375rem;
|
| 95 |
-
--radius-sm: 0.25rem;
|
| 96 |
-
--radius-modal: 0.75rem;
|
| 97 |
-
|
| 98 |
-
--success: 142 71% 45%;
|
| 99 |
-
--success-foreground: 142 71% 76%;
|
| 100 |
-
--warning: 38 92% 50%;
|
| 101 |
-
--info: 217 91% 60%;
|
| 102 |
-
--gradient-card-hover: linear-gradient(180deg, hsl(0 0% 9%) 0%, hsl(0 0% 7%) 100%);
|
| 103 |
-
|
| 104 |
-
--shadow-card: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
|
| 105 |
-
|
| 106 |
-
--font-sans: 'Inter', system-ui, sans-serif;
|
| 107 |
-
--font-mono: 'JetBrains Mono', monospace;
|
| 108 |
-
|
| 109 |
-
--sidebar-background: 0 0% 5%;
|
| 110 |
-
--sidebar-foreground: 0 0% 98%;
|
| 111 |
-
--sidebar-primary: 0 0% 100%;
|
| 112 |
-
--sidebar-primary-foreground: 0 0% 0%;
|
| 113 |
-
--sidebar-accent: 0 0% 8%;
|
| 114 |
-
--sidebar-accent-foreground: 0 0% 98%;
|
| 115 |
-
--sidebar-border: 0 0% 14%;
|
| 116 |
-
--sidebar-ring: 0 0% 50%;
|
| 117 |
-
}
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
/* Strip native search chrome so focus ring matches all other inputs */
|
| 121 |
-
input[type="search"] {
|
| 122 |
-
-webkit-appearance: none;
|
| 123 |
-
appearance: none;
|
| 124 |
-
}
|
| 125 |
-
input[type="search"]::-webkit-search-cancel-button {
|
| 126 |
-
-webkit-appearance: none;
|
| 127 |
-
appearance: none;
|
| 128 |
-
display: none;
|
| 129 |
-
}
|
| 130 |
-
input[type="search"]::-webkit-search-decoration,
|
| 131 |
-
input[type="search"]::-webkit-search-results-button,
|
| 132 |
-
input[type="search"]::-webkit-search-results-decoration {
|
| 133 |
-
-webkit-appearance: none;
|
| 134 |
-
appearance: none;
|
| 135 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenHands-Design/tailwind.config.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
/** @type {import('tailwindcss').Config} */
|
| 2 |
-
export default {
|
| 3 |
-
darkMode: ["class"],
|
| 4 |
-
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
| 5 |
-
theme: {
|
| 6 |
-
container: {
|
| 7 |
-
center: true,
|
| 8 |
-
padding: "2rem",
|
| 9 |
-
screens: {
|
| 10 |
-
"2xl": "1400px",
|
| 11 |
-
},
|
| 12 |
-
},
|
| 13 |
-
extend: {
|
| 14 |
-
fontFamily: {
|
| 15 |
-
sans: ["Inter", "system-ui", "sans-serif"],
|
| 16 |
-
mono: ["JetBrains Mono", "monospace"],
|
| 17 |
-
},
|
| 18 |
-
colors: {
|
| 19 |
-
border: "hsl(var(--border))",
|
| 20 |
-
input: "hsl(var(--input))",
|
| 21 |
-
ring: "hsl(var(--ring))",
|
| 22 |
-
background: "hsl(var(--background))",
|
| 23 |
-
modal: "hsl(var(--modal-background))",
|
| 24 |
-
foreground: "hsl(var(--foreground))",
|
| 25 |
-
primary: {
|
| 26 |
-
DEFAULT: "hsl(var(--primary))",
|
| 27 |
-
foreground: "hsl(var(--primary-foreground))",
|
| 28 |
-
},
|
| 29 |
-
secondary: {
|
| 30 |
-
DEFAULT: "hsl(var(--secondary))",
|
| 31 |
-
foreground: "hsl(var(--secondary-foreground))",
|
| 32 |
-
},
|
| 33 |
-
destructive: {
|
| 34 |
-
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
| 35 |
-
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
|
| 36 |
-
},
|
| 37 |
-
muted: {
|
| 38 |
-
DEFAULT: "hsl(var(--muted))",
|
| 39 |
-
foreground: "hsl(var(--muted-foreground))",
|
| 40 |
-
hover: "hsl(var(--muted-hover))",
|
| 41 |
-
},
|
| 42 |
-
accent: {
|
| 43 |
-
DEFAULT: "hsl(var(--accent))",
|
| 44 |
-
foreground: "hsl(var(--accent-foreground))",
|
| 45 |
-
},
|
| 46 |
-
popover: {
|
| 47 |
-
DEFAULT: "hsl(var(--popover))",
|
| 48 |
-
foreground: "hsl(var(--popover-foreground))",
|
| 49 |
-
},
|
| 50 |
-
card: {
|
| 51 |
-
DEFAULT: "hsl(var(--card))",
|
| 52 |
-
foreground: "hsl(var(--card-foreground))",
|
| 53 |
-
},
|
| 54 |
-
sidebar: {
|
| 55 |
-
DEFAULT: "hsl(var(--sidebar-background))",
|
| 56 |
-
foreground: "hsl(var(--sidebar-foreground))",
|
| 57 |
-
primary: "hsl(var(--sidebar-primary))",
|
| 58 |
-
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
| 59 |
-
accent: "hsl(var(--sidebar-accent))",
|
| 60 |
-
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
| 61 |
-
border: "hsl(var(--sidebar-border))",
|
| 62 |
-
ring: "hsl(var(--sidebar-ring))",
|
| 63 |
-
},
|
| 64 |
-
success: {
|
| 65 |
-
DEFAULT: "hsl(var(--success) / <alpha-value>)",
|
| 66 |
-
foreground: "hsl(var(--success-foreground) / <alpha-value>)",
|
| 67 |
-
},
|
| 68 |
-
warning: "hsl(var(--warning) / <alpha-value>)",
|
| 69 |
-
info: "hsl(var(--info) / <alpha-value>)",
|
| 70 |
-
},
|
| 71 |
-
borderRadius: {
|
| 72 |
-
modal: "var(--radius-modal)",
|
| 73 |
-
lg: "var(--radius)",
|
| 74 |
-
md: "calc(var(--radius) - 2px)",
|
| 75 |
-
sm: "calc(var(--radius) - 4px)",
|
| 76 |
-
},
|
| 77 |
-
keyframes: {
|
| 78 |
-
"accordion-down": {
|
| 79 |
-
from: { height: "0" },
|
| 80 |
-
to: { height: "var(--radix-accordion-content-height)" },
|
| 81 |
-
},
|
| 82 |
-
"accordion-up": {
|
| 83 |
-
from: { height: "var(--radix-accordion-content-height)" },
|
| 84 |
-
to: { height: "0" },
|
| 85 |
-
},
|
| 86 |
-
"pulse-glow": {
|
| 87 |
-
"0%, 100%": { opacity: "1" },
|
| 88 |
-
"50%": { opacity: "0.5" },
|
| 89 |
-
},
|
| 90 |
-
},
|
| 91 |
-
animation: {
|
| 92 |
-
"accordion-down": "accordion-down 0.2s ease-out",
|
| 93 |
-
"accordion-up": "accordion-up 0.2s ease-out",
|
| 94 |
-
"pulse-glow": "pulse-glow 2s ease-in-out infinite",
|
| 95 |
-
},
|
| 96 |
-
},
|
| 97 |
-
},
|
| 98 |
-
plugins: [require("tailwindcss-animate")],
|
| 99 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alternative_agents_page.py
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
"""Alternative Agents leaderboard page.
|
| 2 |
-
|
| 3 |
-
The canonical OpenHands Index leaderboard (Home + the per-category pages)
|
| 4 |
-
ranks default OpenHands agent runs from ``results/{model}/`` in the
|
| 5 |
-
openhands-index-results repo. Third-party harnesses (Claude Code, Codex,
|
| 6 |
-
Gemini CLI, OpenHands Sub-agents, ...) live under
|
| 7 |
-
``alternative_agents/{type}/{model}/`` and aren't directly comparable to
|
| 8 |
-
default OpenHands runs (different scaffolds, different cost/runtime
|
| 9 |
-
characteristics), so they get their own standalone page instead of being
|
| 10 |
-
mixed into the same ranking.
|
| 11 |
-
|
| 12 |
-
This page is intentionally a single Overall view (no per-category
|
| 13 |
-
subpages) — the alternative-agents dataset is small (one row per
|
| 14 |
-
harness × model) and the goal is "show me all the alternatives at a
|
| 15 |
-
glance", not "drill into Issue Resolution for Codex".
|
| 16 |
-
|
| 17 |
-
To make same-model comparisons easier, the page also appends canonical
|
| 18 |
-
OpenHands rows for any language model that appears in the alternative
|
| 19 |
-
agent dataset. The match is exact, so ``Gemini-3-Pro`` and
|
| 20 |
-
``Gemini-3.1-Pro`` remain distinct entries.
|
| 21 |
-
"""
|
| 22 |
-
import matplotlib
|
| 23 |
-
matplotlib.use('Agg')
|
| 24 |
-
import pandas as pd
|
| 25 |
-
import gradio as gr
|
| 26 |
-
|
| 27 |
-
from simple_data_loader import SimpleLeaderboardViewer
|
| 28 |
-
from ui_components import (
|
| 29 |
-
create_leaderboard_display,
|
| 30 |
-
get_full_leaderboard_data,
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
ALTERNATIVE_AGENTS_INTRO = """
|
| 35 |
-
<div id="alternative-agents-intro">
|
| 36 |
-
<h2>Alternative Agents</h2>
|
| 37 |
-
<p>
|
| 38 |
-
Third-party agent harnesses running the OpenHands Index benchmarks.
|
| 39 |
-
To make direct comparisons easier, this page also includes the
|
| 40 |
-
canonical OpenHands row whenever the exact same language model appears
|
| 41 |
-
under an alternative harness. Cost and runtime numbers still come from
|
| 42 |
-
each harness's own instrumentation and aren't directly comparable
|
| 43 |
-
across harnesses.
|
| 44 |
-
</p>
|
| 45 |
-
</div>
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def _append_openhands_shared_models(
|
| 50 |
-
alternative_df: pd.DataFrame,
|
| 51 |
-
split: str,
|
| 52 |
-
) -> pd.DataFrame:
|
| 53 |
-
if alternative_df.empty or "Language Model" not in alternative_df.columns:
|
| 54 |
-
return alternative_df
|
| 55 |
-
|
| 56 |
-
openhands_df, _ = get_full_leaderboard_data(
|
| 57 |
-
split,
|
| 58 |
-
agent_filter=SimpleLeaderboardViewer.AGENT_FILTER_OPENHANDS,
|
| 59 |
-
)
|
| 60 |
-
if openhands_df.empty or "Language Model" not in openhands_df.columns:
|
| 61 |
-
return alternative_df
|
| 62 |
-
|
| 63 |
-
alternative_models = set(
|
| 64 |
-
alternative_df["Language Model"].dropna().astype(str).str.strip()
|
| 65 |
-
)
|
| 66 |
-
if not alternative_models:
|
| 67 |
-
return alternative_df
|
| 68 |
-
|
| 69 |
-
openhands_shared_df = openhands_df[
|
| 70 |
-
openhands_df["Language Model"].astype(str).str.strip().isin(alternative_models)
|
| 71 |
-
].copy()
|
| 72 |
-
if openhands_shared_df.empty:
|
| 73 |
-
return alternative_df
|
| 74 |
-
|
| 75 |
-
return pd.concat([alternative_df, openhands_shared_df], ignore_index=True, sort=False)
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def build_page():
|
| 79 |
-
gr.HTML(ALTERNATIVE_AGENTS_INTRO)
|
| 80 |
-
|
| 81 |
-
gr.Markdown("---")
|
| 82 |
-
|
| 83 |
-
test_df, test_tag_map = get_full_leaderboard_data(
|
| 84 |
-
"test",
|
| 85 |
-
agent_filter=SimpleLeaderboardViewer.AGENT_FILTER_ALTERNATIVE,
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
if test_df.empty:
|
| 89 |
-
gr.Markdown(
|
| 90 |
-
"No alternative agent submissions yet. New runs land in "
|
| 91 |
-
"`alternative_agents/{type}/{model}/` in "
|
| 92 |
-
"[openhands-index-results](https://github.com/OpenHands/openhands-index-results)."
|
| 93 |
-
)
|
| 94 |
-
return
|
| 95 |
-
|
| 96 |
-
test_df = _append_openhands_shared_models(test_df, split="test")
|
| 97 |
-
|
| 98 |
-
create_leaderboard_display(
|
| 99 |
-
full_df=test_df,
|
| 100 |
-
tag_map=test_tag_map,
|
| 101 |
-
category_name="Overall",
|
| 102 |
-
split_name="test",
|
| 103 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -35,7 +35,6 @@ from app_creation import build_page as build_app_creation_page
|
|
| 35 |
from frontend_development import build_page as build_frontend_page
|
| 36 |
from test_generation import build_page as build_test_generation_page
|
| 37 |
from information_gathering import build_page as build_information_gathering_page
|
| 38 |
-
from alternative_agents_page import build_page as build_alternative_agents_page
|
| 39 |
from about import build_page as build_about_page
|
| 40 |
|
| 41 |
logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
|
|
@@ -374,46 +373,20 @@ with demo.route("Testing", "/testing"):
|
|
| 374 |
with demo.route("Information Gathering", "/information-gathering"):
|
| 375 |
build_information_gathering_page()
|
| 376 |
|
| 377 |
-
with demo.route("Alternative Agents", "/alternative-agents"):
|
| 378 |
-
build_alternative_agents_page()
|
| 379 |
-
|
| 380 |
with demo.route("About", "/about"):
|
| 381 |
build_about_page()
|
| 382 |
|
| 383 |
logger.info("All routes configured")
|
| 384 |
|
| 385 |
# Mount the REST API on /api
|
| 386 |
-
from fastapi import FastAPI
|
| 387 |
-
from fastapi.responses import RedirectResponse
|
| 388 |
-
from starlette.middleware.base import BaseHTTPMiddleware
|
| 389 |
from api import api_app
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
"""Middleware to redirect root path "/" to "/home".
|
| 394 |
-
|
| 395 |
-
This fixes the 307 trailing slash redirect issue (Gradio bug #11071) that
|
| 396 |
-
occurs when Gradio is mounted at "/" - FastAPI's default behavior redirects
|
| 397 |
-
"/" to "//", which breaks routing on HuggingFace Spaces.
|
| 398 |
-
|
| 399 |
-
See: https://github.com/gradio-app/gradio/issues/11071
|
| 400 |
-
"""
|
| 401 |
-
async def dispatch(self, request: Request, call_next):
|
| 402 |
-
if request.url.path == "/":
|
| 403 |
-
return RedirectResponse(url="/home", status_code=302)
|
| 404 |
-
return await call_next(request)
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
# Create a parent FastAPI app with redirect_slashes=False to prevent
|
| 408 |
-
# automatic trailing slash redirects that cause issues with Gradio
|
| 409 |
-
root_app = FastAPI(redirect_slashes=False)
|
| 410 |
-
|
| 411 |
-
# Add middleware to handle root path redirect to /home
|
| 412 |
-
root_app.add_middleware(RootRedirectMiddleware)
|
| 413 |
-
|
| 414 |
root_app.mount("/api", api_app)
|
| 415 |
|
| 416 |
-
# Mount Gradio app
|
| 417 |
app = gr.mount_gradio_app(root_app, demo, path="/")
|
| 418 |
logger.info("REST API mounted at /api, Gradio app mounted at /")
|
| 419 |
|
|
|
|
| 35 |
from frontend_development import build_page as build_frontend_page
|
| 36 |
from test_generation import build_page as build_test_generation_page
|
| 37 |
from information_gathering import build_page as build_information_gathering_page
|
|
|
|
| 38 |
from about import build_page as build_about_page
|
| 39 |
|
| 40 |
logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
|
|
|
|
| 373 |
with demo.route("Information Gathering", "/information-gathering"):
|
| 374 |
build_information_gathering_page()
|
| 375 |
|
|
|
|
|
|
|
|
|
|
| 376 |
with demo.route("About", "/about"):
|
| 377 |
build_about_page()
|
| 378 |
|
| 379 |
logger.info("All routes configured")
|
| 380 |
|
| 381 |
# Mount the REST API on /api
|
| 382 |
+
from fastapi import FastAPI
|
|
|
|
|
|
|
| 383 |
from api import api_app
|
| 384 |
|
| 385 |
+
# Create a parent FastAPI app that will host both the API and Gradio
|
| 386 |
+
root_app = FastAPI()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
root_app.mount("/api", api_app)
|
| 388 |
|
| 389 |
+
# Mount Gradio app - root redirect is handled by the proxy
|
| 390 |
app = gr.mount_gradio_app(root_app, demo, path="/")
|
| 391 |
logger.info("REST API mounted at /api, Gradio app mounted at /")
|
| 392 |
|
assets/harnesses/README.md
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
# Agent harness logos
|
| 2 |
-
|
| 3 |
-
This folder holds the **bottom half** of the composite scatter markers used
|
| 4 |
-
on the [Alternative Agents](../../alternative_agents_page.py) page. Each
|
| 5 |
-
point on that scatter stacks two logos: the model provider on top (from
|
| 6 |
-
`assets/logo-*.svg`) and the harness on the bottom (from this folder).
|
| 7 |
-
|
| 8 |
-
## Expected filenames
|
| 9 |
-
|
| 10 |
-
The scatter code looks up a logo by the exact `agent_name` string that the
|
| 11 |
-
`push-to-index` workflow writes into the index repo's `metadata.json`, then
|
| 12 |
-
maps it through `HARNESS_LOGO_STEMS` in `leaderboard_transformer.py`. Keep
|
| 13 |
-
these filenames in sync with that map.
|
| 14 |
-
|
| 15 |
-
| `agent_name` (in index repo) | File in this folder |
|
| 16 |
-
| --- | --- |
|
| 17 |
-
| `Claude Code` | `claude-code.svg` or `claude-code.png` |
|
| 18 |
-
| `Codex` | `codex-cli.svg` or `codex-cli.png` |
|
| 19 |
-
| `Gemini CLI` | `gemini-cli.svg` or `gemini-cli.png` |
|
| 20 |
-
| `OpenHands` | `openhands.svg` or `openhands.png` |
|
| 21 |
-
| `OpenHands Sub-agents` | `openhands.svg` or `openhands.png` (shared with `OpenHands`) |
|
| 22 |
-
|
| 23 |
-
Both `.svg` and `.png` are accepted — the resolver tries `.svg` first, then
|
| 24 |
-
`.png`. **Prefer SVG when possible**: the HuggingFace Space rejects new
|
| 25 |
-
binary files on plain `git push` and routes PNGs through Xet, so an SVG is
|
| 26 |
-
one less thing to set up.
|
| 27 |
-
|
| 28 |
-
## When a file is missing
|
| 29 |
-
|
| 30 |
-
The scatter falls back to a single marker (just the model provider logo) —
|
| 31 |
-
exactly the same rendering path the canonical OpenHands pages use. Nothing
|
| 32 |
-
crashes and nothing prints a warning in normal operation. This means you
|
| 33 |
-
can roll out logos one harness at a time without waiting for all four.
|
| 34 |
-
|
| 35 |
-
## Sizing and shape
|
| 36 |
-
|
| 37 |
-
- Square canvas. The composite marker is drawn at a fixed aspect ratio, so
|
| 38 |
-
a non-square logo will get squished.
|
| 39 |
-
- Any SVG `viewBox` works — the renderer base64-encodes the file as-is and
|
| 40 |
-
Plotly scales it to the marker's `sizex` / `sizey`. Around `80×80` to
|
| 41 |
-
`256×256` is a good source size.
|
| 42 |
-
- Leave some internal padding (≈10%) so the logo doesn't touch the marker
|
| 43 |
-
edge when two are stacked.
|
| 44 |
-
- No background is required, but a rounded-square coloured tile reads well
|
| 45 |
-
at small sizes because it gives each harness a distinct silhouette even
|
| 46 |
-
when the inner glyph isn't fully legible. Look at the existing
|
| 47 |
-
`assets/logo-*.svg` files for the canonical model provider logos if you
|
| 48 |
-
want a visual reference for sizing.
|
| 49 |
-
|
| 50 |
-
## Adding a new harness
|
| 51 |
-
|
| 52 |
-
1. Decide on the exact `agent_name` that the push-to-index workflow writes
|
| 53 |
-
for the new harness (see `AGENT_NAME_BY_TYPE` in
|
| 54 |
-
`OpenHands/evaluation/push-to-index-job/scripts/push_to_index_from_archive.py`).
|
| 55 |
-
2. Add an entry to `HARNESS_LOGO_STEMS` in
|
| 56 |
-
[`leaderboard_transformer.py`](../../leaderboard_transformer.py) that
|
| 57 |
-
maps the display name to a stem.
|
| 58 |
-
3. Drop `{stem}.svg` (or `.png`) into this folder.
|
| 59 |
-
4. Reload the app and look at `/alternative-agents`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/harnesses/claude-code.svg
DELETED
assets/harnesses/codex-cli.svg
DELETED
assets/harnesses/gemini-cli.svg
DELETED
assets/harnesses/openhands.svg
DELETED
assets/openhands-logotype-design.svg
DELETED
assets/openhands-logotype-on-dark.svg
DELETED
assets/openhands-logotype-on-light.svg
DELETED
content.py
CHANGED
|
@@ -556,11 +556,6 @@ span.wrap[tabindex="0"][role="button"][data-editable="false"] {
|
|
| 556 |
grid-column: 8 !important;
|
| 557 |
white-space: nowrap !important;
|
| 558 |
}
|
| 559 |
-
/* Hide the Alternative Agents page from the top-level nav for now. */
|
| 560 |
-
.nav-holder nav a[href*="alternative-agents"] {
|
| 561 |
-
display: none !important;
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
/* Divider line between header and category nav */
|
| 565 |
.nav-holder nav::after {
|
| 566 |
content: ''; /* Required for pseudo-elements to appear */
|
|
|
|
| 556 |
grid-column: 8 !important;
|
| 557 |
white-space: nowrap !important;
|
| 558 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
/* Divider line between header and category nav */
|
| 560 |
.nav-holder nav::after {
|
| 561 |
content: ''; /* Required for pseudo-elements to appear */
|
docs/screenshots/alternative-agents.png
DELETED
Git LFS Details
|
leaderboard_transformer.py
CHANGED
|
@@ -228,17 +228,17 @@ def get_country_from_model(model_name: str) -> dict:
|
|
| 228 |
def get_marker_icon(model_name: str, openness: str, mark_by: str) -> dict:
|
| 229 |
"""
|
| 230 |
Gets the appropriate icon based on the mark_by selection.
|
| 231 |
-
|
| 232 |
Args:
|
| 233 |
model_name: The model name
|
| 234 |
openness: The openness value (open/closed)
|
| 235 |
mark_by: One of "Company", "Openness", or "Country"
|
| 236 |
-
|
| 237 |
Returns:
|
| 238 |
dict with 'path' and 'name' keys
|
| 239 |
"""
|
| 240 |
from constants import MARK_BY_COMPANY, MARK_BY_OPENNESS, MARK_BY_COUNTRY
|
| 241 |
-
|
| 242 |
if mark_by == MARK_BY_OPENNESS:
|
| 243 |
return get_openness_icon(openness)
|
| 244 |
elif mark_by == MARK_BY_COUNTRY:
|
|
@@ -247,59 +247,6 @@ def get_marker_icon(model_name: str, openness: str, mark_by: str) -> dict:
|
|
| 247 |
return get_company_from_model(model_name)
|
| 248 |
|
| 249 |
|
| 250 |
-
# Map the agent_name stored in the index repo's metadata.json to a file stem
|
| 251 |
-
# inside assets/harnesses/. Kept in sync with AGENT_NAME_BY_TYPE in
|
| 252 |
-
# OpenHands/evaluation push_to_index_from_archive.py — if a new ACP harness
|
| 253 |
-
# lands there, add the corresponding display name and a matching stem here.
|
| 254 |
-
#
|
| 255 |
-
# The scatter plot looks for {stem}.svg first, then {stem}.png in
|
| 256 |
-
# assets/harnesses/. This repo intentionally ships only a README in that
|
| 257 |
-
# folder: drop the logo files in by hand (SVG preferred, PNG works too via
|
| 258 |
-
# HF Xet) and they'll be picked up on the next app restart. If the file is
|
| 259 |
-
# missing, get_harness_icon() returns None and the scatter falls back to the
|
| 260 |
-
# single-marker path — same rendering the canonical OpenHands pages use —
|
| 261 |
-
# so logos can be added one harness at a time without breaking anything.
|
| 262 |
-
HARNESS_LOGO_STEMS: dict[str, str] = {
|
| 263 |
-
"Claude Code": "claude-code",
|
| 264 |
-
"Codex": "codex-cli",
|
| 265 |
-
"Gemini CLI": "gemini-cli",
|
| 266 |
-
"OpenHands": "openhands",
|
| 267 |
-
"OpenHands Sub-agents": "openhands",
|
| 268 |
-
}
|
| 269 |
-
HARNESS_LOGO_DIR = "assets/harnesses"
|
| 270 |
-
HARNESS_LOGO_EXTENSIONS = ("svg", "png")
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
def get_harness_icon(agent_name: Optional[str]) -> Optional[dict]:
|
| 274 |
-
"""Return {'path', 'name'} for the harness logo, or None if not usable.
|
| 275 |
-
|
| 276 |
-
Consumed by the Alternative Agents scatter plot to draw a composite
|
| 277 |
-
marker (model provider on top, harness on bottom). Returns None in any
|
| 278 |
-
of three cases, all of which make the caller skip the harness layer:
|
| 279 |
-
|
| 280 |
-
- ``agent_name`` is empty or missing from the dataframe row.
|
| 281 |
-
- ``agent_name`` isn't in ``HARNESS_LOGO_STEMS`` (new harness that
|
| 282 |
-
hasn't been registered yet — register it and drop in a logo).
|
| 283 |
-
- The logo file for that stem doesn't exist in ``assets/harnesses/``
|
| 284 |
-
yet (the repo ships only the README).
|
| 285 |
-
|
| 286 |
-
That third case is the important one: it lets the Alternative Agents
|
| 287 |
-
page work immediately after checkout even when the harness logo files
|
| 288 |
-
haven't been dropped in. The corresponding points just render like a
|
| 289 |
-
canonical-page marker (model logo only) until the file is added.
|
| 290 |
-
"""
|
| 291 |
-
if not agent_name:
|
| 292 |
-
return None
|
| 293 |
-
stem = HARNESS_LOGO_STEMS.get(str(agent_name).strip())
|
| 294 |
-
if stem is None:
|
| 295 |
-
return None
|
| 296 |
-
for ext in HARNESS_LOGO_EXTENSIONS:
|
| 297 |
-
path = f"{HARNESS_LOGO_DIR}/{stem}.{ext}"
|
| 298 |
-
if os.path.exists(path):
|
| 299 |
-
return {"path": path, "name": agent_name}
|
| 300 |
-
return None
|
| 301 |
-
|
| 302 |
-
|
| 303 |
# Standard layout configuration for all charts
|
| 304 |
STANDARD_LAYOUT = dict(
|
| 305 |
template="plotly_white",
|
|
@@ -708,7 +655,6 @@ def _pretty_column_name(raw_col: str) -> str:
|
|
| 708 |
# Case 1: Handle fixed, special-case mappings first.
|
| 709 |
fixed_mappings = {
|
| 710 |
'id': 'id',
|
| 711 |
-
'agent_name': 'Agent',
|
| 712 |
'SDK version': 'SDK Version',
|
| 713 |
'Openhands version': 'SDK Version', # Legacy support
|
| 714 |
'Language model': 'Language Model',
|
|
@@ -869,21 +815,7 @@ class DataTransformer:
|
|
| 869 |
df_view = df_sorted.copy()
|
| 870 |
|
| 871 |
# --- 3. Add Columns for Agent Openness ---
|
| 872 |
-
|
| 873 |
-
# more than one distinct agent. On the canonical OpenHands pages
|
| 874 |
-
# every row says "OpenHands", so adding the column is just noise;
|
| 875 |
-
# on the Alternative Agents page rows differ (Claude Code / Codex
|
| 876 |
-
# / Gemini CLI / OpenHands Sub-agents), so the column carries
|
| 877 |
-
# signal and disambiguates same-model rows from different
|
| 878 |
-
# harnesses.
|
| 879 |
-
has_mixed_agents = (
|
| 880 |
-
"Agent" in df_view.columns
|
| 881 |
-
and df_view["Agent"].dropna().nunique() > 1
|
| 882 |
-
)
|
| 883 |
-
if has_mixed_agents:
|
| 884 |
-
base_cols = ["id", "Agent", "Language Model", "SDK Version", "Source"]
|
| 885 |
-
else:
|
| 886 |
-
base_cols = ["id", "Language Model", "SDK Version", "Source"]
|
| 887 |
new_cols = ["Openness"]
|
| 888 |
ending_cols = ["Date", "Logs", "Visualization"]
|
| 889 |
|
|
@@ -970,8 +902,7 @@ def _plot_scatter_plotly(
|
|
| 970 |
agent_col: str = 'Agent',
|
| 971 |
name: Optional[str] = None,
|
| 972 |
plot_type: str = 'cost', # 'cost' or 'runtime'
|
| 973 |
-
mark_by: Optional[str] = None
|
| 974 |
-
show_all_labels: bool = False # Show labels for all points vs only Pareto frontier
|
| 975 |
) -> go.Figure:
|
| 976 |
from constants import MARK_BY_DEFAULT
|
| 977 |
if mark_by is None:
|
|
@@ -1087,18 +1018,13 @@ def _plot_scatter_plotly(
|
|
| 1087 |
"""
|
| 1088 |
Builds the complete HTML string for the plot's hover tooltip.
|
| 1089 |
Format: {lm_name} (SDK {version})
|
| 1090 |
-
Harness: {agent} (only when the row carries an Agent —
|
| 1091 |
-
Alternative Agents page only; the
|
| 1092 |
-
canonical OpenHands pages drop the
|
| 1093 |
-
Agent column in view() so this line
|
| 1094 |
-
is skipped there)
|
| 1095 |
Average Score: {score}
|
| 1096 |
Average Cost/Runtime: {value}
|
| 1097 |
Openness: {openness}
|
| 1098 |
"""
|
| 1099 |
h_pad = " "
|
| 1100 |
parts = ["<br>"]
|
| 1101 |
-
|
| 1102 |
# Get and clean the language model name
|
| 1103 |
llm_base_value = row.get('Language Model', '')
|
| 1104 |
llm_base_value = clean_llm_base_list(llm_base_value)
|
|
@@ -1106,21 +1032,13 @@ def _plot_scatter_plotly(
|
|
| 1106 |
lm_name = llm_base_value[0]
|
| 1107 |
else:
|
| 1108 |
lm_name = str(llm_base_value) if llm_base_value else 'Unknown'
|
| 1109 |
-
|
| 1110 |
# Get SDK version
|
| 1111 |
sdk_version = row.get('SDK Version', row.get(agent_col, 'Unknown'))
|
| 1112 |
-
|
| 1113 |
# Title line: {lm_name} (SDK {version})
|
| 1114 |
parts.append(f"{h_pad}<b>{lm_name}</b> (SDK {sdk_version}){h_pad}<br>")
|
| 1115 |
-
|
| 1116 |
-
# Harness line — only on pages where the Agent column is present
|
| 1117 |
-
# (Alternative Agents). Without this, two rows for the same LM run
|
| 1118 |
-
# under different harnesses (e.g. Claude Code vs OpenHands Sub-agents
|
| 1119 |
-
# on claude-sonnet-4-5) are indistinguishable on hover.
|
| 1120 |
-
agent_value = row.get('Agent')
|
| 1121 |
-
if agent_value is not None and pd.notna(agent_value) and str(agent_value).strip():
|
| 1122 |
-
parts.append(f"{h_pad}Harness: <b>{agent_value}</b>{h_pad}<br>")
|
| 1123 |
-
|
| 1124 |
# Average Score
|
| 1125 |
parts.append(f"{h_pad}Average Score: <b>{row[y_col]:.3f}</b>{h_pad}<br>")
|
| 1126 |
|
|
@@ -1193,182 +1111,103 @@ def _plot_scatter_plotly(
|
|
| 1193 |
y_min = min_score - 5 if min_score > 5 else 0
|
| 1194 |
y_max = max_score + 5
|
| 1195 |
|
| 1196 |
-
# Cache base64-encoded logos across rows — every Claude model on the
|
| 1197 |
-
# Alternative Agents page points at the same assets/harness-claude-code.svg,
|
| 1198 |
-
# so decoding once per path is ~N× cheaper than once per point.
|
| 1199 |
-
_logo_cache: dict[str, str] = {}
|
| 1200 |
-
def _encode_logo(path: str) -> Optional[str]:
|
| 1201 |
-
if path in _logo_cache:
|
| 1202 |
-
return _logo_cache[path]
|
| 1203 |
-
if not os.path.exists(path):
|
| 1204 |
-
return None
|
| 1205 |
-
try:
|
| 1206 |
-
with open(path, "rb") as f:
|
| 1207 |
-
encoded = base64.b64encode(f.read()).decode("utf-8")
|
| 1208 |
-
except Exception as e:
|
| 1209 |
-
logger.warning(f"Could not load logo {path}: {e}")
|
| 1210 |
-
return None
|
| 1211 |
-
mime = "svg+xml" if path.lower().endswith(".svg") else "png"
|
| 1212 |
-
uri = f"data:image/{mime};base64,{encoded}"
|
| 1213 |
-
_logo_cache[path] = uri
|
| 1214 |
-
return uri
|
| 1215 |
-
|
| 1216 |
-
# Composite markers: on the Alternative Agents page the dataframe carries
|
| 1217 |
-
# an "Agent" column (Claude Code / Codex / Gemini CLI / OpenHands Sub-agents),
|
| 1218 |
-
# so a point for claude-sonnet-4-5 under Claude Code and under OpenHands
|
| 1219 |
-
# Sub-agents would otherwise share the exact same Anthropic logo marker
|
| 1220 |
-
# and be visually indistinguishable. When Agent is present, we stack
|
| 1221 |
-
# two logos at each point: model provider on top, harness on the bottom.
|
| 1222 |
-
# Canonical OpenHands pages drop the Agent column in view() (via the
|
| 1223 |
-
# has_mixed_agents check), so they fall through to the single-logo path
|
| 1224 |
-
# and render exactly as before.
|
| 1225 |
-
has_harness_column = (
|
| 1226 |
-
"Agent" in data_plot.columns
|
| 1227 |
-
and data_plot["Agent"].dropna().astype(str).str.strip().ne("").any()
|
| 1228 |
-
)
|
| 1229 |
-
|
| 1230 |
-
# Marker sizes. The composite variant fits two logos inside roughly the
|
| 1231 |
-
# same vertical footprint as a single marker, so each half is slightly
|
| 1232 |
-
# smaller and the two halves are offset symmetrically around the point's
|
| 1233 |
-
# true y-coordinate.
|
| 1234 |
-
SINGLE_SIZE_X, SINGLE_SIZE_Y = 0.04, 0.06
|
| 1235 |
-
STACKED_SIZE_X, STACKED_SIZE_Y = 0.035, 0.048
|
| 1236 |
-
STACKED_Y_OFFSET = 0.028 # half-separation between model (top) and harness (bottom)
|
| 1237 |
-
|
| 1238 |
for _, row in data_plot.iterrows():
|
| 1239 |
model_name = row.get('Language Model', '')
|
| 1240 |
openness = row.get('Openness', '')
|
| 1241 |
marker_info = get_marker_icon(model_name, openness, mark_by)
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
sizex=STACKED_SIZE_X * (x_max_log - x_min_log),
|
| 1284 |
-
sizey=STACKED_SIZE_Y * (y_max - y_min),
|
| 1285 |
-
xanchor="center", yanchor="middle",
|
| 1286 |
-
layer="above",
|
| 1287 |
-
))
|
| 1288 |
-
layout_images.append(dict(
|
| 1289 |
-
source=harness_uri,
|
| 1290 |
-
xref="x", yref="y",
|
| 1291 |
-
x=x_log, y=y_val - y_offset,
|
| 1292 |
-
sizex=STACKED_SIZE_X * (x_max_log - x_min_log),
|
| 1293 |
-
sizey=STACKED_SIZE_Y * (y_max - y_min),
|
| 1294 |
-
xanchor="center", yanchor="middle",
|
| 1295 |
-
layer="above",
|
| 1296 |
-
))
|
| 1297 |
-
else:
|
| 1298 |
-
# Single marker - use data coordinates so logo zooms/pans with labels
|
| 1299 |
-
layout_images.append(dict(
|
| 1300 |
-
source=model_logo_uri,
|
| 1301 |
-
xref="x", yref="y",
|
| 1302 |
-
x=x_log, y=y_val,
|
| 1303 |
-
sizex=SINGLE_SIZE_X * (x_max_log - x_min_log),
|
| 1304 |
-
sizey=SINGLE_SIZE_Y * (y_max - y_min),
|
| 1305 |
-
xanchor="center", yanchor="middle",
|
| 1306 |
-
layer="above",
|
| 1307 |
-
))
|
| 1308 |
|
| 1309 |
-
# --- Section 7: Add Model Name Labels ---
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
# Label all data points
|
| 1313 |
-
labels_data = []
|
| 1314 |
-
for _, row in data_plot.iterrows():
|
| 1315 |
-
x_val = row[x_col_to_use]
|
| 1316 |
-
y_val = row[y_col_to_use]
|
| 1317 |
-
|
| 1318 |
-
model_name = row.get('Language Model', '')
|
| 1319 |
-
if isinstance(model_name, list):
|
| 1320 |
-
model_name = model_name[0] if model_name else ''
|
| 1321 |
-
model_name = str(model_name).split('/')[-1]
|
| 1322 |
-
if len(model_name) > 25:
|
| 1323 |
-
model_name = model_name[:22] + '...'
|
| 1324 |
-
|
| 1325 |
-
labels_data.append({'x': x_val, 'y': y_val, 'label': model_name})
|
| 1326 |
-
elif frontier_rows:
|
| 1327 |
-
# Label only Pareto frontier points
|
| 1328 |
-
labels_data = []
|
| 1329 |
|
| 1330 |
for row in frontier_rows:
|
| 1331 |
x_val = row[x_col_to_use]
|
| 1332 |
y_val = row[y_col_to_use]
|
| 1333 |
|
|
|
|
| 1334 |
model_name = row.get('Language Model', '')
|
| 1335 |
if isinstance(model_name, list):
|
| 1336 |
model_name = model_name[0] if model_name else ''
|
|
|
|
| 1337 |
model_name = str(model_name).split('/')[-1]
|
|
|
|
| 1338 |
if len(model_name) > 25:
|
| 1339 |
model_name = model_name[:22] + '...'
|
| 1340 |
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
# For log scale x-axis, annotations need log10(x) coordinates (Plotly issue #2580)
|
| 1347 |
-
for item in labels_data:
|
| 1348 |
-
x_val = item['x']
|
| 1349 |
-
y_val = item['y']
|
| 1350 |
-
label = item['label']
|
| 1351 |
|
| 1352 |
-
#
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
x
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1372 |
|
| 1373 |
# --- Section 8: Configure Layout ---
|
| 1374 |
# Use the same axis ranges as calculated for domain coordinates
|
|
|
|
| 228 |
def get_marker_icon(model_name: str, openness: str, mark_by: str) -> dict:
|
| 229 |
"""
|
| 230 |
Gets the appropriate icon based on the mark_by selection.
|
| 231 |
+
|
| 232 |
Args:
|
| 233 |
model_name: The model name
|
| 234 |
openness: The openness value (open/closed)
|
| 235 |
mark_by: One of "Company", "Openness", or "Country"
|
| 236 |
+
|
| 237 |
Returns:
|
| 238 |
dict with 'path' and 'name' keys
|
| 239 |
"""
|
| 240 |
from constants import MARK_BY_COMPANY, MARK_BY_OPENNESS, MARK_BY_COUNTRY
|
| 241 |
+
|
| 242 |
if mark_by == MARK_BY_OPENNESS:
|
| 243 |
return get_openness_icon(openness)
|
| 244 |
elif mark_by == MARK_BY_COUNTRY:
|
|
|
|
| 247 |
return get_company_from_model(model_name)
|
| 248 |
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
# Standard layout configuration for all charts
|
| 251 |
STANDARD_LAYOUT = dict(
|
| 252 |
template="plotly_white",
|
|
|
|
| 655 |
# Case 1: Handle fixed, special-case mappings first.
|
| 656 |
fixed_mappings = {
|
| 657 |
'id': 'id',
|
|
|
|
| 658 |
'SDK version': 'SDK Version',
|
| 659 |
'Openhands version': 'SDK Version', # Legacy support
|
| 660 |
'Language model': 'Language Model',
|
|
|
|
| 815 |
df_view = df_sorted.copy()
|
| 816 |
|
| 817 |
# --- 3. Add Columns for Agent Openness ---
|
| 818 |
+
base_cols = ["id","Language Model","SDK Version","Source"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
new_cols = ["Openness"]
|
| 820 |
ending_cols = ["Date", "Logs", "Visualization"]
|
| 821 |
|
|
|
|
| 902 |
agent_col: str = 'Agent',
|
| 903 |
name: Optional[str] = None,
|
| 904 |
plot_type: str = 'cost', # 'cost' or 'runtime'
|
| 905 |
+
mark_by: Optional[str] = None # 'Company', 'Openness', or 'Country'
|
|
|
|
| 906 |
) -> go.Figure:
|
| 907 |
from constants import MARK_BY_DEFAULT
|
| 908 |
if mark_by is None:
|
|
|
|
| 1018 |
"""
|
| 1019 |
Builds the complete HTML string for the plot's hover tooltip.
|
| 1020 |
Format: {lm_name} (SDK {version})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
Average Score: {score}
|
| 1022 |
Average Cost/Runtime: {value}
|
| 1023 |
Openness: {openness}
|
| 1024 |
"""
|
| 1025 |
h_pad = " "
|
| 1026 |
parts = ["<br>"]
|
| 1027 |
+
|
| 1028 |
# Get and clean the language model name
|
| 1029 |
llm_base_value = row.get('Language Model', '')
|
| 1030 |
llm_base_value = clean_llm_base_list(llm_base_value)
|
|
|
|
| 1032 |
lm_name = llm_base_value[0]
|
| 1033 |
else:
|
| 1034 |
lm_name = str(llm_base_value) if llm_base_value else 'Unknown'
|
| 1035 |
+
|
| 1036 |
# Get SDK version
|
| 1037 |
sdk_version = row.get('SDK Version', row.get(agent_col, 'Unknown'))
|
| 1038 |
+
|
| 1039 |
# Title line: {lm_name} (SDK {version})
|
| 1040 |
parts.append(f"{h_pad}<b>{lm_name}</b> (SDK {sdk_version}){h_pad}<br>")
|
| 1041 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
# Average Score
|
| 1043 |
parts.append(f"{h_pad}Average Score: <b>{row[y_col]:.3f}</b>{h_pad}<br>")
|
| 1044 |
|
|
|
|
| 1111 |
y_min = min_score - 5 if min_score > 5 else 0
|
| 1112 |
y_max = max_score + 5
|
| 1113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
for _, row in data_plot.iterrows():
|
| 1115 |
model_name = row.get('Language Model', '')
|
| 1116 |
openness = row.get('Openness', '')
|
| 1117 |
marker_info = get_marker_icon(model_name, openness, mark_by)
|
| 1118 |
+
logo_path = marker_info['path']
|
| 1119 |
+
|
| 1120 |
+
# Read the SVG file and encode as base64 data URI
|
| 1121 |
+
if os.path.exists(logo_path):
|
| 1122 |
+
try:
|
| 1123 |
+
with open(logo_path, 'rb') as f:
|
| 1124 |
+
encoded_logo = base64.b64encode(f.read()).decode('utf-8')
|
| 1125 |
+
logo_uri = f"data:image/svg+xml;base64,{encoded_logo}"
|
| 1126 |
+
|
| 1127 |
+
x_val = row[x_col_to_use]
|
| 1128 |
+
y_val = row[y_col_to_use]
|
| 1129 |
+
|
| 1130 |
+
# Convert to domain coordinates (0-1 range)
|
| 1131 |
+
# For log scale x: domain_x = (log10(x) - x_min_log) / (x_max_log - x_min_log)
|
| 1132 |
+
if x_val > 0:
|
| 1133 |
+
log_x = np.log10(x_val)
|
| 1134 |
+
domain_x = (log_x - x_min_log) / (x_max_log - x_min_log)
|
| 1135 |
+
else:
|
| 1136 |
+
domain_x = 0
|
| 1137 |
+
|
| 1138 |
+
# For linear y: domain_y = (y - y_min) / (y_max - y_min)
|
| 1139 |
+
domain_y = (y_val - y_min) / (y_max - y_min) if (y_max - y_min) > 0 else 0.5
|
| 1140 |
+
|
| 1141 |
+
# Clamp to valid range
|
| 1142 |
+
domain_x = max(0, min(1, domain_x))
|
| 1143 |
+
domain_y = max(0, min(1, domain_y))
|
| 1144 |
+
|
| 1145 |
+
layout_images.append(dict(
|
| 1146 |
+
source=logo_uri,
|
| 1147 |
+
xref="x domain", # Use domain coordinates for log scale compatibility
|
| 1148 |
+
yref="y domain",
|
| 1149 |
+
x=domain_x,
|
| 1150 |
+
y=domain_y,
|
| 1151 |
+
sizex=0.04, # Size as fraction of plot width
|
| 1152 |
+
sizey=0.06, # Size as fraction of plot height
|
| 1153 |
+
xanchor="center",
|
| 1154 |
+
yanchor="middle",
|
| 1155 |
+
layer="above"
|
| 1156 |
+
))
|
| 1157 |
+
except Exception as e:
|
| 1158 |
+
logger.warning(f"Could not load logo {logo_path}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1159 |
|
| 1160 |
+
# --- Section 7: Add Model Name Labels to Frontier Points ---
|
| 1161 |
+
if frontier_rows:
|
| 1162 |
+
frontier_labels_data = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1163 |
|
| 1164 |
for row in frontier_rows:
|
| 1165 |
x_val = row[x_col_to_use]
|
| 1166 |
y_val = row[y_col_to_use]
|
| 1167 |
|
| 1168 |
+
# Get the model name for the label
|
| 1169 |
model_name = row.get('Language Model', '')
|
| 1170 |
if isinstance(model_name, list):
|
| 1171 |
model_name = model_name[0] if model_name else ''
|
| 1172 |
+
# Clean the model name (remove path prefixes)
|
| 1173 |
model_name = str(model_name).split('/')[-1]
|
| 1174 |
+
# Truncate long names
|
| 1175 |
if len(model_name) > 25:
|
| 1176 |
model_name = model_name[:22] + '...'
|
| 1177 |
|
| 1178 |
+
frontier_labels_data.append({
|
| 1179 |
+
'x': x_val,
|
| 1180 |
+
'y': y_val,
|
| 1181 |
+
'label': model_name
|
| 1182 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
|
| 1184 |
+
# Add annotations for each frontier label
|
| 1185 |
+
# For log scale x-axis, annotations need log10(x) coordinates (Plotly issue #2580)
|
| 1186 |
+
for item in frontier_labels_data:
|
| 1187 |
+
x_val = item['x']
|
| 1188 |
+
y_val = item['y']
|
| 1189 |
+
label = item['label']
|
| 1190 |
+
|
| 1191 |
+
# Transform x to log10 for annotation positioning on log scale
|
| 1192 |
+
if x_val > 0:
|
| 1193 |
+
x_log = np.log10(x_val)
|
| 1194 |
+
else:
|
| 1195 |
+
x_log = x_min_log
|
| 1196 |
+
|
| 1197 |
+
fig.add_annotation(
|
| 1198 |
+
x=x_log,
|
| 1199 |
+
y=y_val,
|
| 1200 |
+
text=label,
|
| 1201 |
+
showarrow=False,
|
| 1202 |
+
yshift=25, # Move label higher above the icon
|
| 1203 |
+
font=dict(
|
| 1204 |
+
size=10,
|
| 1205 |
+
color='#0D0D0F', # neutral-950
|
| 1206 |
+
family=FONT_FAMILY_SHORT
|
| 1207 |
+
),
|
| 1208 |
+
xanchor='center',
|
| 1209 |
+
yanchor='bottom'
|
| 1210 |
+
)
|
| 1211 |
|
| 1212 |
# --- Section 8: Configure Layout ---
|
| 1213 |
# Use the same axis ranges as calculated for domain coordinates
|
main_page.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import matplotlib
|
| 2 |
matplotlib.use('Agg')
|
| 3 |
import gradio as gr
|
| 4 |
-
import pandas as pd
|
| 5 |
|
| 6 |
|
| 7 |
from ui_components import (
|
|
@@ -27,32 +26,6 @@ from constants import MARK_BY_DEFAULT
|
|
| 27 |
CACHED_VIEWERS = {}
|
| 28 |
CACHED_TAG_MAPS = {}
|
| 29 |
|
| 30 |
-
|
| 31 |
-
def filter_complete_entries(df: pd.DataFrame) -> pd.DataFrame:
|
| 32 |
-
if df.empty:
|
| 33 |
-
return df.copy()
|
| 34 |
-
|
| 35 |
-
category_score_columns = [
|
| 36 |
-
'Issue Resolution Score',
|
| 37 |
-
'Frontend Score',
|
| 38 |
-
'Greenfield Score',
|
| 39 |
-
'Testing Score',
|
| 40 |
-
'Information Gathering Score',
|
| 41 |
-
]
|
| 42 |
-
|
| 43 |
-
if all(column in df.columns for column in category_score_columns):
|
| 44 |
-
return df[df[category_score_columns].notna().all(axis=1)].copy()
|
| 45 |
-
|
| 46 |
-
if 'Categories Completed' in df.columns:
|
| 47 |
-
categories_completed = pd.to_numeric(df['Categories Completed'], errors='coerce')
|
| 48 |
-
return df[categories_completed >= 5].copy()
|
| 49 |
-
|
| 50 |
-
if 'Categories Attempted' in df.columns:
|
| 51 |
-
return df[df['Categories Attempted'] == '5/5'].copy()
|
| 52 |
-
|
| 53 |
-
return df.copy()
|
| 54 |
-
|
| 55 |
-
|
| 56 |
def build_page():
|
| 57 |
with gr.Row(elem_id="intro-row"):
|
| 58 |
with gr.Column(scale=1):
|
|
@@ -65,91 +38,78 @@ def build_page():
|
|
| 65 |
|
| 66 |
test_df, test_tag_map = get_full_leaderboard_data("test")
|
| 67 |
if not test_df.empty:
|
| 68 |
-
|
|
|
|
| 69 |
full_df=test_df,
|
| 70 |
tag_map=test_tag_map,
|
| 71 |
category_name=CATEGORY_NAME,
|
| 72 |
split_name="test"
|
| 73 |
)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
has_complete_entries = len(test_df_complete) > 0
|
| 77 |
-
|
| 78 |
if 'Openness' in test_df.columns:
|
| 79 |
test_df_open = test_df[test_df['Openness'].str.lower() == 'open'].copy()
|
| 80 |
else:
|
| 81 |
test_df_open = test_df.copy()
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
initial_df = test_df_complete if has_complete_entries else test_df
|
| 85 |
-
|
| 86 |
# --- Winners by Category Section ---
|
| 87 |
gr.Markdown("---")
|
| 88 |
gr.HTML('<h2>Winners by Category</h2>', elem_id="winners-header")
|
| 89 |
gr.Markdown("Top 5 performing systems in each benchmark category.")
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
| 96 |
# --- New Visualization Sections ---
|
| 97 |
gr.Markdown("---")
|
| 98 |
-
|
| 99 |
# Evolution Over Time Section
|
| 100 |
gr.HTML('<h2>Evolution Over Time</h2>', elem_id="evolution-header")
|
| 101 |
gr.Markdown("Track how model performance has improved over time based on release dates.")
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
)
|
| 107 |
-
|
| 108 |
gr.Markdown("---")
|
| 109 |
-
|
| 110 |
# Open Model Accuracy by Size Section (always shows open models only by design)
|
| 111 |
gr.HTML('<h2>Open Model Accuracy by Size</h2>', elem_id="size-accuracy-header")
|
| 112 |
gr.Markdown("Compare open-weights model performance against their parameter count.")
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
evolution_fig = create_evolution_over_time_chart(
|
| 127 |
-
size_fig = create_accuracy_by_size_chart(
|
| 128 |
-
|
| 129 |
return winners_html, evolution_fig, size_fig
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
show_open_only_input = show_open_only_checkbox if show_open_only_checkbox is not None else gr.State(value=False)
|
| 133 |
-
extra_section_inputs = [show_incomplete_input, show_open_only_input, mark_by_dropdown]
|
| 134 |
-
|
| 135 |
-
if show_incomplete_checkbox is not None:
|
| 136 |
-
show_incomplete_checkbox.change(
|
| 137 |
-
fn=update_extra_sections,
|
| 138 |
-
inputs=extra_section_inputs,
|
| 139 |
-
outputs=[winners_component, evolution_component, size_component]
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
if show_open_only_checkbox is not None:
|
| 143 |
show_open_only_checkbox.change(
|
| 144 |
fn=update_extra_sections,
|
| 145 |
-
inputs=
|
| 146 |
outputs=[winners_component, evolution_component, size_component]
|
| 147 |
)
|
| 148 |
-
|
| 149 |
if mark_by_dropdown is not None:
|
| 150 |
mark_by_dropdown.change(
|
| 151 |
fn=update_extra_sections,
|
| 152 |
-
inputs=
|
| 153 |
outputs=[winners_component, evolution_component, size_component]
|
| 154 |
)
|
| 155 |
|
|
|
|
| 1 |
import matplotlib
|
| 2 |
matplotlib.use('Agg')
|
| 3 |
import gradio as gr
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
from ui_components import (
|
|
|
|
| 26 |
CACHED_VIEWERS = {}
|
| 27 |
CACHED_TAG_MAPS = {}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def build_page():
|
| 30 |
with gr.Row(elem_id="intro-row"):
|
| 31 |
with gr.Column(scale=1):
|
|
|
|
| 38 |
|
| 39 |
test_df, test_tag_map = get_full_leaderboard_data("test")
|
| 40 |
if not test_df.empty:
|
| 41 |
+
# Get the checkbox and dropdown returned from create_leaderboard_display
|
| 42 |
+
show_open_only_checkbox, mark_by_dropdown = create_leaderboard_display(
|
| 43 |
full_df=test_df,
|
| 44 |
tag_map=test_tag_map,
|
| 45 |
category_name=CATEGORY_NAME,
|
| 46 |
split_name="test"
|
| 47 |
)
|
| 48 |
+
|
| 49 |
+
# Prepare open-only filtered dataframe for Winners and Evolution
|
|
|
|
|
|
|
| 50 |
if 'Openness' in test_df.columns:
|
| 51 |
test_df_open = test_df[test_df['Openness'].str.lower() == 'open'].copy()
|
| 52 |
else:
|
| 53 |
test_df_open = test_df.copy()
|
| 54 |
+
|
|
|
|
|
|
|
|
|
|
| 55 |
# --- Winners by Category Section ---
|
| 56 |
gr.Markdown("---")
|
| 57 |
gr.HTML('<h2>Winners by Category</h2>', elem_id="winners-header")
|
| 58 |
gr.Markdown("Top 5 performing systems in each benchmark category.")
|
| 59 |
+
|
| 60 |
+
# Create both all and open-only versions of winners HTML
|
| 61 |
+
winners_html_all = create_winners_by_category_html(test_df, top_n=5)
|
| 62 |
+
winners_html_open = create_winners_by_category_html(test_df_open, top_n=5)
|
| 63 |
+
|
| 64 |
+
winners_component = gr.HTML(winners_html_all, elem_id="winners-by-category")
|
| 65 |
+
|
| 66 |
# --- New Visualization Sections ---
|
| 67 |
gr.Markdown("---")
|
| 68 |
+
|
| 69 |
# Evolution Over Time Section
|
| 70 |
gr.HTML('<h2>Evolution Over Time</h2>', elem_id="evolution-header")
|
| 71 |
gr.Markdown("Track how model performance has improved over time based on release dates.")
|
| 72 |
+
|
| 73 |
+
# Create initial evolution chart with default mark_by
|
| 74 |
+
evolution_fig_all = create_evolution_over_time_chart(test_df, MARK_BY_DEFAULT)
|
| 75 |
+
|
| 76 |
+
evolution_component = gr.Plot(value=evolution_fig_all, elem_id="evolution-chart")
|
| 77 |
+
|
| 78 |
gr.Markdown("---")
|
| 79 |
+
|
| 80 |
# Open Model Accuracy by Size Section (always shows open models only by design)
|
| 81 |
gr.HTML('<h2>Open Model Accuracy by Size</h2>', elem_id="size-accuracy-header")
|
| 82 |
gr.Markdown("Compare open-weights model performance against their parameter count.")
|
| 83 |
+
|
| 84 |
+
size_fig = create_accuracy_by_size_chart(test_df, MARK_BY_DEFAULT)
|
| 85 |
+
size_component = gr.Plot(value=size_fig, elem_id="size-accuracy-chart")
|
| 86 |
+
|
| 87 |
+
# Update function for Winners, Evolution, and Size charts based on filters
|
| 88 |
+
def update_extra_sections(show_open_only, mark_by):
|
| 89 |
+
# Select the appropriate dataframe based on open_only filter
|
| 90 |
+
df_to_use = test_df_open if show_open_only else test_df
|
| 91 |
+
|
| 92 |
+
# Winners HTML (not affected by mark_by, only open_only)
|
| 93 |
+
winners_html = winners_html_open if show_open_only else winners_html_all
|
| 94 |
+
|
| 95 |
+
# Regenerate charts with current mark_by setting
|
| 96 |
+
evolution_fig = create_evolution_over_time_chart(df_to_use, mark_by)
|
| 97 |
+
size_fig = create_accuracy_by_size_chart(test_df, mark_by) # Size chart always uses full df (filters internally)
|
| 98 |
+
|
| 99 |
return winners_html, evolution_fig, size_fig
|
| 100 |
+
|
| 101 |
+
# Connect both checkbox and dropdown to update all extra sections
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
if show_open_only_checkbox is not None:
|
| 103 |
show_open_only_checkbox.change(
|
| 104 |
fn=update_extra_sections,
|
| 105 |
+
inputs=[show_open_only_checkbox, mark_by_dropdown],
|
| 106 |
outputs=[winners_component, evolution_component, size_component]
|
| 107 |
)
|
| 108 |
+
|
| 109 |
if mark_by_dropdown is not None:
|
| 110 |
mark_by_dropdown.change(
|
| 111 |
fn=update_extra_sections,
|
| 112 |
+
inputs=[show_open_only_checkbox if show_open_only_checkbox else gr.State(value=False), mark_by_dropdown],
|
| 113 |
outputs=[winners_component, evolution_component, size_component]
|
| 114 |
)
|
| 115 |
|
setup_data.py
CHANGED
|
@@ -70,39 +70,27 @@ def fetch_data_from_github():
|
|
| 70 |
|
| 71 |
# Look for data files in the cloned repository
|
| 72 |
results_source = clone_dir / "results"
|
| 73 |
-
|
| 74 |
if not results_source.exists():
|
| 75 |
print(f"Results directory not found in repository")
|
| 76 |
return False
|
| 77 |
-
|
| 78 |
# Check if there are any agent result directories
|
| 79 |
result_dirs = list(results_source.iterdir())
|
| 80 |
if not result_dirs:
|
| 81 |
print(f"No agent results found in {results_source}")
|
| 82 |
return False
|
| 83 |
-
|
| 84 |
print(f"Found {len(result_dirs)} agent result directories")
|
| 85 |
-
|
| 86 |
# Create target directory and copy the results structure
|
| 87 |
os.makedirs(target_dir.parent, exist_ok=True)
|
| 88 |
if target_dir.exists():
|
| 89 |
shutil.rmtree(target_dir)
|
| 90 |
-
|
| 91 |
# Copy the entire results directory
|
| 92 |
target_results = target_dir / "results"
|
| 93 |
shutil.copytree(results_source, target_results)
|
| 94 |
-
|
| 95 |
-
# Also copy alternative_agents/ if present, so the loader can pick up
|
| 96 |
-
# ACP runs (acp-claude, acp-codex, acp-gemini, openhands_subagents, ...)
|
| 97 |
-
# alongside the default OpenHands agent results.
|
| 98 |
-
alt_source = clone_dir / "alternative_agents"
|
| 99 |
-
if alt_source.exists():
|
| 100 |
-
alt_target = target_dir / "alternative_agents"
|
| 101 |
-
shutil.copytree(alt_source, alt_target)
|
| 102 |
-
agent_types = sorted(p.name for p in alt_source.iterdir() if p.is_dir())
|
| 103 |
-
print(f"Found alternative agent types: {agent_types}")
|
| 104 |
-
else:
|
| 105 |
-
print("No alternative_agents/ directory in repository (skipping)")
|
| 106 |
|
| 107 |
print(f"Successfully fetched data from GitHub. Files: {list(target_dir.glob('*'))}")
|
| 108 |
|
|
|
|
| 70 |
|
| 71 |
# Look for data files in the cloned repository
|
| 72 |
results_source = clone_dir / "results"
|
| 73 |
+
|
| 74 |
if not results_source.exists():
|
| 75 |
print(f"Results directory not found in repository")
|
| 76 |
return False
|
| 77 |
+
|
| 78 |
# Check if there are any agent result directories
|
| 79 |
result_dirs = list(results_source.iterdir())
|
| 80 |
if not result_dirs:
|
| 81 |
print(f"No agent results found in {results_source}")
|
| 82 |
return False
|
| 83 |
+
|
| 84 |
print(f"Found {len(result_dirs)} agent result directories")
|
| 85 |
+
|
| 86 |
# Create target directory and copy the results structure
|
| 87 |
os.makedirs(target_dir.parent, exist_ok=True)
|
| 88 |
if target_dir.exists():
|
| 89 |
shutil.rmtree(target_dir)
|
| 90 |
+
|
| 91 |
# Copy the entire results directory
|
| 92 |
target_results = target_dir / "results"
|
| 93 |
shutil.copytree(results_source, target_results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
print(f"Successfully fetched data from GitHub. Files: {list(target_dir.glob('*'))}")
|
| 96 |
|
simple_data_loader.py
CHANGED
|
@@ -96,43 +96,17 @@ def load_and_validate_agent_data(agent_dir: Path) -> tuple[Optional[dict], Optio
|
|
| 96 |
|
| 97 |
class SimpleLeaderboardViewer:
|
| 98 |
"""Simple replacement for agent-eval's LeaderboardViewer."""
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
AGENT_FILTER_ALTERNATIVE = "alternative"
|
| 102 |
-
|
| 103 |
-
def __init__(
|
| 104 |
-
self,
|
| 105 |
-
data_dir: str,
|
| 106 |
-
config: str,
|
| 107 |
-
split: str,
|
| 108 |
-
agent_filter: str = AGENT_FILTER_OPENHANDS,
|
| 109 |
-
):
|
| 110 |
"""
|
| 111 |
Args:
|
| 112 |
data_dir: Path to data directory
|
| 113 |
config: Config name (e.g., "1.0.0-dev1")
|
| 114 |
split: Split name (e.g., "validation" or "test")
|
| 115 |
-
agent_filter: Which submissions to include.
|
| 116 |
-
``"openhands"`` (default) loads only the default OpenHands
|
| 117 |
-
agent runs from ``results/{model}/`` — the canonical
|
| 118 |
-
leaderboard. ``"alternative"`` loads only third-party
|
| 119 |
-
harnesses (Claude Code / Codex / Gemini CLI / OpenHands
|
| 120 |
-
Sub-agents) from ``alternative_agents/{type}/{model}/``,
|
| 121 |
-
which power the standalone Alternative Agents page.
|
| 122 |
-
The two are kept on separate pages because their
|
| 123 |
-
cost/runtime numbers aren't apples-to-apples and mixing
|
| 124 |
-
them in one ranking would be misleading.
|
| 125 |
"""
|
| 126 |
-
if agent_filter not in (self.AGENT_FILTER_OPENHANDS, self.AGENT_FILTER_ALTERNATIVE):
|
| 127 |
-
raise ValueError(
|
| 128 |
-
f"agent_filter must be one of "
|
| 129 |
-
f"{{{self.AGENT_FILTER_OPENHANDS!r}, {self.AGENT_FILTER_ALTERNATIVE!r}}}, "
|
| 130 |
-
f"got {agent_filter!r}"
|
| 131 |
-
)
|
| 132 |
self.data_dir = Path(data_dir)
|
| 133 |
self.config = config
|
| 134 |
self.split = split
|
| 135 |
-
self.agent_filter = agent_filter
|
| 136 |
self.config_path = self.data_dir / config
|
| 137 |
|
| 138 |
# Benchmark to category mappings (single source of truth)
|
|
@@ -153,116 +127,55 @@ class SimpleLeaderboardViewer:
|
|
| 153 |
if benchmark not in self.tag_map[category]:
|
| 154 |
self.tag_map[category].append(benchmark)
|
| 155 |
|
| 156 |
-
# Default agent_name when metadata.json doesn't carry one. Matches the
|
| 157 |
-
# default-agent value used by push_to_index_from_archive.py so legacy
|
| 158 |
-
# entries (which omit the field) still group cleanly with new entries.
|
| 159 |
-
DEFAULT_AGENT_NAME = "OpenHands"
|
| 160 |
-
|
| 161 |
-
def _records_from_agent_dir(self, agent_dir: Path, default_agent_name: str | None = None) -> tuple[list[dict], list[str]]:
|
| 162 |
-
"""Build per-benchmark records from a single agent directory.
|
| 163 |
-
|
| 164 |
-
Shared by ``_load_from_agent_dirs`` (default OpenHands results) and
|
| 165 |
-
``_load_from_alternative_agents_dirs`` (acp-claude / acp-codex / etc.).
|
| 166 |
-
Returns ``(records, validation_errors)``. Returns an empty list of
|
| 167 |
-
records when the directory has no scores or is hidden from the
|
| 168 |
-
leaderboard.
|
| 169 |
-
"""
|
| 170 |
-
records: list[dict] = []
|
| 171 |
-
metadata, scores, errors = load_and_validate_agent_data(agent_dir)
|
| 172 |
-
|
| 173 |
-
if metadata is None or scores is None:
|
| 174 |
-
return records, errors
|
| 175 |
-
|
| 176 |
-
if metadata.get('hide_from_leaderboard', False):
|
| 177 |
-
logger.info(f"Skipping {agent_dir.name}: hide_from_leaderboard is True")
|
| 178 |
-
return records, errors
|
| 179 |
-
|
| 180 |
-
# Resolve the agent display name. Prefer the value stamped into
|
| 181 |
-
# metadata.json by push-to-index; fall back to the directory's
|
| 182 |
-
# default (e.g. "Claude Code" for acp-claude/) and finally to
|
| 183 |
-
# "OpenHands" for legacy results/ entries that predate the field.
|
| 184 |
-
agent_name = (
|
| 185 |
-
metadata.get('agent_name')
|
| 186 |
-
or default_agent_name
|
| 187 |
-
or self.DEFAULT_AGENT_NAME
|
| 188 |
-
)
|
| 189 |
-
|
| 190 |
-
for score_entry in scores:
|
| 191 |
-
record = {
|
| 192 |
-
'agent_name': agent_name,
|
| 193 |
-
'agent_version': metadata.get('agent_version', 'Unknown'),
|
| 194 |
-
'llm_base': metadata.get('model', 'unknown'),
|
| 195 |
-
'openness': metadata.get('openness', 'unknown'),
|
| 196 |
-
'submission_time': score_entry.get('submission_time', metadata.get('submission_time', '')),
|
| 197 |
-
'release_date': metadata.get('release_date', ''),
|
| 198 |
-
'parameter_count_b': metadata.get('parameter_count_b'),
|
| 199 |
-
'active_parameter_count_b': metadata.get('active_parameter_count_b'),
|
| 200 |
-
'score': score_entry.get('score'),
|
| 201 |
-
'metric': score_entry.get('metric', 'unknown'),
|
| 202 |
-
'cost_per_instance': score_entry.get('cost_per_instance'),
|
| 203 |
-
'average_runtime': score_entry.get('average_runtime'),
|
| 204 |
-
'tags': [score_entry.get('benchmark')],
|
| 205 |
-
'full_archive': score_entry.get('full_archive', ''),
|
| 206 |
-
'eval_visualization_page': score_entry.get('eval_visualization_page', ''),
|
| 207 |
-
}
|
| 208 |
-
records.append(record)
|
| 209 |
-
return records, errors
|
| 210 |
-
|
| 211 |
def _load_from_agent_dirs(self):
|
| 212 |
-
"""Load
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
``{config}/alternative_agents/{type}/{model}/`` (acp-claude,
|
| 219 |
-
acp-codex, acp-gemini, openhands_subagents, ...). The dedicated
|
| 220 |
-
Alternative Agents page uses this.
|
| 221 |
-
|
| 222 |
-
Returns ``None`` if no records were found (which makes the caller
|
| 223 |
-
render an empty-state placeholder).
|
| 224 |
-
"""
|
| 225 |
all_records = []
|
| 226 |
all_validation_errors = []
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
#
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
| 266 |
# Log validation errors if any
|
| 267 |
if all_validation_errors:
|
| 268 |
logger.warning(f"Schema validation errors ({len(all_validation_errors)} total):")
|
|
@@ -270,10 +183,10 @@ class SimpleLeaderboardViewer:
|
|
| 270 |
logger.warning(f" - {error}")
|
| 271 |
if len(all_validation_errors) > 5:
|
| 272 |
logger.warning(f" ... and {len(all_validation_errors) - 5} more")
|
| 273 |
-
|
| 274 |
if not all_records:
|
| 275 |
-
return None #
|
| 276 |
-
|
| 277 |
return pd.DataFrame(all_records)
|
| 278 |
|
| 279 |
def _load(self):
|
|
@@ -293,36 +206,26 @@ class SimpleLeaderboardViewer:
|
|
| 293 |
# Group by agent (version + model combination) to aggregate results across datasets
|
| 294 |
transformed_records = []
|
| 295 |
|
| 296 |
-
# Create a unique identifier
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
# one row when both submit to the leaderboard.
|
| 300 |
-
df['agent_name'] = df['agent_name'].fillna(self.DEFAULT_AGENT_NAME)
|
| 301 |
-
df['agent_id'] = (
|
| 302 |
-
df['agent_name'].astype(str)
|
| 303 |
-
+ '_' + df['agent_version'].astype(str)
|
| 304 |
-
+ '_' + df['llm_base'].astype(str)
|
| 305 |
-
)
|
| 306 |
-
|
| 307 |
for agent_id in df['agent_id'].unique():
|
| 308 |
agent_records = df[df['agent_id'] == agent_id]
|
| 309 |
-
|
| 310 |
# Build a single record for this agent
|
| 311 |
first_record = agent_records.iloc[0]
|
| 312 |
agent_version = first_record['agent_version']
|
| 313 |
-
|
| 314 |
-
|
| 315 |
# Normalize openness to "open" or "closed"
|
| 316 |
from aliases import OPENNESS_MAPPING
|
| 317 |
raw_openness = first_record['openness']
|
| 318 |
normalized_openness = OPENNESS_MAPPING.get(raw_openness, raw_openness)
|
| 319 |
-
|
| 320 |
# All 5 categories for the leaderboard
|
| 321 |
ALL_CATEGORIES = ['Issue Resolution', 'Frontend', 'Greenfield', 'Testing', 'Information Gathering']
|
| 322 |
-
|
| 323 |
record = {
|
| 324 |
# Core agent info - use final display names
|
| 325 |
-
'agent_name': agent_name, # Will become "Agent"
|
| 326 |
'SDK version': agent_version, # Will become "SDK Version"
|
| 327 |
'Language model': first_record['llm_base'], # Will become "Language Model"
|
| 328 |
'openness': normalized_openness, # Will become "Openness" (simplified to "open" or "closed")
|
|
@@ -332,7 +235,7 @@ class SimpleLeaderboardViewer:
|
|
| 332 |
'parameter_count_b': first_record.get('parameter_count_b'), # Total params in billions
|
| 333 |
'active_parameter_count_b': first_record.get('active_parameter_count_b'), # Active params for MoE
|
| 334 |
# Additional columns expected by the transformer
|
| 335 |
-
# Use agent_id (
|
| 336 |
'id': agent_id,
|
| 337 |
'source': first_record.get('source', ''), # Will become "Source"
|
| 338 |
'logs': first_record.get('logs', ''), # Will become "Logs"
|
|
|
|
| 96 |
|
| 97 |
class SimpleLeaderboardViewer:
|
| 98 |
"""Simple replacement for agent-eval's LeaderboardViewer."""
|
| 99 |
+
|
| 100 |
+
def __init__(self, data_dir: str, config: str, split: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
"""
|
| 102 |
Args:
|
| 103 |
data_dir: Path to data directory
|
| 104 |
config: Config name (e.g., "1.0.0-dev1")
|
| 105 |
split: Split name (e.g., "validation" or "test")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
self.data_dir = Path(data_dir)
|
| 108 |
self.config = config
|
| 109 |
self.split = split
|
|
|
|
| 110 |
self.config_path = self.data_dir / config
|
| 111 |
|
| 112 |
# Benchmark to category mappings (single source of truth)
|
|
|
|
| 127 |
if benchmark not in self.tag_map[category]:
|
| 128 |
self.tag_map[category].append(benchmark)
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def _load_from_agent_dirs(self):
|
| 131 |
+
"""Load data from new agent-centric directory structure (results/YYYYMMDD_model/)."""
|
| 132 |
+
results_dir = self.config_path / "results"
|
| 133 |
+
|
| 134 |
+
if not results_dir.exists():
|
| 135 |
+
return None # Fall back to old format
|
| 136 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
all_records = []
|
| 138 |
all_validation_errors = []
|
| 139 |
+
|
| 140 |
+
# Iterate through each agent directory
|
| 141 |
+
for agent_dir in results_dir.iterdir():
|
| 142 |
+
if not agent_dir.is_dir():
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
+
# Load and validate using pydantic models
|
| 146 |
+
metadata, scores, errors = load_and_validate_agent_data(agent_dir)
|
| 147 |
+
|
| 148 |
+
if errors:
|
| 149 |
+
all_validation_errors.extend(errors)
|
| 150 |
+
|
| 151 |
+
if metadata is None or scores is None:
|
| 152 |
+
continue
|
| 153 |
+
|
| 154 |
+
# Skip entries that are hidden from the leaderboard
|
| 155 |
+
if metadata.get('hide_from_leaderboard', False):
|
| 156 |
+
logger.info(f"Skipping {agent_dir.name}: hide_from_leaderboard is True")
|
| 157 |
+
continue
|
| 158 |
+
|
| 159 |
+
# Create one record per benchmark (mimicking old JSONL format)
|
| 160 |
+
for score_entry in scores:
|
| 161 |
+
record = {
|
| 162 |
+
'agent_version': metadata.get('agent_version', 'Unknown'),
|
| 163 |
+
'llm_base': metadata.get('model', 'unknown'),
|
| 164 |
+
'openness': metadata.get('openness', 'unknown'),
|
| 165 |
+
'submission_time': metadata.get('submission_time', ''),
|
| 166 |
+
'release_date': metadata.get('release_date', ''), # Model release date
|
| 167 |
+
'parameter_count_b': metadata.get('parameter_count_b'), # Total params in billions
|
| 168 |
+
'active_parameter_count_b': metadata.get('active_parameter_count_b'), # Active params for MoE
|
| 169 |
+
'score': score_entry.get('score'),
|
| 170 |
+
'metric': score_entry.get('metric', 'unknown'),
|
| 171 |
+
'cost_per_instance': score_entry.get('cost_per_instance'),
|
| 172 |
+
'average_runtime': score_entry.get('average_runtime'),
|
| 173 |
+
'tags': [score_entry.get('benchmark')],
|
| 174 |
+
'full_archive': score_entry.get('full_archive', ''), # Download URL for trajectories
|
| 175 |
+
'eval_visualization_page': score_entry.get('eval_visualization_page', ''), # Laminar visualization URL
|
| 176 |
+
}
|
| 177 |
+
all_records.append(record)
|
| 178 |
+
|
| 179 |
# Log validation errors if any
|
| 180 |
if all_validation_errors:
|
| 181 |
logger.warning(f"Schema validation errors ({len(all_validation_errors)} total):")
|
|
|
|
| 183 |
logger.warning(f" - {error}")
|
| 184 |
if len(all_validation_errors) > 5:
|
| 185 |
logger.warning(f" ... and {len(all_validation_errors) - 5} more")
|
| 186 |
+
|
| 187 |
if not all_records:
|
| 188 |
+
return None # Fall back to old format
|
| 189 |
+
|
| 190 |
return pd.DataFrame(all_records)
|
| 191 |
|
| 192 |
def _load(self):
|
|
|
|
| 206 |
# Group by agent (version + model combination) to aggregate results across datasets
|
| 207 |
transformed_records = []
|
| 208 |
|
| 209 |
+
# Create a unique identifier for each agent (version + model)
|
| 210 |
+
df['agent_id'] = df['agent_version'] + '_' + df['llm_base']
|
| 211 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
for agent_id in df['agent_id'].unique():
|
| 213 |
agent_records = df[df['agent_id'] == agent_id]
|
| 214 |
+
|
| 215 |
# Build a single record for this agent
|
| 216 |
first_record = agent_records.iloc[0]
|
| 217 |
agent_version = first_record['agent_version']
|
| 218 |
+
|
|
|
|
| 219 |
# Normalize openness to "open" or "closed"
|
| 220 |
from aliases import OPENNESS_MAPPING
|
| 221 |
raw_openness = first_record['openness']
|
| 222 |
normalized_openness = OPENNESS_MAPPING.get(raw_openness, raw_openness)
|
| 223 |
+
|
| 224 |
# All 5 categories for the leaderboard
|
| 225 |
ALL_CATEGORIES = ['Issue Resolution', 'Frontend', 'Greenfield', 'Testing', 'Information Gathering']
|
| 226 |
+
|
| 227 |
record = {
|
| 228 |
# Core agent info - use final display names
|
|
|
|
| 229 |
'SDK version': agent_version, # Will become "SDK Version"
|
| 230 |
'Language model': first_record['llm_base'], # Will become "Language Model"
|
| 231 |
'openness': normalized_openness, # Will become "Openness" (simplified to "open" or "closed")
|
|
|
|
| 235 |
'parameter_count_b': first_record.get('parameter_count_b'), # Total params in billions
|
| 236 |
'active_parameter_count_b': first_record.get('active_parameter_count_b'), # Active params for MoE
|
| 237 |
# Additional columns expected by the transformer
|
| 238 |
+
# Use agent_id (version_model) as unique identifier for Pareto frontier calculation
|
| 239 |
'id': agent_id,
|
| 240 |
'source': first_record.get('source', ''), # Will become "Source"
|
| 241 |
'logs': first_record.get('logs', ''), # Will become "Logs"
|
tests/test_runtime_sorting.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
import pandas as pd
|
| 2 |
-
|
| 3 |
-
from leaderboard_transformer import format_runtime_column
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
def test_runtime_strings_sort_numerically_in_ascending_order():
|
| 7 |
-
df = pd.DataFrame(
|
| 8 |
-
{
|
| 9 |
-
"Average Score": [0.8, 0.8, 0.8, 0.8, None],
|
| 10 |
-
"Average Runtime": [1323.0, 372.0, 410.0, None, None],
|
| 11 |
-
}
|
| 12 |
-
)
|
| 13 |
-
|
| 14 |
-
formatted = format_runtime_column(df.copy(), "Average Runtime")
|
| 15 |
-
runtimes = formatted["Average Runtime"].tolist()
|
| 16 |
-
|
| 17 |
-
assert sorted(runtimes) == [
|
| 18 |
-
runtimes[1],
|
| 19 |
-
runtimes[2],
|
| 20 |
-
runtimes[0],
|
| 21 |
-
runtimes[3],
|
| 22 |
-
runtimes[4],
|
| 23 |
-
]
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def test_runtime_formatting_preserves_visible_labels():
|
| 27 |
-
df = pd.DataFrame(
|
| 28 |
-
{
|
| 29 |
-
"Average Score": [0.8, 0.8, None],
|
| 30 |
-
"Average Runtime": [45.2, None, None],
|
| 31 |
-
}
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
formatted = format_runtime_column(df.copy(), "Average Runtime")
|
| 35 |
-
values = formatted["Average Runtime"].tolist()
|
| 36 |
-
|
| 37 |
-
assert values[0].endswith("45s")
|
| 38 |
-
assert values[1].endswith("Missing</span>")
|
| 39 |
-
assert values[2].endswith("Not Submitted</span>")
|
| 40 |
-
assert 'display:none' in values[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui_components.py
CHANGED
|
@@ -508,36 +508,28 @@ class DummyViewer:
|
|
| 508 |
# The _load method returns the error DataFrame and an empty tag map
|
| 509 |
return self._error_df, {}
|
| 510 |
|
| 511 |
-
def get_leaderboard_viewer_instance(
|
| 512 |
-
split: str,
|
| 513 |
-
agent_filter: str = SimpleLeaderboardViewer.AGENT_FILTER_OPENHANDS,
|
| 514 |
-
):
|
| 515 |
"""
|
| 516 |
-
Fetches the LeaderboardViewer for a
|
| 517 |
-
|
| 518 |
-
both axes so the OpenHands and Alternative Agents pages don't fight
|
| 519 |
-
over a single slot. On error, returns a stable DummyViewer.
|
| 520 |
"""
|
| 521 |
global CACHED_VIEWERS, CACHED_TAG_MAPS
|
| 522 |
|
| 523 |
-
cache_key = (split, agent_filter)
|
| 524 |
-
|
| 525 |
with _cache_lock:
|
| 526 |
-
if
|
| 527 |
# Cache hit: return the cached viewer and tag map
|
| 528 |
-
return CACHED_VIEWERS[
|
| 529 |
|
| 530 |
# --- Cache miss: try to load data from the source ---
|
| 531 |
try:
|
| 532 |
# First try to load from extracted data directory (local mock data)
|
| 533 |
data_dir = EXTRACTED_DATA_DIR if os.path.exists(EXTRACTED_DATA_DIR) else "mock_results"
|
| 534 |
-
|
| 535 |
-
print(f"Loading data for split '{split}'
|
| 536 |
viewer = SimpleLeaderboardViewer(
|
| 537 |
data_dir=data_dir,
|
| 538 |
config=CONFIG_NAME,
|
| 539 |
-
split=split
|
| 540 |
-
agent_filter=agent_filter,
|
| 541 |
)
|
| 542 |
|
| 543 |
# Simplify tag map creation
|
|
@@ -545,14 +537,14 @@ def get_leaderboard_viewer_instance(
|
|
| 545 |
|
| 546 |
# Cache the results for next time (thread-safe)
|
| 547 |
with _cache_lock:
|
| 548 |
-
CACHED_VIEWERS[
|
| 549 |
-
CACHED_TAG_MAPS[
|
| 550 |
|
| 551 |
return viewer, pretty_tag_map
|
| 552 |
|
| 553 |
except Exception as e:
|
| 554 |
# On ANY error, create a consistent error message and cache a DummyViewer
|
| 555 |
-
error_message = f"Error loading data for split '{split}'
|
| 556 |
print(format_error(error_message))
|
| 557 |
|
| 558 |
dummy_df = pd.DataFrame({"Message": [error_message]})
|
|
@@ -561,8 +553,8 @@ def get_leaderboard_viewer_instance(
|
|
| 561 |
|
| 562 |
# Cache the dummy objects so we don't try to fetch again on this run
|
| 563 |
with _cache_lock:
|
| 564 |
-
CACHED_VIEWERS[
|
| 565 |
-
CACHED_TAG_MAPS[
|
| 566 |
|
| 567 |
return dummy_viewer, dummy_tag_map
|
| 568 |
|
|
@@ -705,7 +697,7 @@ def create_leaderboard_display(
|
|
| 705 |
primary_runtime_col = f"{category_name} Runtime"
|
| 706 |
|
| 707 |
# Function to create cost/performance scatter plot from data
|
| 708 |
-
def create_cost_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT
|
| 709 |
return _plot_scatter_plotly(
|
| 710 |
data=df_data,
|
| 711 |
x=primary_cost_col if primary_cost_col in df_data.columns else None,
|
|
@@ -713,12 +705,11 @@ def create_leaderboard_display(
|
|
| 713 |
agent_col="SDK Version",
|
| 714 |
name=category_name,
|
| 715 |
plot_type='cost',
|
| 716 |
-
mark_by=mark_by
|
| 717 |
-
show_all_labels=show_all_labels
|
| 718 |
)
|
| 719 |
|
| 720 |
# Function to create runtime/performance scatter plot from data
|
| 721 |
-
def create_runtime_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT
|
| 722 |
return _plot_scatter_plotly(
|
| 723 |
data=df_data,
|
| 724 |
x=primary_runtime_col if primary_runtime_col in df_data.columns else None,
|
|
@@ -726,8 +717,7 @@ def create_leaderboard_display(
|
|
| 726 |
agent_col="SDK Version",
|
| 727 |
name=category_name,
|
| 728 |
plot_type='runtime',
|
| 729 |
-
mark_by=mark_by
|
| 730 |
-
show_all_labels=show_all_labels
|
| 731 |
)
|
| 732 |
|
| 733 |
# Create initial cost scatter plots for all filter combinations
|
|
@@ -794,13 +784,6 @@ def create_leaderboard_display(
|
|
| 794 |
)
|
| 795 |
else:
|
| 796 |
show_open_only_checkbox = None
|
| 797 |
-
|
| 798 |
-
# Add checkbox for showing all labels on scatter plot
|
| 799 |
-
show_all_labels_checkbox = gr.Checkbox(
|
| 800 |
-
label="Show all labels on scatter plots",
|
| 801 |
-
value=False,
|
| 802 |
-
elem_id="show-all-labels-toggle"
|
| 803 |
-
)
|
| 804 |
|
| 805 |
with gr.Column(scale=1):
|
| 806 |
mark_by_dropdown = gr.Dropdown(
|
|
@@ -844,7 +827,7 @@ def create_leaderboard_display(
|
|
| 844 |
)
|
| 845 |
|
| 846 |
# Update function for filters - handles checkboxes and mark_by dropdown
|
| 847 |
-
def update_display(show_incomplete, show_open_only, mark_by
|
| 848 |
# Determine which dataframe to show based on checkbox states
|
| 849 |
if show_open_only:
|
| 850 |
df_to_show = df_display_open if show_incomplete else df_display_complete_open
|
|
@@ -853,9 +836,9 @@ def create_leaderboard_display(
|
|
| 853 |
df_to_show = df_display_all if show_incomplete else df_display_complete
|
| 854 |
view_df = df_view_full if show_incomplete else df_view_complete
|
| 855 |
|
| 856 |
-
# Regenerate plots with current mark_by
|
| 857 |
-
cost_plot = create_cost_scatter_plot(view_df, mark_by
|
| 858 |
-
runtime_plot = create_runtime_scatter_plot(view_df, mark_by
|
| 859 |
return df_to_show, cost_plot, runtime_plot
|
| 860 |
|
| 861 |
# Connect checkboxes and dropdown to the update function
|
|
@@ -866,7 +849,6 @@ def create_leaderboard_display(
|
|
| 866 |
# Add a dummy value for show_open_only when checkbox doesn't exist
|
| 867 |
filter_inputs = [show_incomplete_checkbox, gr.State(value=False)]
|
| 868 |
filter_inputs.append(mark_by_dropdown)
|
| 869 |
-
filter_inputs.append(show_all_labels_checkbox)
|
| 870 |
|
| 871 |
show_incomplete_checkbox.change(
|
| 872 |
fn=update_display,
|
|
@@ -884,11 +866,6 @@ def create_leaderboard_display(
|
|
| 884 |
inputs=filter_inputs,
|
| 885 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 886 |
)
|
| 887 |
-
show_all_labels_checkbox.change(
|
| 888 |
-
fn=update_display,
|
| 889 |
-
inputs=filter_inputs,
|
| 890 |
-
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 891 |
-
)
|
| 892 |
else:
|
| 893 |
dataframe_component = gr.DataFrame(
|
| 894 |
headers=df_headers,
|
|
@@ -903,15 +880,15 @@ def create_leaderboard_display(
|
|
| 903 |
)
|
| 904 |
|
| 905 |
# Update function for mark_by and optional open_only checkbox
|
| 906 |
-
def update_display_no_complete(show_open_only, mark_by
|
| 907 |
if show_open_only:
|
| 908 |
df_to_show = df_display_open
|
| 909 |
view_df = df_view_open
|
| 910 |
else:
|
| 911 |
df_to_show = df_display_all
|
| 912 |
view_df = df_view_full
|
| 913 |
-
cost_plot = create_cost_scatter_plot(view_df, mark_by
|
| 914 |
-
runtime_plot = create_runtime_scatter_plot(view_df, mark_by
|
| 915 |
return df_to_show, cost_plot, runtime_plot
|
| 916 |
|
| 917 |
filter_inputs_no_complete = []
|
|
@@ -920,7 +897,6 @@ def create_leaderboard_display(
|
|
| 920 |
else:
|
| 921 |
filter_inputs_no_complete.append(gr.State(value=False))
|
| 922 |
filter_inputs_no_complete.append(mark_by_dropdown)
|
| 923 |
-
filter_inputs_no_complete.append(show_all_labels_checkbox)
|
| 924 |
|
| 925 |
if show_open_only_checkbox is not None:
|
| 926 |
show_open_only_checkbox.change(
|
|
@@ -933,18 +909,13 @@ def create_leaderboard_display(
|
|
| 933 |
inputs=filter_inputs_no_complete,
|
| 934 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 935 |
)
|
| 936 |
-
show_all_labels_checkbox.change(
|
| 937 |
-
fn=update_display_no_complete,
|
| 938 |
-
inputs=filter_inputs_no_complete,
|
| 939 |
-
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 940 |
-
)
|
| 941 |
|
| 942 |
legend_markdown = create_legend_markdown(category_name)
|
| 943 |
gr.HTML(value=legend_markdown, elem_id="legend-markdown")
|
| 944 |
|
| 945 |
# Add a timer to periodically check for data updates and refresh the UI
|
| 946 |
# This runs every 60 seconds to check if new data is available
|
| 947 |
-
def check_and_refresh_data(show_incomplete, show_open_only=False, mark_by=MARK_BY_DEFAULT
|
| 948 |
"""Check if data has been refreshed and return updated data if so."""
|
| 949 |
current_version = get_data_version()
|
| 950 |
if current_version > initial_data_version:
|
|
@@ -954,7 +925,7 @@ def create_leaderboard_display(
|
|
| 954 |
if not new_df.empty:
|
| 955 |
new_transformer = DataTransformer(new_df, new_tag_map)
|
| 956 |
new_df_view_full, _ = new_transformer.view(tag=category_name, use_plotly=True)
|
| 957 |
-
|
| 958 |
# Prepare both complete and all entries versions
|
| 959 |
if 'Categories Attempted' in new_df_view_full.columns:
|
| 960 |
new_df_view_complete = new_df_view_full[new_df_view_full['Categories Attempted'] == '5/5'].copy()
|
|
@@ -974,16 +945,16 @@ def create_leaderboard_display(
|
|
| 974 |
new_df_display_open = prepare_df_for_display(new_df_view_open)
|
| 975 |
new_df_display_complete_open = prepare_df_for_display(new_df_view_complete_open)
|
| 976 |
|
| 977 |
-
# Create new scatter plots for all combinations (with current mark_by
|
| 978 |
-
new_cost_scatter_complete = create_cost_scatter_plot(new_df_view_complete, mark_by
|
| 979 |
-
new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by
|
| 980 |
-
new_cost_scatter_open = create_cost_scatter_plot(new_df_view_open, mark_by
|
| 981 |
-
new_cost_scatter_complete_open = create_cost_scatter_plot(new_df_view_complete_open, mark_by
|
| 982 |
|
| 983 |
-
new_runtime_scatter_complete = create_runtime_scatter_plot(new_df_view_complete, mark_by
|
| 984 |
-
new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by
|
| 985 |
-
new_runtime_scatter_open = create_runtime_scatter_plot(new_df_view_open, mark_by
|
| 986 |
-
new_runtime_scatter_complete_open = create_runtime_scatter_plot(new_df_view_complete_open, mark_by
|
| 987 |
|
| 988 |
# Return the appropriate data based on checkbox states
|
| 989 |
if show_open_only:
|
|
@@ -1014,25 +985,18 @@ def create_leaderboard_display(
|
|
| 1014 |
|
| 1015 |
# Connect the timer to the refresh function
|
| 1016 |
if show_incomplete_checkbox is not None:
|
|
|
|
| 1017 |
if show_open_only_checkbox is not None:
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
# timer tick (no session context), so use a wrapper that fixes show_open_only=False.
|
| 1026 |
-
def _timer_refresh_no_open(show_incomplete, mark_by, show_all_labels):
|
| 1027 |
-
return check_and_refresh_data(show_incomplete, False, mark_by, show_all_labels)
|
| 1028 |
-
refresh_timer.tick(
|
| 1029 |
-
fn=_timer_refresh_no_open,
|
| 1030 |
-
inputs=[show_incomplete_checkbox, mark_by_dropdown, show_all_labels_checkbox],
|
| 1031 |
-
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 1032 |
-
)
|
| 1033 |
else:
|
| 1034 |
# If no incomplete checkbox, always show all data (but still filter by open if needed)
|
| 1035 |
-
def check_and_refresh_all(show_open_only=False, mark_by=MARK_BY_DEFAULT
|
| 1036 |
current_version = get_data_version()
|
| 1037 |
if current_version > initial_data_version:
|
| 1038 |
print(f"[REFRESH] Data version changed, reloading...")
|
|
@@ -1045,8 +1009,8 @@ def create_leaderboard_display(
|
|
| 1045 |
new_df_view_full = new_df_view_full[new_df_view_full['Openness'].str.lower() == 'open'].copy()
|
| 1046 |
|
| 1047 |
new_df_display_all = prepare_df_for_display(new_df_view_full)
|
| 1048 |
-
new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by
|
| 1049 |
-
new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by
|
| 1050 |
return new_df_display_all, new_cost_scatter_all, new_runtime_scatter_all
|
| 1051 |
|
| 1052 |
if show_open_only:
|
|
@@ -1056,20 +1020,20 @@ def create_leaderboard_display(
|
|
| 1056 |
if show_open_only_checkbox is not None:
|
| 1057 |
refresh_timer.tick(
|
| 1058 |
fn=check_and_refresh_all,
|
| 1059 |
-
inputs=[show_open_only_checkbox, mark_by_dropdown
|
| 1060 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 1061 |
)
|
| 1062 |
else:
|
| 1063 |
-
def check_and_refresh_simple(mark_by=MARK_BY_DEFAULT
|
| 1064 |
-
return check_and_refresh_all(False, mark_by
|
| 1065 |
refresh_timer.tick(
|
| 1066 |
fn=check_and_refresh_simple,
|
| 1067 |
-
inputs=[mark_by_dropdown
|
| 1068 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 1069 |
)
|
| 1070 |
|
| 1071 |
-
# Return the
|
| 1072 |
-
return
|
| 1073 |
|
| 1074 |
# # --- Detailed Benchmark Display ---
|
| 1075 |
def create_benchmark_details_display(
|
|
@@ -1304,17 +1268,12 @@ def create_benchmark_details_display(
|
|
| 1304 |
legend_markdown = create_legend_markdown(benchmark_name)
|
| 1305 |
gr.HTML(value=legend_markdown, elem_id="legend-markdown")
|
| 1306 |
|
| 1307 |
-
def get_full_leaderboard_data(
|
| 1308 |
-
split: str,
|
| 1309 |
-
agent_filter: str = SimpleLeaderboardViewer.AGENT_FILTER_OPENHANDS,
|
| 1310 |
-
) -> tuple[pd.DataFrame, dict]:
|
| 1311 |
"""
|
| 1312 |
-
Loads and transforms the complete dataset for a
|
| 1313 |
-
|
| 1314 |
-
that don't pass it stay on the canonical leaderboard. The Alternative
|
| 1315 |
-
Agents page passes ``"alternative"`` to get the third-party harnesses.
|
| 1316 |
"""
|
| 1317 |
-
viewer_or_data, raw_tag_map = get_leaderboard_viewer_instance(split
|
| 1318 |
|
| 1319 |
if isinstance(viewer_or_data, (SimpleLeaderboardViewer, DummyViewer)):
|
| 1320 |
raw_df, _ = viewer_or_data._load()
|
|
|
|
| 508 |
# The _load method returns the error DataFrame and an empty tag map
|
| 509 |
return self._error_df, {}
|
| 510 |
|
| 511 |
+
def get_leaderboard_viewer_instance(split: str):
|
|
|
|
|
|
|
|
|
|
| 512 |
"""
|
| 513 |
+
Fetches the LeaderboardViewer for a split, using a thread-safe cache to avoid
|
| 514 |
+
re-downloading data. On error, returns a stable DummyViewer object.
|
|
|
|
|
|
|
| 515 |
"""
|
| 516 |
global CACHED_VIEWERS, CACHED_TAG_MAPS
|
| 517 |
|
|
|
|
|
|
|
| 518 |
with _cache_lock:
|
| 519 |
+
if split in CACHED_VIEWERS:
|
| 520 |
# Cache hit: return the cached viewer and tag map
|
| 521 |
+
return CACHED_VIEWERS[split], CACHED_TAG_MAPS.get(split, {"Overall": []})
|
| 522 |
|
| 523 |
# --- Cache miss: try to load data from the source ---
|
| 524 |
try:
|
| 525 |
# First try to load from extracted data directory (local mock data)
|
| 526 |
data_dir = EXTRACTED_DATA_DIR if os.path.exists(EXTRACTED_DATA_DIR) else "mock_results"
|
| 527 |
+
|
| 528 |
+
print(f"Loading data for split '{split}' from: {data_dir}/{CONFIG_NAME}")
|
| 529 |
viewer = SimpleLeaderboardViewer(
|
| 530 |
data_dir=data_dir,
|
| 531 |
config=CONFIG_NAME,
|
| 532 |
+
split=split
|
|
|
|
| 533 |
)
|
| 534 |
|
| 535 |
# Simplify tag map creation
|
|
|
|
| 537 |
|
| 538 |
# Cache the results for next time (thread-safe)
|
| 539 |
with _cache_lock:
|
| 540 |
+
CACHED_VIEWERS[split] = viewer
|
| 541 |
+
CACHED_TAG_MAPS[split] = pretty_tag_map # Cache the pretty map directly
|
| 542 |
|
| 543 |
return viewer, pretty_tag_map
|
| 544 |
|
| 545 |
except Exception as e:
|
| 546 |
# On ANY error, create a consistent error message and cache a DummyViewer
|
| 547 |
+
error_message = f"Error loading data for split '{split}': {e}"
|
| 548 |
print(format_error(error_message))
|
| 549 |
|
| 550 |
dummy_df = pd.DataFrame({"Message": [error_message]})
|
|
|
|
| 553 |
|
| 554 |
# Cache the dummy objects so we don't try to fetch again on this run
|
| 555 |
with _cache_lock:
|
| 556 |
+
CACHED_VIEWERS[split] = dummy_viewer
|
| 557 |
+
CACHED_TAG_MAPS[split] = dummy_tag_map
|
| 558 |
|
| 559 |
return dummy_viewer, dummy_tag_map
|
| 560 |
|
|
|
|
| 697 |
primary_runtime_col = f"{category_name} Runtime"
|
| 698 |
|
| 699 |
# Function to create cost/performance scatter plot from data
|
| 700 |
+
def create_cost_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT):
|
| 701 |
return _plot_scatter_plotly(
|
| 702 |
data=df_data,
|
| 703 |
x=primary_cost_col if primary_cost_col in df_data.columns else None,
|
|
|
|
| 705 |
agent_col="SDK Version",
|
| 706 |
name=category_name,
|
| 707 |
plot_type='cost',
|
| 708 |
+
mark_by=mark_by
|
|
|
|
| 709 |
)
|
| 710 |
|
| 711 |
# Function to create runtime/performance scatter plot from data
|
| 712 |
+
def create_runtime_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT):
|
| 713 |
return _plot_scatter_plotly(
|
| 714 |
data=df_data,
|
| 715 |
x=primary_runtime_col if primary_runtime_col in df_data.columns else None,
|
|
|
|
| 717 |
agent_col="SDK Version",
|
| 718 |
name=category_name,
|
| 719 |
plot_type='runtime',
|
| 720 |
+
mark_by=mark_by
|
|
|
|
| 721 |
)
|
| 722 |
|
| 723 |
# Create initial cost scatter plots for all filter combinations
|
|
|
|
| 784 |
)
|
| 785 |
else:
|
| 786 |
show_open_only_checkbox = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
|
| 788 |
with gr.Column(scale=1):
|
| 789 |
mark_by_dropdown = gr.Dropdown(
|
|
|
|
| 827 |
)
|
| 828 |
|
| 829 |
# Update function for filters - handles checkboxes and mark_by dropdown
|
| 830 |
+
def update_display(show_incomplete, show_open_only, mark_by):
|
| 831 |
# Determine which dataframe to show based on checkbox states
|
| 832 |
if show_open_only:
|
| 833 |
df_to_show = df_display_open if show_incomplete else df_display_complete_open
|
|
|
|
| 836 |
df_to_show = df_display_all if show_incomplete else df_display_complete
|
| 837 |
view_df = df_view_full if show_incomplete else df_view_complete
|
| 838 |
|
| 839 |
+
# Regenerate plots with current mark_by setting
|
| 840 |
+
cost_plot = create_cost_scatter_plot(view_df, mark_by)
|
| 841 |
+
runtime_plot = create_runtime_scatter_plot(view_df, mark_by)
|
| 842 |
return df_to_show, cost_plot, runtime_plot
|
| 843 |
|
| 844 |
# Connect checkboxes and dropdown to the update function
|
|
|
|
| 849 |
# Add a dummy value for show_open_only when checkbox doesn't exist
|
| 850 |
filter_inputs = [show_incomplete_checkbox, gr.State(value=False)]
|
| 851 |
filter_inputs.append(mark_by_dropdown)
|
|
|
|
| 852 |
|
| 853 |
show_incomplete_checkbox.change(
|
| 854 |
fn=update_display,
|
|
|
|
| 866 |
inputs=filter_inputs,
|
| 867 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 868 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
else:
|
| 870 |
dataframe_component = gr.DataFrame(
|
| 871 |
headers=df_headers,
|
|
|
|
| 880 |
)
|
| 881 |
|
| 882 |
# Update function for mark_by and optional open_only checkbox
|
| 883 |
+
def update_display_no_complete(show_open_only, mark_by):
|
| 884 |
if show_open_only:
|
| 885 |
df_to_show = df_display_open
|
| 886 |
view_df = df_view_open
|
| 887 |
else:
|
| 888 |
df_to_show = df_display_all
|
| 889 |
view_df = df_view_full
|
| 890 |
+
cost_plot = create_cost_scatter_plot(view_df, mark_by)
|
| 891 |
+
runtime_plot = create_runtime_scatter_plot(view_df, mark_by)
|
| 892 |
return df_to_show, cost_plot, runtime_plot
|
| 893 |
|
| 894 |
filter_inputs_no_complete = []
|
|
|
|
| 897 |
else:
|
| 898 |
filter_inputs_no_complete.append(gr.State(value=False))
|
| 899 |
filter_inputs_no_complete.append(mark_by_dropdown)
|
|
|
|
| 900 |
|
| 901 |
if show_open_only_checkbox is not None:
|
| 902 |
show_open_only_checkbox.change(
|
|
|
|
| 909 |
inputs=filter_inputs_no_complete,
|
| 910 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 911 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 912 |
|
| 913 |
legend_markdown = create_legend_markdown(category_name)
|
| 914 |
gr.HTML(value=legend_markdown, elem_id="legend-markdown")
|
| 915 |
|
| 916 |
# Add a timer to periodically check for data updates and refresh the UI
|
| 917 |
# This runs every 60 seconds to check if new data is available
|
| 918 |
+
def check_and_refresh_data(show_incomplete, show_open_only=False, mark_by=MARK_BY_DEFAULT):
|
| 919 |
"""Check if data has been refreshed and return updated data if so."""
|
| 920 |
current_version = get_data_version()
|
| 921 |
if current_version > initial_data_version:
|
|
|
|
| 925 |
if not new_df.empty:
|
| 926 |
new_transformer = DataTransformer(new_df, new_tag_map)
|
| 927 |
new_df_view_full, _ = new_transformer.view(tag=category_name, use_plotly=True)
|
| 928 |
+
|
| 929 |
# Prepare both complete and all entries versions
|
| 930 |
if 'Categories Attempted' in new_df_view_full.columns:
|
| 931 |
new_df_view_complete = new_df_view_full[new_df_view_full['Categories Attempted'] == '5/5'].copy()
|
|
|
|
| 945 |
new_df_display_open = prepare_df_for_display(new_df_view_open)
|
| 946 |
new_df_display_complete_open = prepare_df_for_display(new_df_view_complete_open)
|
| 947 |
|
| 948 |
+
# Create new scatter plots for all combinations (with current mark_by)
|
| 949 |
+
new_cost_scatter_complete = create_cost_scatter_plot(new_df_view_complete, mark_by) if len(new_df_display_complete) > 0 else go.Figure()
|
| 950 |
+
new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by)
|
| 951 |
+
new_cost_scatter_open = create_cost_scatter_plot(new_df_view_open, mark_by) if len(new_df_view_open) > 0 else go.Figure()
|
| 952 |
+
new_cost_scatter_complete_open = create_cost_scatter_plot(new_df_view_complete_open, mark_by) if len(new_df_view_complete_open) > 0 else go.Figure()
|
| 953 |
|
| 954 |
+
new_runtime_scatter_complete = create_runtime_scatter_plot(new_df_view_complete, mark_by) if len(new_df_display_complete) > 0 else go.Figure()
|
| 955 |
+
new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by)
|
| 956 |
+
new_runtime_scatter_open = create_runtime_scatter_plot(new_df_view_open, mark_by) if len(new_df_view_open) > 0 else go.Figure()
|
| 957 |
+
new_runtime_scatter_complete_open = create_runtime_scatter_plot(new_df_view_complete_open, mark_by) if len(new_df_view_complete_open) > 0 else go.Figure()
|
| 958 |
|
| 959 |
# Return the appropriate data based on checkbox states
|
| 960 |
if show_open_only:
|
|
|
|
| 985 |
|
| 986 |
# Connect the timer to the refresh function
|
| 987 |
if show_incomplete_checkbox is not None:
|
| 988 |
+
timer_inputs = [show_incomplete_checkbox]
|
| 989 |
if show_open_only_checkbox is not None:
|
| 990 |
+
timer_inputs.append(show_open_only_checkbox)
|
| 991 |
+
timer_inputs.append(mark_by_dropdown) # Always include mark_by
|
| 992 |
+
refresh_timer.tick(
|
| 993 |
+
fn=check_and_refresh_data,
|
| 994 |
+
inputs=timer_inputs,
|
| 995 |
+
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 996 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 997 |
else:
|
| 998 |
# If no incomplete checkbox, always show all data (but still filter by open if needed)
|
| 999 |
+
def check_and_refresh_all(show_open_only=False, mark_by=MARK_BY_DEFAULT):
|
| 1000 |
current_version = get_data_version()
|
| 1001 |
if current_version > initial_data_version:
|
| 1002 |
print(f"[REFRESH] Data version changed, reloading...")
|
|
|
|
| 1009 |
new_df_view_full = new_df_view_full[new_df_view_full['Openness'].str.lower() == 'open'].copy()
|
| 1010 |
|
| 1011 |
new_df_display_all = prepare_df_for_display(new_df_view_full)
|
| 1012 |
+
new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by)
|
| 1013 |
+
new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by)
|
| 1014 |
return new_df_display_all, new_cost_scatter_all, new_runtime_scatter_all
|
| 1015 |
|
| 1016 |
if show_open_only:
|
|
|
|
| 1020 |
if show_open_only_checkbox is not None:
|
| 1021 |
refresh_timer.tick(
|
| 1022 |
fn=check_and_refresh_all,
|
| 1023 |
+
inputs=[show_open_only_checkbox, mark_by_dropdown],
|
| 1024 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 1025 |
)
|
| 1026 |
else:
|
| 1027 |
+
def check_and_refresh_simple(mark_by=MARK_BY_DEFAULT):
|
| 1028 |
+
return check_and_refresh_all(False, mark_by)
|
| 1029 |
refresh_timer.tick(
|
| 1030 |
fn=check_and_refresh_simple,
|
| 1031 |
+
inputs=[mark_by_dropdown],
|
| 1032 |
outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
|
| 1033 |
)
|
| 1034 |
|
| 1035 |
+
# Return the show_open_only_checkbox and mark_by_dropdown so they can be used to update other sections
|
| 1036 |
+
return show_open_only_checkbox, mark_by_dropdown
|
| 1037 |
|
| 1038 |
# # --- Detailed Benchmark Display ---
|
| 1039 |
def create_benchmark_details_display(
|
|
|
|
| 1268 |
legend_markdown = create_legend_markdown(benchmark_name)
|
| 1269 |
gr.HTML(value=legend_markdown, elem_id="legend-markdown")
|
| 1270 |
|
| 1271 |
+
def get_full_leaderboard_data(split: str) -> tuple[pd.DataFrame, dict]:
|
|
|
|
|
|
|
|
|
|
| 1272 |
"""
|
| 1273 |
+
Loads and transforms the complete dataset for a given split.
|
| 1274 |
+
This function handles caching and returns the final "pretty" DataFrame and tag map.
|
|
|
|
|
|
|
| 1275 |
"""
|
| 1276 |
+
viewer_or_data, raw_tag_map = get_leaderboard_viewer_instance(split)
|
| 1277 |
|
| 1278 |
if isinstance(viewer_or_data, (SimpleLeaderboardViewer, DummyViewer)):
|
| 1279 |
raw_df, _ = viewer_or_data._load()
|