Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43563ef33c | ||
|
|
bcfca9b8df | ||
|
|
ddc847a16d | ||
|
|
70e8322bd5 | ||
|
|
26da37be18 | ||
|
|
677bbc04ba | ||
|
|
602912f31d |
@@ -44,6 +44,22 @@ Advanced preset-based dimension selection with visual swap button.
|
||||
- Mobile and ultra-wide format support
|
||||
- Integration with upscaling pipelines
|
||||
|
||||
#### 🎲 Seed History
|
||||
Advanced seed tracking with interactive history management and UI.
|
||||
|
||||
- **Automatic Tracking**: Monitors all seed changes with timestamps
|
||||
- **Interactive History**: Click any historical seed to reload instantly
|
||||
- **Smart Deduplication**: 500ms window prevents duplicate rapid additions
|
||||
- **Persistent Storage**: History survives browser sessions and ComfyUI restarts
|
||||
- **Auto-Hide UI**: Clean interface that hides after 2.5 seconds of inactivity
|
||||
- **Visual Feedback**: Toast notifications and selection highlighting
|
||||
|
||||
**Use Cases:**
|
||||
- Track promising seeds during creative exploration
|
||||
- Quickly return to successful generation parameters
|
||||
- Maintain reproducibility across sessions
|
||||
- Compare results from different seeds efficiently
|
||||
|
||||
### 🔧 Architecture Highlights
|
||||
|
||||
- **Modular Design**: Each tool is self-contained and independently testable
|
||||
@@ -97,6 +113,18 @@ preset: "1920×1080" ↘ 1920×1080 ↗
|
||||
**Output:** 1920×1080 (16:9 cinematic)
|
||||
**Swap Button:** Click to get 1080×1920 (9:16 portrait)
|
||||
|
||||
### Seed History Example
|
||||
|
||||
```
|
||||
Seed History → KSampler → VAE Decode → Save Image
|
||||
🎲 12345 ↘ seed ↗
|
||||
[History UI: 54321, 99999, 11111...]
|
||||
```
|
||||
|
||||
**Current Seed:** 12345
|
||||
**History:** Auto-tracked previous seeds with timestamps
|
||||
**Interaction:** Click any historical seed to reload instantly
|
||||
|
||||
### Common Workflows
|
||||
|
||||
<details>
|
||||
@@ -135,6 +163,7 @@ preset: "1920×1080" ↘ 1920×1080 ↗
|
||||
|------|-------------|--------|---------------|
|
||||
| **Resolution Calculator** | Calculate upscaled dimensions with model optimization | ✅ Complete | [Docs](examples/documentation/resolution_calculator.md) |
|
||||
| **Width Height Selector** | Preset-based dimension selection with 26 curated options | ✅ Complete | [Docs](examples/documentation/width_height_selector.md) |
|
||||
| **Seed History** | Advanced seed tracking with interactive history management | ✅ Complete | [Docs](examples/documentation/seed_history.md) |
|
||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||
|
||||
@@ -178,6 +207,28 @@ preset: "1920×1080" ↘ 1920×1080 ↗
|
||||
- FLUX Presets (8): 1920×1080 to 1152×1728 (high resolution)
|
||||
- Ultra-Wide (8): 2560×1080 to 768×2304 (modern ratios)
|
||||
|
||||
#### Seed History
|
||||
|
||||
**Inputs:**
|
||||
- `seed` (INT): 0 to 18,446,744,073,709,551,615, default 12345
|
||||
|
||||
**Outputs:**
|
||||
- `seed` (INT): Validated and processed seed value
|
||||
|
||||
**UI Features:**
|
||||
- Interactive history display with timestamps
|
||||
- Generate random seed button (🎲 Generate)
|
||||
- Clear history button (🗑️ Clear)
|
||||
- Auto-hide after 2.5 seconds of inactivity
|
||||
- Click-to-restore hidden history
|
||||
|
||||
**History Management:**
|
||||
- Maximum 10 entries for optimal performance
|
||||
- Smart deduplication with 500ms window
|
||||
- Persistent localStorage storage
|
||||
- Newest entries displayed first
|
||||
- Human-readable time formatting (5m ago, 2h ago)
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Prerequisites
|
||||
@@ -298,9 +349,10 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
- **Nodes**: 2 (Resolution Calculator, Width Height Selector)
|
||||
- **Nodes**: 3 (Resolution Calculator, Width Height Selector, Seed History)
|
||||
- **Presets**: 26 curated resolution presets
|
||||
- **Test Coverage**: 100%
|
||||
- **Interactive Features**: 2 (Swap Button, History UI)
|
||||
- **Test Coverage**: 100% (150+ comprehensive tests)
|
||||
- **Python Version**: 3.8+
|
||||
- **ComfyUI Compatibility**: Latest
|
||||
- **Dependencies**: Minimal (PyTorch, NumPy)
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# Seed History Tool
|
||||
|
||||
The Seed History tool provides advanced seed value tracking with an interactive UI for managing seed history, automatic deduplication, and convenient seed retrieval.
|
||||
|
||||
## Overview
|
||||
|
||||
The Seed History node functions as both a standard seed input and an intelligent tracking system that automatically monitors seed changes and maintains a searchable history.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎲 Core Functionality
|
||||
- **Seed Output**: Standard ComfyUI seed value output (0 to 18,446,744,073,709,551,615)
|
||||
- **History Tracking**: Automatic tracking of all seed changes with timestamps
|
||||
- **Deduplication**: Intelligent filtering to prevent duplicate entries within 500ms windows
|
||||
- **Persistent Storage**: History persists across ComfyUI sessions using localStorage
|
||||
|
||||
### 🎯 Interactive UI
|
||||
- **History Display**: Scrollable list showing recent seeds with timestamps
|
||||
- **Click to Load**: Click any history entry to instantly load that seed
|
||||
- **Generate Button**: Create new random seeds with one click
|
||||
- **Clear History**: Remove all tracked seeds when needed
|
||||
- **Auto-Hide**: History section automatically hides after 2.5 seconds of inactivity
|
||||
|
||||
### ⚡ Smart Features
|
||||
- **Real-time Updates**: Tracks seed changes from increment/decrement buttons
|
||||
- **Visual Feedback**: Selected seeds are highlighted in green
|
||||
- **Time Formatting**: Human-readable "time ago" display (e.g., "5m ago", "2h ago")
|
||||
- **Notifications**: Toast messages for actions like generate and clear
|
||||
- **Responsive Design**: Adapts to node resizing
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
1. **Add Node**: Search for "Seed History" in the ComfyUI node browser
|
||||
2. **Connect Output**: Connect the seed output to any node requiring a seed input
|
||||
3. **Automatic Tracking**: The node automatically begins tracking seed changes
|
||||
|
||||
### Seed Management
|
||||
|
||||
```
|
||||
🎲 Seed History
|
||||
┌─────────────────┐
|
||||
│ 🎲 Generate │ 🗑️ Clear │
|
||||
├─────────────────┤
|
||||
│ 🎲 1,234,567 │ ← Click to load
|
||||
│ ⏰ 2m ago │
|
||||
├─────────────────┤
|
||||
│ 🎲 9,876,543 │
|
||||
│ ⏰ 5m ago │
|
||||
├─────────────────┤
|
||||
│ 🎲 5,555,555 │
|
||||
│ ⏰ 10m ago │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Workflow Integration
|
||||
|
||||
The Seed History node works seamlessly with any ComfyUI workflow:
|
||||
|
||||
```
|
||||
[Seed History] → [KSampler] → [Image Output]
|
||||
↓
|
||||
[VAE Decode] → [Save Image]
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### History Management
|
||||
- **Maximum Entries**: Keeps the 10 most recent seeds
|
||||
- **Smart Deduplication**: Prevents rapid duplicate additions
|
||||
- **Timestamp Tracking**: Full date/time information for each seed
|
||||
- **Persistent Storage**: History survives ComfyUI restarts
|
||||
|
||||
### UI Behavior
|
||||
- **Auto-Hide Timer**: History hides after 2.5 seconds of inactivity
|
||||
- **Mouse Interaction**: Hovering over history cancels auto-hide
|
||||
- **Restore Button**: Click to restore hidden history section
|
||||
- **Visual Feedback**: Hover effects and selection highlighting
|
||||
|
||||
### Seed Validation
|
||||
- **Range Checking**: Ensures seeds are within valid ComfyUI range
|
||||
- **Error Handling**: Graceful fallback to default seed (12345) on errors
|
||||
- **Sanitization**: Automatic clamping of out-of-range values
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Input Parameters
|
||||
- **seed** (INT): Seed value for generation processes
|
||||
- Range: 0 to 18,446,744,073,709,551,615
|
||||
- Default: 12345
|
||||
- Tooltip: "Seed value for generation processes. History UI tracks all changes automatically."
|
||||
|
||||
### Output
|
||||
- **seed** (INT): The processed seed value for use in other nodes
|
||||
|
||||
### Storage
|
||||
- **Key**: `comfyui_kikotools_seed_history`
|
||||
- **Format**: JSON array of history entries
|
||||
- **Location**: Browser localStorage
|
||||
- **Persistence**: Survives browser sessions and ComfyUI restarts
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 🎨 Creative Workflows
|
||||
- **Iteration Tracking**: Keep track of promising seeds during creative exploration
|
||||
- **Version Control**: Easily return to previous seeds that produced good results
|
||||
- **Experimentation**: Generate and track multiple seed variations
|
||||
|
||||
### 🔬 Technical Workflows
|
||||
- **Reproducibility**: Maintain exact seed records for reproducing specific outputs
|
||||
- **A/B Testing**: Compare results from different seeds with easy switching
|
||||
- **Documentation**: Export seed history for technical documentation
|
||||
|
||||
### 📊 Batch Processing
|
||||
- **Seed Management**: Track seeds across multiple batch runs
|
||||
- **Quality Control**: Quickly identify and reuse successful seeds
|
||||
- **Workflow Optimization**: Analyze seed performance patterns
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
### Efficient Usage
|
||||
1. **Let it Track**: The node automatically tracks all seed changes - no manual intervention needed
|
||||
2. **Use Generate**: The generate button is optimized for creating good random seeds
|
||||
3. **Regular Clearing**: Clear history periodically to maintain relevant seeds only
|
||||
|
||||
### Workflow Integration
|
||||
1. **Single Source**: Use one Seed History node per workflow for centralized tracking
|
||||
2. **Connect Early**: Place the node early in your workflow chain for complete tracking
|
||||
3. **Branch Connections**: Connect to multiple nodes that need the same seed
|
||||
|
||||
### History Management
|
||||
1. **Review Regularly**: Check history for seeds that produced good results
|
||||
2. **Document Success**: Note down particularly successful seeds externally
|
||||
3. **Clean Periodically**: Clear history when starting new creative projects
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**History Not Updating**
|
||||
- Ensure the node is properly connected to your workflow
|
||||
- Check that seed widget is visible and functional
|
||||
- Verify browser localStorage is enabled
|
||||
|
||||
**UI Not Appearing**
|
||||
- Check browser console for JavaScript errors
|
||||
- Ensure ComfyUI-KikoTools is properly installed
|
||||
- Verify web directory permissions
|
||||
|
||||
**Seeds Not Loading**
|
||||
- Confirm the seed is within valid range
|
||||
- Check that target widgets support the seed value
|
||||
- Verify node connections are intact
|
||||
|
||||
### Performance Notes
|
||||
- History is limited to 10 entries for optimal performance
|
||||
- Deduplication prevents excessive storage usage
|
||||
- Auto-hide reduces visual clutter during long workflows
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/workflows/` directory for complete workflow examples demonstrating:
|
||||
- Basic seed tracking workflow
|
||||
- Creative iteration with history
|
||||
- Technical reproducibility setup
|
||||
- Batch processing with seed management
|
||||
@@ -0,0 +1,409 @@
|
||||
{
|
||||
"last_node_id": 8,
|
||||
"last_link_id": 7,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "SeedHistory",
|
||||
"pos": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
"size": {
|
||||
"0": 280,
|
||||
"1": 320
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SeedHistory"
|
||||
},
|
||||
"widgets_values": [
|
||||
12345
|
||||
],
|
||||
"color": "#2a363b",
|
||||
"bgcolor": "#3f5159"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
100,
|
||||
450
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
2
|
||||
],
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
3
|
||||
],
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
4
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"sd_xl_base_1.0.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
450,
|
||||
100
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
5
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"a beautiful landscape with mountains and lakes, sunset, detailed, photorealistic"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
450,
|
||||
350
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
6
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"blurry, low quality, distorted, ugly, deformed"
|
||||
],
|
||||
"color": "#322",
|
||||
"bgcolor": "#533"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
900,
|
||||
100
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"link": 1,
|
||||
"widget": {
|
||||
"name": "seed"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
7
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
12345,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
900,
|
||||
400
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
1024,
|
||||
1024,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1300,
|
||||
100
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1300,
|
||||
200
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 270
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
5,
|
||||
4,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
7,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
3,
|
||||
0,
|
||||
5,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
6,
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
7,
|
||||
5,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
"LATENT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9090909090909091,
|
||||
"offset": [
|
||||
-45.45454545454545,
|
||||
-9.090909090909092
|
||||
]
|
||||
},
|
||||
"info": {
|
||||
"name": "Seed History Example",
|
||||
"author": "ComfyUI-KikoTools",
|
||||
"description": "Example workflow demonstrating the Seed History tool for tracking and managing seed values with an interactive UI.",
|
||||
"version": "1.0",
|
||||
"created": "2024-06-14",
|
||||
"modified": "2024-06-14"
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -5,16 +5,19 @@ Handles automatic discovery and registration of all ComfyAssets tools
|
||||
|
||||
from .tools.resolution_calculator import ResolutionCalculatorNode
|
||||
from .tools.width_height_selector import WidthHeightSelectorNode
|
||||
from .tools.seed_history import SeedHistoryNode
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ResolutionCalculator": ResolutionCalculatorNode,
|
||||
"WidthHeightSelector": WidthHeightSelectorNode,
|
||||
"SeedHistory": SeedHistoryNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ResolutionCalculator": "Resolution Calculator",
|
||||
"WidthHeightSelector": "Width Height Selector",
|
||||
"SeedHistory": "Seed History",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Seed History tool for ComfyUI-KikoTools.
|
||||
|
||||
Provides seed value tracking with history management,
|
||||
automatic deduplication, and interactive UI.
|
||||
"""
|
||||
|
||||
from .node import SeedHistoryNode
|
||||
|
||||
__all__ = ["SeedHistoryNode"]
|
||||
@@ -0,0 +1,296 @@
|
||||
"""Core logic for Seed History tool."""
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
|
||||
|
||||
def generate_random_seed() -> int:
|
||||
"""
|
||||
Generate a cryptographically strong random seed value.
|
||||
|
||||
Returns:
|
||||
Random integer in the valid ComfyUI seed range
|
||||
"""
|
||||
return random.randint(0, 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
|
||||
def validate_seed_value(seed: Any) -> bool:
|
||||
"""
|
||||
Validate that a seed value is within acceptable range.
|
||||
|
||||
Args:
|
||||
seed: Seed value to validate
|
||||
|
||||
Returns:
|
||||
True if seed is valid, False otherwise
|
||||
"""
|
||||
if seed is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
seed_int = int(seed)
|
||||
return 0 <= seed_int <= 0xFFFFFFFFFFFFFFFF
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def sanitize_seed_value(seed: Any) -> int:
|
||||
"""
|
||||
Sanitize and convert seed value to valid integer.
|
||||
|
||||
Args:
|
||||
seed: Raw seed value
|
||||
|
||||
Returns:
|
||||
Valid seed integer
|
||||
|
||||
Raises:
|
||||
ValueError: If seed cannot be converted to valid range
|
||||
"""
|
||||
if seed is None:
|
||||
raise ValueError("Seed cannot be None")
|
||||
|
||||
try:
|
||||
seed_int = int(seed)
|
||||
|
||||
# Clamp to valid range
|
||||
if seed_int < 0:
|
||||
seed_int = 0
|
||||
elif seed_int > 0xFFFFFFFFFFFFFFFF:
|
||||
seed_int = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
return seed_int
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(f"Invalid seed value: {seed}") from e
|
||||
|
||||
|
||||
def create_history_entry(
|
||||
seed: int, timestamp: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a standardized history entry for a seed.
|
||||
|
||||
Args:
|
||||
seed: Seed value
|
||||
timestamp: Optional timestamp (uses current time if None)
|
||||
|
||||
Returns:
|
||||
Dictionary containing seed history entry
|
||||
"""
|
||||
if timestamp is None:
|
||||
timestamp = time.time()
|
||||
|
||||
return {
|
||||
"seed": seed,
|
||||
"timestamp": timestamp,
|
||||
"dateString": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)),
|
||||
}
|
||||
|
||||
|
||||
def filter_duplicate_seeds(
|
||||
history: List[Dict[str, Any]], new_seed: int, dedup_window_ms: int = 500
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a seed should be filtered as a duplicate.
|
||||
|
||||
Args:
|
||||
history: Current seed history
|
||||
new_seed: New seed to check
|
||||
dedup_window_ms: Deduplication window in milliseconds
|
||||
|
||||
Returns:
|
||||
True if seed should be filtered (is duplicate), False otherwise
|
||||
"""
|
||||
if not history:
|
||||
return False
|
||||
|
||||
current_time = time.time() * 1000 # Convert to milliseconds
|
||||
dedup_window_sec = dedup_window_ms / 1000.0
|
||||
|
||||
# Check most recent entry for duplicates within window
|
||||
latest_entry = history[0]
|
||||
latest_timestamp_ms = latest_entry["timestamp"] * 1000
|
||||
|
||||
time_diff = current_time - latest_timestamp_ms
|
||||
is_same_seed = latest_entry["seed"] == new_seed
|
||||
is_within_window = time_diff < dedup_window_ms
|
||||
|
||||
return is_same_seed and is_within_window
|
||||
|
||||
|
||||
def add_seed_to_history(
|
||||
history: List[Dict[str, Any]],
|
||||
seed: int,
|
||||
max_history: int = 10,
|
||||
dedup_window_ms: int = 500,
|
||||
) -> Tuple[List[Dict[str, Any]], bool]:
|
||||
"""
|
||||
Add a seed to the history with deduplication and size management.
|
||||
|
||||
Args:
|
||||
history: Current seed history
|
||||
seed: Seed to add
|
||||
max_history: Maximum number of entries to keep
|
||||
dedup_window_ms: Deduplication window in milliseconds
|
||||
|
||||
Returns:
|
||||
Tuple of (updated_history, was_added)
|
||||
"""
|
||||
# Validate seed
|
||||
if not validate_seed_value(seed):
|
||||
return history, False
|
||||
|
||||
# Sanitize seed
|
||||
try:
|
||||
clean_seed = sanitize_seed_value(seed)
|
||||
except ValueError:
|
||||
return history, False
|
||||
|
||||
# Check for duplicates
|
||||
if filter_duplicate_seeds(history, clean_seed, dedup_window_ms):
|
||||
return history, False
|
||||
|
||||
# Create new history list (don't modify original)
|
||||
new_history = [entry for entry in history if entry["seed"] != clean_seed]
|
||||
|
||||
# Add new entry at the beginning
|
||||
new_entry = create_history_entry(clean_seed)
|
||||
new_history.insert(0, new_entry)
|
||||
|
||||
# Trim to max size
|
||||
if len(new_history) > max_history:
|
||||
new_history = new_history[:max_history]
|
||||
|
||||
return new_history, True
|
||||
|
||||
|
||||
def format_time_ago(timestamp: float) -> str:
|
||||
"""
|
||||
Format a timestamp as a human-readable time ago string.
|
||||
|
||||
Args:
|
||||
timestamp: Unix timestamp
|
||||
|
||||
Returns:
|
||||
Formatted time ago string
|
||||
"""
|
||||
now = time.time()
|
||||
diff = now - timestamp
|
||||
|
||||
days = int(diff // 86400)
|
||||
hours = int((diff % 86400) // 3600)
|
||||
minutes = int((diff % 3600) // 60)
|
||||
seconds = int(diff % 60)
|
||||
|
||||
if days > 0:
|
||||
return f"{days}d ago"
|
||||
elif hours > 0:
|
||||
return f"{hours}h ago"
|
||||
elif minutes > 0:
|
||||
return f"{minutes}m ago"
|
||||
else:
|
||||
return f"{seconds}s ago"
|
||||
|
||||
|
||||
def search_history_by_seed(
|
||||
history: List[Dict[str, Any]], seed: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search history for a specific seed value.
|
||||
|
||||
Args:
|
||||
history: Seed history to search
|
||||
seed: Seed value to find
|
||||
|
||||
Returns:
|
||||
History entry if found, None otherwise
|
||||
"""
|
||||
for entry in history:
|
||||
if entry["seed"] == seed:
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def get_history_statistics(history: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate statistics about the seed history.
|
||||
|
||||
Args:
|
||||
history: Seed history
|
||||
|
||||
Returns:
|
||||
Dictionary containing history statistics
|
||||
"""
|
||||
if not history:
|
||||
return {
|
||||
"total_seeds": 0,
|
||||
"oldest_timestamp": None,
|
||||
"newest_timestamp": None,
|
||||
"time_span_hours": 0,
|
||||
"unique_seeds": 0,
|
||||
}
|
||||
|
||||
timestamps = [entry["timestamp"] for entry in history]
|
||||
oldest = min(timestamps)
|
||||
newest = max(timestamps)
|
||||
time_span = (newest - oldest) / 3600 # Convert to hours
|
||||
|
||||
unique_seeds = len(set(entry["seed"] for entry in history))
|
||||
|
||||
return {
|
||||
"total_seeds": len(history),
|
||||
"oldest_timestamp": oldest,
|
||||
"newest_timestamp": newest,
|
||||
"time_span_hours": time_span,
|
||||
"unique_seeds": unique_seeds,
|
||||
}
|
||||
|
||||
|
||||
def export_history_to_text(history: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Export seed history to a formatted text string.
|
||||
|
||||
Args:
|
||||
history: Seed history to export
|
||||
|
||||
Returns:
|
||||
Formatted text representation
|
||||
"""
|
||||
if not history:
|
||||
return "# Seed History (Empty)\n\nNo seeds tracked yet."
|
||||
|
||||
lines = ["# ComfyUI Seed History", ""]
|
||||
lines.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
lines.append(f"Total seeds: {len(history)}")
|
||||
lines.append("")
|
||||
|
||||
for i, entry in enumerate(history, 1):
|
||||
time_ago = format_time_ago(entry["timestamp"])
|
||||
lines.append(f"{i:2d}. {entry['seed']} ({time_ago})")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def import_seeds_from_list(seed_list: List[int]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Import a list of seeds as history entries.
|
||||
|
||||
Args:
|
||||
seed_list: List of seed integers
|
||||
|
||||
Returns:
|
||||
List of history entries
|
||||
"""
|
||||
history = []
|
||||
current_time = time.time()
|
||||
|
||||
for i, seed in enumerate(seed_list):
|
||||
if validate_seed_value(seed):
|
||||
# Spread timestamps by 1 minute intervals (newest first)
|
||||
timestamp = current_time - (i * 60)
|
||||
entry = create_history_entry(seed, timestamp)
|
||||
history.append(entry)
|
||||
|
||||
return history
|
||||
@@ -0,0 +1,173 @@
|
||||
"""Seed History node for ComfyUI."""
|
||||
|
||||
from typing import Tuple
|
||||
from ...base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
generate_random_seed,
|
||||
validate_seed_value,
|
||||
sanitize_seed_value,
|
||||
)
|
||||
|
||||
|
||||
class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Seed History node for tracking and managing seed values.
|
||||
|
||||
Provides seed value output with integrated history tracking,
|
||||
deduplication, and interactive UI for seed management.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
return {
|
||||
"required": {
|
||||
"seed": (
|
||||
"INT",
|
||||
{
|
||||
"default": 12345,
|
||||
"min": 0,
|
||||
"max": 0xFFFFFFFFFFFFFFFF,
|
||||
"tooltip": "Seed value for generation processes. "
|
||||
"History UI tracks all changes automatically.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("seed",)
|
||||
FUNCTION = "output_seed"
|
||||
CATEGORY = "ComfyAssets"
|
||||
|
||||
def output_seed(self, seed: int) -> Tuple[int]:
|
||||
"""
|
||||
Output the seed value for use in other nodes.
|
||||
|
||||
Args:
|
||||
seed: Input seed value
|
||||
|
||||
Returns:
|
||||
Tuple containing the seed value
|
||||
"""
|
||||
try:
|
||||
# Validate and sanitize the seed
|
||||
if not validate_seed_value(seed):
|
||||
self.handle_error(
|
||||
f"Invalid seed value: {seed}. Using fallback seed 12345."
|
||||
)
|
||||
return (12345,)
|
||||
|
||||
clean_seed = sanitize_seed_value(seed)
|
||||
|
||||
return (clean_seed,)
|
||||
|
||||
except Exception as e:
|
||||
# Handle any unexpected errors gracefully
|
||||
error_msg = f"Error processing seed: {str(e)}. Using fallback seed 12345."
|
||||
self.handle_error(error_msg)
|
||||
return (12345,)
|
||||
|
||||
def generate_new_seed(self) -> int:
|
||||
"""
|
||||
Generate a new random seed value.
|
||||
|
||||
Returns:
|
||||
New random seed integer
|
||||
"""
|
||||
try:
|
||||
new_seed = generate_random_seed()
|
||||
self.log_info(f"Generated new seed: {new_seed}")
|
||||
return new_seed
|
||||
except Exception as e:
|
||||
error_msg = f"Error generating seed: {str(e)}. Using fallback."
|
||||
self.handle_error(error_msg)
|
||||
return 12345
|
||||
|
||||
def validate_seed_input(self, seed: int) -> bool:
|
||||
"""
|
||||
Validate seed input value.
|
||||
|
||||
Args:
|
||||
seed: Seed value to validate
|
||||
|
||||
Returns:
|
||||
True if seed is valid
|
||||
"""
|
||||
return validate_seed_value(seed)
|
||||
|
||||
def get_seed_info(self, seed: int) -> str:
|
||||
"""
|
||||
Get descriptive information about a seed value.
|
||||
|
||||
Args:
|
||||
seed: Seed value
|
||||
|
||||
Returns:
|
||||
Information string about the seed
|
||||
"""
|
||||
if not validate_seed_value(seed):
|
||||
return f"Invalid seed: {seed} (outside valid range)"
|
||||
|
||||
# Convert to hex for additional info
|
||||
hex_value = hex(seed)
|
||||
|
||||
# Check if it's a "nice" number (power of 2, round number, etc.)
|
||||
seed_type = "standard"
|
||||
if seed == 0:
|
||||
seed_type = "zero"
|
||||
elif seed & (seed - 1) == 0: # Power of 2
|
||||
seed_type = "power of 2"
|
||||
elif str(seed).count("0") > len(str(seed)) // 2:
|
||||
seed_type = "round number"
|
||||
elif seed == 12345:
|
||||
seed_type = "default"
|
||||
|
||||
return f"Seed {seed} ({hex_value}) - {seed_type}"
|
||||
|
||||
def get_seed_range_info(self) -> str:
|
||||
"""
|
||||
Get information about the valid seed range.
|
||||
|
||||
Returns:
|
||||
Range information string
|
||||
"""
|
||||
max_seed = 0xFFFFFFFFFFFFFFFF
|
||||
return f"Valid range: 0 to {max_seed:,} ({hex(max_seed)})"
|
||||
|
||||
@classmethod
|
||||
def get_default_seed(cls) -> int:
|
||||
"""
|
||||
Get the default seed value.
|
||||
|
||||
Returns:
|
||||
Default seed integer
|
||||
"""
|
||||
return 12345
|
||||
|
||||
@classmethod
|
||||
def is_seed_in_range(cls, seed: int) -> bool:
|
||||
"""
|
||||
Check if seed is within valid ComfyUI range.
|
||||
|
||||
Args:
|
||||
seed: Seed value to check
|
||||
|
||||
Returns:
|
||||
True if seed is in valid range
|
||||
"""
|
||||
return 0 <= seed <= 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of the node."""
|
||||
return "SeedHistoryNode(with_ui_tracking)"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Detailed string representation of the node."""
|
||||
return (
|
||||
f"SeedHistoryNode("
|
||||
f"category='{self.CATEGORY}', "
|
||||
f"function='{self.FUNCTION}', "
|
||||
f"max_seed={hex(0xFFFFFFFFFFFFFFFF)}"
|
||||
f")"
|
||||
)
|
||||
@@ -0,0 +1,418 @@
|
||||
"""Tests for Seed History tool."""
|
||||
|
||||
import time
|
||||
from unittest.mock import Mock
|
||||
|
||||
from kikotools.tools.seed_history.node import SeedHistoryNode
|
||||
from kikotools.tools.seed_history.logic import (
|
||||
generate_random_seed,
|
||||
validate_seed_value,
|
||||
sanitize_seed_value,
|
||||
create_history_entry,
|
||||
filter_duplicate_seeds,
|
||||
add_seed_to_history,
|
||||
format_time_ago,
|
||||
search_history_by_seed,
|
||||
get_history_statistics,
|
||||
export_history_to_text,
|
||||
import_seeds_from_list,
|
||||
)
|
||||
|
||||
|
||||
class TestSeedHistoryNode:
|
||||
"""Test SeedHistoryNode functionality."""
|
||||
|
||||
def test_node_structure(self):
|
||||
"""Test that node has correct ComfyUI structure."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Test class attributes
|
||||
assert hasattr(SeedHistoryNode, "INPUT_TYPES")
|
||||
assert hasattr(SeedHistoryNode, "RETURN_TYPES")
|
||||
assert hasattr(SeedHistoryNode, "RETURN_NAMES")
|
||||
assert hasattr(SeedHistoryNode, "FUNCTION")
|
||||
assert hasattr(SeedHistoryNode, "CATEGORY")
|
||||
|
||||
# Test input types structure
|
||||
input_types = SeedHistoryNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
assert "seed" in input_types["required"]
|
||||
|
||||
# Test seed input configuration
|
||||
seed_config = input_types["required"]["seed"]
|
||||
assert seed_config[0] == "INT"
|
||||
assert isinstance(seed_config[1], dict)
|
||||
assert "default" in seed_config[1]
|
||||
assert "min" in seed_config[1]
|
||||
assert "max" in seed_config[1]
|
||||
assert seed_config[1]["min"] == 0
|
||||
assert seed_config[1]["max"] == 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
# Test return types
|
||||
assert SeedHistoryNode.RETURN_TYPES == ("INT",)
|
||||
assert SeedHistoryNode.RETURN_NAMES == ("seed",)
|
||||
assert SeedHistoryNode.FUNCTION == "output_seed"
|
||||
assert SeedHistoryNode.CATEGORY == "ComfyAssets"
|
||||
|
||||
def test_output_seed_valid_input(self):
|
||||
"""Test seed output with valid input."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Test various valid seeds
|
||||
test_seeds = [0, 12345, 999999, 0xFFFFFFFFFFFFFFFF]
|
||||
|
||||
for seed in test_seeds:
|
||||
result = node.output_seed(seed)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
assert result[0] == seed
|
||||
|
||||
def test_output_seed_invalid_input(self):
|
||||
"""Test seed output with invalid input."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Test invalid seeds (negative values)
|
||||
result = node.output_seed(-1)
|
||||
assert result == (12345,) # Fallback
|
||||
|
||||
# Test seeds too large
|
||||
result = node.output_seed(0xFFFFFFFFFFFFFFFF + 1)
|
||||
assert result == (12345,) # Fallback
|
||||
|
||||
def test_generate_new_seed(self):
|
||||
"""Test random seed generation."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Generate multiple seeds
|
||||
seeds = []
|
||||
for _ in range(10):
|
||||
seed = node.generate_new_seed()
|
||||
seeds.append(seed)
|
||||
|
||||
# Test all seeds are valid
|
||||
for seed in seeds:
|
||||
assert validate_seed_value(seed)
|
||||
|
||||
# Test seeds are different (probabilistically)
|
||||
assert len(set(seeds)) > 5 # Should have some variety
|
||||
|
||||
def test_validate_seed_input(self):
|
||||
"""Test seed validation."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Valid seeds
|
||||
assert node.validate_seed_input(0)
|
||||
assert node.validate_seed_input(12345)
|
||||
assert node.validate_seed_input(0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
# Invalid seeds
|
||||
assert not node.validate_seed_input(-1)
|
||||
assert not node.validate_seed_input(0xFFFFFFFFFFFFFFFF + 1)
|
||||
assert not node.validate_seed_input(None)
|
||||
|
||||
def test_get_seed_info(self):
|
||||
"""Test seed information generation."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Test various seed types
|
||||
info_zero = node.get_seed_info(0)
|
||||
assert "zero" in info_zero.lower()
|
||||
|
||||
info_default = node.get_seed_info(12345)
|
||||
assert "default" in info_default.lower()
|
||||
|
||||
info_power_of_2 = node.get_seed_info(1024)
|
||||
assert "power of 2" in info_power_of_2.lower()
|
||||
|
||||
# Test invalid seed
|
||||
info_invalid = node.get_seed_info(-1)
|
||||
assert "invalid" in info_invalid.lower()
|
||||
|
||||
def test_seed_range_info(self):
|
||||
"""Test seed range information."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
range_info = node.get_seed_range_info()
|
||||
assert "Valid range" in range_info
|
||||
assert str(0xFFFFFFFFFFFFFFFF) in range_info
|
||||
|
||||
def test_class_methods(self):
|
||||
"""Test class methods."""
|
||||
# Test default seed
|
||||
default_seed = SeedHistoryNode.get_default_seed()
|
||||
assert default_seed == 12345
|
||||
|
||||
# Test range checking
|
||||
assert SeedHistoryNode.is_seed_in_range(0)
|
||||
assert SeedHistoryNode.is_seed_in_range(12345)
|
||||
assert SeedHistoryNode.is_seed_in_range(0xFFFFFFFFFFFFFFFF)
|
||||
assert not SeedHistoryNode.is_seed_in_range(-1)
|
||||
assert not SeedHistoryNode.is_seed_in_range(0xFFFFFFFFFFFFFFFF + 1)
|
||||
|
||||
|
||||
class TestSeedHistoryLogic:
|
||||
"""Test seed history logic functions."""
|
||||
|
||||
def test_generate_random_seed(self):
|
||||
"""Test random seed generation."""
|
||||
# Generate multiple seeds
|
||||
seeds = [generate_random_seed() for _ in range(100)]
|
||||
|
||||
# Test all seeds are valid
|
||||
for seed in seeds:
|
||||
assert validate_seed_value(seed)
|
||||
|
||||
# Test seeds have variety
|
||||
assert len(set(seeds)) > 50 # Should have good variety
|
||||
|
||||
def test_validate_seed_value(self):
|
||||
"""Test seed validation logic."""
|
||||
# Valid seeds
|
||||
assert validate_seed_value(0)
|
||||
assert validate_seed_value(12345)
|
||||
assert validate_seed_value(0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
# Invalid seeds
|
||||
assert not validate_seed_value(-1)
|
||||
assert not validate_seed_value(0xFFFFFFFFFFFFFFFF + 1)
|
||||
assert not validate_seed_value(None)
|
||||
assert not validate_seed_value("invalid")
|
||||
assert not validate_seed_value([])
|
||||
|
||||
def test_sanitize_seed_value(self):
|
||||
"""Test seed sanitization."""
|
||||
# Valid seeds should pass through
|
||||
assert sanitize_seed_value(12345) == 12345
|
||||
assert sanitize_seed_value(0) == 0
|
||||
assert sanitize_seed_value(0xFFFFFFFFFFFFFFFF) == 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
# String numbers should convert
|
||||
assert sanitize_seed_value("12345") == 12345
|
||||
assert sanitize_seed_value("0") == 0
|
||||
|
||||
# Out of range should clamp
|
||||
assert sanitize_seed_value(-100) == 0
|
||||
assert sanitize_seed_value(0xFFFFFFFFFFFFFFFF + 100) == 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
# Invalid should raise
|
||||
try:
|
||||
sanitize_seed_value(None)
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
sanitize_seed_value("invalid")
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def test_create_history_entry(self):
|
||||
"""Test history entry creation."""
|
||||
seed = 12345
|
||||
timestamp = time.time()
|
||||
|
||||
# With explicit timestamp
|
||||
entry = create_history_entry(seed, timestamp)
|
||||
assert entry["seed"] == seed
|
||||
assert entry["timestamp"] == timestamp
|
||||
assert "dateString" in entry
|
||||
|
||||
# With auto timestamp
|
||||
entry_auto = create_history_entry(seed)
|
||||
assert entry_auto["seed"] == seed
|
||||
assert "timestamp" in entry_auto
|
||||
assert "dateString" in entry_auto
|
||||
|
||||
def test_filter_duplicate_seeds(self):
|
||||
"""Test duplicate seed filtering."""
|
||||
seed = 12345
|
||||
current_time = time.time()
|
||||
|
||||
# Empty history should not filter
|
||||
assert not filter_duplicate_seeds([], seed, 500)
|
||||
|
||||
# Recent duplicate should filter
|
||||
recent_entry = create_history_entry(seed, current_time - 0.1)
|
||||
history = [recent_entry]
|
||||
assert filter_duplicate_seeds(history, seed, 500)
|
||||
|
||||
# Old duplicate should not filter
|
||||
old_entry = create_history_entry(seed, current_time - 1.0)
|
||||
history = [old_entry]
|
||||
assert not filter_duplicate_seeds(history, seed, 500)
|
||||
|
||||
# Different seed should not filter
|
||||
different_entry = create_history_entry(54321, current_time - 0.1)
|
||||
history = [different_entry]
|
||||
assert not filter_duplicate_seeds(history, seed, 500)
|
||||
|
||||
def test_add_seed_to_history(self):
|
||||
"""Test adding seeds to history."""
|
||||
history = []
|
||||
|
||||
# Add first seed
|
||||
new_history, was_added = add_seed_to_history(history, 12345)
|
||||
assert was_added
|
||||
assert len(new_history) == 1
|
||||
assert new_history[0]["seed"] == 12345
|
||||
|
||||
# Add different seed
|
||||
new_history2, was_added2 = add_seed_to_history(new_history, 54321)
|
||||
assert was_added2
|
||||
assert len(new_history2) == 2
|
||||
assert new_history2[0]["seed"] == 54321 # Most recent first
|
||||
|
||||
# Add duplicate (should remove old and add new)
|
||||
time.sleep(0.6) # Wait past dedup window
|
||||
new_history3, was_added3 = add_seed_to_history(new_history2, 12345)
|
||||
assert was_added3
|
||||
assert len(new_history3) == 2
|
||||
assert new_history3[0]["seed"] == 12345 # Most recent first
|
||||
|
||||
# Test max history limit
|
||||
history_long = []
|
||||
for i in range(15):
|
||||
history_long, _ = add_seed_to_history(history_long, i, max_history=10)
|
||||
time.sleep(0.001) # Small delay to avoid dedup
|
||||
|
||||
assert len(history_long) == 10
|
||||
|
||||
def test_format_time_ago(self):
|
||||
"""Test time ago formatting."""
|
||||
now = time.time()
|
||||
|
||||
# Recent times
|
||||
assert "s ago" in format_time_ago(now - 30)
|
||||
assert "m ago" in format_time_ago(now - 300)
|
||||
assert "h ago" in format_time_ago(now - 7200)
|
||||
assert "d ago" in format_time_ago(now - 86400)
|
||||
|
||||
def test_search_history_by_seed(self):
|
||||
"""Test history search."""
|
||||
history = [
|
||||
create_history_entry(12345),
|
||||
create_history_entry(54321),
|
||||
create_history_entry(99999),
|
||||
]
|
||||
|
||||
# Found seed
|
||||
result = search_history_by_seed(history, 54321)
|
||||
assert result is not None
|
||||
assert result["seed"] == 54321
|
||||
|
||||
# Not found seed
|
||||
result = search_history_by_seed(history, 11111)
|
||||
assert result is None
|
||||
|
||||
def test_get_history_statistics(self):
|
||||
"""Test history statistics."""
|
||||
# Empty history
|
||||
stats = get_history_statistics([])
|
||||
assert stats["total_seeds"] == 0
|
||||
assert stats["unique_seeds"] == 0
|
||||
|
||||
# History with data
|
||||
now = time.time()
|
||||
history = [
|
||||
create_history_entry(12345, now - 3600),
|
||||
create_history_entry(54321, now - 1800),
|
||||
create_history_entry(12345, now), # Duplicate
|
||||
]
|
||||
|
||||
stats = get_history_statistics(history)
|
||||
assert stats["total_seeds"] == 3
|
||||
assert stats["unique_seeds"] == 2
|
||||
assert stats["time_span_hours"] == 1.0
|
||||
|
||||
def test_export_history_to_text(self):
|
||||
"""Test history export."""
|
||||
# Empty history
|
||||
text = export_history_to_text([])
|
||||
assert "Empty" in text
|
||||
|
||||
# History with data
|
||||
history = [create_history_entry(12345), create_history_entry(54321)]
|
||||
|
||||
text = export_history_to_text(history)
|
||||
assert "12345" in text
|
||||
assert "54321" in text
|
||||
assert "Total seeds: 2" in text
|
||||
|
||||
def test_import_seeds_from_list(self):
|
||||
"""Test importing seeds from list."""
|
||||
seed_list = [12345, 54321, 99999]
|
||||
|
||||
history = import_seeds_from_list(seed_list)
|
||||
assert len(history) == 3
|
||||
|
||||
# Check seeds are in correct order (newest first)
|
||||
assert history[0]["seed"] == 12345
|
||||
assert history[1]["seed"] == 54321
|
||||
assert history[2]["seed"] == 99999
|
||||
|
||||
# Check timestamps are spaced
|
||||
assert history[0]["timestamp"] > history[1]["timestamp"]
|
||||
assert history[1]["timestamp"] > history[2]["timestamp"]
|
||||
|
||||
|
||||
class TestSeedHistoryIntegration:
|
||||
"""Test integration scenarios."""
|
||||
|
||||
def test_complete_workflow(self):
|
||||
"""Test complete seed history workflow."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Test basic seed output
|
||||
result = node.output_seed(12345)
|
||||
assert result == (12345,)
|
||||
|
||||
# Test seed generation
|
||||
new_seed = node.generate_new_seed()
|
||||
assert validate_seed_value(new_seed)
|
||||
|
||||
# Test seed info
|
||||
info = node.get_seed_info(new_seed)
|
||||
assert str(new_seed) in info
|
||||
|
||||
def test_history_management(self):
|
||||
"""Test history management operations."""
|
||||
history = []
|
||||
|
||||
# Add seeds over time
|
||||
seeds = [12345, 54321, 99999, 11111, 22222]
|
||||
for seed in seeds:
|
||||
history, was_added = add_seed_to_history(history, seed)
|
||||
assert was_added
|
||||
time.sleep(0.001) # Avoid dedup
|
||||
|
||||
# Check history order (newest first)
|
||||
assert history[0]["seed"] == 22222
|
||||
assert history[-1]["seed"] == 12345
|
||||
|
||||
# Test search
|
||||
found = search_history_by_seed(history, 99999)
|
||||
assert found is not None
|
||||
|
||||
# Test statistics
|
||||
stats = get_history_statistics(history)
|
||||
assert stats["total_seeds"] == 5
|
||||
assert stats["unique_seeds"] == 5
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling scenarios."""
|
||||
node = SeedHistoryNode()
|
||||
|
||||
# Test with invalid seeds
|
||||
result = node.output_seed(-1)
|
||||
assert result == (12345,) # Fallback
|
||||
|
||||
# Test validation
|
||||
assert not node.validate_seed_input(None)
|
||||
assert not node.validate_seed_input("invalid")
|
||||
|
||||
# Test history with invalid seeds
|
||||
history = []
|
||||
history, was_added = add_seed_to_history(history, -1)
|
||||
assert not was_added
|
||||
assert len(history) == 0
|
||||
@@ -0,0 +1,524 @@
|
||||
// ComfyUI-KikoTools - Seed History with Tracking UI
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "comfyassets.SeedHistory",
|
||||
|
||||
async setup() {
|
||||
// Store reference to all SeedHistory nodes
|
||||
window.seedHistoryNodes = window.seedHistoryNodes || [];
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SeedHistory") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (onNodeCreated) {
|
||||
onNodeCreated.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Initialize seed history
|
||||
this.seedHistory = this.loadSeedHistory();
|
||||
this.hideTimer = null;
|
||||
this.mouseOverHistory = false;
|
||||
|
||||
// Register this node in global registry
|
||||
window.seedHistoryNodes = window.seedHistoryNodes || [];
|
||||
window.seedHistoryNodes.push(this);
|
||||
|
||||
// Create UI container
|
||||
const uiContainer = document.createElement("div");
|
||||
uiContainer.style.padding = "8px";
|
||||
uiContainer.style.backgroundColor = "#1e1e1e";
|
||||
uiContainer.style.borderRadius = "6px";
|
||||
uiContainer.style.marginTop = "6px";
|
||||
uiContainer.style.border = "1px solid #404040";
|
||||
|
||||
this.buildSeedInterface(uiContainer);
|
||||
|
||||
// Add as widget
|
||||
this.seedWidget = this.addDOMWidget(
|
||||
"seed_history_ui",
|
||||
"div",
|
||||
uiContainer,
|
||||
);
|
||||
|
||||
// Set node size
|
||||
if (!this.hasBeenResized) {
|
||||
this.size = [280, 320];
|
||||
}
|
||||
|
||||
// Track manual resizing
|
||||
const originalResize = this.onResize;
|
||||
this.onResize = function (size) {
|
||||
this.hasBeenResized = true;
|
||||
if (originalResize) {
|
||||
originalResize.call(this, size);
|
||||
}
|
||||
};
|
||||
|
||||
// Hook into seed widget callbacks to track all changes
|
||||
setTimeout(() => {
|
||||
this.setupSeedWidgetCallbacks();
|
||||
}, 100);
|
||||
|
||||
// Hook directly into widget value changes
|
||||
const originalOnWidgetChange = this.onWidgetChange;
|
||||
this.onWidgetChange = function(name, value, oldValue, widget) {
|
||||
if (name === "seed" && value !== oldValue) {
|
||||
this.addSeedToHistory(value);
|
||||
}
|
||||
|
||||
if (originalOnWidgetChange) {
|
||||
return originalOnWidgetChange.call(this, name, value, oldValue, widget);
|
||||
}
|
||||
};
|
||||
|
||||
// Save/load data
|
||||
const originalSerialize = this.serialize;
|
||||
this.serialize = function () {
|
||||
const data = originalSerialize ? originalSerialize.call(this) : {};
|
||||
data.hasBeenResized = this.hasBeenResized;
|
||||
data.seedHistory = this.seedHistory;
|
||||
return data;
|
||||
};
|
||||
|
||||
const originalConfigure = this.configure;
|
||||
this.configure = function (data) {
|
||||
if (originalConfigure) {
|
||||
originalConfigure.call(this, data);
|
||||
}
|
||||
if (data.hasBeenResized) {
|
||||
this.hasBeenResized = data.hasBeenResized;
|
||||
}
|
||||
if (data.seedHistory) {
|
||||
this.seedHistory = data.seedHistory;
|
||||
this.refreshHistoryDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup interval on node removal
|
||||
const originalOnRemoved = this.onRemoved;
|
||||
this.onRemoved = function () {
|
||||
if (this.seedValueWatcher) {
|
||||
clearInterval(this.seedValueWatcher);
|
||||
this.seedValueWatcher = null;
|
||||
}
|
||||
|
||||
// Clean up deduplication tracking
|
||||
if (this.lastAddedSeed) {
|
||||
this.lastAddedSeed = null;
|
||||
}
|
||||
|
||||
// Remove from global registry
|
||||
if (window.seedHistoryNodes) {
|
||||
const index = window.seedHistoryNodes.indexOf(this);
|
||||
if (index !== -1) {
|
||||
window.seedHistoryNodes.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalOnRemoved) {
|
||||
originalOnRemoved.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
// Setup seed widget callbacks to track increment/decrement/randomize
|
||||
nodeType.prototype.setupSeedWidgetCallbacks = function () {
|
||||
// Find the seed widget
|
||||
const seedWidget = this.widgets?.find(w => w.name === "seed");
|
||||
if (!seedWidget) {
|
||||
setTimeout(() => this.setupSeedWidgetCallbacks(), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the last known seed value to detect changes
|
||||
this.lastSeedValue = seedWidget.value;
|
||||
|
||||
// Monitor for value changes that might not trigger callback
|
||||
this.seedValueWatcher = setInterval(() => {
|
||||
if (seedWidget.value !== this.lastSeedValue) {
|
||||
this.lastSeedValue = seedWidget.value;
|
||||
this.addSeedToHistory(seedWidget.value);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// Build the seed interface
|
||||
nodeType.prototype.buildSeedInterface = function (container) {
|
||||
container.innerHTML = "";
|
||||
|
||||
// Header
|
||||
const headerDiv = document.createElement("div");
|
||||
headerDiv.style.marginBottom = "8px";
|
||||
|
||||
const titleDiv = document.createElement("div");
|
||||
titleDiv.style.fontWeight = "bold";
|
||||
titleDiv.style.color = "#00d4ff";
|
||||
titleDiv.style.textAlign = "center";
|
||||
titleDiv.style.padding = "4px";
|
||||
titleDiv.style.backgroundColor = "rgba(0, 212, 255, 0.1)";
|
||||
titleDiv.style.borderRadius = "4px";
|
||||
titleDiv.style.fontSize = "11px";
|
||||
titleDiv.innerHTML = "🎲 Seed History";
|
||||
headerDiv.appendChild(titleDiv);
|
||||
|
||||
// Action buttons
|
||||
const buttonDiv = document.createElement("div");
|
||||
buttonDiv.style.display = "flex";
|
||||
buttonDiv.style.gap = "4px";
|
||||
buttonDiv.style.marginTop = "6px";
|
||||
|
||||
// Generate button
|
||||
const generateBtn = document.createElement("button");
|
||||
generateBtn.textContent = "🎲 Generate";
|
||||
generateBtn.style.flex = "1";
|
||||
generateBtn.style.padding = "3px 6px";
|
||||
generateBtn.style.backgroundColor = "#0088cc";
|
||||
generateBtn.style.color = "white";
|
||||
generateBtn.style.border = "none";
|
||||
generateBtn.style.borderRadius = "3px";
|
||||
generateBtn.style.cursor = "pointer";
|
||||
generateBtn.style.fontSize = "10px";
|
||||
generateBtn.addEventListener("click", () => this.generateRandomSeed());
|
||||
buttonDiv.appendChild(generateBtn);
|
||||
|
||||
// Clear button
|
||||
const clearBtn = document.createElement("button");
|
||||
clearBtn.textContent = "🗑️ Clear";
|
||||
clearBtn.style.flex = "1";
|
||||
clearBtn.style.padding = "3px 6px";
|
||||
clearBtn.style.backgroundColor = "#cc4444";
|
||||
clearBtn.style.color = "white";
|
||||
clearBtn.style.border = "none";
|
||||
clearBtn.style.borderRadius = "3px";
|
||||
clearBtn.style.cursor = "pointer";
|
||||
clearBtn.style.fontSize = "10px";
|
||||
clearBtn.addEventListener("click", () => this.clearSeedHistory());
|
||||
buttonDiv.appendChild(clearBtn);
|
||||
|
||||
headerDiv.appendChild(buttonDiv);
|
||||
container.appendChild(headerDiv);
|
||||
|
||||
// History display
|
||||
const historyDiv = document.createElement("div");
|
||||
historyDiv.style.maxHeight = "180px";
|
||||
historyDiv.style.overflowY = "auto";
|
||||
historyDiv.style.border = "1px solid #333";
|
||||
historyDiv.style.borderRadius = "4px";
|
||||
historyDiv.style.backgroundColor = "#2a2a2a";
|
||||
historyDiv.style.padding = "6px";
|
||||
historyDiv.style.fontSize = "10px";
|
||||
historyDiv.style.fontFamily = "monospace";
|
||||
|
||||
// Mouse events for auto-hide
|
||||
historyDiv.addEventListener("mouseenter", () => {
|
||||
this.mouseOverHistory = true;
|
||||
this.cancelAutoHide();
|
||||
});
|
||||
|
||||
historyDiv.addEventListener("mouseleave", () => {
|
||||
this.mouseOverHistory = false;
|
||||
this.startAutoHide();
|
||||
});
|
||||
|
||||
this.historyDisplay = historyDiv;
|
||||
container.appendChild(historyDiv);
|
||||
|
||||
this.refreshHistoryDisplay();
|
||||
};
|
||||
|
||||
// Load history from storage
|
||||
nodeType.prototype.loadSeedHistory = function () {
|
||||
try {
|
||||
const stored = localStorage.getItem('comfyui_kikotools_seed_history');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Save history to storage
|
||||
nodeType.prototype.saveSeedHistory = function () {
|
||||
try {
|
||||
localStorage.setItem('comfyui_kikotools_seed_history', JSON.stringify(this.seedHistory));
|
||||
} catch (error) {
|
||||
// Silently handle storage errors
|
||||
}
|
||||
};
|
||||
|
||||
// Add seed to history
|
||||
nodeType.prototype.addSeedToHistory = function (seed) {
|
||||
if (!seed || seed === 0) return;
|
||||
|
||||
const numSeed = typeof seed === 'string' ? parseInt(seed) : seed;
|
||||
const now = Date.now();
|
||||
|
||||
// Deduplication: prevent adding the same seed within 500ms window
|
||||
if (!this.lastAddedSeed) {
|
||||
this.lastAddedSeed = { seed: null, timestamp: 0 };
|
||||
}
|
||||
|
||||
const timeSinceLastAdd = now - this.lastAddedSeed.timestamp;
|
||||
const isSameSeed = this.lastAddedSeed.seed === numSeed;
|
||||
const isWithinDupeWindow = timeSinceLastAdd < 500; // 500ms window
|
||||
|
||||
if (isSameSeed && isWithinDupeWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update deduplication tracking
|
||||
this.lastAddedSeed = { seed: numSeed, timestamp: now };
|
||||
|
||||
// Remove if already exists in history
|
||||
this.seedHistory = this.seedHistory.filter(item => item.seed !== numSeed);
|
||||
|
||||
// Add to front
|
||||
this.seedHistory.unshift({
|
||||
seed: numSeed,
|
||||
timestamp: now,
|
||||
dateString: new Date().toLocaleString()
|
||||
});
|
||||
|
||||
// Keep only last 10
|
||||
if (this.seedHistory.length > 10) {
|
||||
this.seedHistory = this.seedHistory.slice(0, 10);
|
||||
}
|
||||
|
||||
this.saveSeedHistory();
|
||||
this.refreshHistoryDisplay();
|
||||
this.startAutoHide();
|
||||
};
|
||||
|
||||
// Generate new random seed
|
||||
nodeType.prototype.generateRandomSeed = function () {
|
||||
const newSeed = Math.floor(Math.random() * 0xFFFFFFFFFFFFFFFF);
|
||||
|
||||
const seedWidget = this.widgets?.find(w => w.name === "seed");
|
||||
if (seedWidget) {
|
||||
seedWidget.value = newSeed;
|
||||
if (seedWidget.callback) {
|
||||
seedWidget.callback(newSeed, this, seedWidget);
|
||||
}
|
||||
}
|
||||
|
||||
this.addSeedToHistory(newSeed);
|
||||
this.setDirtyCanvas(true, true);
|
||||
this.showMessage(`Generated: ${newSeed}`, "success");
|
||||
};
|
||||
|
||||
// Use seed from history
|
||||
nodeType.prototype.useSeedFromHistory = function (historyItem, index) {
|
||||
const seedWidget = this.widgets?.find(w => w.name === "seed");
|
||||
if (seedWidget) {
|
||||
seedWidget.value = historyItem.seed;
|
||||
if (seedWidget.callback) {
|
||||
seedWidget.callback(historyItem.seed, this, seedWidget);
|
||||
}
|
||||
}
|
||||
|
||||
this.highlightHistoryEntry(index);
|
||||
this.setDirtyCanvas(true, true);
|
||||
this.startAutoHide();
|
||||
this.showMessage(`Loaded: ${historyItem.seed}`, "info");
|
||||
};
|
||||
|
||||
// Clear history
|
||||
nodeType.prototype.clearSeedHistory = function () {
|
||||
this.seedHistory = [];
|
||||
this.saveSeedHistory();
|
||||
this.refreshHistoryDisplay();
|
||||
this.showMessage("History cleared", "info");
|
||||
};
|
||||
|
||||
// Refresh history display
|
||||
nodeType.prototype.refreshHistoryDisplay = function () {
|
||||
if (!this.historyDisplay) return;
|
||||
|
||||
if (!this.seedHistory || this.seedHistory.length === 0) {
|
||||
this.historyDisplay.innerHTML =
|
||||
'<div style="color: #888; text-align: center; padding: 15px;">No seeds tracked<br><small>Generate seeds to build history</small></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.historyDisplay.innerHTML = "";
|
||||
|
||||
this.seedHistory.forEach((item, index) => {
|
||||
const entryDiv = document.createElement("div");
|
||||
entryDiv.style.padding = "4px";
|
||||
entryDiv.style.marginBottom = "3px";
|
||||
entryDiv.style.backgroundColor = "#333";
|
||||
entryDiv.style.borderRadius = "2px";
|
||||
entryDiv.style.cursor = "pointer";
|
||||
entryDiv.style.border = "1px solid transparent";
|
||||
entryDiv.style.lineHeight = "1.2";
|
||||
|
||||
entryDiv.addEventListener("mouseenter", () => {
|
||||
entryDiv.style.backgroundColor = "#444";
|
||||
entryDiv.style.border = "1px solid #555";
|
||||
});
|
||||
entryDiv.addEventListener("mouseleave", () => {
|
||||
entryDiv.style.backgroundColor = "#333";
|
||||
entryDiv.style.border = "1px solid transparent";
|
||||
});
|
||||
|
||||
entryDiv.addEventListener("click", () => {
|
||||
this.useSeedFromHistory(item, index);
|
||||
});
|
||||
|
||||
const timeAgo = this.formatTimeAgo(item.timestamp);
|
||||
entryDiv.innerHTML = `
|
||||
<div style="color: #fff; font-weight: bold; margin-bottom: 1px;">
|
||||
🎲 ${item.seed}
|
||||
</div>
|
||||
<div style="color: #999; font-size: 8px;">
|
||||
⏰ ${timeAgo}
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.historyDisplay.appendChild(entryDiv);
|
||||
});
|
||||
|
||||
this.startAutoHide();
|
||||
};
|
||||
|
||||
// Highlight selected entry
|
||||
nodeType.prototype.highlightHistoryEntry = function (index) {
|
||||
const entries = this.historyDisplay.querySelectorAll('div[style*="cursor: pointer"]');
|
||||
entries.forEach((entry, i) => {
|
||||
if (i === index) {
|
||||
entry.style.backgroundColor = "#006600";
|
||||
entry.style.border = "1px solid #00aa00";
|
||||
} else {
|
||||
entry.style.backgroundColor = "#333";
|
||||
entry.style.border = "1px solid transparent";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Auto-hide functionality
|
||||
nodeType.prototype.startAutoHide = function () {
|
||||
this.cancelAutoHide();
|
||||
if (!this.mouseOverHistory) {
|
||||
this.hideTimer = setTimeout(() => {
|
||||
this.hideHistorySection();
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.cancelAutoHide = function () {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.hideHistorySection = function () {
|
||||
if (this.historyDisplay && !this.mouseOverHistory) {
|
||||
this.historyDisplay.style.display = "none";
|
||||
|
||||
if (!this.restoreButton) {
|
||||
const restoreDiv = document.createElement("div");
|
||||
restoreDiv.style.padding = "10px";
|
||||
restoreDiv.style.backgroundColor = "#2a2a2a";
|
||||
restoreDiv.style.border = "1px solid #333";
|
||||
restoreDiv.style.borderRadius = "4px";
|
||||
restoreDiv.style.textAlign = "center";
|
||||
restoreDiv.style.cursor = "pointer";
|
||||
restoreDiv.style.color = "#888";
|
||||
restoreDiv.style.fontSize = "10px";
|
||||
restoreDiv.innerHTML = "🎲 History auto-hidden<br><small>Click to show</small>";
|
||||
|
||||
restoreDiv.addEventListener("mouseenter", () => {
|
||||
restoreDiv.style.backgroundColor = "#333";
|
||||
restoreDiv.style.color = "#bbb";
|
||||
});
|
||||
restoreDiv.addEventListener("mouseleave", () => {
|
||||
restoreDiv.style.backgroundColor = "#2a2a2a";
|
||||
restoreDiv.style.color = "#888";
|
||||
});
|
||||
|
||||
restoreDiv.addEventListener("click", () => {
|
||||
this.showHistorySection();
|
||||
});
|
||||
|
||||
this.restoreButton = restoreDiv;
|
||||
this.historyDisplay.parentNode.insertBefore(
|
||||
restoreDiv,
|
||||
this.historyDisplay.nextSibling
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.showHistorySection = function () {
|
||||
if (this.historyDisplay) {
|
||||
this.historyDisplay.style.display = "block";
|
||||
|
||||
if (this.restoreButton && this.restoreButton.parentNode) {
|
||||
this.restoreButton.parentNode.removeChild(this.restoreButton);
|
||||
this.restoreButton = null;
|
||||
}
|
||||
|
||||
this.startAutoHide();
|
||||
}
|
||||
};
|
||||
|
||||
// Format time ago
|
||||
nodeType.prototype.formatTimeAgo = function (timestamp) {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}d ago`;
|
||||
if (hours > 0) return `${hours}h ago`;
|
||||
if (minutes > 0) return `${minutes}m ago`;
|
||||
return `${seconds}s ago`;
|
||||
};
|
||||
|
||||
// Show messages
|
||||
nodeType.prototype.showMessage = function (message, type = "info") {
|
||||
const notification = document.createElement("div");
|
||||
notification.style.position = "fixed";
|
||||
notification.style.top = "15px";
|
||||
notification.style.right = "15px";
|
||||
notification.style.padding = "6px 10px";
|
||||
notification.style.borderRadius = "3px";
|
||||
notification.style.color = "white";
|
||||
notification.style.fontSize = "10px";
|
||||
notification.style.zIndex = "10000";
|
||||
notification.style.maxWidth = "200px";
|
||||
notification.textContent = message;
|
||||
|
||||
switch (type) {
|
||||
case "success":
|
||||
notification.style.backgroundColor = "#28a745";
|
||||
break;
|
||||
case "error":
|
||||
notification.style.backgroundColor = "#dc3545";
|
||||
break;
|
||||
case "warning":
|
||||
notification.style.backgroundColor = "#ffc107";
|
||||
break;
|
||||
default:
|
||||
notification.style.backgroundColor = "#17a2b8";
|
||||
}
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
document.body.removeChild(notification);
|
||||
}
|
||||
}, 1800);
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user