Add auto-generated index, SDG visualizations, DevTools, and infrastructure
Dashboard & Index: - index.html: Auto-generated gallery with 6 categories (101 total demos) - generate_index.py: Automated index generation script - DASHBOARD.md: Project dashboard documentation - Now includes Mapbox globe category with 9 visualizations Additional Visualizations: - sdg_viz_10-14.html: 5 new SDG network visualizations - claude_devtool_5-8.html: 4 new Claude Code developer tools - README.md for DevTools category Infrastructure: - .claude/commands/prime-threejs.md: Three.js priming command - .githooks/: Git hooks for automation - ai_docs/claude-code-hooks-multi-agent-observability/: Documentation Auto-generated gallery now showcases all web-enhanced infinite loop outputs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c847316dfd
commit
73511cc6f9
|
|
@ -5,3 +5,6 @@ RUN:
|
|||
|
||||
READ:
|
||||
ai_docs/full-initial.md
|
||||
.claude/commands/infinite-web.md
|
||||
DASHBOARD.md
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# Context The Full Initial Infinite Agentic Loop
|
||||
|
||||
**Variables:**
|
||||
|
||||
count: $ARGUMENTS
|
||||
|
||||
|
||||
**ARGUMENTS PARSING:**
|
||||
Parse the following arguments from "$ARGUMENTS":
|
||||
1. `count` - Number of iterations (1-N or "infinite")
|
||||
|
||||
**Workflow:**
|
||||
|
||||
RUN:
|
||||
git ls-files
|
||||
|
||||
READ:
|
||||
ai_docs/full-initial.md
|
||||
.claude/commands/infinite-web.md
|
||||
DASHBOARD.md
|
||||
ai_docs/threejs_infinite_loop_manual.md
|
||||
specs/threejs_url_strategy.json
|
||||
specs/threejs_visualization_progressive.md
|
||||
threejs_viz/
|
||||
|
||||
|
||||
READ:
|
||||
The three most recent threejs_viz html files.
|
||||
|
||||
Run the infinite-web command for threejs iteration for `count` iterations.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
# Git pre-commit hook - Auto-update dashboard before commit
|
||||
#
|
||||
# Installation:
|
||||
# cp .githooks/pre-commit .git/hooks/pre-commit
|
||||
# chmod +x .git/hooks/pre-commit
|
||||
|
||||
# Check if any demo files are being committed
|
||||
demo_changes=$(git diff --cached --name-only | grep -E "(threejs_viz|sdg_viz|src|src_infinite|src_group).*\.html$")
|
||||
|
||||
if [ -n "$demo_changes" ]; then
|
||||
echo "🔍 Demo files detected in commit..."
|
||||
echo "🔄 Regenerating dashboard..."
|
||||
|
||||
# Regenerate dashboard
|
||||
python3 generate_index.py
|
||||
|
||||
# Check if index.html was modified
|
||||
if git diff --name-only | grep -q "index.html"; then
|
||||
echo "✅ Dashboard updated - staging index.html"
|
||||
git add index.html
|
||||
else
|
||||
echo "ℹ️ Dashboard already up to date"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ No demo files in commit - skipping dashboard update"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
# Dashboard Maintenance Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The `index.html` dashboard automatically displays all demos in the project. It's designed to stay up-to-date with minimal effort.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### View Dashboard
|
||||
```bash
|
||||
# Start server (if not running)
|
||||
python3 -m http.server 8889
|
||||
|
||||
# Open in browser
|
||||
firefox http://localhost:8889/
|
||||
```
|
||||
|
||||
### Update Dashboard
|
||||
```bash
|
||||
# Regenerate after adding new demos
|
||||
./generate_index.py
|
||||
|
||||
# Or make it executable and run directly
|
||||
chmod +x generate_index.py
|
||||
./generate_index.py
|
||||
```
|
||||
|
||||
## Auto-Update Strategies
|
||||
|
||||
### Option 1: Manual Regeneration (Simplest)
|
||||
|
||||
Run after generating new demos:
|
||||
```bash
|
||||
./generate_index.py
|
||||
```
|
||||
|
||||
**Best for:** Occasional updates, full control
|
||||
|
||||
### Option 2: File Watcher (Development)
|
||||
|
||||
Auto-regenerate when files change:
|
||||
```bash
|
||||
# Install watcher (Ubuntu/Debian)
|
||||
sudo apt install inotify-tools
|
||||
|
||||
# Start watcher
|
||||
./watch_and_update.sh
|
||||
```
|
||||
|
||||
**Best for:** Active development, instant updates
|
||||
|
||||
### Option 3: Git Hook (Automated)
|
||||
|
||||
Auto-regenerate before commits:
|
||||
```bash
|
||||
# Copy hook to git hooks directory
|
||||
cp .githooks/pre-commit .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
**Best for:** Team workflows, version control
|
||||
|
||||
### Option 4: Integrate with Generation
|
||||
|
||||
Add to your infinite loop workflow:
|
||||
```python
|
||||
# After agent completion in infinite loop
|
||||
python3 generate_index.py
|
||||
```
|
||||
|
||||
**Best for:** Seamless workflow integration
|
||||
|
||||
## How It Works
|
||||
|
||||
### Generator Script (`generate_index.py`)
|
||||
|
||||
**What it does:**
|
||||
1. Scans demo directories (`threejs_viz/`, `sdg_viz/`, `src/`, `src_infinite/`, `src_group/`)
|
||||
2. Extracts titles and descriptions from HTML files
|
||||
3. Updates the `demos` object in `index.html`
|
||||
4. Updates category counts and statistics
|
||||
5. Preserves all styling and functionality
|
||||
|
||||
**What it scans:**
|
||||
- `threejs_viz/threejs_viz_*.html` → Three.js demos
|
||||
- `sdg_viz/sdg_viz_*.html` → SDG network visualizations
|
||||
- `mapbox_test/mapbox_globe_*/index.html` → Mapbox globe visualizations
|
||||
- `claude_code_devtools/claude_devtool_*.html` → Claude Code developer tools
|
||||
- `src/ui_hybrid_*.html` → UI single-file components
|
||||
- `src_infinite/ui_hybrid_*.html` → Infinite mode UI
|
||||
- `src_group/ui_hybrid_*/index.html` → Modular UI components
|
||||
|
||||
**Current stats:** 101 demos across 6 categories
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
infinite-agents/
|
||||
├── index.html # Dashboard (auto-generated data section)
|
||||
├── generate_index.py # Generator script
|
||||
├── watch_and_update.sh # File watcher script
|
||||
├── DASHBOARD.md # This guide
|
||||
│
|
||||
├── threejs_viz/ # Three.js demos
|
||||
│ ├── threejs_viz_1.html
|
||||
│ ├── threejs_viz_2.html
|
||||
│ └── ...
|
||||
│
|
||||
├── sdg_viz/ # SDG network demos
|
||||
│ ├── sdg_viz_1.html
|
||||
│ ├── sdg_viz_2.html
|
||||
│ └── ...
|
||||
│
|
||||
├── mapbox_test/ # Mapbox globe visualizations
|
||||
│ ├── mapbox_globe_1/
|
||||
│ │ └── index.html
|
||||
│ ├── mapbox_globe_2/
|
||||
│ │ └── index.html
|
||||
│ └── ...
|
||||
│
|
||||
├── claude_code_devtools/ # Claude Code developer tools
|
||||
│ ├── claude_devtool_1.html
|
||||
│ ├── claude_devtool_2.html
|
||||
│ └── ...
|
||||
│
|
||||
├── src/ # UI hybrid (single file)
|
||||
│ ├── ui_hybrid_1.html
|
||||
│ ├── ui_hybrid_2.html
|
||||
│ └── ...
|
||||
│
|
||||
├── src_infinite/ # UI hybrid (infinite mode)
|
||||
│ ├── ui_hybrid_1.html
|
||||
│ ├── ui_hybrid_2.html
|
||||
│ └── ...
|
||||
│
|
||||
└── src_group/ # UI hybrid (modular)
|
||||
├── ui_hybrid_1/
|
||||
│ └── index.html
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Add New Demo Category
|
||||
|
||||
Edit `generate_index.py` and add scanning logic:
|
||||
|
||||
```python
|
||||
def generate_demo_data():
|
||||
demos = {
|
||||
'threejs': [],
|
||||
'sdg': [],
|
||||
'uiSingle': [],
|
||||
'uiModular': [],
|
||||
'newCategory': [] # Add new category
|
||||
}
|
||||
|
||||
# Add scanning for new category
|
||||
new_files = scan_directory('new_dir', 'pattern_*.html')
|
||||
for i, filepath in enumerate(new_files, 1):
|
||||
demos['newCategory'].append({
|
||||
'number': i,
|
||||
'title': extract_title_from_html(filepath),
|
||||
'description': extract_description_from_html(filepath),
|
||||
'path': filepath,
|
||||
'type': 'New Type',
|
||||
'techniques': []
|
||||
})
|
||||
|
||||
return demos
|
||||
```
|
||||
|
||||
Then update `index.html` template to add the category section.
|
||||
|
||||
### Modify Extraction Logic
|
||||
|
||||
Edit `extract_title_from_html()` or `extract_description_from_html()` in `generate_index.py`:
|
||||
|
||||
```python
|
||||
def extract_title_from_html(filepath):
|
||||
"""Customize how titles are extracted."""
|
||||
# Add custom regex patterns
|
||||
# Try different HTML elements
|
||||
# Handle edge cases
|
||||
pass
|
||||
```
|
||||
|
||||
### Change Dashboard Styling
|
||||
|
||||
Edit `index.html` directly - the generator only updates the `demos` object and stats, preserving all styling.
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
### After Manual Demo Creation
|
||||
|
||||
```bash
|
||||
# 1. Create new demo
|
||||
nano threejs_viz/threejs_viz_6.html
|
||||
|
||||
# 2. Regenerate dashboard
|
||||
./generate_index.py
|
||||
|
||||
# 3. Refresh browser to see changes
|
||||
```
|
||||
|
||||
### After Infinite Loop Generation
|
||||
|
||||
Add to your agent completion callback:
|
||||
|
||||
```python
|
||||
# In your infinite loop orchestrator
|
||||
def on_generation_complete():
|
||||
print("🔄 Updating dashboard...")
|
||||
subprocess.run(['python3', 'generate_index.py'])
|
||||
print("✅ Dashboard updated!")
|
||||
```
|
||||
|
||||
### With Version Control
|
||||
|
||||
```bash
|
||||
# 1. Generate new demos
|
||||
./your_generation_command.sh
|
||||
|
||||
# 2. Update dashboard
|
||||
./generate_index.py
|
||||
|
||||
# 3. Commit everything together
|
||||
git add threejs_viz/ index.html
|
||||
git commit -m "Add 5 new Three.js demos"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Dashboard shows old count
|
||||
|
||||
**Problem:** Statistics don't match actual files
|
||||
**Solution:** Run `./generate_index.py` manually
|
||||
|
||||
### Can't execute script
|
||||
|
||||
**Problem:** Permission denied
|
||||
**Solution:** `chmod +x generate_index.py watch_and_update.sh`
|
||||
|
||||
### Titles look wrong
|
||||
|
||||
**Problem:** Extraction isn't finding correct titles
|
||||
**Solution:** Check HTML file structure, customize extraction in `generate_index.py`
|
||||
|
||||
### Watcher doesn't work
|
||||
|
||||
**Problem:** `inotifywait` command not found
|
||||
**Solution:** Install with `sudo apt install inotify-tools`
|
||||
|
||||
### Server not accessible
|
||||
|
||||
**Problem:** Can't open http://localhost:8889/
|
||||
**Solution:** Check if server is running: `python3 -m http.server 8889`
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Generate JSON API
|
||||
|
||||
Export demo list as JSON for external tools:
|
||||
|
||||
```bash
|
||||
# Add to generate_index.py
|
||||
import json
|
||||
|
||||
demos = generate_demo_data()
|
||||
with open('demos.json', 'w') as f:
|
||||
json.dump(demos, f, indent=2)
|
||||
```
|
||||
|
||||
### Create Category Pages
|
||||
|
||||
Generate separate pages per category:
|
||||
|
||||
```bash
|
||||
# Add to workflow
|
||||
./generate_index.py --category threejs --output threejs.html
|
||||
./generate_index.py --category sdg --output sdg.html
|
||||
```
|
||||
|
||||
### Statistics Dashboard
|
||||
|
||||
Track growth over time:
|
||||
|
||||
```bash
|
||||
# Log counts on each generation
|
||||
./generate_index.py | tee -a dashboard_stats.log
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Regenerate after bulk changes** - After generating multiple demos, run once instead of per-demo
|
||||
2. **Use watcher during development** - Auto-update while actively creating
|
||||
3. **Git hook for teams** - Ensure dashboard stays in sync across team
|
||||
4. **Backup before customization** - Copy `index.html` before major changes
|
||||
5. **Test extraction** - Verify titles/descriptions look good after adding new demo types
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Update dashboard
|
||||
./generate_index.py
|
||||
|
||||
# Watch for changes
|
||||
./watch_and_update.sh
|
||||
|
||||
# Start server
|
||||
python3 -m http.server 8889
|
||||
|
||||
# View dashboard
|
||||
firefox http://localhost:8889/
|
||||
|
||||
# Check status
|
||||
find threejs_viz sdg_viz src src_infinite src_group -name "*.html" | wc -l
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements to consider:
|
||||
|
||||
- [ ] Screenshot thumbnails for each demo
|
||||
- [ ] Iframe preview on hover
|
||||
- [ ] Automatically populate cards with screen shots
|
||||
- [ ] Demo Tags
|
||||
- [ ] Use Playwright for automated testing and evaluation of demos
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** October 9, 2025
|
||||
**Current Version:** Dynamic auto-discovery
|
||||
**Total Demos:** 101 (and counting!)
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit d21613e0bf77d3392590a232884a942c2aa44fbb
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
# Claude Code Developer Tools
|
||||
|
||||
> **Generated via Web-Enhanced Infinite Agentic Loop**
|
||||
> 8 progressive self-contained tools for Claude Code observability, search, and coordination
|
||||
|
||||
## Overview
|
||||
|
||||
This collection demonstrates the **web-enhanced infinite agentic loop** pattern, where each tool iteration:
|
||||
1. Fetches web documentation using WebFetch
|
||||
2. Learns 2-3 specific techniques from the source
|
||||
3. Applies those techniques to build a production-quality developer tool
|
||||
4. Builds upon patterns from previous iterations
|
||||
|
||||
**Total Generated**: 8 tools (258KB)
|
||||
**Pattern**: Foundation → Intermediate → Advanced → Expert
|
||||
**Approach**: Progressive web learning with parallel agent coordination
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Tool Suite
|
||||
|
||||
### Wave 1: Foundation Tools (Iterations 1-4)
|
||||
|
||||
#### 1. Transcript File Loader (29KB)
|
||||
**Web Source**: [MDN FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader)
|
||||
|
||||
**Techniques Learned**:
|
||||
- `readAsText()` for JSONL file parsing
|
||||
- Progress tracking with `onprogress` events
|
||||
- Error handling with `onerror` callbacks
|
||||
|
||||
**Features**:
|
||||
- Drag-and-drop file upload
|
||||
- JSONL line-by-line parsing
|
||||
- Message display with role-based color coding
|
||||
- Statistics dashboard (total, user, assistant counts)
|
||||
- Progress bar for large files
|
||||
|
||||
**Purpose**: Load and view Claude Code transcript files in a clean, developer-friendly interface.
|
||||
|
||||
---
|
||||
|
||||
#### 2. Session Cache Manager (45KB)
|
||||
**Web Source**: [MDN Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API)
|
||||
|
||||
**Techniques Learned**:
|
||||
- LocalStorage `setItem/getItem` patterns
|
||||
- JSON serialization for complex data
|
||||
- Storage events for cross-tab synchronization
|
||||
- Quota management and size calculation
|
||||
|
||||
**Features**:
|
||||
- Cache transcript sessions in browser storage
|
||||
- Search across cached sessions
|
||||
- Storage statistics (size, count, age)
|
||||
- Export/import cache data as JSON
|
||||
- Cross-tab sync using storage events
|
||||
|
||||
**Purpose**: Persistent browser-based storage for building a searchable session library.
|
||||
|
||||
---
|
||||
|
||||
#### 3. Session Timeline Visualizer (27KB)
|
||||
**Web Source**: [MDN Canvas Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)
|
||||
|
||||
**Techniques Learned**:
|
||||
- `fillRect()` and `fillStyle` for drawing
|
||||
- `fillText()` for text rendering on canvas
|
||||
- Mouse event coordinate conversion for interactions
|
||||
|
||||
**Features**:
|
||||
- Canvas-based horizontal timeline
|
||||
- Messages as color-coded circles (user/assistant/tool)
|
||||
- Zoom controls (0.5x to 5x)
|
||||
- Click-and-drag panning
|
||||
- Hover tooltips with message previews
|
||||
|
||||
**Purpose**: Visual timeline to understand conversation flow and message patterns.
|
||||
|
||||
---
|
||||
|
||||
#### 4. Dashboard Layout Tool (28KB)
|
||||
**Web Source**: [MDN CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout)
|
||||
|
||||
**Techniques Learned**:
|
||||
- `grid-template-areas` for named layout regions
|
||||
- `auto-fit` with `minmax()` for responsive grids
|
||||
- `gap` property for consistent spacing
|
||||
|
||||
**Features**:
|
||||
- 6-panel dashboard layout
|
||||
- Responsive design (desktop/tablet/mobile)
|
||||
- Animated stat cards
|
||||
- Tool usage charts
|
||||
- Performance metrics with progress bars
|
||||
|
||||
**Purpose**: Demonstrate professional dashboard layouts using modern CSS Grid.
|
||||
|
||||
---
|
||||
|
||||
### Wave 2: Intermediate Tools (Iterations 5-8)
|
||||
|
||||
#### 5. SVG Tool Usage Chart (24KB)
|
||||
**Web Source**: [MDN SVG Tutorial](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial)
|
||||
|
||||
**Techniques Learned**:
|
||||
- `viewBox` for responsive scaling
|
||||
- SVG `<rect>` elements for bar charts
|
||||
- SVG `<text>` with precise positioning
|
||||
- CSS styling of SVG elements
|
||||
|
||||
**Features**:
|
||||
- Scalable horizontal bar chart
|
||||
- Tool usage statistics and percentages
|
||||
- Interactive hover tooltips
|
||||
- Perfect scaling at any resolution
|
||||
- Sample data with 13 common tools
|
||||
|
||||
**Purpose**: Visualize tool usage frequency from transcripts using crisp, scalable SVG.
|
||||
|
||||
---
|
||||
|
||||
#### 6. D3.js Search Interface (33KB)
|
||||
**Web Source**: [D3.js Getting Started](https://d3js.org/getting-started)
|
||||
|
||||
**Techniques Learned**:
|
||||
- D3 data binding with enter/update/exit pattern
|
||||
- Linear scale functions for score visualization
|
||||
- Transitions and animations with `transition().duration()`
|
||||
|
||||
**Features**:
|
||||
- Fuzzy search with Levenshtein distance
|
||||
- Score-based ranking (0-100%)
|
||||
- Multi-facet filtering (role, date, tools)
|
||||
- Context highlighting of matches
|
||||
- D3-powered result rendering with smooth animations
|
||||
|
||||
**Purpose**: Advanced search interface with intelligent fuzzy matching and visual relevance scoring.
|
||||
|
||||
---
|
||||
|
||||
#### 7. Interactive Analytics Dashboard (35KB)
|
||||
**Web Source**: [Observable D3 Bar Chart](https://observablehq.com/@d3/bar-chart)
|
||||
|
||||
**Techniques Learned**:
|
||||
- `scaleBand()` for ordinal scales
|
||||
- Interactive tooltips with precise positioning
|
||||
- Dynamic sorting with D3's groupSort pattern
|
||||
- Data join for efficient DOM updates
|
||||
|
||||
**Features**:
|
||||
- Multiple D3 charts: bar, line, donut
|
||||
- Sortable visualizations
|
||||
- Time range filtering (day/week/month)
|
||||
- Session productivity metrics
|
||||
- Export charts as SVG/JSON
|
||||
|
||||
**Purpose**: Comprehensive analytics dashboard for understanding coding patterns and productivity.
|
||||
|
||||
---
|
||||
|
||||
#### 8. Advanced Pattern Search Tool (37KB)
|
||||
**Web Source**: [MDN Regular Expressions Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
|
||||
|
||||
**Techniques Learned**:
|
||||
- Lookahead/lookbehind assertions (`(?=...)`, `(?<=...)`)
|
||||
- Named capturing groups (`(?<name>pattern)`)
|
||||
- Complete regex flag support (g, i, m, s, u)
|
||||
|
||||
**Features**:
|
||||
- Live regex validation as you type
|
||||
- 18 pre-built pattern library
|
||||
- Visual regex tester with sample text
|
||||
- Named capture group extraction
|
||||
- Multi-file search across sessions
|
||||
- Export matches as JSON
|
||||
|
||||
**Purpose**: Power-user pattern matching with advanced regex for precise data extraction.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progressive Learning Demonstrated
|
||||
|
||||
### Foundation Level (1-4)
|
||||
- **File I/O**: FileReader, drag-and-drop, progress tracking
|
||||
- **Storage**: LocalStorage, JSON serialization, cross-tab sync
|
||||
- **Visualization**: Canvas drawing, mouse interactions
|
||||
- **Layout**: CSS Grid, responsive design
|
||||
|
||||
### Intermediate Level (5-8)
|
||||
- **Scalable Graphics**: SVG with viewBox, paths, transforms
|
||||
- **Data Binding**: D3.js selection, enter/update/exit, scales
|
||||
- **Interactivity**: Tooltips, sorting, filtering, animations
|
||||
- **Pattern Matching**: Advanced regex, lookahead/behind, named groups
|
||||
|
||||
### Knowledge Accumulation
|
||||
Each tool builds on previous learnings:
|
||||
- Tool 6 uses FileReader from Tool 1
|
||||
- Tool 7 combines Canvas concepts from Tool 3 with D3 from Tool 6
|
||||
- Tool 8 extends search patterns from Tool 6 with regex power
|
||||
- All tools share dark theme UI and developer-focused design
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Opening Tools
|
||||
Each tool is self-contained. Simply open any `.html` file in a modern browser:
|
||||
|
||||
```bash
|
||||
# Open in default browser
|
||||
xdg-open claude_devtool_1.html
|
||||
|
||||
# Or use specific browser
|
||||
firefox claude_devtool_1.html
|
||||
chrome claude_devtool_1.html
|
||||
```
|
||||
|
||||
### Loading Transcript Data
|
||||
Claude Code transcripts are stored at: `~/.claude/projects/[project-id]/transcript.jsonl`
|
||||
|
||||
1. Navigate to a project directory
|
||||
2. Copy or load the `transcript.jsonl` file
|
||||
3. Use Tool 1 to load and view
|
||||
4. Use Tool 2 to cache for quick access
|
||||
5. Use Tools 3-8 for analysis and visualization
|
||||
|
||||
### Recommended Workflow
|
||||
1. **Tool 1**: Load transcripts initially
|
||||
2. **Tool 2**: Cache important sessions
|
||||
3. **Tool 3**: Visualize timeline
|
||||
4. **Tool 7**: Analyze productivity metrics
|
||||
5. **Tool 6 or 8**: Search for specific patterns
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Design Philosophy
|
||||
|
||||
### Developer-First
|
||||
- **Dark Theme**: Easy on eyes during long coding sessions
|
||||
- **Monospace Fonts**: Code-friendly typography
|
||||
- **Keyboard Shortcuts**: Power-user navigation
|
||||
- **Performance**: Fast load, responsive interactions
|
||||
|
||||
### Self-Contained
|
||||
- **Zero External Dependencies**: All CSS/JS inline (except D3 CDN)
|
||||
- **Offline Capable**: Works without internet after initial load
|
||||
- **Browser Storage**: Leverage localStorage, no backend needed
|
||||
- **Progressive Enhancement**: Basic functionality without advanced features
|
||||
|
||||
### Web-Enhanced
|
||||
- **Real Web Sources**: Every tool learned from actual MDN/D3 documentation
|
||||
- **Documented Learning**: Each tool cites its source URL
|
||||
- **Applied Techniques**: Specific patterns demonstrated in code
|
||||
- **Progressive Difficulty**: Foundation → Intermediate → Advanced → Expert
|
||||
|
||||
---
|
||||
|
||||
## 📐 Architecture
|
||||
|
||||
### Web-Enhanced Infinite Loop Process
|
||||
|
||||
```
|
||||
Phase 0: Initial Web Priming
|
||||
├── Fetch 3 foundational resources
|
||||
├── Build knowledge base
|
||||
└── Prepare for iteration
|
||||
|
||||
Phase 1-2: Specification & Context Analysis
|
||||
├── Read spec requirements
|
||||
├── Analyze existing tools
|
||||
└── Plan progressive learning
|
||||
|
||||
Phase 3: URL Strategy Planning
|
||||
├── Map iterations to difficulty levels
|
||||
└── Assign specific URLs to agents
|
||||
|
||||
Phase 4: Parallel Agent Deployment
|
||||
├── Launch 4 agents per wave
|
||||
├── Each agent fetches unique URL
|
||||
├── Agents apply learned techniques
|
||||
└── Generate self-contained tools
|
||||
|
||||
Phase 5: Quality Validation
|
||||
├── Verify web integration
|
||||
├── Check spec compliance
|
||||
└── Document learnings
|
||||
```
|
||||
|
||||
### Agent Coordination
|
||||
- **Wave 1 (Tools 1-4)**: Foundation patterns, parallel launch
|
||||
- **Wave 2 (Tools 5-8)**: Intermediate techniques, building on foundations
|
||||
- Each agent autonomous with clear web research assignment
|
||||
- No duplicate URLs across iterations
|
||||
- Progressive sophistication increase
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Waves (Planned)
|
||||
|
||||
### Wave 3: Advanced Tools (9-12)
|
||||
- **Tool 9**: Web Workers for background search processing
|
||||
- **Tool 10**: IndexedDB for large-scale data storage
|
||||
- **Tool 11**: Force-directed conversation graph (D3)
|
||||
- **Tool 12**: Hierarchical sunburst for nested data
|
||||
|
||||
### Wave 4: Coordination Tools (13-16)
|
||||
- **Tool 13**: Broadcast Channel for cross-tab messaging
|
||||
- **Tool 14**: WebSocket coordination hub
|
||||
- **Tool 15**: Service Worker background sync
|
||||
- **Tool 16**: Shared context manager
|
||||
|
||||
### Wave 5: Expert Tools (17-20)
|
||||
- **Tool 17**: WebRTC peer-to-peer data sharing
|
||||
- **Tool 18**: Real-time collaboration board
|
||||
- **Tool 19**: Agent communication hub
|
||||
- **Tool 20**: Distributed knowledge graph
|
||||
|
||||
### Wave 6: ML-Enhanced (21+)
|
||||
- **Tool 21**: TensorFlow.js semantic search
|
||||
- **Tool 22**: Smart context recommender
|
||||
- **Tool 23**: Predictive tool selector
|
||||
- **Tool 24**: Auto-tagger with NLP
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### Specifications
|
||||
- **Main Spec**: `../specs/claude_code_devtools_progressive.md`
|
||||
- **URL Strategy**: `../specs/claude_code_devtools_url_strategy.json`
|
||||
|
||||
### Web Sources Referenced
|
||||
1. MDN FileReader API
|
||||
2. MDN Web Storage API
|
||||
3. MDN Canvas Element
|
||||
4. MDN CSS Grid Layout
|
||||
5. MDN SVG Tutorial
|
||||
6. D3.js Getting Started
|
||||
7. Observable D3 Bar Chart
|
||||
8. MDN Regular Expressions
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Achievements
|
||||
|
||||
### Technical
|
||||
✅ 8 self-contained developer tools (258KB total)
|
||||
✅ Progressive web learning from real documentation
|
||||
✅ Zero external dependencies (except D3 CDN)
|
||||
✅ Modern web APIs: FileReader, LocalStorage, Canvas, SVG, D3
|
||||
✅ Advanced patterns: regex lookahead/behind, named groups, data binding
|
||||
|
||||
### Process
|
||||
✅ Web-enhanced infinite loop successfully demonstrated
|
||||
✅ Parallel agent coordination with unique web assignments
|
||||
✅ Knowledge accumulation across iterations
|
||||
✅ Documented learning with source attribution
|
||||
✅ Progressive difficulty: foundation → intermediate
|
||||
|
||||
### Design
|
||||
✅ Consistent dark theme across all tools
|
||||
✅ Developer-friendly UI with monospace fonts
|
||||
✅ Responsive layouts for all screen sizes
|
||||
✅ Professional animations and interactions
|
||||
✅ Comprehensive inline documentation
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technology Stack
|
||||
|
||||
- **HTML5**: Semantic structure, Canvas, SVG
|
||||
- **CSS3**: Grid, Flexbox, Transitions, Custom Properties
|
||||
- **JavaScript ES6+**: Async/await, Classes, Arrow functions
|
||||
- **D3.js v7**: Data binding, scales, transitions
|
||||
- **Web APIs**: FileReader, LocalStorage, Canvas, Storage Events
|
||||
- **Regex**: Advanced patterns with lookbehind/ahead
|
||||
|
||||
---
|
||||
|
||||
## 📝 License
|
||||
|
||||
These tools are generated as examples of the web-enhanced infinite agentic loop pattern. Use them as reference for building Claude Code developer tooling.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
**Web Documentation Sources**:
|
||||
- Mozilla Developer Network (MDN)
|
||||
- D3.js Official Documentation
|
||||
- Observable (D3 examples)
|
||||
|
||||
**Generation Method**:
|
||||
- Claude Code custom slash commands
|
||||
- Web-enhanced infinite agentic loop
|
||||
- Parallel agent coordination
|
||||
- Progressive web learning
|
||||
|
||||
---
|
||||
|
||||
**Generated**: October 9, 2025
|
||||
**Process**: Web-Enhanced Infinite Agentic Loop
|
||||
**Total Iterations**: 8 (Foundation + Intermediate)
|
||||
**Remaining Waves**: 4 (Advanced, Coordination, Expert, ML-Enhanced)
|
||||
|
|
@ -0,0 +1,669 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tool Usage Chart - Claude Code DevTools</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #1a1b26;
|
||||
--bg-secondary: #24283b;
|
||||
--bg-tertiary: #414868;
|
||||
--text-primary: #c0caf5;
|
||||
--text-secondary: #9aa5ce;
|
||||
--text-muted: #565f89;
|
||||
--accent-blue: #7aa2f7;
|
||||
--accent-green: #9ece6a;
|
||||
--accent-purple: #bb9af7;
|
||||
--accent-orange: #ff9e64;
|
||||
--accent-red: #f7768e;
|
||||
--accent-cyan: #7dcfff;
|
||||
--border-color: #414868;
|
||||
--shadow: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tool-interface {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 4px 12px var(--shadow);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-btn, .action-btn {
|
||||
background: var(--accent-blue);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.file-upload-btn:hover, .action-btn:hover {
|
||||
background: var(--accent-purple);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px var(--shadow);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--accent-green);
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.results {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 4px 12px var(--shadow);
|
||||
}
|
||||
|
||||
.results h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--accent-green);
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#toolChart {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* SVG Styling */
|
||||
.bar {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bar:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
fill: var(--text-primary);
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
fill: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
font-size: 12px;
|
||||
fill: var(--text-muted);
|
||||
}
|
||||
|
||||
.grid-line {
|
||||
stroke: var(--border-color);
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 4 4;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px var(--shadow);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.tooltip.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-weight: 600;
|
||||
color: var(--accent-blue);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.tooltip-detail {
|
||||
margin: 0.25rem 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-blue);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.docs {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 4px 12px var(--shadow);
|
||||
}
|
||||
|
||||
.docs h2 {
|
||||
color: var(--accent-purple);
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.doc-content h3 {
|
||||
color: var(--accent-cyan);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.doc-content ul, .doc-content ol {
|
||||
margin-left: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.doc-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.doc-content code {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 2px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--accent-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Tool Usage Chart</h1>
|
||||
<p class="tagline">Visualize Claude Code tool usage patterns with scalable SVG charts</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="tool-interface">
|
||||
<div class="control-group">
|
||||
<label for="fileInput">Load Claude Code Transcript (JSONL)</label>
|
||||
<input type="file" id="fileInput" accept=".jsonl,.json">
|
||||
<label for="fileInput" class="file-upload-btn">Choose File</label>
|
||||
<button class="action-btn" onclick="loadSampleData()">Load Sample Data</button>
|
||||
<div id="fileInfo" class="file-info" style="display: none;"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="results">
|
||||
<h2>Tool Usage Analysis</h2>
|
||||
|
||||
<div id="statsContainer" style="display: none;">
|
||||
<div class="stats-summary">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="totalToolCalls">0</div>
|
||||
<div class="stat-label">Total Tool Calls</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="uniqueTools">0</div>
|
||||
<div class="stat-label">Unique Tools Used</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="mostUsedTool">-</div>
|
||||
<div class="stat-label">Most Used Tool</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<svg id="toolChart" viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- Chart will be rendered here -->
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="empty-state">
|
||||
<div class="empty-state-icon">📊</div>
|
||||
<h3>No Data Loaded</h3>
|
||||
<p>Upload a Claude Code transcript or load sample data to see tool usage visualization</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="docs">
|
||||
<h2>About This Tool</h2>
|
||||
<div class="doc-content">
|
||||
<h3>Purpose</h3>
|
||||
<p>This tool analyzes Claude Code transcripts and visualizes which tools were used and how frequently. It helps developers understand their coding patterns, identify frequently used tools, and gain insights into their workflow efficiency.</p>
|
||||
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li>Parse JSONL transcript files to extract tool usage data</li>
|
||||
<li>Interactive horizontal bar chart with hover tooltips</li>
|
||||
<li>Responsive SVG visualization that scales perfectly to any screen size</li>
|
||||
<li>Statistics summary showing total calls, unique tools, and most used tool</li>
|
||||
<li>Sample data included for immediate exploration</li>
|
||||
<li>Dark theme optimized for developer workflows</li>
|
||||
<li>Tool usage sorted by frequency with percentage calculations</li>
|
||||
</ul>
|
||||
|
||||
<h3>Web Research Integration</h3>
|
||||
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial" target="_blank">MDN SVG Tutorial</a></p>
|
||||
<p><strong>Techniques Applied:</strong></p>
|
||||
<ul>
|
||||
<li><code>viewBox</code> attribute for responsive scaling - The chart uses <code>viewBox="0 0 1000 600"</code> with <code>preserveAspectRatio</code> to create a perfectly scalable visualization that maintains proportions across all screen sizes</li>
|
||||
<li>SVG <code>rect</code> elements for bar charts - Each tool's usage is represented by a dynamically sized rectangle element with smooth transitions and hover effects</li>
|
||||
<li>SVG <code>text</code> elements with positioning - Labels, values, and percentages are positioned using SVG text elements with precise x/y coordinates for optimal readability</li>
|
||||
<li>CSS styling of SVG elements - Applied CSS transitions, hover effects, and theming to SVG elements for a polished, interactive experience</li>
|
||||
</ul>
|
||||
|
||||
<h3>Usage</h3>
|
||||
<ol>
|
||||
<li>Click "Choose File" and select a Claude Code transcript file (JSONL format), or click "Load Sample Data" to see an example</li>
|
||||
<li>The tool will parse the transcript and extract all tool calls from assistant messages</li>
|
||||
<li>View the statistics summary showing total calls, unique tools, and the most frequently used tool</li>
|
||||
<li>Examine the horizontal bar chart showing each tool's usage frequency</li>
|
||||
<li>Hover over any bar to see detailed information including tool name, count, and percentage</li>
|
||||
<li>The chart automatically scales to fit your screen while maintaining perfect proportions</li>
|
||||
</ol>
|
||||
|
||||
<h3>Technical Details</h3>
|
||||
<p>The tool uses the File API to read transcript files, parses JSONL line-by-line, and extracts tool names from message content blocks. The SVG chart is dynamically generated using DOM manipulation, with each bar's width calculated proportionally to the maximum usage count. The viewBox technique ensures the chart remains crisp and perfectly scaled on any display resolution.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Claude Code DevTools | Generated via web-enhanced infinite loop</p>
|
||||
<p>Web Source: <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial" target="_blank">MDN SVG Tutorial</a></p>
|
||||
</footer>
|
||||
|
||||
<div class="tooltip" id="tooltip">
|
||||
<div class="tooltip-title"></div>
|
||||
<div class="tooltip-detail"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global state
|
||||
let toolUsageData = {};
|
||||
let totalCalls = 0;
|
||||
|
||||
// File input handler
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
|
||||
fileInput.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
readTranscriptFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Read and parse transcript file
|
||||
function readTranscriptFile(file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
const content = e.target.result;
|
||||
parseTranscript(content, file.name);
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
alert('Error reading file. Please try again.');
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Parse JSONL transcript
|
||||
function parseTranscript(content, fileName) {
|
||||
toolUsageData = {};
|
||||
totalCalls = 0;
|
||||
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
let messageCount = 0;
|
||||
|
||||
lines.forEach(line => {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
messageCount++;
|
||||
|
||||
// Extract tool calls from assistant messages
|
||||
if (entry.message && entry.message.role === 'assistant' && entry.message.content) {
|
||||
const content = entry.message.content;
|
||||
|
||||
// Handle both string content and array of content blocks
|
||||
if (Array.isArray(content)) {
|
||||
content.forEach(block => {
|
||||
if (block.type === 'tool_use' && block.name) {
|
||||
const toolName = block.name;
|
||||
toolUsageData[toolName] = (toolUsageData[toolName] || 0) + 1;
|
||||
totalCalls++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing line:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Update UI
|
||||
fileInfo.textContent = `Loaded: ${fileName} (${messageCount} messages, ${totalCalls} tool calls)`;
|
||||
fileInfo.style.display = 'block';
|
||||
|
||||
if (totalCalls > 0) {
|
||||
renderChart();
|
||||
} else {
|
||||
alert('No tool usage data found in this transcript.');
|
||||
}
|
||||
}
|
||||
|
||||
// Load sample data for demonstration
|
||||
function loadSampleData() {
|
||||
toolUsageData = {
|
||||
'Read': 145,
|
||||
'Write': 82,
|
||||
'Bash': 67,
|
||||
'Edit': 58,
|
||||
'Grep': 43,
|
||||
'Glob': 38,
|
||||
'WebFetch': 22,
|
||||
'WebSearch': 18,
|
||||
'TodoWrite': 15,
|
||||
'NotebookEdit': 9,
|
||||
'SlashCommand': 7,
|
||||
'BashOutput': 5,
|
||||
'KillShell': 2
|
||||
};
|
||||
|
||||
totalCalls = Object.values(toolUsageData).reduce((a, b) => a + b, 0);
|
||||
|
||||
fileInfo.textContent = 'Loaded: Sample Data (511 tool calls from typical coding session)';
|
||||
fileInfo.style.display = 'block';
|
||||
|
||||
renderChart();
|
||||
}
|
||||
|
||||
// Render SVG bar chart
|
||||
function renderChart() {
|
||||
// Sort tools by usage count
|
||||
const sortedTools = Object.entries(toolUsageData)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
const uniqueTools = sortedTools.length;
|
||||
const mostUsedTool = sortedTools[0][0];
|
||||
|
||||
// Update stats
|
||||
document.getElementById('totalToolCalls').textContent = totalCalls;
|
||||
document.getElementById('uniqueTools').textContent = uniqueTools;
|
||||
document.getElementById('mostUsedTool').textContent = mostUsedTool;
|
||||
document.getElementById('statsContainer').style.display = 'block';
|
||||
document.getElementById('emptyState').style.display = 'none';
|
||||
|
||||
// SVG dimensions (viewBox coordinates)
|
||||
const width = 1000;
|
||||
const height = 600;
|
||||
const margin = { top: 40, right: 180, bottom: 40, left: 180 };
|
||||
const chartWidth = width - margin.left - margin.right;
|
||||
const chartHeight = height - margin.top - margin.bottom;
|
||||
|
||||
// Calculate bar dimensions
|
||||
const barHeight = Math.min(40, chartHeight / uniqueTools - 10);
|
||||
const barSpacing = (chartHeight - (barHeight * uniqueTools)) / (uniqueTools + 1);
|
||||
const maxCount = Math.max(...Object.values(toolUsageData));
|
||||
|
||||
// Color palette for bars
|
||||
const colors = [
|
||||
'#7aa2f7', '#9ece6a', '#bb9af7', '#ff9e64', '#f7768e',
|
||||
'#7dcfff', '#e0af68', '#73daca', '#89ddff', '#c0caf5'
|
||||
];
|
||||
|
||||
// Create SVG content
|
||||
const svg = document.getElementById('toolChart');
|
||||
svg.innerHTML = ''; // Clear existing content
|
||||
|
||||
// Create SVG group for the chart
|
||||
const chartGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
chartGroup.setAttribute('transform', `translate(${margin.left}, ${margin.top})`);
|
||||
svg.appendChild(chartGroup);
|
||||
|
||||
// Draw grid lines
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const x = (chartWidth / 5) * i;
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('class', 'grid-line');
|
||||
line.setAttribute('x1', x);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', x);
|
||||
line.setAttribute('y2', chartHeight);
|
||||
chartGroup.appendChild(line);
|
||||
|
||||
// Add axis labels
|
||||
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
label.setAttribute('class', 'axis-label');
|
||||
label.setAttribute('x', x);
|
||||
label.setAttribute('y', chartHeight + 25);
|
||||
label.setAttribute('text-anchor', 'middle');
|
||||
label.textContent = Math.round((maxCount / 5) * i);
|
||||
chartGroup.appendChild(label);
|
||||
}
|
||||
|
||||
// Draw bars
|
||||
sortedTools.forEach(([toolName, count], index) => {
|
||||
const y = barSpacing + (barSpacing + barHeight) * index;
|
||||
const barWidth = (count / maxCount) * chartWidth;
|
||||
const percentage = ((count / totalCalls) * 100).toFixed(1);
|
||||
const color = colors[index % colors.length];
|
||||
|
||||
// Bar rectangle
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('class', 'bar');
|
||||
rect.setAttribute('x', 0);
|
||||
rect.setAttribute('y', y);
|
||||
rect.setAttribute('width', barWidth);
|
||||
rect.setAttribute('height', barHeight);
|
||||
rect.setAttribute('fill', color);
|
||||
rect.setAttribute('rx', 4);
|
||||
chartGroup.appendChild(rect);
|
||||
|
||||
// Tool name label (left side)
|
||||
const nameLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
nameLabel.setAttribute('class', 'bar-label');
|
||||
nameLabel.setAttribute('x', -10);
|
||||
nameLabel.setAttribute('y', y + barHeight / 2 + 5);
|
||||
nameLabel.setAttribute('text-anchor', 'end');
|
||||
nameLabel.textContent = toolName;
|
||||
chartGroup.appendChild(nameLabel);
|
||||
|
||||
// Count and percentage label (right side of bar)
|
||||
const valueLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
valueLabel.setAttribute('class', 'bar-value');
|
||||
valueLabel.setAttribute('x', barWidth + 10);
|
||||
valueLabel.setAttribute('y', y + barHeight / 2 + 5);
|
||||
valueLabel.setAttribute('text-anchor', 'start');
|
||||
valueLabel.textContent = `${count} (${percentage}%)`;
|
||||
chartGroup.appendChild(valueLabel);
|
||||
|
||||
// Add hover event for tooltip
|
||||
rect.addEventListener('mouseenter', (e) => showTooltip(e, toolName, count, percentage));
|
||||
rect.addEventListener('mousemove', (e) => moveTooltip(e));
|
||||
rect.addEventListener('mouseleave', hideTooltip);
|
||||
});
|
||||
|
||||
// Chart title
|
||||
const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
title.setAttribute('x', chartWidth / 2);
|
||||
title.setAttribute('y', -10);
|
||||
title.setAttribute('text-anchor', 'middle');
|
||||
title.setAttribute('fill', '#7aa2f7');
|
||||
title.setAttribute('font-size', '18');
|
||||
title.setAttribute('font-weight', '600');
|
||||
title.textContent = 'Tool Usage Frequency';
|
||||
chartGroup.appendChild(title);
|
||||
}
|
||||
|
||||
// Tooltip functions
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
function showTooltip(event, toolName, count, percentage) {
|
||||
const titleEl = tooltip.querySelector('.tooltip-title');
|
||||
const detailEl = tooltip.querySelector('.tooltip-detail');
|
||||
|
||||
titleEl.textContent = toolName;
|
||||
detailEl.innerHTML = `
|
||||
<div>Calls: ${count}</div>
|
||||
<div>Percentage: ${percentage}%</div>
|
||||
<div>Total Session Calls: ${totalCalls}</div>
|
||||
`;
|
||||
|
||||
tooltip.classList.add('show');
|
||||
moveTooltip(event);
|
||||
}
|
||||
|
||||
function moveTooltip(event) {
|
||||
const x = event.clientX + 15;
|
||||
const y = event.clientY + 15;
|
||||
|
||||
tooltip.style.left = x + 'px';
|
||||
tooltip.style.top = y + 'px';
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.classList.remove('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,928 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Advanced Transcript Search - Claude Code DevTools</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #0d1117;
|
||||
--bg-secondary: #161b22;
|
||||
--bg-tertiary: #21262d;
|
||||
--border-color: #30363d;
|
||||
--text-primary: #c9d1d9;
|
||||
--text-secondary: #8b949e;
|
||||
--text-muted: #6e7681;
|
||||
--accent-blue: #58a6ff;
|
||||
--accent-green: #3fb950;
|
||||
--accent-purple: #bc8cff;
|
||||
--accent-orange: #ff9966;
|
||||
--accent-red: #f85149;
|
||||
--shadow: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
box-shadow: 0 4px 8px var(--shadow);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.search-interface {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--accent-blue);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #6cb6ff;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--accent-green);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#results {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.result-item.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.role-user {
|
||||
background: rgba(88, 166, 255, 0.2);
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.role-assistant {
|
||||
background: rgba(63, 185, 80, 0.2);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.score-bar-bg {
|
||||
width: 100px;
|
||||
height: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.score-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent-orange), var(--accent-green));
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-green);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
color: var(--text-primary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: rgba(255, 153, 102, 0.3);
|
||||
color: var(--accent-orange);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-tools {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.docs {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-top: 3rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.docs h2 {
|
||||
color: var(--accent-blue);
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.docs h3 {
|
||||
color: var(--accent-purple);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.docs ul, .docs ol {
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.docs li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.docs strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.docs a {
|
||||
color: var(--accent-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.docs a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.doc-content {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--accent-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.keyboard-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-color);
|
||||
font-family: inherit;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Advanced Transcript Search</h1>
|
||||
<p class="tagline">Fuzzy search across Claude Code transcripts with D3.js-powered visualizations</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="search-interface">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
id="searchInput"
|
||||
placeholder="Search messages, code, tool calls... (fuzzy matching enabled)"
|
||||
autofocus
|
||||
>
|
||||
<button onclick="performSearch()">Search</button>
|
||||
<button class="secondary" onclick="clearSearch()">Clear</button>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label for="roleFilter">Role</label>
|
||||
<select id="roleFilter" onchange="performSearch()">
|
||||
<option value="all">All Roles</option>
|
||||
<option value="user">User</option>
|
||||
<option value="assistant">Assistant</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="dateFilter">Date Range</label>
|
||||
<select id="dateFilter" onchange="performSearch()">
|
||||
<option value="all">All Time</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="week">Last 7 Days</option>
|
||||
<option value="month">Last 30 Days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="toolFilter">Tools Used</label>
|
||||
<select id="toolFilter" onchange="performSearch()">
|
||||
<option value="all">All Tools</option>
|
||||
<option value="Read">Read</option>
|
||||
<option value="Write">Write</option>
|
||||
<option value="Bash">Bash</option>
|
||||
<option value="Edit">Edit</option>
|
||||
<option value="Grep">Grep</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="scoreThreshold">Min Score</label>
|
||||
<select id="scoreThreshold" onchange="performSearch()">
|
||||
<option value="0">0% - All Results</option>
|
||||
<option value="25">25% - Weak Match</option>
|
||||
<option value="50" selected>50% - Good Match</option>
|
||||
<option value="75">75% - Strong Match</option>
|
||||
<option value="90">90% - Exact Match</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="keyboard-hint">
|
||||
<kbd>Ctrl</kbd> + <kbd>K</kbd> to focus search • <kbd>Enter</kbd> to search • <kbd>Esc</kbd> to clear
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stats-bar" id="statsBar" style="display: none;">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Results:</span>
|
||||
<span class="stat-value" id="resultCount">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Avg Score:</span>
|
||||
<span class="stat-value" id="avgScore">0%</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Search Time:</span>
|
||||
<span class="stat-value" id="searchTime">0ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<section class="docs">
|
||||
<h2>About This Tool</h2>
|
||||
<div class="doc-content">
|
||||
<h3>Purpose</h3>
|
||||
<p>Advanced search interface for Claude Code JSONL transcripts featuring fuzzy matching, score-based ranking, and D3.js-powered result visualization. Enables developers to quickly find relevant conversations, code snippets, and tool usage patterns across coding sessions.</p>
|
||||
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><strong>Fuzzy Search Algorithm:</strong> Tolerates typos and approximate matches using Levenshtein distance</li>
|
||||
<li><strong>D3.js Data Binding:</strong> Dynamic result rendering with smooth enter/exit transitions</li>
|
||||
<li><strong>Linear Scale Visualization:</strong> Score bars showing match relevance from 0-100%</li>
|
||||
<li><strong>Multi-Facet Filtering:</strong> Filter by role, date range, tools, and minimum score threshold</li>
|
||||
<li><strong>Context Highlighting:</strong> Matching terms highlighted in result snippets</li>
|
||||
<li><strong>Score-Based Ranking:</strong> Results sorted by relevance with visual indicators</li>
|
||||
<li><strong>Keyboard Navigation:</strong> Power-user shortcuts for efficient searching</li>
|
||||
<li><strong>Real-Time Statistics:</strong> Live result count, average score, and search performance metrics</li>
|
||||
</ul>
|
||||
|
||||
<h3>Web Research Integration</h3>
|
||||
<p><strong>Source:</strong> <a href="https://d3js.org/getting-started" target="_blank">D3.js Getting Started Guide</a></p>
|
||||
<p><strong>Techniques Applied:</strong></p>
|
||||
<ul>
|
||||
<li><strong>D3 Data Joining & Selection:</strong> Using selectAll().data().join() pattern for efficient DOM updates when search results change</li>
|
||||
<li><strong>Linear Scale Functions:</strong> Implemented d3.scaleLinear() to map relevance scores (0-100) to visual bar widths, creating intuitive score visualization</li>
|
||||
<li><strong>Smooth Transitions:</strong> Applied D3 transition() with duration and delay for staggered result animations, creating polished user experience</li>
|
||||
</ul>
|
||||
|
||||
<h3>Usage</h3>
|
||||
<ol>
|
||||
<li>Type your search query in the search box (fuzzy matching automatically enabled)</li>
|
||||
<li>Optionally apply filters for role, date range, tools, or minimum score</li>
|
||||
<li>Press Enter or click Search to execute</li>
|
||||
<li>Results appear with relevance scores shown as visual bars</li>
|
||||
<li>Matching terms are highlighted in context snippets</li>
|
||||
<li>Use keyboard shortcuts for faster workflow: Ctrl+K to focus, Esc to clear</li>
|
||||
</ol>
|
||||
|
||||
<h3>Sample Data</h3>
|
||||
<p>This tool includes realistic sample transcript data demonstrating various Claude Code interactions including file operations, git commands, web research, and code generation. In production, load your actual JSONL transcript files.</p>
|
||||
|
||||
<h3>D3.js Integration Details</h3>
|
||||
<p><strong>Why D3.js for Search Results?</strong></p>
|
||||
<p>Traditional DOM manipulation creates jerky, unnatural result updates. D3's data-binding approach enables:</p>
|
||||
<ul>
|
||||
<li><strong>Declarative Updates:</strong> Define what should exist based on data, D3 handles the transitions</li>
|
||||
<li><strong>Enter/Exit Patterns:</strong> Smooth animations when results appear or disappear</li>
|
||||
<li><strong>Scale Functions:</strong> Automatic mapping of data ranges to visual properties</li>
|
||||
<li><strong>Performance:</strong> Efficient DOM updates only where data changed</li>
|
||||
</ul>
|
||||
|
||||
<h3>Fuzzy Search Algorithm</h3>
|
||||
<p>Implements simplified Levenshtein distance calculation for typo tolerance. Scores based on:</p>
|
||||
<ul>
|
||||
<li>Character-by-character matching (case-insensitive)</li>
|
||||
<li>Sequence matching bonuses</li>
|
||||
<li>Position weighting (earlier matches score higher)</li>
|
||||
<li>Normalized to 0-100 scale for consistency</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Claude Code DevTools | Iteration 6 - D3.js Search Interface</p>
|
||||
<p>Web Source: <a href="https://d3js.org/getting-started" target="_blank">https://d3js.org/getting-started</a></p>
|
||||
<p style="margin-top: 0.5rem; font-size: 0.85rem;">Generated via web-enhanced infinite loop • D3.js v7</p>
|
||||
</footer>
|
||||
|
||||
<!-- D3.js Library -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Sample transcript data for demonstration
|
||||
const sampleTranscriptData = [
|
||||
{
|
||||
uuid: "msg-001",
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(),
|
||||
role: "user",
|
||||
content: "Can you help me implement a fuzzy search algorithm in JavaScript?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-002",
|
||||
timestamp: new Date(Date.now() - 3500000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "I'll help you implement a fuzzy search algorithm using Levenshtein distance. This allows matching even with typos.",
|
||||
tools: ["Read", "Write"]
|
||||
},
|
||||
{
|
||||
uuid: "msg-003",
|
||||
timestamp: new Date(Date.now() - 3000000).toISOString(),
|
||||
role: "user",
|
||||
content: "How do I visualize search results with D3.js?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-004",
|
||||
timestamp: new Date(Date.now() - 2900000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "D3.js provides powerful data binding for visualizations. I'll show you how to use scales and transitions for search result bars.",
|
||||
tools: ["WebFetch", "Write"]
|
||||
},
|
||||
{
|
||||
uuid: "msg-005",
|
||||
timestamp: new Date(Date.now() - 2400000).toISOString(),
|
||||
role: "user",
|
||||
content: "Can you add highlighting for matched terms in the search results?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-006",
|
||||
timestamp: new Date(Date.now() - 2300000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "I'll implement text highlighting by wrapping matched terms in span elements with a highlight class. This makes search results easier to scan.",
|
||||
tools: ["Edit"]
|
||||
},
|
||||
{
|
||||
uuid: "msg-007",
|
||||
timestamp: new Date(Date.now() - 1800000).toISOString(),
|
||||
role: "user",
|
||||
content: "How can I filter results by role and date range?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-008",
|
||||
timestamp: new Date(Date.now() - 1700000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "I'll add filter functions that combine with the search algorithm. The filters will work together to narrow results by role, date, and tool usage.",
|
||||
tools: ["Edit", "Read"]
|
||||
},
|
||||
{
|
||||
uuid: "msg-009",
|
||||
timestamp: new Date(Date.now() - 1200000).toISOString(),
|
||||
role: "user",
|
||||
content: "What's the best way to score search relevance?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-010",
|
||||
timestamp: new Date(Date.now() - 1100000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "For search relevance scoring, we can use a combination of fuzzy matching score, term frequency, and position weighting. Higher scores for exact matches and earlier positions.",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-011",
|
||||
timestamp: new Date(Date.now() - 600000).toISOString(),
|
||||
role: "user",
|
||||
content: "Show me how to implement smooth animations when results update",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-012",
|
||||
timestamp: new Date(Date.now() - 500000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "D3's transition() function creates smooth animations. I'll add staggered delays so results appear sequentially with fade-in and slide-up effects.",
|
||||
tools: ["Edit", "Write"]
|
||||
},
|
||||
{
|
||||
uuid: "msg-013",
|
||||
timestamp: new Date(Date.now() - 300000).toISOString(),
|
||||
role: "user",
|
||||
content: "Can you add keyboard shortcuts like Ctrl+K for search focus?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-014",
|
||||
timestamp: new Date(Date.now() - 200000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "I'll implement keyboard event listeners for power-user shortcuts: Ctrl+K to focus search, Enter to execute, and Esc to clear.",
|
||||
tools: ["Edit"]
|
||||
},
|
||||
{
|
||||
uuid: "msg-015",
|
||||
timestamp: new Date(Date.now() - 60000).toISOString(),
|
||||
role: "user",
|
||||
content: "How do I export search results to a file?",
|
||||
tools: []
|
||||
},
|
||||
{
|
||||
uuid: "msg-016",
|
||||
timestamp: new Date(Date.now() - 30000).toISOString(),
|
||||
role: "assistant",
|
||||
content: "I can add an export function that converts results to JSON or CSV format and triggers a browser download using the File API.",
|
||||
tools: ["Grep", "Bash", "Write"]
|
||||
}
|
||||
];
|
||||
|
||||
let transcriptData = sampleTranscriptData;
|
||||
let currentResults = [];
|
||||
|
||||
// D3.js Scale for score visualization
|
||||
const scoreScale = d3.scaleLinear()
|
||||
.domain([0, 100])
|
||||
.range([0, 100]); // Percentage width
|
||||
|
||||
// Fuzzy search algorithm using simplified Levenshtein distance
|
||||
function fuzzyMatch(query, text) {
|
||||
if (!query || !text) return 0;
|
||||
|
||||
query = query.toLowerCase();
|
||||
text = text.toLowerCase();
|
||||
|
||||
// Exact match bonus
|
||||
if (text.includes(query)) return 100;
|
||||
|
||||
let score = 0;
|
||||
let queryIndex = 0;
|
||||
let consecutiveMatches = 0;
|
||||
|
||||
for (let i = 0; i < text.length && queryIndex < query.length; i++) {
|
||||
if (text[i] === query[queryIndex]) {
|
||||
score += 10;
|
||||
queryIndex++;
|
||||
consecutiveMatches++;
|
||||
// Bonus for consecutive matches
|
||||
if (consecutiveMatches > 1) {
|
||||
score += consecutiveMatches * 2;
|
||||
}
|
||||
} else {
|
||||
consecutiveMatches = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Penalty for incomplete match
|
||||
if (queryIndex < query.length) {
|
||||
score -= (query.length - queryIndex) * 5;
|
||||
}
|
||||
|
||||
// Normalize to 0-100
|
||||
score = Math.max(0, Math.min(100, score));
|
||||
return score;
|
||||
}
|
||||
|
||||
// Highlight matching terms in text
|
||||
function highlightMatches(text, query) {
|
||||
if (!query) return text;
|
||||
|
||||
const regex = new RegExp(`(${query.split('').join('.*?')})`, 'gi');
|
||||
return text.replace(regex, '<span class="highlight">$1</span>');
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
function filterByDate(timestamp, range) {
|
||||
if (range === 'all') return true;
|
||||
|
||||
const msgDate = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now - msgDate;
|
||||
|
||||
switch(range) {
|
||||
case 'today':
|
||||
return diff < 86400000; // 24 hours
|
||||
case 'week':
|
||||
return diff < 604800000; // 7 days
|
||||
case 'month':
|
||||
return diff < 2592000000; // 30 days
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Main search function
|
||||
function performSearch() {
|
||||
const startTime = performance.now();
|
||||
|
||||
const query = document.getElementById('searchInput').value.trim();
|
||||
const roleFilter = document.getElementById('roleFilter').value;
|
||||
const dateFilter = document.getElementById('dateFilter').value;
|
||||
const toolFilter = document.getElementById('toolFilter').value;
|
||||
const scoreThreshold = parseInt(document.getElementById('scoreThreshold').value);
|
||||
|
||||
// Filter and score results
|
||||
let results = transcriptData.map(msg => {
|
||||
const score = fuzzyMatch(query, msg.content);
|
||||
return { ...msg, score };
|
||||
});
|
||||
|
||||
// Apply filters
|
||||
results = results.filter(r => {
|
||||
if (r.score < scoreThreshold) return false;
|
||||
if (roleFilter !== 'all' && r.role !== roleFilter) return false;
|
||||
if (!filterByDate(r.timestamp, dateFilter)) return false;
|
||||
if (toolFilter !== 'all' && !r.tools.includes(toolFilter)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort by score descending
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
|
||||
currentResults = results;
|
||||
|
||||
const endTime = performance.now();
|
||||
const searchTime = Math.round(endTime - startTime);
|
||||
|
||||
// Update stats
|
||||
updateStats(results.length, results, searchTime);
|
||||
|
||||
// Render results with D3.js
|
||||
renderResultsWithD3(results, query);
|
||||
}
|
||||
|
||||
// Update statistics bar
|
||||
function updateStats(count, results, time) {
|
||||
const statsBar = document.getElementById('statsBar');
|
||||
statsBar.style.display = count > 0 ? 'flex' : 'none';
|
||||
|
||||
document.getElementById('resultCount').textContent = count;
|
||||
document.getElementById('searchTime').textContent = time + 'ms';
|
||||
|
||||
if (count > 0) {
|
||||
const avgScore = Math.round(results.reduce((sum, r) => sum + r.score, 0) / count);
|
||||
document.getElementById('avgScore').textContent = avgScore + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// Render results using D3.js data binding
|
||||
function renderResultsWithD3(results, query) {
|
||||
const container = d3.select('#results');
|
||||
|
||||
if (results.length === 0) {
|
||||
container.html(`
|
||||
<div class="empty-state">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<p>No results found. Try adjusting your search or filters.</p>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// D3 data binding with enter/update/exit pattern
|
||||
const resultItems = container
|
||||
.selectAll('.result-item')
|
||||
.data(results, d => d.uuid);
|
||||
|
||||
// Exit: remove old results
|
||||
resultItems.exit()
|
||||
.transition()
|
||||
.duration(300)
|
||||
.style('opacity', 0)
|
||||
.remove();
|
||||
|
||||
// Enter: add new results
|
||||
const enter = resultItems.enter()
|
||||
.append('div')
|
||||
.attr('class', 'result-item')
|
||||
.style('opacity', 0);
|
||||
|
||||
enter.append('div')
|
||||
.attr('class', 'result-header')
|
||||
.html(d => `
|
||||
<div class="result-meta">
|
||||
<span class="role-badge role-${d.role}">${d.role}</span>
|
||||
<span class="timestamp">${new Date(d.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="score-container">
|
||||
<div class="score-bar-bg">
|
||||
<div class="score-bar" style="width: ${scoreScale(d.score)}%"></div>
|
||||
</div>
|
||||
<span class="score-text">${d.score}%</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
enter.append('div')
|
||||
.attr('class', 'result-content')
|
||||
.html(d => highlightMatches(d.content, query));
|
||||
|
||||
enter.append('div')
|
||||
.attr('class', 'result-tools')
|
||||
.html(d => d.tools.length > 0
|
||||
? d.tools.map(tool => `<span class="tool-badge">${tool}</span>`).join('')
|
||||
: '<span style="color: var(--text-muted); font-size: 0.85rem;">No tools used</span>'
|
||||
);
|
||||
|
||||
// Update + Enter: apply to all items
|
||||
const merged = enter.merge(resultItems);
|
||||
|
||||
// Smooth staggered animation using D3 transitions
|
||||
merged.transition()
|
||||
.duration(500)
|
||||
.delay((d, i) => i * 50) // Stagger delay
|
||||
.style('opacity', 1)
|
||||
.on('end', function() {
|
||||
d3.select(this).classed('visible', true);
|
||||
});
|
||||
|
||||
// Animate score bars
|
||||
merged.selectAll('.score-bar')
|
||||
.transition()
|
||||
.duration(800)
|
||||
.delay((d, i) => i * 50)
|
||||
.style('width', d => scoreScale(d.score) + '%');
|
||||
}
|
||||
|
||||
// Clear search
|
||||
function clearSearch() {
|
||||
document.getElementById('searchInput').value = '';
|
||||
document.getElementById('roleFilter').value = 'all';
|
||||
document.getElementById('dateFilter').value = 'all';
|
||||
document.getElementById('toolFilter').value = 'all';
|
||||
document.getElementById('scoreThreshold').value = '50';
|
||||
|
||||
document.getElementById('results').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<p>Enter a search query to find messages in your transcripts.</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('statsBar').style.display = 'none';
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl+K or Cmd+K to focus search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
document.getElementById('searchInput').focus();
|
||||
}
|
||||
|
||||
// Escape to clear
|
||||
if (e.key === 'Escape') {
|
||||
clearSearch();
|
||||
}
|
||||
|
||||
// Enter to search when focused
|
||||
if (e.key === 'Enter' && document.activeElement.id === 'searchInput') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with empty state
|
||||
clearSearch();
|
||||
|
||||
// Auto-search on input (debounced)
|
||||
let searchTimeout;
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const query = document.getElementById('searchInput').value.trim();
|
||||
if (query.length > 0) {
|
||||
performSearch();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate index.html dashboard by scanning demo directories.
|
||||
Automatically discovers all HTML demos and updates the dashboard.
|
||||
|
||||
Usage:
|
||||
python3 generate_index.py
|
||||
|
||||
Or make executable and run:
|
||||
chmod +x generate_index.py
|
||||
./generate_index.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
def scan_directory(path, pattern="*.html"):
|
||||
"""Scan directory for HTML files matching pattern."""
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
|
||||
files = sorted(Path(path).glob(pattern))
|
||||
return [str(f.relative_to('.')) for f in files]
|
||||
|
||||
def extract_title_from_html(filepath):
|
||||
"""Extract title from HTML file."""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read(2000) # Read first 2KB
|
||||
|
||||
# Try to find title in various formats
|
||||
title_match = re.search(r'<title>(.*?)</title>', content, re.IGNORECASE)
|
||||
if title_match:
|
||||
return title_match.group(1).strip()
|
||||
|
||||
# Try h1 tag
|
||||
h1_match = re.search(r'<h1[^>]*>(.*?)</h1>', content, re.IGNORECASE)
|
||||
if h1_match:
|
||||
return h1_match.group(1).strip()
|
||||
|
||||
# Try h2 in info div
|
||||
h2_match = re.search(r'<h2[^>]*>(.*?)</h2>', content, re.IGNORECASE)
|
||||
if h2_match:
|
||||
return h2_match.group(1).strip()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read {filepath}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def extract_description_from_html(filepath):
|
||||
"""Extract description from HTML file."""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read(5000) # Read first 5KB
|
||||
|
||||
# Look for description in meta tag
|
||||
meta_match = re.search(r'<meta name="description" content="(.*?)"', content, re.IGNORECASE)
|
||||
if meta_match:
|
||||
return meta_match.group(1).strip()
|
||||
|
||||
# Look for technique/learning in info panel
|
||||
technique_match = re.search(r'<strong>Technique:</strong>\s*(.*?)</p>', content, re.IGNORECASE | re.DOTALL)
|
||||
if technique_match:
|
||||
return technique_match.group(1).strip()
|
||||
|
||||
# Look for first paragraph in info div
|
||||
p_match = re.search(r'<div[^>]*id="info"[^>]*>.*?<p[^>]*>(.*?)</p>', content, re.IGNORECASE | re.DOTALL)
|
||||
if p_match:
|
||||
text = p_match.group(1).strip()
|
||||
# Remove HTML tags
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
return text[:150]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read {filepath}: {e}")
|
||||
|
||||
return "Interactive demo"
|
||||
|
||||
def generate_demo_data():
|
||||
"""Generate demo data by scanning directories."""
|
||||
demos = {
|
||||
'threejs': [],
|
||||
'sdg': [],
|
||||
'mapbox': [],
|
||||
'claudeDevTools': [],
|
||||
'uiSingle': [],
|
||||
'uiModular': []
|
||||
}
|
||||
|
||||
# Scan Three.js demos
|
||||
threejs_files = scan_directory('threejs_viz', 'threejs_viz_*.html')
|
||||
for i, filepath in enumerate(threejs_files, 1):
|
||||
title = extract_title_from_html(filepath) or f"Three.js Viz {i}"
|
||||
description = extract_description_from_html(filepath)
|
||||
|
||||
demos['threejs'].append({
|
||||
'number': i,
|
||||
'title': title.replace('Three.js - ', ''),
|
||||
'description': description,
|
||||
'path': filepath,
|
||||
'type': 'Foundation' if i <= 5 else 'Intermediate' if i <= 12 else 'Advanced',
|
||||
'techniques': []
|
||||
})
|
||||
|
||||
# Scan SDG demos
|
||||
sdg_files = scan_directory('sdg_viz', 'sdg_viz_*.html')
|
||||
for i, filepath in enumerate(sdg_files, 1):
|
||||
title = extract_title_from_html(filepath) or f"SDG Network Viz {i}"
|
||||
description = extract_description_from_html(filepath)
|
||||
|
||||
demos['sdg'].append({
|
||||
'number': i,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'path': filepath,
|
||||
'type': 'Network',
|
||||
'techniques': ['D3.js', 'Force Simulation']
|
||||
})
|
||||
|
||||
# Scan Mapbox Globe demos
|
||||
mapbox_dirs = sorted(Path('mapbox_test').glob('mapbox_globe_*/index.html')) if os.path.exists('mapbox_test') else []
|
||||
for i, filepath in enumerate(mapbox_dirs, 1):
|
||||
title = extract_title_from_html(str(filepath)) or f"Mapbox Globe {i}"
|
||||
description = extract_description_from_html(str(filepath))
|
||||
|
||||
# Remove "Globe Viz N: " prefix if present
|
||||
if title.startswith('Globe Viz'):
|
||||
parts = title.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
title = parts[1].strip()
|
||||
|
||||
demos['mapbox'].append({
|
||||
'number': i,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'path': str(filepath),
|
||||
'type': 'Globe Visualization',
|
||||
'techniques': ['Mapbox GL JS', '3D Globe', 'GeoJSON']
|
||||
})
|
||||
|
||||
# Scan Claude Code DevTools demos
|
||||
devtools_files = scan_directory('claude_code_devtools', 'claude_devtool_*.html')
|
||||
for i, filepath in enumerate(devtools_files, 1):
|
||||
title = extract_title_from_html(filepath) or f"DevTool {i}"
|
||||
description = extract_description_from_html(filepath)
|
||||
|
||||
# Remove " - Claude Code DevTools" suffix if present
|
||||
if ' - Claude Code DevTools' in title:
|
||||
title = title.replace(' - Claude Code DevTools', '')
|
||||
|
||||
demos['claudeDevTools'].append({
|
||||
'number': i,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'path': filepath,
|
||||
'type': 'DevTool',
|
||||
'techniques': ['Developer Tools', 'Web APIs']
|
||||
})
|
||||
|
||||
# Scan UI Single File demos
|
||||
src_files = scan_directory('src', 'ui_hybrid_*.html')
|
||||
for i, filepath in enumerate(src_files, 1):
|
||||
demos['uiSingle'].append({
|
||||
'number': i,
|
||||
'title': f"UI Hybrid {i}",
|
||||
'description': 'Themed hybrid UI component combining multiple interface elements',
|
||||
'path': filepath,
|
||||
'type': 'Single File',
|
||||
'techniques': ['Themed Design', 'Hybrid Components']
|
||||
})
|
||||
|
||||
# Scan UI Infinite demos
|
||||
src_infinite_files = scan_directory('src_infinite', 'ui_hybrid_*.html')
|
||||
offset = len(demos['uiSingle'])
|
||||
for i, filepath in enumerate(src_infinite_files, 1):
|
||||
demos['uiSingle'].append({
|
||||
'number': offset + i,
|
||||
'title': f"UI Hybrid {i} (Infinite)",
|
||||
'description': 'Infinite mode generated themed component',
|
||||
'path': filepath,
|
||||
'type': 'Single File (Infinite)',
|
||||
'techniques': ['Infinite Generation', 'Progressive Complexity']
|
||||
})
|
||||
|
||||
# Scan UI Modular demos
|
||||
modular_dirs = sorted(Path('src_group').glob('ui_hybrid_*/index.html')) if os.path.exists('src_group') else []
|
||||
for i, filepath in enumerate(modular_dirs, 1):
|
||||
demos['uiModular'].append({
|
||||
'number': i,
|
||||
'title': f"UI Hybrid {i} (Modular)",
|
||||
'description': 'Professional 3-file architecture with separated HTML, CSS, and JavaScript',
|
||||
'path': str(filepath),
|
||||
'type': 'Modular',
|
||||
'techniques': ['Separation of Concerns', 'Modular Architecture']
|
||||
})
|
||||
|
||||
return demos
|
||||
|
||||
def generate_index_html(demos):
|
||||
"""Generate the complete index.html file."""
|
||||
|
||||
total_demos = sum(len(demos[cat]) for cat in demos)
|
||||
threejs_count = len(demos['threejs'])
|
||||
sdg_count = len(demos['sdg'])
|
||||
mapbox_count = len(demos['mapbox'])
|
||||
devtools_count = len(demos['claudeDevTools'])
|
||||
ui_count = len(demos['uiSingle']) + len(demos['uiModular'])
|
||||
|
||||
# Read template (current index.html structure)
|
||||
template_path = 'index.html'
|
||||
if os.path.exists(template_path):
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
template = f.read()
|
||||
else:
|
||||
print("Error: index.html template not found")
|
||||
return None
|
||||
|
||||
# Replace the demos data in the JavaScript section
|
||||
demos_json = json.dumps(demos, indent=8)
|
||||
|
||||
# Find and replace the demos object in the script
|
||||
pattern = r'const demos = \{[\s\S]*?\};'
|
||||
replacement = f'const demos = {demos_json};'
|
||||
|
||||
updated_html = re.sub(pattern, replacement, template)
|
||||
|
||||
# Update stats in HTML
|
||||
updated_html = re.sub(
|
||||
r'<div class="stat-number" id="totalDemos">\d+</div>',
|
||||
f'<div class="stat-number" id="totalDemos">{total_demos}</div>',
|
||||
updated_html
|
||||
)
|
||||
|
||||
updated_html = re.sub(
|
||||
r'<div class="stat-number" id="threejsCount">\d+</div>',
|
||||
f'<div class="stat-number" id="threejsCount">{threejs_count}</div>',
|
||||
updated_html
|
||||
)
|
||||
|
||||
updated_html = re.sub(
|
||||
r'<div class="stat-number" id="uiCount">\d+</div>',
|
||||
f'<div class="stat-number" id="uiCount">{ui_count}</div>',
|
||||
updated_html
|
||||
)
|
||||
|
||||
# Update category counts
|
||||
updated_html = re.sub(
|
||||
r'(<div class="category-title">[\s\S]*?Three\.js 3D Visualizations[\s\S]*?</div>\s*<div class="category-count">)\d+ demos',
|
||||
f'\\g<1>{threejs_count} demos',
|
||||
updated_html
|
||||
)
|
||||
|
||||
updated_html = re.sub(
|
||||
r'(<div class="category-title">[\s\S]*?SDG Network Visualizations[\s\S]*?</div>\s*<div class="category-count">)\d+ demos',
|
||||
f'\\g<1>{sdg_count} demos',
|
||||
updated_html
|
||||
)
|
||||
|
||||
updated_html = re.sub(
|
||||
r'(<div class="category-title">[\s\S]*?Themed Hybrid UI Components[\s\S]*?</div>\s*<div class="category-count">)\d+ demos',
|
||||
f'\\g<1>{len(demos["uiSingle"])} demos',
|
||||
updated_html
|
||||
)
|
||||
|
||||
updated_html = re.sub(
|
||||
r'(<div class="category-title">[\s\S]*?Modular UI Components[\s\S]*?</div>\s*<div class="category-count">)\d+ demos',
|
||||
f'\\g<1>{len(demos["uiModular"])} demos',
|
||||
updated_html
|
||||
)
|
||||
|
||||
# Add generation timestamp comment
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
comment = f'<!-- Auto-generated: {timestamp} by generate_index.py -->\n'
|
||||
updated_html = re.sub(r'<!DOCTYPE html>', f'{comment}<!DOCTYPE html>', updated_html)
|
||||
|
||||
return updated_html
|
||||
|
||||
def main():
|
||||
"""Main execution."""
|
||||
print("🔍 Scanning demo directories...")
|
||||
|
||||
demos = generate_demo_data()
|
||||
|
||||
# Print summary
|
||||
print(f"\n📊 Found demos:")
|
||||
print(f" • Three.js: {len(demos['threejs'])}")
|
||||
print(f" • SDG Networks: {len(demos['sdg'])}")
|
||||
print(f" • Mapbox Globes: {len(demos['mapbox'])}")
|
||||
print(f" • Claude DevTools: {len(demos['claudeDevTools'])}")
|
||||
print(f" • UI Single File: {len(demos['uiSingle'])}")
|
||||
print(f" • UI Modular: {len(demos['uiModular'])}")
|
||||
print(f" • Total: {sum(len(demos[cat]) for cat in demos)}")
|
||||
|
||||
print("\n✨ Generating index.html...")
|
||||
|
||||
html = generate_index_html(demos)
|
||||
|
||||
if html:
|
||||
# Write updated index.html
|
||||
with open('index.html', 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
print("✅ index.html updated successfully!")
|
||||
print("\n🚀 View dashboard: http://localhost:8889/")
|
||||
else:
|
||||
print("❌ Failed to generate index.html")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
1215
index.html
1215
index.html
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,630 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDG Visualization 10 - Practical Bipartite Dashboard</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#main-area {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.node {
|
||||
cursor: grab;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.node:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.node-topic {
|
||||
fill: #0066FF;
|
||||
stroke: #FFD700;
|
||||
}
|
||||
|
||||
.node-source {
|
||||
fill: #FF0000;
|
||||
stroke: #FFFFFF;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: #4fc3f7;
|
||||
stroke-opacity: 0.4;
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
stroke-opacity: 0.8;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
fill: #ffffff;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
#header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 20px 25px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #0066FF;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-size: 22px;
|
||||
color: #0066FF;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#header p {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#legend {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 18px 22px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #FF0000;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#legend h3 {
|
||||
font-size: 14px;
|
||||
color: #FF0000;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.legend-circle {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.legend-topic {
|
||||
background: #0066FF;
|
||||
border: 2px solid #FFD700;
|
||||
}
|
||||
|
||||
.legend-source {
|
||||
background: #FF0000;
|
||||
border: 2px solid #FFFFFF;
|
||||
}
|
||||
|
||||
#side-panel {
|
||||
width: 350px;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border-left: 3px solid #0066FF;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#side-panel.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#side-panel h2 {
|
||||
font-size: 24px;
|
||||
color: #0066FF;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-type {
|
||||
font-size: 14px;
|
||||
color: #FFD700;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.panel-section h3 {
|
||||
font-size: 14px;
|
||||
color: #4fc3f7;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.connection-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 3px solid #0066FF;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.close-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: transparent;
|
||||
border: 2px solid #FF0000;
|
||||
color: #FF0000;
|
||||
font-size: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.close-panel:hover {
|
||||
background: #FF0000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 350px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 15px 20px;
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
border-top: 2px solid rgba(0, 102, 255, 0.3);
|
||||
}
|
||||
|
||||
#footer strong {
|
||||
color: #0066FF;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: #4fc3f7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #0066FF;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.tooltip.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-weight: bold;
|
||||
color: #0066FF;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tooltip-details {
|
||||
color: #ccc;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="main-area">
|
||||
<svg id="svg"></svg>
|
||||
|
||||
<div id="header">
|
||||
<h1>SDG Data Dashboard</h1>
|
||||
<p>Topics ↔ Data Sources</p>
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<h3>Node Types</h3>
|
||||
<div class="legend-item">
|
||||
<div class="legend-circle legend-topic"></div>
|
||||
<span>Topics (Blue)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-circle legend-source"></div>
|
||||
<span>Data Sources (Red)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tooltip" id="tooltip">
|
||||
<div class="tooltip-title"></div>
|
||||
<div class="tooltip-details"></div>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<strong>Dashboard Features:</strong> Bipartite graph layout • Working drag-and-drop • Instant rendering (no animations) • Click nodes for details • Sonic color scheme (Blue topics, Red data sources)
|
||||
<br>
|
||||
<strong>Web Learning:</strong> Bipartite graph design patterns - Two-sided layout positioning, visual separation techniques, connection optimization
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="side-panel">
|
||||
<button class="close-panel">×</button>
|
||||
<h2 id="panel-name">Node Name</h2>
|
||||
<div class="panel-type" id="panel-type">NODE TYPE</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h3>Description</h3>
|
||||
<p id="panel-description">Node description will appear here.</p>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h3>Connections</h3>
|
||||
<div id="panel-connections"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ============================================================================
|
||||
// DATA STRUCTURE
|
||||
// ============================================================================
|
||||
|
||||
const topics = [
|
||||
{ id: 't1', name: 'Climate Change', description: 'Global climate action, carbon reduction, and environmental sustainability initiatives' },
|
||||
{ id: 't2', name: 'Public Health', description: 'Healthcare access, disease prevention, and global health monitoring' },
|
||||
{ id: 't3', name: 'Education', description: 'Educational access, literacy rates, and learning outcomes worldwide' },
|
||||
{ id: 't4', name: 'Water Quality', description: 'Clean water access, sanitation, and water resource management' },
|
||||
{ id: 't5', name: 'Biodiversity', description: 'Species conservation, ecosystem health, and environmental protection' },
|
||||
{ id: 't6', name: 'Economic Growth', description: 'GDP trends, employment rates, and sustainable economic development' }
|
||||
];
|
||||
|
||||
const sources = [
|
||||
{ id: 's1', name: 'NOAA API', description: 'National Oceanic and Atmospheric Administration - Climate and weather data' },
|
||||
{ id: 's2', name: 'WHO Database', description: 'World Health Organization - Global health statistics and disease tracking' },
|
||||
{ id: 's3', name: 'World Bank', description: 'Economic indicators, development metrics, and poverty data' },
|
||||
{ id: 's4', name: 'USGS', description: 'US Geological Survey - Water resources and environmental data' },
|
||||
{ id: 's5', name: 'GBIF', description: 'Global Biodiversity Information Facility - Species occurrence data' },
|
||||
{ id: 's6', name: 'NASA EarthData', description: 'Earth science data from satellites and research missions' }
|
||||
];
|
||||
|
||||
// Connection mapping: which sources provide data for which topics
|
||||
const connections = [
|
||||
{ topic: 't1', source: 's1' },
|
||||
{ topic: 't1', source: 's6' },
|
||||
{ topic: 't2', source: 's2' },
|
||||
{ topic: 't2', source: 's3' },
|
||||
{ topic: 't3', source: 's3' },
|
||||
{ topic: 't4', source: 's4' },
|
||||
{ topic: 't4', source: 's3' },
|
||||
{ topic: 't5', source: 's5' },
|
||||
{ topic: 't5', source: 's6' },
|
||||
{ topic: 't6', source: 's3' },
|
||||
{ topic: 't1', source: 's4' },
|
||||
{ topic: 't5', source: 's1' }
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// SVG SETUP
|
||||
// ============================================================================
|
||||
|
||||
const svg = d3.select('#svg');
|
||||
const width = window.innerWidth - 350; // Account for side panel
|
||||
const height = window.innerHeight;
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
// ============================================================================
|
||||
// BIPARTITE LAYOUT POSITIONING
|
||||
// ============================================================================
|
||||
|
||||
const leftX = width * 0.25;
|
||||
const rightX = width * 0.75;
|
||||
const nodeRadius = 25;
|
||||
|
||||
// Position topics on the left
|
||||
topics.forEach((topic, i) => {
|
||||
const spacing = height / (topics.length + 1);
|
||||
topic.x = leftX;
|
||||
topic.y = spacing * (i + 1);
|
||||
topic.fx = leftX; // Fix x position
|
||||
topic.type = 'topic';
|
||||
});
|
||||
|
||||
// Position sources on the right
|
||||
sources.forEach((source, i) => {
|
||||
const spacing = height / (sources.length + 1);
|
||||
source.x = rightX;
|
||||
source.y = spacing * (i + 1);
|
||||
source.fx = rightX; // Fix x position
|
||||
source.type = 'source';
|
||||
});
|
||||
|
||||
// Combine all nodes
|
||||
const nodes = [...topics, ...sources];
|
||||
|
||||
// Create links with proper references
|
||||
const links = connections.map(conn => ({
|
||||
source: nodes.find(n => n.id === conn.topic),
|
||||
target: nodes.find(n => n.id === conn.source)
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// D3 FORCE SIMULATION (NO ENTRANCE ANIMATION)
|
||||
// ============================================================================
|
||||
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links)
|
||||
.distance(200)
|
||||
.strength(0.3))
|
||||
.force('charge', d3.forceManyBody()
|
||||
.strength(-50))
|
||||
.force('y', d3.forceY(d => d.y).strength(0.1))
|
||||
.force('collision', d3.forceCollide().radius(nodeRadius + 5))
|
||||
.alphaDecay(0.05)
|
||||
.velocityDecay(0.6);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER LINKS
|
||||
// ============================================================================
|
||||
|
||||
const link = g.append('g')
|
||||
.selectAll('path')
|
||||
.data(links)
|
||||
.join('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', d => {
|
||||
return `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`;
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// RENDER NODES
|
||||
// ============================================================================
|
||||
|
||||
const node = g.append('g')
|
||||
.selectAll('circle')
|
||||
.data(nodes)
|
||||
.join('circle')
|
||||
.attr('class', d => `node node-${d.type}`)
|
||||
.attr('r', nodeRadius)
|
||||
.attr('cx', d => d.x)
|
||||
.attr('cy', d => d.y);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER LABELS
|
||||
// ============================================================================
|
||||
|
||||
const labels = g.append('g')
|
||||
.selectAll('text')
|
||||
.data(nodes)
|
||||
.join('text')
|
||||
.attr('class', 'node-label')
|
||||
.attr('x', d => d.x)
|
||||
.attr('y', d => d.y + 4)
|
||||
.text(d => d.name);
|
||||
|
||||
// ============================================================================
|
||||
// DRAG BEHAVIOR (MUST WORK!)
|
||||
// ============================================================================
|
||||
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fy = d.y; // Only allow vertical dragging (x is fixed)
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fy = null; // Release vertical constraint when done
|
||||
}
|
||||
|
||||
const drag = d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended);
|
||||
|
||||
node.call(drag);
|
||||
|
||||
// ============================================================================
|
||||
// SIMULATION TICK (UPDATE POSITIONS)
|
||||
// ============================================================================
|
||||
|
||||
simulation.on('tick', () => {
|
||||
link.attr('d', d => {
|
||||
return `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`;
|
||||
});
|
||||
|
||||
node
|
||||
.attr('cx', d => d.x)
|
||||
.attr('cy', d => d.y);
|
||||
|
||||
labels
|
||||
.attr('x', d => d.x)
|
||||
.attr('y', d => d.y + 4);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// CLICK INTERACTION (SIDE PANEL)
|
||||
// ============================================================================
|
||||
|
||||
const sidePanel = document.getElementById('side-panel');
|
||||
const panelName = document.getElementById('panel-name');
|
||||
const panelType = document.getElementById('panel-type');
|
||||
const panelDescription = document.getElementById('panel-description');
|
||||
const panelConnections = document.getElementById('panel-connections');
|
||||
|
||||
node.on('click', function(event, d) {
|
||||
event.stopPropagation();
|
||||
|
||||
// Update panel content
|
||||
panelName.textContent = d.name;
|
||||
panelType.textContent = d.type === 'topic' ? 'TOPIC' : 'DATA SOURCE';
|
||||
panelType.style.color = d.type === 'topic' ? '#0066FF' : '#FF0000';
|
||||
panelDescription.textContent = d.description;
|
||||
|
||||
// Find connections
|
||||
const nodeConnections = links.filter(link =>
|
||||
link.source.id === d.id || link.target.id === d.id
|
||||
);
|
||||
|
||||
panelConnections.innerHTML = '';
|
||||
if (nodeConnections.length === 0) {
|
||||
panelConnections.innerHTML = '<p style="color: #666;">No connections</p>';
|
||||
} else {
|
||||
nodeConnections.forEach(link => {
|
||||
const connectedNode = link.source.id === d.id ? link.target : link.source;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'connection-item';
|
||||
div.style.borderLeftColor = connectedNode.type === 'topic' ? '#0066FF' : '#FF0000';
|
||||
div.textContent = connectedNode.name;
|
||||
panelConnections.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// Show panel
|
||||
sidePanel.classList.add('visible');
|
||||
});
|
||||
|
||||
// Close panel button
|
||||
document.querySelector('.close-panel').addEventListener('click', () => {
|
||||
sidePanel.classList.remove('visible');
|
||||
});
|
||||
|
||||
// Close panel when clicking outside
|
||||
svg.on('click', () => {
|
||||
sidePanel.classList.remove('visible');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// HOVER TOOLTIP
|
||||
// ============================================================================
|
||||
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
node.on('mouseenter', function(event, d) {
|
||||
const tooltipTitle = tooltip.querySelector('.tooltip-title');
|
||||
const tooltipDetails = tooltip.querySelector('.tooltip-details');
|
||||
|
||||
tooltipTitle.textContent = d.name;
|
||||
tooltipDetails.textContent = `${d.type === 'topic' ? 'Topic' : 'Data Source'} • Click for details`;
|
||||
|
||||
tooltip.classList.add('visible');
|
||||
});
|
||||
|
||||
node.on('mousemove', function(event) {
|
||||
tooltip.style.left = (event.pageX + 15) + 'px';
|
||||
tooltip.style.top = (event.pageY + 15) + 'px';
|
||||
});
|
||||
|
||||
node.on('mouseleave', function() {
|
||||
tooltip.classList.remove('visible');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// LINK TOOLTIPS
|
||||
// ============================================================================
|
||||
|
||||
link.on('mouseenter', function(event, d) {
|
||||
const tooltipTitle = tooltip.querySelector('.tooltip-title');
|
||||
const tooltipDetails = tooltip.querySelector('.tooltip-details');
|
||||
|
||||
tooltipTitle.textContent = 'Connection';
|
||||
tooltipDetails.textContent = `${d.source.name} ↔ ${d.target.name}`;
|
||||
|
||||
tooltip.classList.add('visible');
|
||||
});
|
||||
|
||||
link.on('mousemove', function(event) {
|
||||
tooltip.style.left = (event.pageX + 15) + 'px';
|
||||
tooltip.style.top = (event.pageY + 15) + 'px';
|
||||
});
|
||||
|
||||
link.on('mouseleave', function() {
|
||||
tooltip.classList.remove('visible');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// WINDOW RESIZE HANDLER
|
||||
// ============================================================================
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
location.reload(); // Simple solution for demo
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// INSTANT RENDER (NO ENTRANCE ANIMATIONS)
|
||||
// ============================================================================
|
||||
|
||||
// Simulation runs a few ticks immediately to settle the layout
|
||||
for (let i = 0; i < 50; i++) {
|
||||
simulation.tick();
|
||||
}
|
||||
|
||||
// Update initial positions
|
||||
link.attr('d', d => `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`);
|
||||
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
||||
labels.attr('x', d => d.x).attr('y', d => d.y + 4);
|
||||
|
||||
// Then let simulation continue naturally for smooth interactions
|
||||
simulation.alpha(0.3).restart();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,397 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDG Network - Iteration 12: Refined Aesthetics & Beautiful Nodes</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', 'Roboto', sans-serif;
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 50%, #0a0e27 100%);
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.edge {
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease, stroke-width 0.3s ease;
|
||||
}
|
||||
|
||||
.edge:hover {
|
||||
opacity: 1;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.node {
|
||||
cursor: move;
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.node-circle {
|
||||
transition: transform 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
.node:hover .node-circle {
|
||||
transform: scale(1.2);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
fill: #ffffff;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8),
|
||||
-1px -1px 3px rgba(0, 0, 0, 0.8);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
background: rgba(15, 20, 40, 0.92);
|
||||
padding: 20px 25px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.legend-color.topic {
|
||||
background: linear-gradient(135deg, #0066FF 0%, #0044CC 100%);
|
||||
border: 3px solid #FFD700;
|
||||
box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
|
||||
}
|
||||
|
||||
.legend-color.datasource {
|
||||
background: linear-gradient(135deg, #FF0000 0%, #CC0000 100%);
|
||||
border: 3px solid #FFFFFF;
|
||||
box-shadow: 0 0 15px rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 30px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.6);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div class="title">SDG Network Dashboard</div>
|
||||
<svg id="svg"></svg>
|
||||
<div class="legend">
|
||||
<div class="legend-title">Node Types</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color topic"></div>
|
||||
<span>SDG Topics</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color datasource"></div>
|
||||
<span>Data Sources</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Drag nodes to explore connections | Hover for details</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Web Source: https://observablehq.com/@d3/color-schemes
|
||||
// Learning Applied: Color harmony with complementary colors (blue/yellow, red/white)
|
||||
// Contrast ratios for accessibility, gradient color schemes for depth
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
const svg = d3.select("#svg")
|
||||
.attr("viewBox", [0, 0, width, height]);
|
||||
|
||||
// Define gradients for nodes
|
||||
const defs = svg.append("defs");
|
||||
|
||||
// Topic gradient (blue)
|
||||
const topicGradient = defs.append("radialGradient")
|
||||
.attr("id", "topicGradient");
|
||||
topicGradient.append("stop")
|
||||
.attr("offset", "0%")
|
||||
.attr("stop-color", "#3399FF");
|
||||
topicGradient.append("stop")
|
||||
.attr("offset", "100%")
|
||||
.attr("stop-color", "#0044CC");
|
||||
|
||||
// Data source gradient (red)
|
||||
const datasourceGradient = defs.append("radialGradient")
|
||||
.attr("id", "datasourceGradient");
|
||||
datasourceGradient.append("stop")
|
||||
.attr("offset", "0%")
|
||||
.attr("stop-color", "#FF3333");
|
||||
datasourceGradient.append("stop")
|
||||
.attr("offset", "100%")
|
||||
.attr("stop-color", "#CC0000");
|
||||
|
||||
// Inner glow filter for nodes
|
||||
const glowFilter = defs.append("filter")
|
||||
.attr("id", "glow");
|
||||
glowFilter.append("feGaussianBlur")
|
||||
.attr("stdDeviation", "3")
|
||||
.attr("result", "coloredBlur");
|
||||
const feMerge = glowFilter.append("feMerge");
|
||||
feMerge.append("feMergeNode")
|
||||
.attr("in", "coloredBlur");
|
||||
feMerge.append("feMergeNode")
|
||||
.attr("in", "SourceGraphic");
|
||||
|
||||
// Data: Bipartite network with SDG topics and data sources
|
||||
const topics = [
|
||||
{ id: "SDG1", name: "No Poverty", type: "topic" },
|
||||
{ id: "SDG3", name: "Good Health", type: "topic" },
|
||||
{ id: "SDG4", name: "Quality Education", type: "topic" },
|
||||
{ id: "SDG7", name: "Clean Energy", type: "topic" },
|
||||
{ id: "SDG13", name: "Climate Action", type: "topic" },
|
||||
{ id: "SDG15", name: "Life on Land", type: "topic" }
|
||||
];
|
||||
|
||||
const dataSources = [
|
||||
{ id: "UN", name: "UN Statistics", type: "datasource" },
|
||||
{ id: "WorldBank", name: "World Bank", type: "datasource" },
|
||||
{ id: "WHO", name: "WHO Data", type: "datasource" },
|
||||
{ id: "IEA", name: "IEA Reports", type: "datasource" },
|
||||
{ id: "NASA", name: "NASA Climate", type: "datasource" },
|
||||
{ id: "FAO", name: "FAO Statistics", type: "datasource" }
|
||||
];
|
||||
|
||||
const nodes = [...topics, ...dataSources];
|
||||
|
||||
// Create meaningful connections
|
||||
const links = [
|
||||
{ source: "SDG1", target: "UN" },
|
||||
{ source: "SDG1", target: "WorldBank" },
|
||||
{ source: "SDG3", target: "WHO" },
|
||||
{ source: "SDG3", target: "UN" },
|
||||
{ source: "SDG4", target: "UN" },
|
||||
{ source: "SDG4", target: "WorldBank" },
|
||||
{ source: "SDG7", target: "IEA" },
|
||||
{ source: "SDG7", target: "WorldBank" },
|
||||
{ source: "SDG13", target: "NASA" },
|
||||
{ source: "SDG13", target: "IEA" },
|
||||
{ source: "SDG13", target: "UN" },
|
||||
{ source: "SDG15", target: "FAO" },
|
||||
{ source: "SDG15", target: "NASA" },
|
||||
{ source: "SDG15", target: "UN" }
|
||||
];
|
||||
|
||||
// Bipartite layout with perfect spacing
|
||||
const leftX = width * 0.25;
|
||||
const rightX = width * 0.75;
|
||||
const verticalSpacing = height / (Math.max(topics.length, dataSources.length) + 1);
|
||||
|
||||
topics.forEach((node, i) => {
|
||||
node.x = leftX;
|
||||
node.y = verticalSpacing * (i + 1.5);
|
||||
node.fx = leftX;
|
||||
node.fy = node.y;
|
||||
});
|
||||
|
||||
dataSources.forEach((node, i) => {
|
||||
node.x = rightX;
|
||||
node.y = verticalSpacing * (i + 1.5);
|
||||
node.fx = rightX;
|
||||
node.fy = node.y;
|
||||
});
|
||||
|
||||
// Create gradient definitions for edges
|
||||
links.forEach((link, i) => {
|
||||
const gradientId = `edgeGradient${i}`;
|
||||
const gradient = defs.append("linearGradient")
|
||||
.attr("id", gradientId)
|
||||
.attr("gradientUnits", "userSpaceOnUse");
|
||||
|
||||
gradient.append("stop")
|
||||
.attr("offset", "0%")
|
||||
.attr("stop-color", "#0066FF")
|
||||
.attr("stop-opacity", 0.6);
|
||||
|
||||
gradient.append("stop")
|
||||
.attr("offset", "100%")
|
||||
.attr("stop-color", "#FF0000")
|
||||
.attr("stop-opacity", 0.6);
|
||||
|
||||
link.gradientId = gradientId;
|
||||
});
|
||||
|
||||
// Draw edges
|
||||
const edgeGroup = svg.append("g").attr("class", "edges");
|
||||
|
||||
const edges = edgeGroup.selectAll("path")
|
||||
.data(links)
|
||||
.join("path")
|
||||
.attr("class", "edge")
|
||||
.attr("stroke", d => `url(#${d.gradientId})`)
|
||||
.attr("d", d => {
|
||||
const sourceNode = nodes.find(n => n.id === d.source);
|
||||
const targetNode = nodes.find(n => n.id === d.target);
|
||||
|
||||
const dx = targetNode.x - sourceNode.x;
|
||||
const dy = targetNode.y - sourceNode.y;
|
||||
const dr = Math.sqrt(dx * dx + dy * dy) * 0.5;
|
||||
|
||||
return `M ${sourceNode.x},${sourceNode.y} Q ${(sourceNode.x + targetNode.x) / 2},${(sourceNode.y + targetNode.y) / 2 - 50} ${targetNode.x},${targetNode.y}`;
|
||||
});
|
||||
|
||||
// Draw nodes
|
||||
const nodeGroup = svg.append("g").attr("class", "nodes");
|
||||
|
||||
const nodeElements = nodeGroup.selectAll("g")
|
||||
.data(nodes)
|
||||
.join("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", d => `translate(${d.x},${d.y})`)
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended));
|
||||
|
||||
// Large beautiful node circles with borders and effects
|
||||
nodeElements.append("circle")
|
||||
.attr("class", "node-circle")
|
||||
.attr("r", 30)
|
||||
.attr("fill", d => d.type === "topic" ? "url(#topicGradient)" : "url(#datasourceGradient)")
|
||||
.attr("stroke", d => d.type === "topic" ? "#FFD700" : "#FFFFFF")
|
||||
.attr("stroke-width", 4)
|
||||
.attr("filter", "url(#glow)");
|
||||
|
||||
// Node labels
|
||||
nodeElements.append("text")
|
||||
.attr("class", "node-label")
|
||||
.attr("dy", 50)
|
||||
.text(d => d.name)
|
||||
.style("font-size", "15px")
|
||||
.style("font-weight", "600");
|
||||
|
||||
// Update edge gradients based on actual positions
|
||||
function updateEdgeGradients() {
|
||||
links.forEach((link, i) => {
|
||||
const sourceNode = nodes.find(n => n.id === link.source);
|
||||
const targetNode = nodes.find(n => n.id === link.target);
|
||||
|
||||
d3.select(`#${link.gradientId}`)
|
||||
.attr("x1", sourceNode.x)
|
||||
.attr("y1", sourceNode.y)
|
||||
.attr("x2", targetNode.x)
|
||||
.attr("y2", targetNode.y);
|
||||
});
|
||||
}
|
||||
|
||||
updateEdgeGradients();
|
||||
|
||||
// Drag functions with working behavior
|
||||
function dragstarted(event, d) {
|
||||
d3.select(this).raise();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
d.x = event.x;
|
||||
d.y = event.y;
|
||||
|
||||
d3.select(this)
|
||||
.attr("transform", `translate(${d.x},${d.y})`);
|
||||
|
||||
// Update connected edges
|
||||
edges.attr("d", link => {
|
||||
const sourceNode = nodes.find(n => n.id === link.source);
|
||||
const targetNode = nodes.find(n => n.id === link.target);
|
||||
|
||||
const dx = targetNode.x - sourceNode.x;
|
||||
const dy = targetNode.y - sourceNode.y;
|
||||
|
||||
return `M ${sourceNode.x},${sourceNode.y} Q ${(sourceNode.x + targetNode.x) / 2},${(sourceNode.y + targetNode.y) / 2 - 50} ${targetNode.x},${targetNode.y}`;
|
||||
});
|
||||
|
||||
updateEdgeGradients();
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
// Keep node at dragged position
|
||||
}
|
||||
|
||||
// Hover effects for connected edges
|
||||
nodeElements.on("mouseenter", function(event, d) {
|
||||
edges.style("opacity", link => {
|
||||
return (link.source === d.id || link.target === d.id) ? 1 : 0.2;
|
||||
});
|
||||
}).on("mouseleave", function() {
|
||||
edges.style("opacity", 0.6);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue